Compare commits

...

347 Commits

Author SHA1 Message Date
Dane Everitt
96256ac63e [security] fix vulnerability when handling remote file redirects
Also adds the ability for an admin to just completely disable this service if it is not needed on the node.
2021-01-09 17:52:27 -08:00
Dane Everitt
6701aa6dc1 Merge branch 'dane/self-upgrade' into develop 2021-01-09 17:38:18 -08:00
Dane Everitt
ff8926bba8 bye bye command 2021-01-09 17:37:58 -08:00
Dane Everitt
217ca72eb3 Merge pull request #85 from pterodactyl/schrej/formatting
organize imports with gopls format
2021-01-09 17:31:46 -08:00
Jakob Schrettenbrunner
648072436f organize imports with gopls format 2021-01-10 01:22:39 +00:00
Dane Everitt
6fe2468a5a foundation for self-upgrade logic 2021-01-08 22:49:19 -08:00
Dane Everitt
948d927eb9 Cleanup command running a bit 2021-01-08 22:19:23 -08:00
Dane Everitt
b2eaa3f7f8 Update CHANGELOG.md 2021-01-08 21:31:06 -08:00
Dane Everitt
93417dddb1 Update CHANGELOG.md 2021-01-08 21:23:25 -08:00
Dane Everitt
044c46fc9a Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2021-01-08 21:21:37 -08:00
Dane Everitt
c9d972d544 Revert usage of ContainerWait, return to io.Copy blocking
Until https://github.com/moby/moby/issues/41827 is resolved this code causes chaos to unfold on machines and causes servers to be non-terminatable.

This logic was intially changed to logical purposes, but this io.Copy logic works perfectly fine (even if not immediately intuitive).
2021-01-08 21:21:09 -08:00
Matthew Penner
0aab4b1ac2 environment(docker): re-attach to container logs after EOF 2021-01-08 08:19:33 -07:00
Matthew Penner
4f4b4fd2e6 environment(docker): cleanup code 2021-01-08 08:15:40 -07:00
Matthew Penner
66c9be357c Potential fix for servers being marked as stopping after being marked as offline 2021-01-07 19:32:15 -07:00
Matthew Penner
1d36811dfe Fix v being shown twice on wings boot 2021-01-07 16:44:09 -07:00
Dane Everitt
6e74123c65 Update CHANGELOG.md 2021-01-06 21:42:09 -08:00
Dane Everitt
b82f5f9a32 [security] deny downloading files from internal locations 2021-01-06 21:34:18 -08:00
Dane Everitt
1937d0366d cleanup; fix environment stats not reporting network TX correctly 2021-01-06 20:47:44 -08:00
Dane Everitt
963a906c30 Less obtuse logic for polling resource usage when attaching a container 2021-01-06 20:36:29 -08:00
Jakob
3f6eb7e41a no need for additional decode (#81)
file paths used to be url-encoded twice, which is no longer the case.
2021-01-03 17:20:16 -08:00
Omar Kamel
a822c7c340 typo in docker-compose file (#82)
minor typo i noticed while messing around
2021-01-03 16:24:28 -08:00
Matthew Penner
b8fb86f5a4 Update Dockerfile to use busybox 1.33.0 2021-01-03 12:46:06 -07:00
Matthew Penner
ee0c7f09b3 Fix user problems when running inside of Docker 2021-01-02 12:58:58 -07:00
Matthew Penner
d3ddf8cf39 Mark server as not transferring after archive failure 2021-01-02 10:11:25 -07:00
Dane Everitt
bf554e8ed2 Update CHANGELOG.md 2020-12-30 18:24:37 -08:00
Dane Everitt
d6e189df5e Update commentary 2020-12-27 16:53:40 -08:00
Dane Everitt
68749616ad Ignore symlink errors 2020-12-27 16:30:00 -08:00
Dane Everitt
7549eb13a0 Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-12-27 16:17:17 -08:00
Dane Everitt
902f9f5944 Include more (and customizable) context with backup logs 2020-12-27 16:16:40 -08:00
Matthew Penner
2cf24994d9 Uncomment important code 2020-12-27 13:56:45 -07:00
Matthew Penner
3a7c4822f8 Improve logged stacktraces 2020-12-27 13:55:58 -07:00
Matthew Penner
b26db99ee7 Fix part_id field in logs 2020-12-27 13:02:50 -07:00
Matthew Penner
de4d2f4724 Make logs follow a more consistent format 2020-12-27 13:01:34 -07:00
Matthew Penner
640e30de8a Ignore symlinks with missing target, add better backup logging, update copyright year to 2021 2020-12-27 12:54:18 -07:00
Matthew Penner
e75118e0f0 Maybe this will fix shit? 2020-12-27 12:21:26 -07:00
Matthew Penner
a4c8b8714b Bump all dependencies 2020-12-27 11:49:08 -07:00
Matthew Penner
ddb683efb6 Remove UploadID field from backups (#80) 2020-12-26 12:27:52 -08:00
Matthew Penner
5be6e20b03 Send archive status request before sending the transfer status event 2020-12-26 11:42:44 -07:00
Dane Everitt
1ba3631cc1 Don't follow if there is an error 2020-12-25 17:09:35 -08:00
Dane Everitt
94d41bc1f5 Use the system.ScanReader function to replace this logic 2020-12-25 17:07:57 -08:00
Dane Everitt
a4c68eed16 Use sync.Once here to instantiate the event handler 2020-12-25 17:07:40 -08:00
Dane Everitt
22c53c365a Fix installer to not explode on long lines; closes pterodactyl/panel#2773 2020-12-25 17:05:01 -08:00
Dane Everitt
59c30c2842 Fix use of atomics in codebase 2020-12-25 17:04:18 -08:00
Dane Everitt
3842f054a5 These need spaces? 2020-12-25 15:14:21 -08:00
Dane Everitt
8fa4c50379 Fix analysis file per Github rec 2020-12-25 15:10:31 -08:00
Dane Everitt
510d46289b security: don't reveal wings version in remote API calls 2020-12-25 15:06:17 -08:00
Dane Everitt
6e5b14c466 Prevent malicious code from causing a server UUID to be used that is not valid 2020-12-25 15:01:43 -08:00
Dane Everitt
b3922864f2 Simplify origin checking 2020-12-25 14:40:28 -08:00
Dane Everitt
31d4c1d34f Close the file when done 2020-12-25 14:01:25 -08:00
Dane Everitt
0cdfdc725c Cleanup ignored file reader 2020-12-25 14:00:01 -08:00
Dane Everitt
bb132243ed Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-12-25 13:57:33 -08:00
Dane Everitt
d96115325a Add file helper 2020-12-25 13:57:26 -08:00
Matthew Penner
a450abc080 Don't read directories either, add completion log for transfers 2020-12-25 14:46:33 -07:00
Matthew Penner
0a45ea44a4 Return a user-friendly error for an invalid file mode 2020-12-25 14:41:38 -07:00
Matthew Penner
e05c601325 Lets not read an entire file into memory 2020-12-25 14:40:53 -07:00
Matthew Penner
5c78cb9ab3 Add transfer logging output (#77)
Co-authored-by: Dane Everitt <dane@daneeveritt.com>
2020-12-25 13:32:41 -08:00
Matthew Penner
901ab1157d Rework archiving logic to be more consistent and less impactful on disk IO (#79)
Co-authored-by: Dane Everitt <dane@daneeveritt.com>
2020-12-25 11:52:57 -08:00
Dane Everitt
c0523df696 Add a server context that gets canceled when a server is deleted 2020-12-25 11:21:09 -08:00
Dane Everitt
f7f5623c71 Set UA and limit simultaneous downloads 2020-12-24 09:01:00 -08:00
Dane Everitt
184013b652 Include endpoints for getting active downloads for a server 2020-12-20 13:14:07 -08:00
Dane Everitt
f8282c56cb Avoid race conditions from mismatched use of atomic & sync.Mutex 2020-12-20 13:13:10 -08:00
Dane Everitt
c8d297a056 Support canceling an in-progress download 2020-12-20 12:53:40 -08:00
Dane Everitt
c718da20e3 Check disk space before trying a write from the downloader; don't make empty directories if we can't even write the file 2020-12-20 11:17:53 -08:00
Dane Everitt
9c53436470 Check disk before even trying to run the download 2020-12-20 11:08:01 -08:00
Dane Everitt
17daa2071f Update base logic for handling server file downloads 2020-12-20 10:59:07 -08:00
Dane Everitt
3495fb1c76 Less racey logic for resource usage 2020-12-16 22:03:35 -08:00
Dane Everitt
0c93e5ed02 Properly handle decoding paths 2020-12-16 21:38:56 -08:00
Dane Everitt
737e1fcef6 Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-12-16 20:21:24 -08:00
Dane Everitt
16118874cf Don't obliterate the lock 2020-12-16 20:21:22 -08:00
Matthew Penner
b161ccafaf Remove 'arm' build from release workflow 2020-12-16 08:43:55 -07:00
Dane Everitt
59a111de39 Invite our lord and savior emperror back to the table 2020-12-15 21:56:53 -08:00
Dane Everitt
c0a641247b Different handling of errors on routes; push towards using middleware 2020-12-15 21:53:34 -08:00
Dane Everitt
057cdbd927 Use the underlying error 2020-12-15 21:14:20 -08:00
Dane Everitt
0ecc166dcd Update error reporting middleware 2020-12-15 21:08:00 -08:00
Dane Everitt
3a26a5d39d Convert all filesystem error types into the same Error struct 2020-12-15 20:51:13 -08:00
Dane Everitt
9ae75a399b Update middleware.go 2020-12-15 20:20:47 -08:00
Dane Everitt
01b766dacc Remove dupe 2020-12-15 20:20:22 -08:00
Dane Everitt
904e0a574d Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-12-15 20:20:16 -08:00
Dane Everitt
acd6dc62d0 Update middleware logic 2020-12-15 20:19:09 -08:00
Caleb
8f26c31df6 Support downloading remote files to a server via the API
Co-authored-by: Dane Everitt <dane@daneeveritt.com>
2020-12-15 19:59:18 -08:00
Matthew Penner
84c05efaa5 Update Dockerfile 2020-12-15 16:45:40 -07:00
Matthew Penner
d72d96f9d0 😊 is for me? 2020-12-15 16:24:15 -07:00
Matthew Penner
981f04fbd8 PauseChamp 2020-12-15 16:15:54 -07:00
Matthew Penner
463dd6f4ec ( .. •˘___˘• .. ) 2020-12-15 16:08:06 -07:00
Matthew Penner
02034211c1 Update GitHub Actions workflows 2020-12-15 15:59:06 -07:00
Matthew Penner
fafda283b1 Update Dockerfile, add .dockerignore 2020-12-15 15:58:47 -07:00
Dane Everitt
199be20717 Use custom flag for ignoring certificate errors, not the debug flag 2020-12-12 09:56:01 -08:00
Dane Everitt
1c825d2a74 Merge pull request #75 from BrainStone/patch-1
Explictily disable log compression
2020-12-12 09:21:53 -08:00
Matthew Penner
e936f22419 Tweaks to server transfer logging 2020-12-11 17:24:35 -07:00
Yannick Schinko
c253a4bac0 Explictily disable log compression
Log compression is only relevant when `max-file` is 2 or higher.

Older versions of docker (Docker version 19.03.14, build 5eb3275d40 for example (currently the latest version on Debian 9)) error out when compression is enabled in the ``/etc/docker/daemon.json`` file:

```
Error response from daemon: failed to initialize logging driver: compress cannot be true when max-file is less than 2 or max-size is not set
```

While newer docker versions just don't care setting this explicitly to false like in ``intsall.go``, it doesn't hurt being explicit here:

de51fd1c51/server/install.go (L481-L485)
2020-12-12 00:51:11 +01:00
Dane Everitt
ecb15a224a Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-12-08 21:24:33 -08:00
Dane Everitt
73f1399e47 Don't expect double-encoded values here 2020-12-08 21:24:31 -08:00
Matthew Penner
8e29ffed50 Add configurable disk write speed limit for backups (#74)
* Add configurable disk write speed limit for backups
2020-12-08 09:13:48 -07:00
Matthew Penner
2d4dd05ec9 Remove arm from build-test workflow 2020-12-08 09:12:45 -07:00
Matthew Penner
fb3460f5f6 Fix build-test workflow 2020-12-08 09:12:16 -07:00
Matthew Penner
cff7667155 fix build-test workflow running twice on pull_requests 2020-12-08 09:11:15 -07:00
Dane Everitt
7871c0928f Remove --brute; hella slow and not much of a difference in the end 2020-12-06 15:57:03 -08:00
Dane Everitt
04d714f19c Update CHANGELOG.md 2020-12-06 15:49:46 -08:00
Dane Everitt
a18f60bd05 Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-12-06 15:35:04 -08:00
Dane Everitt
deea5babbc Publish message when error occurs during crash handling 2020-12-06 15:34:58 -08:00
Matthew Penner
e41b3dc09a Merge branch 'matthewpi/backups-patch-1' into develop
# Conflicts:
#	loggers/cli/cli.go
2020-12-06 16:18:15 -07:00
Matthew Penner
6366794838 Actually initialize the cache 2020-12-06 16:15:54 -07:00
Matthew Penner
c01a39d881 Add caching to build-test workflow 2020-12-06 16:12:41 -07:00
Dane Everitt
3f2ce59766 Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-12-06 15:10:14 -08:00
Dane Everitt
bcf0c72e47 Correctly report errors to admin users over the websocket; closes pterodactyl/panel#2709 2020-12-06 15:10:08 -08:00
Matthew Penner
bd5892b70c yeet useless boi 2020-12-06 16:05:57 -07:00
Dane Everitt
70ea61f22f test first I guess 2020-12-06 15:04:00 -08:00
Dane Everitt
1b0c2e1764 * 2020-12-06 15:02:50 -08:00
Dane Everitt
3d532f6e0b Simplify logic 2020-12-06 14:58:35 -08:00
Matthew Penner
81fd1a3758 Make logged stacktraces more useful 2020-12-06 15:55:35 -07:00
Matthew Penner
b52c3fb61e Cleanup backup/backup_s3.go 2020-12-06 15:25:11 -07:00
Matthew Penner
bc3d92f9e6 Change backup upload id cache to use the cache package 2020-12-06 15:23:44 -07:00
Matthew Penner
ee08829a28 s3 backups: handle CompleteMultipartUpload and AbortMultipartUpload on the panel 2020-12-06 13:56:17 -07:00
Dane Everitt
83f0d2c953 Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-12-06 10:53:57 -08:00
Dane Everitt
605be3ebad Update README.md 2020-12-06 10:53:55 -08:00
Matthew Penner
ca6dc2c964 Remove .travis.yml 2020-12-03 18:32:59 -07:00
Dane Everitt
dc41126e25 Merge pull request #72 from TortleWortle/patch-1
Allow multiple publishing on multiple interfaces on same port.
2020-12-02 08:18:38 -08:00
TortleWortle
da4c542724 Update allocations.go 2020-12-02 14:27:37 +01:00
Tortle
e7d93a5248 Update allocations.go 2020-12-01 23:03:05 +01:00
Tortle
51aa4c73cd Update allocations.go 2020-12-01 21:20:01 +01:00
Tortle
4a7510d36f Allow multiple publishing on multiple interfaces on same port.
Fixes the issue where you cannot have multiple ip addresses on the same port for a server.
2020-12-01 20:43:40 +01:00
Matthew Penner
ba0a1a651e Only use the permission bits for ModeBits 2020-11-29 16:13:07 -07:00
Matthew Penner
068f41393d Return mode_bits when listing files in a directory 2020-11-29 14:20:24 -07:00
Matthew Penner
169e8b8f8b Fix chmod endpoint mode value 2020-11-29 13:48:15 -07:00
Matthew Penner
c6e2889075 Fix chmod endpoint 2020-11-29 13:44:28 -07:00
Matthew Penner
f62f714863 Add chmod endpoint to server files 2020-11-29 13:07:45 -07:00
Dane Everitt
da9ace5d9d Create FUNDING.yml 2020-11-29 11:58:20 -08:00
Dane Everitt
97345123ce Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-11-29 11:48:03 -08:00
Dane Everitt
8aa9105ed3 Make paths lowercase for cert serving; closes pterodactyl/panel#2745 2020-11-29 11:47:52 -08:00
Matthew Penner
e8088f85d0 Lets not try to log the server ID twice 2020-11-29 12:35:02 -07:00
Matthew Penner
0c8476c79b Cleanup router/router_transfer.go 2020-11-29 12:33:44 -07:00
Matthew Penner
a7a66e8bc0 Notify panel of failed archive generation when transferring a server 2020-11-29 12:31:54 -07:00
Matthew Penner
65a861a9b6 Remove temporary transfer archives; closes https://github.com/pterodactyl/panel/issues/2742 2020-11-28 17:00:52 -07:00
Matthew Penner
de51fd1c51 Error handling improvements (#71)
* Remove `emperror.dev/errors`, remove all `errors#Wrap` and `errors#WithStack` calls
* Improve logging in `server/backup.go`
2020-11-28 16:57:10 -07:00
Matthew Penner
40c70673cd Use registry authentication when pulling install image, fixes https://github.com/pterodactyl/panel/issues/2589 2020-11-20 14:36:29 -07:00
Matthew Penner
73b221d022 Switch old Server#GetState() calls over to Server#Environment#State() 2020-11-20 14:35:29 -07:00
Matthew Penner
287b286940 Oh, that's why it was done like that 2020-11-18 18:44:25 -07:00
Matthew Penner
1d0e85cf55 Attempt to fix 'archive/tar: write too long' error when creating a backup 2020-11-18 16:30:34 -07:00
Matthew Penner
30ec6dc78d fix panic when getting error stack 2020-11-15 13:34:19 -07:00
Matthew Penner
121a4d1146 Attempt to fix codeql-analysis.yml 2020-11-15 13:04:03 -07:00
Matthew Penner
b9be373671 Attempt to force GitHub Actions to use go1.15.5 2020-11-15 13:00:33 -07:00
Dane Everitt
aedd0e406c appease docker compose [skip docker] 2020-11-14 20:50:09 -08:00
Dane Everitt
82b23ef638 Add support for automated docker build 2020-11-14 20:42:12 -08:00
Dane Everitt
d970ec35b7 Update dockerfile to build quicker 2020-11-14 19:22:29 -08:00
Dane Everitt
e2872e786e Tiny optimization for handling string output from server console 2020-11-12 21:41:35 -08:00
Dane Everitt
f81e35d960 Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-11-12 20:50:18 -08:00
Dane Everitt
672fb860ea Don't add a stack for deadline exceeded when terminating 2020-11-12 20:50:15 -08:00
Dane Everitt
8081c83de4 Allow disabling disk checking with 0 value for check interval 2020-11-12 20:50:03 -08:00
Dane Everitt
f379d0e54a Merge pull request #68 from gzzchh/develop
Turn off the CGO to make it static linked
2020-11-11 10:58:49 -08:00
Dane Everitt
ffb6bd72ef Don't accidentally reset a server's state when starting it after a system reboot; closes pterodactyl/panel#2695 2020-11-10 21:21:20 -08:00
Dane Everitt
488ef9de54 Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-11-10 20:36:42 -08:00
Dane Everitt
34349d4b48 Don't report a context cancelation as an error 2020-11-10 20:36:40 -08:00
Matthew Penner
2197d82957 Clean paths from AllowedMounts to ensure consistency 2020-11-09 16:58:01 -07:00
Dane Everitt
20ece60a72 Fix files not being editable correctly in the panel; closes pterodactyl/panel#2679 2020-11-08 17:06:40 -08:00
Dane Everitt
de0c67d066 Re-enable disabled tests 2020-11-08 15:17:47 -08:00
Dane Everitt
68bdcb3cbc Fix error handling and get tests back to working 2020-11-08 15:15:39 -08:00
Dane Everitt
205c4d541e Update changelog 2020-11-08 14:40:07 -08:00
Dane Everitt
ef999a039c Perhaps better error stacks for backups/archives; ref #2418 2020-11-08 14:07:26 -08:00
Dane Everitt
be9d1a3986 Replace error handling package with emperror; add better reporting for errors escaping server root 2020-11-08 13:52:20 -08:00
Dane Everitt
0989c78d4b Fix the profiling... 2020-11-08 12:33:24 -08:00
Dane Everitt
1683675807 Add more advanced profiling support 2020-11-08 12:12:21 -08:00
Dane Everitt
536f00a5e5 Add support for both memory and cpu profiling 2020-11-08 11:59:04 -08:00
gzzchh
33e584b447 Turn off the CGO to make it static linked 2020-11-08 14:42:55 +08:00
Dane Everitt
4b17ac4f1c Fix atomic string handling for JSON output 2020-11-06 22:22:33 -08:00
Dane Everitt
944d381778 Improve server state logical handling; allow setting state directly on the environment 2020-11-06 21:53:00 -08:00
Dane Everitt
3fce1b98d5 Create atomic string to allow for simpler logic 2020-11-06 21:14:29 -08:00
Dane Everitt
a74be8f4eb configurable socket log count; closes pterodactyl/panel#2659 2020-11-06 20:56:21 -08:00
Dane Everitt
af9ed4bff1 less weird log message 2020-11-03 21:02:10 -08:00
Dane Everitt
08d1efb475 Support denying a JWT JTI via the API 2020-11-03 21:01:50 -08:00
Dane Everitt
65664b63e7 Add support for denying JWT JTI's that are generated before a specific time 2020-11-03 20:33:33 -08:00
Dane Everitt
912d95de24 Ensure environment starts in default offline state, rather than an empty string; closes pterodactyl/panel#2519 2020-11-02 19:43:49 -08:00
Dane Everitt
13c253780a Use 60 seconds, not 15 2020-11-01 17:26:45 -08:00
Dane Everitt
fe572beada Stop servers when exceeding their disk limits; closes pterodactyl/panel#2638 2020-11-01 17:20:42 -08:00
Dane Everitt
384b9a3c28 Correctly report errors when uploading files 2020-11-01 16:33:02 -08:00
Matthew Penner
05cfb59e18 Add better logging for s3 backup uploads 2020-11-01 15:36:16 -07:00
Dane Everitt
317e54acc5 Merge pull request #67 from pterodactyl/issue/2599
Switch to s3 multipart uploads for backups
2020-11-01 14:34:27 -08:00
Matthew Penner
5475cb02c1 Cleanup s3 backup uploads, add request timeouts 2020-11-01 15:23:46 -07:00
Matthew Penner
1239b1c0ca Switch to s3 multipart uploads for backups 2020-11-01 15:23:46 -07:00
Dane Everitt
b8598e90d4 Fix query parameter association on GET requests 2020-11-01 14:23:03 -08:00
Dane Everitt
fcccda2761 Endpoint fixes for GET requests and error handling 2020-11-01 14:04:57 -08:00
Dane Everitt
f67889c2ca Remove unused code 2020-11-01 13:17:17 -08:00
Dane Everitt
b8766d3c82 Start at next page, not current page 2020-10-31 13:38:19 -07:00
Dane Everitt
ca3becfb55 Correctly handle server responses and instantiation 2020-10-31 11:13:40 -07:00
Dane Everitt
41a67933eb Use a paginated loop to fetch servers from the Panel rather than a single massive request 2020-10-31 11:05:53 -07:00
Dane Everitt
334b3e8d10 Refactor HTTP endpoints to be less complicated and follow better standards 2020-10-31 10:04:20 -07:00
Dane Everitt
c4703f5541 Update CHANGELOG.md 2020-10-22 22:02:01 -07:00
Dane Everitt
1f3394b82d Update release.yml 2020-10-22 21:42:34 -07:00
Dane Everitt
bae63c4321 Fix bad yaml 2020-10-22 21:41:00 -07:00
Dane Everitt
f99640a42d Update CHANGELOG.md 2020-10-22 21:36:24 -07:00
Dane Everitt
c73d632e0d Use correct case-insensitive regex; closes pterodactyl/panel#2546 2020-10-22 20:22:27 -07:00
Dane Everitt
903902e123 Match multiple times 2020-10-22 20:17:58 -07:00
Dane Everitt
1c787b5f26 Fix handling of super long lines to not hit a scanner error; closes pterodactyl/panel#2549
Also fixes return handling in games like Minecraft to properly display the full line output.
2020-10-19 18:13:52 -07:00
Dane Everitt
3f9ac8b89a Fix local images not being pulled correctly; closes #2559 2020-10-19 16:18:33 -07:00
Dane Everitt
560c832cc6 Apply timezone cleaning to final result, closes #2546 2020-10-19 16:13:59 -07:00
Dane Everitt
13058ad64b Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-10-19 15:27:11 -07:00
Dane Everitt
305cd512a7 Update README.md 2020-10-19 15:27:10 -07:00
Matthew Penner
3cd17a2856 Add arm to release workflow 2020-10-17 15:55:05 -06:00
Matthew Penner
56789196d4 Disable flaky test so I can test cross-platform building 2020-10-17 15:47:30 -06:00
Matthew Penner
70804dd20f Remoe stat_arm.go, make stat_linux.go with with both 32 and 64 bit systems 2020-10-17 15:45:14 -06:00
Matthew Penner
19d821aab5 Fix arm build 2020-10-17 15:41:13 -06:00
Matthew Penner
4ce35d3d17 Fix race in filesystem_test.go 2020-10-17 15:31:40 -06:00
Matthew Penner
a62b588ace Add matrix to build-test.yml 2020-10-17 15:13:06 -06:00
Dane Everitt
9b54be06bb Remove unused config values 2020-10-17 14:09:02 -07:00
Dane Everitt
c031d37b91 Use single call to create/return the docker client 2020-10-17 14:04:56 -07:00
Dane Everitt
19051c99ef Support arm builds 2020-10-17 13:52:38 -07:00
Jakob
99fd416cee add version negotiation fix to changelog 2020-10-17 21:49:43 +02:00
Jakob
acf09180f0 Merge pull request #66 from pterodactyl/fix/version-negotiation
Fix version negotiation for installs and diagnostics
2020-10-17 21:43:00 +02:00
Jakob
b19fc88a95 add version negotiation to diagnostics docker client 2020-10-17 21:42:23 +02:00
Jakob
e185f597ba add version negotiation to server install docker client 2020-10-17 21:41:01 +02:00
Dane Everitt
3973747c9c Update README.md 2020-10-17 12:11:06 -07:00
Dane Everitt
947279a07c Don't abort entire boot process due to one bad server egg; closes pterodactyl/panel#2448 2020-10-17 12:06:47 -07:00
Dane Everitt
ad1ed0f24a Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-10-17 11:35:22 -07:00
Dane Everitt
80387bc294 Use more easily configurable timezone, remove /etc/timezone mounts from containers; closes pterodactyl/panel#2513
If this does not completely solve the issue in containers then we need to evaluate the image being used to determine what changes need to happen to the image itself to support the timezone.

ref pterodactyl/panel#2239
ref pterodactyl/panel#2329
ref pterodactyl/panel#2389
2020-10-17 11:35:20 -07:00
Dane Everitt
e8dbba5cc0 Merge pull request #65 from AreYouRlyScared/patch-3
Update diagnostics.go
2020-10-14 08:22:30 -07:00
Charles Morgan
f50f580dcc Update diagnostics.go 2020-10-14 05:06:14 -04:00
Dane Everitt
7e8033d96c Update README.md 2020-10-12 21:12:36 -07:00
Dane Everitt
1b94eadc68 Update CHANGELOG.md 2020-10-11 16:27:44 -07:00
Dane Everitt
3a496af9f1 Merge pull request #64 from parkervcp/fix_file_parser
fix file parsing
2020-10-11 15:27:53 -07:00
Dane Everitt
7f4771068f Code cleanup 2020-10-11 15:27:27 -07:00
Dane Everitt
3463c223e8 Merge branch 'develop' into pr/64 2020-10-11 15:25:07 -07:00
Dane Everitt
6c291d9b0e Minor tweaks to diagnostics for cleaner info, changes endpoint; closes pterodactyl/panel#2463 2020-10-11 15:23:55 -07:00
Dane Everitt
e29789114c Fix server disk usage not being reported properly; closes pterodactyl/panel#2445 2020-10-11 15:02:37 -07:00
Michael Parker
4c9aaeb769 fix file parsing
resolves #2393

loads, edits, and re-writes the file instead of inline edits that seem to break.

This inefficient but it works in my testing.
2020-10-07 18:41:05 -04:00
Dane Everitt
37e59e6928 Close connected sockets when a server is deleted; closes pterodactyl/panel#2428 2020-10-03 20:46:29 -07:00
Dane Everitt
e02c197585 More archiver code cleanup; ref pterodactyl/panel#2438 2020-10-03 19:13:41 -07:00
Dane Everitt
d9109cbf5a Correctly detect os.IsNotExist error; ref pterodactyl/panel#2438 2020-10-03 19:09:12 -07:00
Dane Everitt
6bc8b1a567 Merge branch 'dane/fs-tests' into develop 2020-10-01 21:30:39 -07:00
Dane Everitt
244640d0c1 [security] remove "SafeJoin" function
This function was not actually safe in theory. If an unknown stat source was passed in it would be possible for a symlinked file to not be detected as a symlink, thus skipping any safe path resolutions.

This would happen if the stat source was a regular os.Stat call and not an os.Lstat call, but since there is no way to differentiate between those two in the code, it is safer to just manually apply this logic in the positions where we _know_ for certain that we're working with the results of an Lstat call.
2020-10-01 21:28:38 -07:00
Dane Everitt
e3e89a2ecc Cover symlink attacks with test cases 2020-10-01 21:13:42 -07:00
Dane Everitt
90ae815b1d Return tests to passing state 2020-10-01 20:40:25 -07:00
Dane Everitt
1fc15e82f1 Merge pull request #63 from pterodactyl/fix/diagnostics-logs
add log output to diagnostics cmd
2020-10-01 19:41:10 -07:00
Jakob Schrettenbrunner
3a83f65f27 add log output to diagnostics cmd 2020-10-01 23:04:58 +00:00
Jakob
37b09255d5 Merge pull request #62 from stanier/develop
Removed stray `.` in `./mnt/install`
2020-10-02 00:29:18 +02:00
Keyton Stanier
b92fab83c8 Removed stray . in ./mnt/install 2020-10-01 18:00:26 -04:00
Dane Everitt
367fdfad54 Simplify copy file logic 2020-09-30 21:53:50 -07:00
Dane Everitt
ee460686d6 Make delete more synchronous 2020-09-30 21:47:42 -07:00
Dane Everitt
9b7c0fb7f3 Steal tests from other branch that is being discarded, attempt to get at least one of them to pass; WIP 2020-09-30 21:46:32 -07:00
Dane Everitt
0f7bb1a371 Refactor filesystem to not be dependent on a server struct 2020-09-27 12:24:08 -07:00
Dane Everitt
de30e2fcc9 Dont attempt to get size within archive process, will return empty; ref pterodactyl/panel#2420
The stat call is operating against an unflushed file if called in the archive function, so you'll just get the emtpy archive size, rather than the final size.

Plus, we only used the file stat in one place, so slight efficiency win?
2020-09-27 11:16:38 -07:00
Dane Everitt
a0fa5a94b6 Support websocket event to send server stats; ref pterodactyl/panel#2414 2020-09-27 09:46:00 -07:00
Dane Everitt
3655b172f3 Send JWT errors as specific event type 2020-09-27 09:09:34 -07:00
Dane Everitt
4748d7cb29 Don't spam errors to the output if attempting to send a command to an unattached starting instance; closes pterodactyl/panel#2385 2020-09-26 17:35:11 -07:00
Dane Everitt
a20861fa8e Write newline when editing files using the text parser; closes pterodactyl/panel#2393 2020-09-26 16:12:21 -07:00
Dane Everitt
62d0c7fa24 Mask errors from websocket being closed; closes pterodactyl/panel#2387 2020-09-26 16:10:32 -07:00
Dane Everitt
0b9ad3de05 More file size confirmations before performing some actions; closes pterodactyl/panel#2420 2020-09-26 16:02:41 -07:00
Dane Everitt
3391d5803e Use same values between panel and wings when calculating disk 2020-09-25 20:31:33 -07:00
Dane Everitt
a3cb1b5ae0 Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-09-25 20:03:06 -07:00
Dane Everitt
fb24ad58b4 Don't allow a reinstall while server is processing a power action; closes pterodactyl/panel#2409 2020-09-25 20:03:04 -07:00
Dane Everitt
ae5005baa3 Don't log disk space errors 2020-09-25 20:02:38 -07:00
Jakob Schrettenbrunner
f79868ff6c why tf do we create docker clients in two locations... 2020-09-26 04:31:43 +02:00
Dane Everitt
60211271b2 Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-09-25 19:23:37 -07:00
Dane Everitt
2cc06e0528 closes pterodactyl/panel#2413 2020-09-25 19:23:31 -07:00
Jakob
6cbcfece8e enable client version negotiation for docker env (#61)
* enable client version negotiation for docker env

* derp

* another try
2020-09-25 19:19:27 -07:00
Dane Everitt
7646c27c5a Update README.md 2020-09-24 21:26:17 -07:00
Dane Everitt
7624502ec4 Don't swap the int and cause a negative disk space... closes pterodactyl/panel#2401 2020-09-24 21:19:46 -07:00
Dane Everitt
bf1233def4 Don't ignore disk space limits when copying/archiving; closes pterodactyl/panel#2400 2020-09-24 21:18:10 -07:00
Matthew Penner
1a3ba9efca Better handle readdirent errors 2020-09-24 21:48:59 -06:00
Dane Everitt
897c4869de Return a nicer error if the file name is too long; closes pterodactyl/panel#2403 2020-09-24 20:02:42 -07:00
Dane Everitt
f0a4efb242 Attempt to create directory structure for config file if missing 2020-09-22 21:01:50 -07:00
Dane Everitt
342c3ea565 Do not panic if there is no text on the line; closes pterodactyl/panel#2369 2020-09-22 21:01:32 -07:00
Dane Everitt
522c6c17e4 Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-09-22 20:41:16 -07:00
Dane Everitt
d8cd8ae36a Don't treat certain "expected" errors from the filesystem as 500 errors; closes pterodactyl/panel#2376 2020-09-22 20:41:14 -07:00
Matthew Penner
62d5248b0c Remove stacktrace from image exists locally warning 2020-09-20 14:20:42 -06:00
Matthew Penner
ff597672db Fix #2380 2020-09-20 12:51:12 -06:00
Matthew Penner
aa39cefcf1 Fix AllowedMounts value from potentially being overwritten remotely 2020-09-17 21:59:41 -06:00
Charles Morgan
033e8e7573 Add GoReportcard badge (#57)
Adds GoReportcard Badge
2020-09-17 20:48:09 -07:00
Michael (Parker) Parker
aa78071543 update docker configs (#50)
* update docker configs

dockerfile with an updated start command

docker-compose file adds custom network name so it can be used in firewalld commands.

* update compose file

mount changes
remove /srv/daemon-data
remove /etc/timezone

variable changes
add TZ

* add note about old data folder.

* update to go 1.15

Update base image to go version 1.15
2020-09-17 20:48:01 -07:00
Dane Everitt
48aeeff818 Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-09-17 20:45:19 -07:00
Dane Everitt
864c37f17c Use 2k lines as the per loop limit 2020-09-17 20:45:13 -07:00
Matthew Penner
c7405aebe5 Update release.yml to use go1.15.2 2020-09-17 21:39:48 -06:00
Matthew Penner
9ff2d53466 Update build-test.yml to use go1.15.2 2020-09-17 21:39:20 -06:00
Dane Everitt
6ba49df485 Protect against zip bombs; closes pterodactyl/panel#883 2020-09-17 20:37:34 -07:00
Dane Everitt
6b25ac3665 Fix websocket error spam; only send known JWT issues along to the socket itself, not to wings console; closes pterodactyl/panel#2354 2020-09-17 20:30:51 -07:00
Dane Everitt
783832fc71 Set the docker image correctly when a server is updated; closes pterodactyl/panel#2356 2020-09-17 20:20:39 -07:00
Dane Everitt
815539b3da Fix log rotation error due to missing templates dir in compiled build 2020-09-17 20:16:27 -07:00
Dane Everitt
6ba1b75696 Add console throttling; closes pterodactyl/panel#2214 (#60) 2020-09-17 20:13:04 -07:00
Dane Everitt
ce76b9339e better error handling for busy files; closes pterodactyl/panel#2332 2020-09-15 19:53:00 -07:00
Dane Everitt
6ba15e9884 Better error handling from responses 2020-09-13 13:55:40 -07:00
Dane Everitt
f2a6d6b3c5 Websocket cleanup 2020-09-12 22:12:23 -07:00
Dane Everitt
0295603943 Speed up wings boot when restoring from cleared docker environment
Doesn't pull images for any server that is not supposed to be running at boot time
2020-09-12 22:08:50 -07:00
Dane Everitt
ce2659fdd7 Simplify environment creation for server 2020-09-12 21:48:04 -07:00
Dane Everitt
be49e08f4f Show note in console when image is being pulled, show pull status to admins 2020-09-12 21:37:48 -07:00
Dane Everitt
3ee76ea2bc Cleanup 2020-09-12 20:26:02 -07:00
Dane Everitt
d7fbf29cc1 Remove debug lines 2020-09-12 20:17:36 -07:00
Dane Everitt
d02e37620d Use workerpools to enforce FIFO without blocking other topics 2020-09-12 20:13:59 -07:00
Dane Everitt
53bd0d57ad Replace logic with PublishJson 2020-09-12 20:13:48 -07:00
Dane Everitt
b779c98717 Fix files unarchiving to the root; closes pterodactyl/panel#2333 2020-09-12 19:25:29 -07:00
Dane Everitt
4ac19bd29d Refactor confusing & fragile event bus logic to use callbacks and not channels; ref pterodactyl/panel#2298 2020-09-12 09:26:17 -07:00
Dane Everitt
8407ea21da Fix state retrevial race condition 2020-09-12 09:12:30 -07:00
Dane Everitt
fa6f56caa8 Remove pointless debug log 2020-09-11 23:18:51 -07:00
Dane Everitt
5a62f83ec8 Don't run pre-boot actions if the server is already running 2020-09-11 23:11:57 -07:00
Dane Everitt
8bcb3d7c62 Remove deadlock specific code 2020-09-11 23:03:35 -07:00
Dane Everitt
b2eebcaf6d Fix deadlocks in event listener system; closes pterodactyl/panel#2298
Fixes deadlocks that occurred when events were registered while other events were being unsubscribed and data was being flooded to these listeners. A complete mess, I hate this code, it is going to break again, but jesus I'm so tired.
2020-09-11 23:01:54 -07:00
Matthew Penner
45bcb9cd68 Lets not attempt to pull 16384 log lines 2020-09-11 22:52:07 -06:00
Dane Everitt
e1ff4db330 Also fix builds for non-releases 2020-09-11 21:00:37 -07:00
Dane Everitt
606143b3ad Fix flags for workflow, strips 8MB off final binary size 2020-09-11 20:59:39 -07:00
Dane Everitt
57221bdd30 Make disk checking timeout configurable 2020-09-11 20:24:23 -07:00
Dane Everitt
8f6494b092 Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-09-10 20:17:16 -07:00
Dane Everitt
c415abf971 Safety logic check, don't try to stop a stopped server when suspending; closes #2318
Couldn't actually reproduce this, but a good sanity check I guess.
2020-09-10 20:16:16 -07:00
Matthew Penner
e10844d32c Fix problems with CORS and websocket allowed origins 2020-09-10 21:08:00 -06:00
Dane Everitt
0cd8dc2b5f avoid deadlocks while processing tons of data over server event listeners; closes pterodactyl/panel#2298 2020-09-10 20:05:01 -07:00
Dane Everitt
a31e805c5a Return 400 error not 304 when bad data is passed 2020-09-09 21:12:06 -07:00
Dane Everitt
cff705f807 Support arrays within JSON and YAML documents better; closes pterodactyl/panel#1992 2020-09-09 20:27:41 -07:00
Dane Everitt
c19fc25882 Try to parse arrays more correctly; not quite working just yet but the concept seems to work 2020-09-07 17:27:23 -07:00
Dane Everitt
fff9a89ebb Correctly detect not exist error 2020-09-07 15:57:11 -07:00
Dane Everitt
891e5baa27 Don't restart if the stop command is manually sent to the console 2020-09-07 15:53:44 -07:00
Dane Everitt
001bbfad1b avoid panic from environment; ref pterodactyl/panel#2307 2020-09-07 15:37:35 -07:00
Dane Everitt
5bead443ad Handle port bindings correctly when using 127.0.0.1
closes pterodactyl/panel#2307
2020-09-07 15:33:47 -07:00
Dane Everitt
77cf57d1ea Merge pull request #56 from pterodactyl/issue/2308
Fix file parser messing with server.properties
2020-09-07 13:17:47 -07:00
Dane Everitt
d743d8cfeb Merge pull request #54 from cyberkitsune/fast-unlimited-disk
Speed up server power on for servers with unlimited size
2020-09-07 13:05:37 -07:00
Matthew Penner
a81146d730 Potential fix for console becoming unresponsive (#55)
* Potentially fix console logs not being pulled after a server has been running for a while

* Add container_id to resource polling debug logs
2020-09-07 13:04:56 -07:00
Matthew Penner
d50f9a83b6 Fix #2308 2020-09-05 14:50:03 -06:00
Matthew Penner
7ba32aca84 Fix some typos and run gofmt on all .go files 2020-09-05 13:08:40 -06:00
Dane Everitt
b9f6e17a7d Additional error logging 2020-09-03 21:21:42 -07:00
Dane Everitt
d99225c0fb Add additional logging around resource polling 2020-09-03 21:19:06 -07:00
Dane Everitt
490f874128 Prevent bungee output breaking my CLI output...
Theres enough magic ANSI codes in here to kill a horse... this also causes logging ouput to get truncated
2020-09-03 21:18:55 -07:00
Dane Everitt
70afbbfc68 Remove last zap references 2020-09-03 20:29:53 -07:00
Dane Everitt
e09cc3d2dd Automatically write a logrotate file to the disk when wings boots if directory exists; closes pterodactyl/panel#2312 2020-09-03 20:13:51 -07:00
Dane Everitt
b6008108ac Update data path if the server data directory is a symlink 2020-09-01 19:24:50 -07:00
CyberKitsune
1d22e84f21 Allow a stale value on startup for disk size if the disk is unlimited. 2020-08-31 22:10:57 -07:00
Dane Everitt
481df3d543 Only create the directory structure if it is a file being renamed; closes pterodactyl/panel#2292 2020-08-31 21:02:06 -07:00
Dane Everitt
cbf914e7a1 Additional code cleanup for #53 2020-08-31 20:46:28 -07:00
CyberKitsune
d742acf308 Minimize blocking in Filesystem.getCachedDiskUsage (#53) 2020-08-31 20:27:41 -07:00
Dane Everitt
5f1d9ff151 Don't throw a 500 error if the backup is missing; closes pterodactyl/panel#2280 2020-08-31 20:24:07 -07:00
Dane Everitt
1e633ae302 Bring the sftp server code into the package itself 2020-08-31 20:14:04 -07:00
Dane Everitt
7d084e3049 Correctly return the suspension status of the server 2020-08-30 09:54:33 -07:00
Dane Everitt
c69a0bb107 Don't run install scripts if disabled; closes pterodactyl/panel#2265 2020-08-30 09:41:14 -07:00
Dane Everitt
9780cf902d Merge pull request #52 from cyberkitsune/develop
Inform user the daemon is checking disk size on server startup
2020-08-30 09:14:21 -07:00
CyberKitsune
f1343c1d77 Publish console message on initial startup for disk sizing 2020-08-29 19:45:47 -07:00
Dane Everitt
3c662d5b07 Update README.md 2020-08-29 16:13:27 -07:00
Dane Everitt
7d8710824c Fix startup variables not being properly updated on server reboot; closes pterodactyl/panel#2255 2020-08-27 21:08:33 -07:00
Dane Everitt
711ee2258c Minimize code duplication for environment variables; ref pterodactyl/panel#2255 2020-08-27 20:28:29 -07:00
Dane Everitt
60acee2df5 avoid panic crash due to bad environment variable values; closes pterodactyl/panel#2275 2020-08-27 20:05:07 -07:00
Dane Everitt
0dde54fc8f Don't blow up if a file gets removed during the backup 2020-08-27 19:57:35 -07:00
Dane Everitt
0e474c8b24 Better error captures 2020-08-27 19:57:22 -07:00
Dane Everitt
68ab705aac Don't push directories into the backup unless associated with a file 2020-08-27 19:55:43 -07:00
Dane Everitt
a7ca6b2e34 Correctly report backup API errors; ref pterodactyl/panel#2271 2020-08-27 19:35:57 -07:00
Dane Everitt
5f1ceeff90 Add ability for users to disable checking file permissions when starting a server; closes pterodactly/panel#2272 2020-08-27 19:02:22 -07:00
Dane Everitt
c7e732d084 2.5 minutes for lookups, not every minute 2020-08-24 20:52:05 -07:00
Dane Everitt
9eb795b1bb Re-add disk space function 2020-08-24 20:46:19 -07:00
Dane Everitt
a1288565f0 Significant CPU and syscall performance improvements when iterating large directories 2020-08-24 20:45:54 -07:00
Dane Everitt
f82c91afbe Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-08-24 19:45:25 -07:00
Dane Everitt
b35ac76720 Optimizations to the filepath walker function to reduce CPU and I/O issues 2020-08-24 19:45:24 -07:00
Matthew Penner
9f27119044 Fix log directory not being created, again.. 2020-08-24 20:22:19 -06:00
Dane Everitt
9cd416611f Merge pull request #51 from pterodactyl/fix/2257
Fix log directory not being created
2020-08-24 19:08:12 -07:00
Matthew Penner
459c370229 Create install directory when creating the logs directory 2020-08-24 15:10:57 -06:00
Matthew Penner
b3a2a76f25 Fix log directory not being created 2020-08-24 11:29:40 -06:00
114 changed files with 8352 additions and 3873 deletions

56
.dockerignore Normal file
View File

@@ -0,0 +1,56 @@
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
.idea/*
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
# dep related files and folders
/vendor*
# ignore logfiles (/* so the .gitkeep override works)
/logs/*
# ignore configuration file
/config.yml
# Ignore Vagrant stuff
/.vagrant
# Builds by gox
/build
# Go Coverage tool
/coverage.out
# The built executable
wings
wings.exe
# IDE/Editor files (VS Code)
/.vscode
# test files
test_*/
# Keep all gitkeep files (This needs to stay at the bottom)
!.gitkeep
debug
.DS_Store
*.pprof
*.pdf
Dockerfile
CHANGELOG.md
Makefile
README.md
wings-api.paw

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: [DaneEveritt]
custom: ["https://paypal.me/PterodactylSoftware"]

View File

@@ -1,33 +1,79 @@
name: "Build & Test" name: Run Tests
on: on:
push: push:
branches-ignore: branches:
- 'master' - 'develop'
- 'release/**'
pull_request: pull_request:
branches:
- 'develop'
jobs: jobs:
build: build:
runs-on: ubuntu-20.04 strategy:
fail-fast: false
matrix:
os: [ ubuntu-20.04 ]
go: [ 1.15.6 ]
goos: [ linux ]
goarch: [ amd64, arm64 ]
runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v2 - name: Code Checkout
- uses: actions/setup-go@v2 uses: actions/checkout@v2
- name: Setup Go v${{ matrix.go }}
uses: actions/setup-go@v2
with: with:
go-version: '^1.15' go-version: ${{ matrix.go }}
- name: Print Environment
id: env
run: |
printf "Go Executable Path: $(which go)\n"
printf "Go Version: $(go version)\n"
printf "\n\nGo Environment:\n\n"
go env
printf "\n\nSystem Environment:\n\n"
env
echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\//}"
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
echo "::set-output name=go_cache::$(go env GOCACHE)"
- name: Build Cache
uses: actions/cache@v2
with:
path: ${{ steps.env.outputs.go_cache }}
key: ${{ runner.os }}-${{ matrix.go }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go }}-go
- name: Get Dependencies
run: |
go get -v -t -d ./...
- name: Build - name: Build
run: GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -ldflags "-X github.com/pterodactyl/wings/system.Version=dev-${GIT_COMMIT:0:7}" -o build/wings_linux_amd64 -v wings.go env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: 0
SRC_PATH: github.com/pterodactyl/wings
run: |
go build -v -trimpath -ldflags="-s -w -X ${SRC_PATH}/system.Version=dev-${GIT_COMMIT:0:7}" -o build/wings_${{ matrix.goos }}_${{ matrix.goarch }} wings.go
upx build/wings_${{ matrix.goos }}_${{ matrix.goarch }}
chmod +x build/wings_${{ matrix.goos }}_${{ matrix.goarch }}
- name: Test - name: Test
run: go test ./... run: go test ./...
- name: Compress binary and make it executable - name: Upload Artifact
if: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'pull_request' }} uses: actions/upload-artifact@v2
run: upx build/wings_linux_amd64 && chmod +x build/wings_linux_amd64
- uses: actions/upload-artifact@v2
if: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'pull_request' }} if: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'pull_request' }}
with: with:
name: wings_linux_amd64 name: wings_${{ matrix.goos }}_${{ matrix.goarch }}
path: build/wings_linux_amd64 path: build/wings_${{ matrix.goos }}_${{ matrix.goarch }}

View File

@@ -1,35 +1,32 @@
name: "Code scanning - action" name: CodeQL
on: on:
push: push:
branches:
- 'develop'
pull_request: pull_request:
branches:
- 'develop'
schedule: schedule:
- cron: '0 21 * * 6' - cron: '0 9 * * 4'
jobs: jobs:
CodeQL-Build: analyze:
name: Analyze
runs-on: ubuntu-latest runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
language:
- go
steps: steps:
- name: Checkout repository - name: Code Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
with: - name: Checkout Head
# We must fetch at least the immediate parents so that if this is run: git checkout HEAD^2
# a pull request then we can checkout the head. if: ${{ github.event_name == 'pull_request' }}
fetch-depth: 2 - name: Initialize CodeQL
uses: github/codeql-action/init@v1
# If this run was triggered by a pull request event, then checkout with:
# the head of the pull request instead of the merge commit. languages: ${{ matrix.language }}
- run: git checkout HEAD^2 - name: Perform CodeQL Analysis
if: ${{ github.event_name == 'pull_request' }} uses: github/codeql-action/analyze@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
# Override language selection by uncommenting this and choosing your languages
with:
languages: go
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

68
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,68 @@
name: Publish Docker Image
on:
push:
branches:
- 'develop'
tags:
- 'v*'
jobs:
push:
name: Push
runs-on: ubuntu-20.04
# Always run against a tag, even if the commit into the tag has [docker skip] within the commit message.
if: "!contains(github.ref, 'develop') || (!contains(github.event.head_commit.message, 'skip docker') && !contains(github.event.head_commit.message, 'docker skip'))"
steps:
- name: Code Checkout
uses: actions/checkout@v2
- name: Docker Meta
id: docker_meta
uses: crazy-max/ghaction-docker-meta@v1
with:
images: ghcr.io/pterodactyl/wings
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Install buildx
uses: docker/setup-buildx-action@v1
with:
version: v0.5.1
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Get Build Information
id: build_info
run: |
echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\//}"
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
- name: Release Production Build
uses: docker/build-push-action@v2
if: "!contains(github.ref, 'develop')"
with:
build-args: |
VERSION=${{ steps.build_info.outputs.version_tag }}
labels: ${{ steps.docker_meta.outputs.labels }}
push: true
tags: ${{ steps.docker_meta.outputs.tags }}
- name: Release Development Build
uses: docker/build-push-action@v2
if: "contains(github.ref, 'develop')"
with:
build-args: |
VERSION=dev-${{ steps.build_info.outputs.short_sha }}
labels: ${{ steps.docker_meta.outputs.labels }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_meta.outputs.tags }}

View File

@@ -1,4 +1,4 @@
name: "Release" name: Create Release
on: on:
push: push:
@@ -8,22 +8,29 @@ on:
jobs: jobs:
release: release:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - name: Code Checkout
uses: actions/checkout@v2
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: '^1.15' go-version: '1.15.6'
- name: Build - name: Build
env: env:
REF: ${{ github.ref }} REF: ${{ github.ref }}
run: GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -ldflags "-X github.com/pterodactyl/wings/system.Version=${REF:11}" -o build/wings_linux_amd64 -v wings.go run: |
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${REF:11}" -o build/wings_linux_amd64 -v wings.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${REF:11}" -o build/wings_linux_arm64 -v wings.go
- name: Test - name: Test
run: go test ./... run: go test ./...
- name: Compress binary and make it executable - name: Compress binary and make it executable
run: upx --brute build/wings_linux_amd64 && chmod +x build/wings_linux_amd64 run: |
upx build/wings_linux_amd64 && chmod +x build/wings_linux_amd64
upx build/wings_linux_arm64 && chmod +x build/wings_linux_arm64
- name: Extract changelog - name: Extract changelog
env: env:
@@ -35,8 +42,9 @@ jobs:
- name: Create checksum and add to changelog - name: Create checksum and add to changelog
run: | run: |
SUM=`cd build && sha256sum wings_linux_amd64` SUM=`cd build && sha256sum wings_linux_amd64`
echo -e "\n#### SHA256 Checksum\n\n\`\`\`\n$SUM\n\`\`\`\n" >> ./RELEASE_CHANGELOG SUM2=`cd build && sha256sum wings_linux_arm64`
echo $SUM > checksum.txt echo -e "\n#### SHA256 Checksum\n\`\`\`\n$SUM\n$SUM2\n\`\`\`\n" >> ./RELEASE_CHANGELOG
echo -e "$SUM\n$SUM2" > checksums.txt
- name: Create release branch - name: Create release branch
env: env:
@@ -63,9 +71,8 @@ jobs:
body_path: ./RELEASE_CHANGELOG body_path: ./RELEASE_CHANGELOG
draft: true draft: true
prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }} prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
- name: Upload binary - name: Upload amd64 Binary
id: upload-release-binary
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -74,14 +81,23 @@ jobs:
asset_path: build/wings_linux_amd64 asset_path: build/wings_linux_amd64
asset_name: wings_linux_amd64 asset_name: wings_linux_amd64
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
- name: Upload arm64 Binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: build/wings_linux_arm64
asset_name: wings_linux_arm64
asset_content_type: application/octet-stream
- name: Upload checksum - name: Upload checksum
id: upload-release-checksum
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./checksum.txt asset_path: ./checksums.txt
asset_name: checksum.txt asset_name: checksums.txt
asset_content_type: text/plain asset_content_type: text/plain

3
.gitignore vendored
View File

@@ -45,5 +45,6 @@ test_*/
# Keep all gitkeep files (This needs to stay at the bottom) # Keep all gitkeep files (This needs to stay at the bottom)
!.gitkeep !.gitkeep
debug debug
data/.states.json
.DS_Store .DS_Store
*.pprof
*.pdf

View File

@@ -1,47 +0,0 @@
os: linux
dist: xenial
language: go
go:
- 1.13.x
go_import_path: "github.com/pterodactyl/wings"
services:
- docker
install:
- mkdir -p $GOPATH/bin
# Install used tools
- go get github.com/mitchellh/gox
- go get github.com/haya14busa/goverage
- go get github.com/schrej/godacov
- go mod download
script:
- make cross-build
- goverage -v -coverprofile=coverage.out ./...
- godacov -t $CODACY_TOKEN -r ./coverage.out -c $TRAVIS_COMMIT
deploy:
provider: releases
api_key:
secure: HQ8AvnSsOW2aDUKv25sU83SswK9rReGeFi68SotLGPdWyFBWJbp/JEHhw9swSqvhLPykx5QqLnRPG4nomOp2i5dVTXgM/7C3wQ2ULymkJDZqDJEAxjm1IuNsjXgcFqp0zcNXL3g0moaorHS2XZpzbgaewlCyYoEb+3SZUGzOCPIjSFvoIBaAYx6kRn+pyWo1I0mQChno2i7SGvAoZwh/hZIO6L5FZe5PcpBs/SxkZ+/shsGMk7CIyNMhG6CQTE1tlr+ZenluXjtliZfc4XwkHG/9MICNl8ihUrnN6YfdvJZXLQvolZQ0QJ5Eyb04jQd1yzKR1hcLx2S42IAWxaWTy5QxSN8QyG5wBRNg567ib5FEqY4M1nyQnWQbUbaiYloYBp14aR1L9DQw8+xmXnlgnTUPq1w+cOpQLeY/RENCalgHe7NoI3lClC2b7/c1j+O7RA68yYUFUod0y7ZXcCwsJkbRk7xgyDEAGs+rq8wLknj6f8y8cfNm179lRARwblnmo9uA43Tlee8DBSziSvJy/mYMzdIQeb+PHuznXjr4fze7x+zvronkiD/JH8MjJl3SWaE7DGtc5jz4+aRxU3rMbHwToEOY6u5pIsvz5PRFYWBvKX2+VoxmdR+m1qhAxsg0wtbA0CTnqgHNGMIFDWVTDQSy8LvJt+usUn1RtrYyyiI=
file_glob: true
file: build/*
on:
tags: true
branch: master
notifications:
email: false
webhooks:
urls:
- https://misc.schrej.net/travistodiscord/pterodev.php
on_success: change
on_failure: always
on_error: always
on_cancel: always
on_start: never

View File

@@ -1,5 +1,350 @@
# Changelog # Changelog
## v1.2.3
### Fixed
* **[Security]** Fixes a remaining security vulnerability in the code handling remote file downloads for servers relating to redirect validation.
### Added
* Adds a configuration key at `api.disable_remote_download` that can be set to `true` to completely download the remote download system.
## v1.2.2
### Fixed
* Reverts changes to logic handling blocking until a server process is done running when polling stats. This change exposed a bug in the underlying Docker system causing servers to enter a state in which Wings was unable to terminate the process and Docker commands would hang if executed against the container.
### Changed
* Adds logic to handle a console stream unexpectedly returning an EOF when reading console logs. New code should automatically re-attach the stream avoiding issues where the console would stop live updating for servers.
## v1.2.1
### Fixed
* Fixes servers not be properly marked as no longer transfering if an error occurs during the archive process.
* Fixes problems with user detection when running Wings inside a Docker container.
* Fixes filename decoding issues with multiple endpoints related to the file manager (namely move/copy/delete).
* **[Security]** Fixes vulnerability allowing a malicious user to abuse the remote file download utilitity to scan or access resources on the local network.
* Fixes network `tx` stats not correctly being reported (was previously reporting `rx` for both `rx` and `tx`).
### Changed
* Cleans up the logic related to polling resources for the server to make a little more sense and not do pointless `io.Copy()` operations.
## v1.2.0
### Fixed
* Fixes log compression being set on the Docker containers being created to avoid errors on some versions of Docker.
* Cleaned up logic handling server resource usage to avoid race conditions in the future and make the logic simpler.
* Fixes directories being created when writing a file before checking if there was space for the file to even be written to the disk.
* Significant performance and resource usage fixes to backups and server transfers to avoid obliterating machine `i/o` and causing excessive resource exhaustion on busy systems or low end machines.
* Fixes server install process to not unintentionally exit and cause invalid states if a line during the install process was too long.
* Fixes symlink error handling in backups to not unexpectedly tank a request. Any errors due to a symlink are now ignored and will not impact the generation of a backup (including for server transfers).
### Changed
* Changed `--debug` flag to no longer ignore certificate errors on requests. Use `--ignore-certificate-errors` to ignore any certificate errors encountered when in development environments.
* Changed all Filesystem related errors to be of the same internal error type making error checking significantly easier and less error prone.
* Improves log output stacktraces to be more accurate as to the source of the issue.
### Added
* Adds support for downloading files to a server's data directory and optionally checking the status of or canceling in-progress downloads.
* Adds a `context.Context` to `server.Server` structs allowing for cancelation of long running background tasks when a server is deleted without additional complexity on developer's end.
## v1.1.3
### Fixed
* Fixes `archive/tar: write too long` error when creating a server backup.
* Fixes server installation docker images not using authentication properly during the pull.
* Fixes temporary transfer files not being removed after the transfer is completed.
* Fixes TLS certificate checking to be all lowercase to avoid any lookup issues when an all-caps domain is provided.
* Fixes multiple interfaces with the same port not being publishable for a server.
* Fixes errors encountered during websocket processes being incorrectly passed back to the Panel as a JWT error rather than a generic Wings error for admin users.
### Added
* Added logic to notify the Panel when archive generation fails.
* Added endpoint to run `chmod` commands against server files and updated API response to include the mode bits when requesting files.
### Changed
* Updated internals to call `Server.Environment.State()` rather than deprecated `Server.GetState()` functions.
* Improved error handling logic and massively simplified error passing around the codebase.
## v1.1.2
### Fixed
* Fixes binaries built as part of the release process not being usable in MUSL based environments (such as our Docker images).
* Fixes server states being incorrectly set back to offline when a server is started after a system restart.
### Changed
* Improved logic for cleaning `allowed_mount` paths for consistency.
* Certain context cancelation deadline errors are no longer wrong reported at an error level (since they're expected).
* Very minor micro-optimizations for some string handling with server console output.
### Added
* Added a hidden option to disable all disk checking for servers by setting the `disk_check_interval` to `0` in the config file.
## v1.1.1
### Fixed
* Fixes certain files returning invalid data in the request due to a bad header set after sending data down the line.
## v1.1.0
This release **requires** `Panel@1.1.0` or later to run due to API changes.
### Added
* Adds support for denying client JWT access to specific token keys generated before Wings starts, or before an arbitrary date from an API call.
* Adds support for a configurable number of log messages to be returned when connecting to a server socket and requesting the logs.
* Adds support for both CPU and Memory profiling of Wings via a CLI argument.
### Fixed
* Errors encountered while uploading files to Wings are now properly reported back to the client rather than causing a generic 500 error.
* Servers exceeding their disk limit are now properly stopped when they exceed limits while running.
* Fixes server environment starting as an empty value rather than an "offline" value.
### Changed
* Cleaned up code internals for handling API requests to make it easier on new developers and use a more sane system.
* Server configuration retrieval from the Panel is now done in a paginated loop rather than a single large call to allow systems with thousands of instances to boot properly.
* Switches to multipart S3 uploads to handle backups larger than 5GB in size.
* Switches the error handling package from `pkg/errors` to `emperror` to avoid overwriting existing stack traces associated with an error and provide additional functionality.
## v1.0.1
### Added
* Adds support for ARM to build outputs for wings.
### Fixed
* Fixed a few docker clients not having version negotiation enabled.
* Fixes local images prefixed with `~` getting pulled from remote sources rather than just using the local copy.
* Fixes console output breaking with certain games when excessive line length was output.
* Fixes an error when console lines were too long that would cause the console to stop updating until the server was restarted,
### Changed
* Simplified timezone logic for containers by properly grabbing the system timezone and then passing that through to containers with the `TZ=` environment variable.
## v1.0.0
This is the first official stable release of Wings! Please be aware that while this specific version changelog is very short,
it technically includes all of the previous beta and alpha releases within it. For the sake of version history and following
along though, I've only included the differences between this version and the previous RC build.
### Fixed
* Fixes file parser not properly appending newline character to modified lines.
* Fixes server disk usage not being properly reported back to the API and websocket.
### Changed
* Changes diagnostics endpoint URL to be `ptero.co` rather than `hastebin.com`.
* Diagnostics report now includes system time for easier debugging of logs and container issues.
## v1.0.0-rc.7
### Fixed
* **[Security]** Prevents the `allowed_mounts` configuration value from being set by a remote API call.
* Fixes an unexpected error when attempting to make a copy of an archive file.
* Fixes certain expected filesystems errors being returned to the API as a 500 error rather than a 4XX series error.
* Fixes a panic crash when there is no text on a line and the parser is attempting to determine if the line is a comment.
* Fixes multiple filesystem operations to correctly check, increment, and decrement disk usage for a server.
* Fixes a negative disk space usage issue when deleting a file due to a bad int swap.
* Errors from a websocket connection being closed will no longer spam the console.
* Fixes an extraneous `.` in the install script for servers causing errors in some scenarios.
* Fixes unexpected error spam due to a change in how `os.ErrNotExist` is returned from some functions.
### Changed
* Stacktrace is no longer emitted when warning that an image exists locally.
* Configuration parser now attempts to create the directory structure leading to a configuration file if it is missing.
* If a file name is too long for the system a nice error is returned to the caller.
* Enables client version negotiation for Docker to support more versions.
* Disk space errors are no longer logged into the wings logs.
* Servers can no longer be reinstalled while another power action is currently running. This avoids data collisions and wings running into issues with the container state.
* Wings now uses `1024` rather than `1000` bytes when calculating the disk space usage for a server to match how the Panel reports usage.
* JWT errors in the websocket are now sent back to the connection as a specific event type allowing them to be handled even if the timer fails to execute or executes but is not being listened for.
* A server struct is no longer embedded in the virtual filesystem allowing for easier testing and modularization of the codebase.
* Server websockets are now closed when a server is deleted, disconnecting any currently connected clients.
### Removed
* **[Security]** Removes the `SafeJoin` function which could wrongly assume the state of a file and allow a user to escape the root if the developer implementing the call calls `Stat` rather than `Lstat`.
## v1.0.0-rc.6
### Fixed
* Fixes race condition when checking if the running state of a server has changed.
* Fixes files mistakenly unpacking themselves to the root directory rather than the directory they are told to unpack to.
* Fixes console output not being sent to the websocket in the same order that it was received.
* Fixes a file busy error causing a 500 error response when unpacking an archive rather than a 400 error with a message indicating what is wrong to the user.
* Fixes docker image not properly updating when a server is started.
### Changed
* Replaces fragile event bus logic with a more robust and easier to understand system. This fixes all remaining console and stats output issues that have been reported.
* Cleans up API response error messaging to avoid empty errors being logged which cannot be acted upon.
* Adds support for retrying a file write a few times with a back-off if the file is busy when the write occurs.
### Added
* Docker image pull data is shown in the console when an admin is connected to the websocket.
* Adds console throttling to stop a server if too much data is being piped out of the console. This logic mimics the logic present in the old Nodejs daemon, but with a 2x line count limit (1000 -> 2000) per period.
## v1.0.0-rc.5
### Fixed
* Fixes long standing bug with console output not properly sending back to the client with server stats in certain edge case scenarios. This was "fixed" in `rc.4` but ended up breaking many servers in significantly more painful manners, but did allow us to better track down the issue.
* Fixes build flags during release process to correctly strip unnessary code allowing the final binary size to be reduced back down to `~5MB`
* Fixes Wings returning the last `16384` lines of the log file when connecting to the websocket.
* Fixes pre-boot actions always running for a server start event even if the server is already running.
### Added
* Adds support for configuring the amount of time that can elapse between server disk size checking before the value is considered stale.
## v1.0.0-rc.4
### Fixed
* Fixes server files being inaccessible if the root data path is a symlink to another location on the machine.
* Fixes some console output being written to the logs accidentally truncating other lines due to special ANSI sequences being output.
* Fixes `server.properties` files getting mangled by the automatic configuration editor when booting a server.
* Fixes a missed stream close when stopping resource polling that would lead to memory leaks.
* Fixes port bindings being incorrectly re-assigned when using `127.0.0.1` with Docker. These are now properly re-mapped to the `pterodactyl0` interface so that networking operates as expected for the server.
* Fixes handling of values within arrays in `yaml` and `json` configuration files.
* Fixes a 304 error being returned rather than a 400 error when a file upload is invalid.
* Fixes deadlocks and response delays when processing server console and stat events. This was causing server consoles to become completely unresponsive after random periods of time and was only resolvable via a Wings restart.
### Changed
* Allows a stale value to be present when starting a server if the server is allowed an unlimited amount of disk space.
* Removed all remaining traces of the `zap` logging library from the codebase.
* Servers no longer auto-restart as if they crashed when a stop command is manually sent via the server console to the instance.
* Changes the CORS checks to allow `*` as a remote origin.
### Added
* Added an auto-generated logrotate file which is written to the normal logrotate directory when wings is first started.
* Added additional debug logging within the internals of the resource polling for servers to better trace unexpected behavior.
* Adds additional logic check to avoid even trying to stop a suspended server if it is already stopped.
## v1.0.0-rc.3
### Fixed
* Errors during the backup process are now correctly reported to the Panel and logged into the output correctly.
* Empty directories are no longer pushed into the backup file list (which was causing errors to occur previously).
* Covered an edge case to prevent errors if a file gets deleted while a backup is in progress.
* Fixed a bug causing Wings to panic and crash if an invalid environment variable value was passed through. These invalid values are now logged to the output to better detect and an empty string is returned in their place.
* Fixed startup variables and other server information not properly being updated when a server is restarted.
* Suspension state of a server is now properly returned by the API.
* Fixes an error being thrown if a backup does not exist on the local machine. A 404 is now properly returned and handled by the Panel when this occurs.
* Fixes an error when attempting to move or rename a folder due to the target being created accidentally before the rename occurred.
* Fixes install scripts running even when the checkbox to not run them on installation was selected in the Panel.
### Changed
* Disk space checking modified to not block in as many parts of the codebase and allow returning a stale cache value where that is appropriate. Thanks @cyberkitsune!
* SFTP package code is now merged into the codebase to make keeping up with all of the code simpler, and reduce the complexity of the SFTP server.
### Added
* Added the ability for a node to be configured to skip file permissions checking when starting a server.
* Added console output message to indicate that server disk space checking is occurring rather than silently sitting there.
## v1.0.0-rc.2
### Fixed
* Fixes significant performance degradation due to excessive `syscall` actions when determining directory sizes on large servers. This was previously causing CPU & I/O lockups on servers and should be significantly more performant and less impactful on the system now.
* Fixes panic crash when booting wings with no log directory created.
### Changed
* Changed default interval for disk space calculation from every 60 seconds to every 2.5 minutes.
## v1.0.0-rc.1
### Fixed
* Servers are no longer incorrectly marked as stopping when they are, in fact, offline.
* Release build version is now correctly output when starting wings.
* Termination signals can now always be sent to a server instance even if the instance is currently starting/stopping.
* Removed the file chown on wings boot to avoid slowing down the boot process unnecessarily when working with hundreds of servers on a node.
* Fixed a multitude of race conditions throughout the code that cropped up during testing and made power handling even more robust for server instances.
* In general multiple fragile areas of the codebase have been improved and are more likely to return useful errors should they break.
* Addressed serious CPU usage issues when generating backups, and additionally decreased the amount of time they take to generate.
### Changed
* Internal server now uses more secure and recommended TLS settings.
* Environment handling is now completely separate from the server package itself, allowing the environments to no longer be tightly coupled to the server.
* `/tmp` directory mounted into containers can now be programmatically managed and uses better defaults to avoid people even needing to edit it.
### Added
* Wings logs are now properly persisted to the disk.
* Adds the ability for an egg to use ANSI-stripped matching when determining if a server is done booting.
## v1.0.0-beta.9
### Fixed
* Fixes server resource usage stats not being returned correctly by the API endpoint.
* Fixes an exception thrown when attempting to write server installation logs.
* Fixes error handling to provide a more accurate stack-trace in more scenarios where one is missing initially.
* Fixes a memory leak and zombie event listeners when disconnecting from a server's websocket.
* Fixes a race condition when wings is attempting to register/de-register event subscribers.
* Server data directories now correctly have their permissions set recursively when booting Wings.
* Fixes a race condition when a server's console stream was not fully closed before the next power action was started.
### Changed
* Server power handling is now handled in a synchronous manner. This avoids endless bugs and race conditions that would crop up if someone triggered two restart processes back to back. The new logic prevents performing any additional power actions until the currently executing action is completed.
* Server disk usage is now correctly calculated when restarting the daemon as long as the server data directory exists.
* Multiple code pathways within the boot process were cleaned up and modified to be less IO intensive and overall easier to reason about as a developer.
* Additional timezone data is now mounted into containers to better improve the ability for instances running to be using the correct timezone.
### Added
* Adds basic internal file upload endpoints (these are currently not exposed anywhere in the Panel).
* Added additional process events for installation start and completion.
* Additional CORS allowed origins for the websocket can now be defined in the configuration file.
* Adds the ability to authenticate with a docker registry when pulling images.
## v1.0.0-beta.8
### Fixed
* Server state is synced with the Panel before performing a reinstall to ensure that the latest information is used.
* Wings no longer crashes when a non-string environment variable value is passed through.
* SFTP server authentication no longer attempts to contact the Panel to validate credentials if the format is known to already be incorrect.
* Some previously missing error stacks are now properly returned when encountered.
* Renaming a file no longer triggers an error if the base path does not exist.
* Disk space is now properly calculated for a server even if it is assigned unlimited space.
* **[Security]** Prevent symlink files from unintentionally chmodding their target file when a server is booted when the target file resides outside the server data directory.
* **[Security]** Cleans up multiple code pathways that may unintentionally allow a malicious user to impact files outside their home directory.
* Fixes numerous race conditions during a server's boot process that may lead to unintended data states.
* Fixes an error thrown when attempting to delete a file that points to a symlink outside the server data directory.
* Deleting a symlink will no longer attempt to remove the source file, only the symlink itself.
* Websocket no longer blocks when handling a long running process for a user.
### Changed
* Install log for servers is now more detailed and more useful for debugging what might be going wrong.
* Certain file management API endpoints now support passing through multiple paths at once to make mass actions easier to perform.
* Re-worked file walking implementation to not cause a server crash when working with very large directories, and avoid race conditions when recursively walking directories.
* Server configuration structure re-worked to be more manageable in the code base and avoid additional race conditions and complexity while making on-the-fly changes to it.
### Added
* Support for configuring additional file mount points in a container via the Panel.
* Support for automated SSL certificate generation when booting the Daemon.
* Added wings diagnostics command.
* New API endpoints to compress and decompress files on a server.
## v1.0.0-beta.7
### Fixed
* Stacktraces are now displayed once in the error output, rather than twice when certain errors are encountered.
* Additional errors that did not previously output a stack trace have been fixed to output.
* Fixes a bug where server space available would be triggered when creating a new server from a remote configuration before that file location existed on the disk, leading to an error.
* Fixes context timeouts while pulling server docker images. Time was upped from 10 seconds to 15 minutes.
* Configuration file replacement values are now properly de-escaped when writing to the disk. `\/no\/more\/slashes`
* `.properties` files are now saved correctly to the disk with newlines, rather than shoved on a single line.
* `./wings configure` command can now properly save the configuration to the disk.
* Custom SSL locations are no longer obliterated when making changes to the Node's configuration via the Panel.
### Changed
* Exclusive lock is now acquired when performing a server installation to avoid two install processes being triggered at the same time. This also allows an install to be properly canceled if the server is deleted before it is completed.
## v1.0.0-beta.6
### Fixed
* Server status is no longer sent to all connected websocket clients when a new client connects to the socket.
* Server disk usage is sent back over the socket when connecting.
* Default configuration for the SFTP server is now properly returned to `on` rather than `off`
* Server boot is no longer blocked if there is an error fetching the Docker image as long as that image exists on the host.
* The websocket no longer gets locked up when Wings attempts to send an error to the client.
* Fixed a crash loop when an error is thrown during pre-boot phase of server startup.
* Errors with `BindJSON` in API endpoints are now properly handled and returned.
* Fixed warning about Gin running in non-release mode, even when the binary is running in release mode.
### Changed
* Switched logging libraries to output data in a clearer format more suited to the CLI where this application runs.
* Cleaner debug messaging in debug mode from the router.
## v1.0.0-beta.5
### Fixed
* Default config location settled on `/etc/pterodactyl/config.yml`; wings will now check all of the previous locations for the configuration and move it automatically to the new location.
* Deleting a server no longer fails the process if the container cannot be found.
* Fixes permissions checking for subusers connecting to the SFTP instance.
* S3 backups now properly send back hash data to the panel.
* Server installation containers are now always deleted, even if the installer process fails for the instance.
* Files and folders with special characters and spaces in them no longer return a 404 error.
* Servers using eggs with bad configurations will no longer cause the daemon to fail booting; these bad configurations are simply skipped over and a warning is emitted into the logs.
* Environment variables passed to containers no longer contain improper quotes around them.
* Matching on array indexes in configurations now works correctly; `foo[0]` is quietly transformed into the proper `foo.0` syntax.
### Added
* New banner error message when the daemon is unable to locate the configuration file. This should better clarify what the user needs to do in order to resolve the issue.
* Adds ability to configure the default networking driver used by docker.
## v1.0.0-beta.4
### Fixed
* Fixes unexpected nil-pointer panic when attempting to start some newly created servers, or any server that was missing a container on the system.
* Fixes memory usage of process being reported differently than the `docker stats` output leading to some confusion. These numbers should be more correct now.
* Fixes possible nil-pointer panic when detecting a deleted container as being crashed.
## v1.0.0-beta.3 ## v1.0.0-beta.3
### Fixed ### Fixed
* Daemon will no longer crash if someone requests a websocket for a deleted server. * Daemon will no longer crash if someone requests a websocket for a deleted server.
@@ -11,6 +356,39 @@
### Changed ### Changed
* Memory overhead for containers is now 5/10/15% higher than the passed limit to account for JVM heap and prevent crashing. * Memory overhead for containers is now 5/10/15% higher than the passed limit to account for JVM heap and prevent crashing.
## v1.0.0-beta.2
### Changed
* Backup functionality made significantly more modular to ease adding additional methods in the future.
* Websocket permissions changed to use same name as in panel.
* Container memory hard-limits are now adjusted up by 15% (< 2G memory), 10% (< 4G memory), or 5% to avoid unexpected OOM crashes for memory heavy games.
* Wings executable is now 80% smaller thanks to better compilation arguments.
### Added
* Adds support for ignoring files and directories when generating a new backup.
* Added internal directory walker with callback-continue support.
### Fixed
* Fixed linux distro matching when booting the daemon.
* Fixes DNS to be configurable for docker containers that are made for servers.
* Fixes incorrect file truncation when making modifications to a server's configuration files.
## v1.0.0-beta.1
### Added
* Added support for passing specific threads to the docker environment when running a server.
* Added support for reinstalling an existing server.
* Added support for restarting a server.
* Adds support for transferring servers between daemon instances.
* Added auto-deploy command to fetch configuration from Panel automatically.
### Changed
* Server file and backup downloads now hit a direct endpoint with a one-time JWT to avoid proxying large downloads through the panel.
### Fixed
* Fixes a go routine causing a memory and CPU leak.
* Fixed improper chown of server directories on boot.
* Miscellaneous fixes to CPU usage, abandoned go-routines, and race conditions.
* Fixes support for Alpine based systems.
## v1.0.0-alpha.2 ## v1.0.0-alpha.2
### Added ### Added
* Ability to run an installation process for a server and notify the panel when completed. * Ability to run an installation process for a server and notify the panel when completed.
@@ -20,4 +398,4 @@
* Fixed bugs caused when unmarshaling data into a struct with default values. * Fixed bugs caused when unmarshaling data into a struct with default values.
* Timezone is properly set in containers by using the `TZ=` environment variable rather than mounting timezone data into the filesystem. * Timezone is properly set in containers by using the `TZ=` environment variable rather than mounting timezone data into the filesystem.
* Server data directly is now properly created when running the install process. * Server data directly is now properly created when running the install process.
* Time drift caused in the socket JWT is now handled in a better manner and less likely to unexpectedly error out. * Time drift caused in the socket JWT is now handled in a better manner and less likely to unexpectedly error out.

View File

@@ -1,14 +1,33 @@
# ---------------------------------- # Stage 1 (Build)
# Pterodactyl Panel Dockerfile FROM golang:1.15-alpine3.12 AS builder
# ----------------------------------
FROM golang:1.14-alpine ARG VERSION
COPY . /go/wings/
WORKDIR /go/wings/
RUN apk add --no-cache upx \
&& go build -ldflags="-s -w" \
&& upx --brute wings
FROM alpine:latest RUN apk add --update --no-cache git=2.26.2-r0 make=4.3-r0 upx=3.96-r0
COPY --from=0 /go/wings/wings /usr/bin/
CMD ["wings","--config", "/var/lib/pterodactyl/config.yml"] WORKDIR /app/
COPY go.mod go.sum /app/
RUN go mod download
COPY . /app/
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=$VERSION" \
-v \
-trimpath \
-o wings \
wings.go
RUN upx wings
# --------------------------------------- #
# Stage 2 (Final)
FROM busybox:1.33.0
RUN echo "ID=\"busybox\"" > /etc/os-release
COPY --from=builder /app/wings /usr/bin/
CMD [ "wings", "--config", "/etc/pterodactyl/config.yml" ]

View File

@@ -1,5 +1,10 @@
build: build:
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -gcflags "all=-trimpath=$(PWD)" -o build/wings_linux_amd64 -v wings.go GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -gcflags "all=-trimpath=$(pwd)" -o build/wings_linux_amd64 -v wings.go
GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -gcflags "all=-trimpath=$(pwd)" -o build/wings_linux_arm64 -v wings.go
debug:
go build -race
./wings --debug --ignore-certificate-errors --config config.yml
compress: compress:
upx --brute build/wings_* upx --brute build/wings_*

View File

@@ -1,16 +1,40 @@
# Alpha Project [![Logo Image](https://cdn.pterodactyl.io/logos/new/pterodactyl_logo.png)](https://pterodactyl.io)
Please refrain from opening PRs or Issues at this time. This project is still under heavy development, and until we have a solid foundation and plan for how everything will connect, we will not be accepting PRs or feature suggestions.
# Pterodactyl wings [![travis](https://img.shields.io/travis/pterodactyl/wings.svg?style=flat-square)](https://travis-ci.org/pterodactyl/wings) [![codacy quality](https://img.shields.io/codacy/grade/27a1576bda86450f853b1052b12fa570.svg?style=flat-square)](https://www.codacy.com/app/schrej/wings/dashboard) [![codacy coverage](https://img.shields.io/codacy/coverage/27a1576bda86450f853b1052b12fa570.svg?style=flat-square)](https://www.codacy.com/app/schrej/wings/files) ![Discord](https://img.shields.io/discord/122900397965705216?label=Discord&logo=Discord&logoColor=white)
![GitHub Releases](https://img.shields.io/github/downloads/pterodactyl/wings/latest/total)
[![Go Report Card](https://goreportcard.com/badge/github.com/pterodactyl/wings)](https://goreportcard.com/report/github.com/pterodactyl/wings)
``` # Pterodactyl Wings
____ Wings is Pterodactyl's server control plane, built for the rapidly changing gaming industry and designed to be
__ Pterodactyl _____/___/_______ _______ ______ highly performant and secure. Wings provides an HTTP API allowing you to interface directly with running server
\_____\ \/\/ / / / __ / ___/ instances, fetch server logs, generate backups, and control all aspects of the server lifecycle.
\___\ / / / / /_/ /___ /
\___/\___/___/___/___/___ /______/
/_______/ alpha
```
A new generation of the Pterodactyl daemon, written in go. In addition, Wings ships with a built-in SFTP server allowing your system to remain free of Pterodactyl specific
dependencies, and allowing users to authenticate with the same credentials they would normally use to access the Panel.
## Sponsors
I would like to extend my sincere thanks to the following sponsors for helping find Pterodactyl's developement.
[Interested in becoming a sponsor?](https://github.com/sponsors/DaneEveritt)
| Company | About |
| ------- | ----- |
| [**WISP**](https://wisp.gg) | Extra features. |
| [**Bloom.host**](https://bloom.host) | Bloom.host offers dedicated core VPS and Minecraft hosting with Ryzen 9 processors. With owned-hardware, we offer truly unbeatable prices on high-performance hosting. |
| [**MineStrator**](https://minestrator.com/) | Looking for a French highend hosting company for you minecraft server? More than 14,000 members on our discord, trust us. |
| [**DedicatedMC**](https://dedicatedmc.io/) | DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance and giving you the best performance money can buy. |
| [**Skynode**](https://www.skynode.pro/) | Skynode provides blazing fast game servers along with a top-notch user experience. Whatever our clients are looking for, we're able to provide it! |
| [**XCORE**](https://xcore-server.de/) | XCORE offers High-End Servers for hosting and gaming since 2012. Fast, excellent and well-known for eSports Gaming. |
| [**RoyaleHosting**](https://royalehosting.net/) | Build your dreams and deploy them with RoyaleHostings reliable servers and network. Easy to use, provisioned in a couple of minutes. |
| [**Spill Hosting**](https://spillhosting.no/) | Spill Hosting is a Norwegian hosting service, which aims to cheap services on quality servers. Premium i9-9900K processors will run your game like a dream. |
| [**DeinServerHost**](https://deinserverhost.de/) | DeinServerHost offers Dedicated, vps and Gameservers for many popular Games like Minecraft and Rust in Germany since 2013. |
## Documentation
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
* [Wings Documentation](https://pterodactyl.io/wings/1.0/installing.html)
* [Community Guides](https://pterodactyl.io/community/about.html)
* Or, get additional help [via Discord](https://discord.gg/pterodactyl)
## Reporting Issues
Please use the [pterodactyl/panel](https://github.com/pterodactyl/panel) repository to report any issues or make
feature requests for Wings. In addition, the [security policy](https://github.com/pterodactyl/panel/security/policy) listed
within that repository also applies to Wings.

View File

@@ -4,40 +4,60 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/apex/log" "io"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/system"
) )
// Initializes the requester instance. // Initializes the requester instance.
func NewRequester() *PanelRequest { func New() *Request {
return &PanelRequest{ return &Request{}
Response: nil,
}
} }
type PanelRequest struct { // A generic type allowing for easy binding use when making requests to API endpoints
Response *http.Response // that only expect a singular argument or something that would not benefit from being
// a typed struct.
//
// Inspired by gin.H, same concept.
type D map[string]interface{}
// Same concept as D, but a map of strings, used for querying GET requests.
type Q map[string]string
// A custom API requester struct for Wings.
type Request struct{}
// A custom response type that allows for commonly used error handling and response
// parsing from the Panel API. This just embeds the normal HTTP response from Go and
// we attach a few helper functions to it.
type Response struct {
*http.Response
}
// A pagination struct matching the expected pagination response from the Panel API.
type Pagination struct {
CurrentPage uint `json:"current_page"`
From uint `json:"from"`
LastPage uint `json:"last_page"`
PerPage uint `json:"per_page"`
To uint `json:"to"`
Total uint `json:"total"`
} }
// Builds the base request instance that can be used with the HTTP client. // Builds the base request instance that can be used with the HTTP client.
func (r *PanelRequest) GetClient() *http.Client { func (r *Request) Client() *http.Client {
return &http.Client{Timeout: time.Second * 30} return &http.Client{Timeout: time.Second * time.Duration(config.Get().RemoteQuery.Timeout)}
} }
func (r *PanelRequest) SetHeaders(req *http.Request) *http.Request { // Returns the given endpoint formatted as a URL to the Panel API.
req.Header.Set("Accept", "application/vnd.pterodactyl.v1+json") func (r *Request) Endpoint(endpoint string) string {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s.%s", config.Get().AuthenticationTokenId, config.Get().AuthenticationToken))
return req
}
func (r *PanelRequest) GetEndpoint(endpoint string) string {
return fmt.Sprintf( return fmt.Sprintf(
"%s/api/remote/%s", "%s/api/remote/%s",
strings.TrimSuffix(config.Get().PanelLocation, "/"), strings.TrimSuffix(config.Get().PanelLocation, "/"),
@@ -45,9 +65,35 @@ func (r *PanelRequest) GetEndpoint(endpoint string) string {
) )
} }
// Makes a HTTP request to the given endpoint, attaching the necessary request headers from
// Wings to ensure that the request is properly handled by the Panel.
func (r *Request) Make(method, url string, body io.Reader, opts ...func(r *http.Request)) (*Response, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", fmt.Sprintf("Pterodactyl Wings/v%s (id:%s)", system.Version, config.Get().AuthenticationTokenId))
req.Header.Set("Accept", "application/vnd.pterodactyl.v1+json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s.%s", config.Get().AuthenticationTokenId, config.Get().AuthenticationToken))
// Make any options calls that will allow us to make modifications to the request
// before it is sent off.
for _, cb := range opts {
cb(req)
}
r.debug(req)
res, err := r.Client().Do(req)
return &Response{Response: res}, err
}
// Logs the request into the debug log with all of the important request bits. // Logs the request into the debug log with all of the important request bits.
// The authorization key will be cleaned up before being output. // The authorization key will be cleaned up before being output.
func (r *PanelRequest) logDebug(req *http.Request) { func (r *Request) debug(req *http.Request) {
headers := make(map[string][]string) headers := make(map[string][]string)
for k, v := range req.Header { for k, v := range req.Header {
if k != "Authorization" || len(v) == 0 { if k != "Authorization" || len(v) == 0 {
@@ -59,55 +105,50 @@ func (r *PanelRequest) logDebug(req *http.Request) {
} }
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"method": req.Method, "method": req.Method,
"endpoint": req.URL.String(), "endpoint": req.URL.String(),
"headers": headers, "headers": headers,
}).Debug("making request to external HTTP endpoint") }).Debug("making request to external HTTP endpoint")
} }
func (r *PanelRequest) Get(url string) (*http.Response, error) { // Makes a GET request to the given Panel API endpoint. If any data is passed as the
c := r.GetClient() // second argument it will be passed through on the request as URL parameters.
func (r *Request) Get(url string, data Q) (*Response, error) {
return r.Make(http.MethodGet, r.Endpoint(url), nil, func(r *http.Request) {
q := r.URL.Query()
for k, v := range data {
q.Set(k, v)
}
req, err := http.NewRequest(http.MethodGet, r.GetEndpoint(url), nil) r.URL.RawQuery = q.Encode()
req = r.SetHeaders(req) })
if err != nil {
return nil, err
}
r.logDebug(req)
return c.Do(req)
} }
func (r *PanelRequest) Post(url string, data []byte) (*http.Response, error) { // Makes a POST request to the given Panel API endpoint.
c := r.GetClient() func (r *Request) Post(url string, data interface{}) (*Response, error) {
b, err := json.Marshal(data)
req, err := http.NewRequest(http.MethodPost, r.GetEndpoint(url), bytes.NewBuffer(data))
req = r.SetHeaders(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
r.logDebug(req) return r.Make(http.MethodPost, r.Endpoint(url), bytes.NewBuffer(b))
return c.Do(req)
} }
// Determines if the API call encountered an error. If no request has been made // Determines if the API call encountered an error. If no request has been made
// the response will be false. // the response will be false. This function will evaluate to true if the response
func (r *PanelRequest) HasError() bool { // code is anything 300 or higher.
func (r *Response) HasError() bool {
if r.Response == nil { if r.Response == nil {
return false return false
} }
return r.Response.StatusCode >= 300 || r.Response.StatusCode < 200 return r.StatusCode >= 300 || r.StatusCode < 200
} }
// Reads the body from the response and returns it, then replaces it on the response // Reads the body from the response and returns it, then replaces it on the response
// so that it can be read again later. // so that it can be read again later. This does not close the response body, so any
func (r *PanelRequest) ReadBody() ([]byte, error) { // functions calling this should be sure to manually defer a Body.Close() call.
func (r *Response) Read() ([]byte, error) {
var b []byte var b []byte
if r.Response == nil { if r.Response == nil {
return nil, errors.New("no response exists on interface") return nil, errors.New("no response exists on interface")
@@ -122,52 +163,35 @@ func (r *PanelRequest) ReadBody() ([]byte, error) {
return b, nil return b, nil
} }
func (r *PanelRequest) HttpResponseCode() int { // Binds a given interface with the data returned in the response. This is a shortcut
if r.Response == nil { // for calling Read and then manually calling json.Unmarshal on the raw bytes.
return 0 func (r *Response) Bind(v interface{}) error {
b, err := r.Read()
if err != nil {
return err
} }
return r.Response.StatusCode return json.Unmarshal(b, &v)
}
func IsRequestError(err error) bool {
_, ok := err.(*RequestError)
return ok
}
type RequestError struct {
Code string `json:"code"`
Status string `json:"status"`
Detail string `json:"detail"`
}
// Returns the error response in a string form that can be more easily consumed.
func (re *RequestError) Error() string {
return fmt.Sprintf("%s: %s (HTTP/%s)", re.Code, re.Detail, re.Status)
}
func (re *RequestError) String() string {
return re.Error()
}
type RequestErrorBag struct {
Errors []RequestError `json:"errors"`
} }
// Returns the error message from the API call as a string. The error message will be formatted // Returns the error message from the API call as a string. The error message will be formatted
// similar to the below example: // similar to the below example:
// //
// HttpNotFoundException: The requested resource does not exist. (HTTP/404) // HttpNotFoundException: The requested resource does not exist. (HTTP/404)
func (r *PanelRequest) Error() *RequestError { func (r *Response) Error() error {
body, _ := r.ReadBody() if !r.HasError() {
return nil
bag := RequestErrorBag{}
json.Unmarshal(body, &bag)
if len(bag.Errors) == 0 {
return new(RequestError)
} }
return &bag.Errors[0] var bag RequestErrorBag
_ = r.Bind(&bag)
e := &RequestError{}
if len(bag.Errors) > 0 {
e = &bag.Errors[0]
}
e.response = r.Response
return e
} }

View File

@@ -1,11 +1,34 @@
package api package api
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/pkg/errors" "strconv"
) )
type BackupRemoteUploadResponse struct {
Parts []string `json:"parts"`
PartSize int64 `json:"part_size"`
}
func (r *Request) GetBackupRemoteUploadURLs(backup string, size int64) (*BackupRemoteUploadResponse, error) {
resp, err := r.Get(fmt.Sprintf("/backups/%s", backup), Q{"size": strconv.FormatInt(size, 10)})
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.HasError() {
return nil, resp.Error()
}
var res BackupRemoteUploadResponse
if err := resp.Bind(&res); err != nil {
return nil, err
}
return &res, nil
}
type BackupRequest struct { type BackupRequest struct {
Checksum string `json:"checksum"` Checksum string `json:"checksum"`
ChecksumType string `json:"checksum_type"` ChecksumType string `json:"checksum_type"`
@@ -15,22 +38,12 @@ type BackupRequest struct {
// Notifies the panel that a specific backup has been completed and is now // Notifies the panel that a specific backup has been completed and is now
// available for a user to view and download. // available for a user to view and download.
func (r *PanelRequest) SendBackupStatus(backup string, data BackupRequest) (*RequestError, error) { func (r *Request) SendBackupStatus(backup string, data BackupRequest) error {
b, err := json.Marshal(data) resp, err := r.Post(fmt.Sprintf("/backups/%s", backup), data)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return err
}
resp, err := r.Post(fmt.Sprintf("/backups/%s", backup), b)
if err != nil {
return nil, errors.WithStack(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
r.Response = resp return resp.Error()
if r.HasError() {
return r.Error(), nil
}
return nil, nil
} }

33
api/error.go Normal file
View File

@@ -0,0 +1,33 @@
package api
import (
"fmt"
"net/http"
)
type RequestErrorBag struct {
Errors []RequestError `json:"errors"`
}
type RequestError struct {
response *http.Response
Code string `json:"code"`
Status string `json:"status"`
Detail string `json:"detail"`
}
func IsRequestError(err error) bool {
_, ok := err.(*RequestError)
return ok
}
// Returns the error response in a string form that can be more easily consumed.
func (re *RequestError) Error() string {
c := 0
if re.response != nil {
c = re.response.StatusCode
}
return fmt.Sprintf("Error response from Panel: %s: %s (HTTP/%d)", re.Code, re.Detail, c)
}

View File

@@ -2,10 +2,11 @@ package api
import ( import (
"encoding/json" "encoding/json"
"github.com/apex/log"
"github.com/pterodactyl/wings/parser"
"regexp" "regexp"
"strings" "strings"
"github.com/apex/log"
"github.com/pterodactyl/wings/parser"
) )
type OutputLineMatcher struct { type OutputLineMatcher struct {

View File

@@ -1,9 +1,15 @@
package api package api
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/pkg/errors" "strconv"
"sync"
"github.com/apex/log"
"github.com/pterodactyl/wings/config"
"golang.org/x/sync/errgroup"
) )
const ( const (
@@ -33,157 +39,166 @@ type InstallationScript struct {
Script string `json:"script"` Script string `json:"script"`
} }
// GetAllServerConfigurations fetches configurations for all servers assigned to this node. type allServerResponse struct {
func (r *PanelRequest) GetAllServerConfigurations() (map[string]*ServerConfigurationResponse, *RequestError, error) { Data []RawServerData `json:"data"`
resp, err := r.Get("/servers") Meta Pagination `json:"meta"`
}
type RawServerData struct {
Uuid string `json:"uuid"`
Settings json.RawMessage `json:"settings"`
ProcessConfiguration json.RawMessage `json:"process_configuration"`
}
// Fetches all of the server configurations from the Panel API. This will initially load the
// first 50 servers, and then check the pagination response to determine if more pages should
// be loaded. If so, those requests are spun-up in additional routines and the final resulting
// slice of all servers will be returned.
func (r *Request) GetServers() ([]RawServerData, error) {
resp, err := r.Get("/servers", Q{"per_page": strconv.Itoa(int(config.Get().RemoteQuery.BootServersPerPage))})
if err != nil { if err != nil {
return nil, nil, errors.WithStack(err) return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
r.Response = resp if resp.HasError() {
return nil, resp.Error()
if r.HasError() {
return nil, r.Error(), nil
} }
b, _ := r.ReadBody() var res allServerResponse
res := map[string]*ServerConfigurationResponse{} if err := resp.Bind(&res); err != nil {
if len(b) == 2 { return nil, err
return res, nil, nil
} }
if err := json.Unmarshal(b, &res); err != nil { var mu sync.Mutex
return nil, nil, errors.WithStack(err) ret := res.Data
// Check for pagination, and if it exists we'll need to then make a request to the API
// for each page that would exist and get all of the resulting servers.
if res.Meta.LastPage > 1 {
pp := res.Meta.PerPage
log.WithField("per_page", pp).
WithField("total_pages", res.Meta.LastPage).
Debug("detected multiple pages of server configurations, fetching remaining...")
g, ctx := errgroup.WithContext(context.Background())
for i := res.Meta.CurrentPage + 1; i <= res.Meta.LastPage; i++ {
page := strconv.Itoa(int(i))
g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
default:
{
resp, err := r.Get("/servers", Q{"page": page, "per_page": strconv.Itoa(int(pp))})
if err != nil {
return err
}
defer resp.Body.Close()
if resp.Error() != nil {
return resp.Error()
}
var servers allServerResponse
if err := resp.Bind(&servers); err != nil {
return err
}
mu.Lock()
defer mu.Unlock()
ret = append(ret, servers.Data...)
return nil
}
}
})
}
if err := g.Wait(); err != nil {
return nil, err
}
} }
return res, nil, nil return ret, nil
} }
// Fetches the server configuration and returns the struct for it. // Fetches the server configuration and returns the struct for it.
func (r *PanelRequest) GetServerConfiguration(uuid string) (*ServerConfigurationResponse, *RequestError, error) { func (r *Request) GetServerConfiguration(uuid string) (ServerConfigurationResponse, error) {
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid)) var cfg ServerConfigurationResponse
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid), nil)
if err != nil { if err != nil {
return nil, nil, errors.WithStack(err) return cfg, err
} }
defer resp.Body.Close() defer resp.Body.Close()
r.Response = resp if resp.HasError() {
return cfg, resp.Error()
if r.HasError() {
return nil, r.Error(), nil
} }
res := &ServerConfigurationResponse{} if err := resp.Bind(&cfg); err != nil {
b, _ := r.ReadBody() return cfg, err
if err := json.Unmarshal(b, res); err != nil {
return nil, nil, errors.WithStack(err)
} }
return res, nil, nil return cfg, nil
} }
// Fetches installation information for the server process. // Fetches installation information for the server process.
func (r *PanelRequest) GetInstallationScript(uuid string) (InstallationScript, *RequestError, error) { func (r *Request) GetInstallationScript(uuid string) (InstallationScript, error) {
res := InstallationScript{} var is InstallationScript
resp, err := r.Get(fmt.Sprintf("/servers/%s/install", uuid), nil)
resp, err := r.Get(fmt.Sprintf("/servers/%s/install", uuid))
if err != nil { if err != nil {
return res, nil, errors.WithStack(err) return is, err
} }
defer resp.Body.Close() defer resp.Body.Close()
r.Response = resp if resp.HasError() {
return is, resp.Error()
if r.HasError() {
return res, r.Error(), nil
} }
b, _ := r.ReadBody() if err := resp.Bind(&is); err != nil {
return is, err
if err := json.Unmarshal(b, &res); err != nil {
return res, nil, errors.WithStack(err)
} }
return res, nil, nil return is, nil
}
type installRequest struct {
Successful bool `json:"successful"`
} }
// Marks a server as being installed successfully or unsuccessfully on the panel. // Marks a server as being installed successfully or unsuccessfully on the panel.
func (r *PanelRequest) SendInstallationStatus(uuid string, successful bool) (*RequestError, error) { func (r *Request) SendInstallationStatus(uuid string, successful bool) error {
b, err := json.Marshal(installRequest{Successful: successful}) resp, err := r.Post(fmt.Sprintf("/servers/%s/install", uuid), D{"successful": successful})
if err != nil { if err != nil {
return nil, errors.WithStack(err) return err
}
resp, err := r.Post(fmt.Sprintf("/servers/%s/install", uuid), b)
if err != nil {
return nil, errors.WithStack(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
r.Response = resp if resp.HasError() {
if r.HasError() { return resp.Error()
return r.Error(), nil
} }
return nil, nil return nil
} }
type archiveRequest struct { func (r *Request) SendArchiveStatus(uuid string, successful bool) error {
Successful bool `json:"successful"` resp, err := r.Post(fmt.Sprintf("/servers/%s/archive", uuid), D{"successful": successful})
}
func (r *PanelRequest) SendArchiveStatus(uuid string, successful bool) (*RequestError, error) {
b, err := json.Marshal(archiveRequest{Successful: successful})
if err != nil { if err != nil {
return nil, errors.WithStack(err) return err
}
resp, err := r.Post(fmt.Sprintf("/servers/%s/archive", uuid), b)
if err != nil {
return nil, errors.WithStack(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
r.Response = resp return resp.Error()
if r.HasError() {
return r.Error(), nil
}
return nil, nil
} }
func (r *PanelRequest) SendTransferFailure(uuid string) (*RequestError, error) { func (r *Request) SendTransferStatus(uuid string, successful bool) error {
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/failure", uuid)) state := "failure"
if successful {
state = "success"
}
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/%s", uuid, state), nil)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return err
} }
defer resp.Body.Close() defer resp.Body.Close()
return resp.Error()
r.Response = resp
if r.HasError() {
return r.Error(), nil
}
return nil, nil
}
func (r *PanelRequest) SendTransferSuccess(uuid string) (*RequestError, error) {
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/success", uuid))
if err != nil {
return nil, errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return r.Error(), nil
}
return nil, nil
} }

View File

@@ -1,41 +1,85 @@
package api package api
import ( import (
"encoding/json" "regexp"
"github.com/pkg/errors"
"github.com/pterodactyl/sftp-server" "emperror.dev/errors"
"github.com/apex/log"
) )
func (r *PanelRequest) ValidateSftpCredentials(request sftp_server.AuthenticationRequest) (*sftp_server.AuthenticationResponse, error) { type SftpAuthRequest struct {
b, err := json.Marshal(request) User string `json:"username"`
if err != nil { Pass string `json:"password"`
return nil, err IP string `json:"ip"`
SessionID []byte `json:"session_id"`
ClientVersion []byte `json:"client_version"`
}
type SftpAuthResponse struct {
Server string `json:"server"`
Token string `json:"token"`
Permissions []string `json:"permissions"`
}
type sftpInvalidCredentialsError struct {
}
func (ice sftpInvalidCredentialsError) Error() string {
return "the credentials provided were invalid"
}
func IsInvalidCredentialsError(err error) bool {
_, ok := err.(*sftpInvalidCredentialsError)
return ok
}
// Usernames all follow the same format, so don't even bother hitting the API if the username is not
// at least in the expected format. This is very basic protection against random bots finding the SFTP
// server and sending a flood of usernames.
var validUsernameRegexp = regexp.MustCompile(`^(?i)(.+)\.([a-z0-9]{8})$`)
func (r *Request) ValidateSftpCredentials(request SftpAuthRequest) (*SftpAuthResponse, error) {
// If the username doesn't meet the expected format that the Panel would even recognize just go ahead
// and bail out of the process here to avoid accidentally brute forcing the panel if a bot decides
// to connect to spam username attempts.
if !validUsernameRegexp.MatchString(request.User) {
log.WithFields(log.Fields{
"subsystem": "sftp",
"username": request.User,
"ip": request.IP,
}).Warn("failed to validate user credentials (invalid format)")
return nil, new(sftpInvalidCredentialsError)
} }
resp, err := r.Post("/sftp/auth", b) resp, err := r.Post("/sftp/auth", request)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
r.Response = resp e := resp.Error()
if e != nil {
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
log.WithFields(log.Fields{
"subsystem": "sftp",
"username": request.User,
"ip": request.IP,
}).Warn(e.Error())
if r.HasError() { return nil, &sftpInvalidCredentialsError{}
if r.HttpResponseCode() >= 400 && r.HttpResponseCode() < 500 {
return nil, new(sftp_server.InvalidCredentialsError)
} }
rerr := errors.New(r.Error().String()) rerr := errors.New(e.Error())
return nil, rerr return nil, rerr
} }
response := new(sftp_server.AuthenticationResponse) var response SftpAuthResponse
body, _ := r.ReadBody() if err := resp.Bind(&response); err != nil {
if err := json.Unmarshal(body, response); err != nil {
return nil, err return nil, err
} }
return response, nil return &response, nil
} }

View File

@@ -1,9 +1,10 @@
package cmd package cmd
import ( import (
"github.com/pterodactyl/wings/config"
"os" "os"
"path/filepath" "path/filepath"
"github.com/pterodactyl/wings/config"
) )
// We've gone through a couple of iterations of where the configuration is stored. This // We've gone through a couple of iterations of where the configuration is stored. This
@@ -57,4 +58,4 @@ func RelocateConfiguration() error {
} }
return os.Chmod(config.DefaultLocation, 0600) return os.Chmod(config.DefaultLocation, 0600)
} }

View File

@@ -4,10 +4,6 @@ import (
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/pterodactyl/wings/config"
"github.com/spf13/cobra"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
@@ -15,6 +11,11 @@ import (
"path" "path"
"regexp" "regexp"
"time" "time"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/pterodactyl/wings/config"
"github.com/spf13/cobra"
) )
var ( var (

View File

@@ -11,20 +11,24 @@ import (
"net/url" "net/url"
"os/exec" "os/exec"
"path" "path"
"strconv"
"strings" "strings"
"time"
"github.com/pterodactyl/wings/environment"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal" "github.com/AlecAivazis/survey/v2/terminal"
"github.com/docker/cli/components/engine/pkg/parsers/operatingsystem"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/parsers/kernel" "github.com/docker/docker/pkg/parsers/kernel"
"github.com/docker/docker/pkg/parsers/operatingsystem"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/system" "github.com/pterodactyl/wings/system"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
const DefaultHastebinUrl = "https://hastebin.com" const DefaultHastebinUrl = "https://ptero.co"
const DefaultLogLines = 200
var ( var (
diagnosticsArgs struct { diagnosticsArgs struct {
@@ -32,6 +36,7 @@ var (
IncludeLogs bool IncludeLogs bool
ReviewBeforeUpload bool ReviewBeforeUpload bool
HastebinURL string HastebinURL string
LogLines int
} }
) )
@@ -43,6 +48,7 @@ var diagnosticsCmd = &cobra.Command{
func init() { func init() {
diagnosticsCmd.PersistentFlags().StringVar(&diagnosticsArgs.HastebinURL, "hastebin-url", DefaultHastebinUrl, "The url of the hastebin instance to use.") diagnosticsCmd.PersistentFlags().StringVar(&diagnosticsArgs.HastebinURL, "hastebin-url", DefaultHastebinUrl, "The url of the hastebin instance to use.")
diagnosticsCmd.PersistentFlags().IntVar(&diagnosticsArgs.LogLines, "log-lines", DefaultLogLines, "The number of log lines to include in the report")
} }
// diagnosticsCmdRun collects diagnostics about wings, it's configuration and the node. // diagnosticsCmdRun collects diagnostics about wings, it's configuration and the node.
@@ -66,7 +72,7 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
Name: "ReviewBeforeUpload", Name: "ReviewBeforeUpload",
Prompt: &survey.Confirm{ Prompt: &survey.Confirm{
Message: "Do you want to review the collected data before uploading to hastebin.com?", Message: "Do you want to review the collected data before uploading to hastebin.com?",
Help: "The data, especially the logs, might contain sensitive information, so you should review it. You will be asked again if you want to uplaod.", Help: "The data, especially the logs, might contain sensitive information, so you should review it. You will be asked again if you want to upload.",
Default: true, Default: true,
}, },
}, },
@@ -82,38 +88,41 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
_ = dockerInfo _ = dockerInfo
output := &strings.Builder{} output := &strings.Builder{}
fmt.Fprintln(output, "Pterodactly Wings - Diagnostics Report") fmt.Fprintln(output, "Pterodactyl Wings - Diagnostics Report")
printHeader(output, "Versions") printHeader(output, "Versions")
fmt.Fprintln(output, "wings:", system.Version) fmt.Fprintln(output, " wings:", system.Version)
if dockerErr == nil { if dockerErr == nil {
fmt.Fprintln(output, "Docker", dockerVersion.Version) fmt.Fprintln(output, "Docker:", dockerVersion.Version)
} }
if v, err := kernel.GetKernelVersion(); err == nil { if v, err := kernel.GetKernelVersion(); err == nil {
fmt.Fprintln(output, "Kernel:", v) fmt.Fprintln(output, "Kernel:", v)
} }
if os, err := operatingsystem.GetOperatingSystem(); err == nil { if os, err := operatingsystem.GetOperatingSystem(); err == nil {
fmt.Fprintln(output, "OS:", os) fmt.Fprintln(output, " OS:", os)
} }
printHeader(output, "Wings Configuration") printHeader(output, "Wings Configuration")
if cfg, err := config.ReadConfiguration(config.DefaultLocation); cfg != nil { cfg, err := config.ReadConfiguration(config.DefaultLocation)
fmt.Fprintln(output, "Panel Location:", redact(cfg.PanelLocation)) if cfg != nil {
fmt.Fprintln(output, "Api Host:", redact(cfg.Api.Host)) fmt.Fprintln(output, " Panel Location:", redact(cfg.PanelLocation))
fmt.Fprintln(output, "Api Port:", cfg.Api.Port) fmt.Fprintln(output, "")
fmt.Fprintln(output, "Api Ssl Enabled:", cfg.Api.Ssl.Enabled) fmt.Fprintln(output, " Internal Webserver:", redact(cfg.Api.Host), ":", cfg.Api.Port)
fmt.Fprintln(output, "Api Ssl Certificate:", redact(cfg.Api.Ssl.CertificateFile)) fmt.Fprintln(output, " SSL Enabled:", cfg.Api.Ssl.Enabled)
fmt.Fprintln(output, "Api Ssl Key:", redact(cfg.Api.Ssl.KeyFile)) fmt.Fprintln(output, " SSL Certificate:", redact(cfg.Api.Ssl.CertificateFile))
fmt.Fprintln(output, "Sftp Address:", redact(cfg.System.Sftp.Address)) fmt.Fprintln(output, " SSL Key:", redact(cfg.Api.Ssl.KeyFile))
fmt.Fprintln(output, "Sftp Port:", cfg.System.Sftp.Port) fmt.Fprintln(output, "")
fmt.Fprintln(output, "Sftp Read Only:", cfg.System.Sftp.ReadOnly) fmt.Fprintln(output, " SFTP Server:", redact(cfg.System.Sftp.Address), ":", cfg.System.Sftp.Port)
fmt.Fprintln(output, "Sftp Diskchecking Disabled:", cfg.System.Sftp.DisableDiskChecking) fmt.Fprintln(output, " SFTP Read-Only:", cfg.System.Sftp.ReadOnly)
fmt.Fprintln(output, "System Root Directory:", cfg.System.RootDirectory) fmt.Fprintln(output, "")
fmt.Fprintln(output, "System Logs Directory:", cfg.System.LogDirectory) fmt.Fprintln(output, " Root Directory:", cfg.System.RootDirectory)
fmt.Fprintln(output, "System Data Directory:", cfg.System.Data) fmt.Fprintln(output, " Logs Directory:", cfg.System.LogDirectory)
fmt.Fprintln(output, "System Archive Directory:", cfg.System.ArchiveDirectory) fmt.Fprintln(output, " Data Directory:", cfg.System.Data)
fmt.Fprintln(output, "System Backup Directory:", cfg.System.BackupDirectory) fmt.Fprintln(output, " Archive Directory:", cfg.System.ArchiveDirectory)
fmt.Fprintln(output, "System Username:", cfg.System.Username) fmt.Fprintln(output, " Backup Directory:", cfg.System.BackupDirectory)
fmt.Fprintln(output, "Debug Enabled:", cfg.Debug) fmt.Fprintln(output, "")
fmt.Fprintln(output, " Username:", cfg.System.Username)
fmt.Fprintln(output, " Server Time:", time.Now().Format(time.RFC1123Z))
fmt.Fprintln(output, " Debug Mode:", cfg.Debug)
} else { } else {
fmt.Println("Failed to load configuration.", err) fmt.Println("Failed to load configuration.", err)
} }
@@ -132,7 +141,7 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
} }
} }
fmt.Fprintln(output, "LoggingDriver:", dockerInfo.LoggingDriver) fmt.Fprintln(output, "LoggingDriver:", dockerInfo.LoggingDriver)
fmt.Fprintln(output, "CgroupDriver:", dockerInfo.CgroupDriver) fmt.Fprintln(output, " CgroupDriver:", dockerInfo.CgroupDriver)
if len(dockerInfo.Warnings) > 0 { if len(dockerInfo.Warnings) > 0 {
for _, w := range dockerInfo.Warnings { for _, w := range dockerInfo.Warnings {
fmt.Fprintln(output, w) fmt.Fprintln(output, w)
@@ -149,7 +158,15 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
printHeader(output, "Latest Wings Logs") printHeader(output, "Latest Wings Logs")
if diagnosticsArgs.IncludeLogs { if diagnosticsArgs.IncludeLogs {
fmt.Fprintln(output, "No logs found. Probably because nobody implemented logging to files yet :(") p := "/var/log/pterodactyl/wings.log"
if cfg != nil {
p = path.Join(cfg.System.LogDirectory, "wings.log")
}
if c, err := exec.Command("tail", "-n", strconv.Itoa(diagnosticsArgs.LogLines), p).Output(); err != nil {
fmt.Fprintln(output, "No logs found or an error occurred.")
} else {
fmt.Fprintf(output, "%s\n", string(c))
}
} else { } else {
fmt.Fprintln(output, "Logs redacted.") fmt.Fprintln(output, "Logs redacted.")
} }
@@ -171,7 +188,7 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
} }
func getDockerInfo() (types.Version, types.Info, error) { func getDockerInfo() (types.Version, types.Info, error) {
cli, err := client.NewClientWithOpts(client.FromEnv) cli, err := environment.DockerClient()
if err != nil { if err != nil {
return types.Version{}, types.Info{}, err return types.Version{}, types.Info{}, err
} }
@@ -210,7 +227,7 @@ func uploadToHastebin(hbUrl, content string) (string, error) {
u.Path = path.Join(u.Path, key) u.Path = path.Join(u.Path, key)
return u.String(), nil return u.String(), nil
} }
return "", errors.New("Couldn't find key in response") return "", errors.New("failed to find key in response")
} }
func redact(s string) string { func redact(s string) string {

View File

@@ -3,68 +3,84 @@ package cmd
import ( import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"github.com/NYTimes/logrotate" log2 "log"
"github.com/apex/log/handlers/multi"
"github.com/gammazero/workerpool"
"golang.org/x/crypto/acme"
"net/http" "net/http"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
"emperror.dev/errors"
"github.com/NYTimes/logrotate"
"github.com/apex/log" "github.com/apex/log"
"github.com/apex/log/handlers/multi"
"github.com/docker/docker/client"
"github.com/gammazero/workerpool"
"github.com/mitchellh/colorstring" "github.com/mitchellh/colorstring"
"github.com/pterodactyl/wings/loggers/cli"
"golang.org/x/crypto/acme/autocert"
"github.com/pkg/errors"
"github.com/pkg/profile" "github.com/pkg/profile"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/loggers/cli"
"github.com/pterodactyl/wings/router" "github.com/pterodactyl/wings/router"
"github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/sftp" "github.com/pterodactyl/wings/sftp"
"github.com/pterodactyl/wings/system" "github.com/pterodactyl/wings/system"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.uber.org/zap" "golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
) )
var configPath = config.DefaultLocation var (
var debug = false configPath = config.DefaultLocation
var shouldRunProfiler = false debug = false
var useAutomaticTls = false )
var tlsHostname = ""
var showVersion = false
var root = &cobra.Command{ var rootCommand = &cobra.Command{
Use: "wings", Use: "wings",
Short: "The wings of the pterodactyl game management panel", Short: "Runs the API server allowing programatic control of game servers for Pterodactyl Panel.",
Long: ``,
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
if useAutomaticTls && len(tlsHostname) == 0 { if tls, _ := cmd.Flags().GetBool("auto-tls"); tls {
fmt.Println("A TLS hostname must be provided when running wings with automatic TLS, e.g.:\n\n ./wings --auto-tls --tls-hostname my.example.com") if host, _ := cmd.Flags().GetString("tls-hostname"); host == "" {
os.Exit(1) fmt.Println("A TLS hostname must be provided when running wings with automatic TLS, e.g.:\n\n ./wings --auto-tls --tls-hostname my.example.com")
os.Exit(1)
}
} }
}, },
Run: rootCmdRun, Run: rootCmdRun,
} }
func init() { var versionCommand = &cobra.Command{
root.PersistentFlags().BoolVar(&showVersion, "version", false, "show the version and exit") Use: "version",
root.PersistentFlags().StringVar(&configPath, "config", config.DefaultLocation, "set the location for the configuration file") Short: "Prints the current executable version and exits.",
root.PersistentFlags().BoolVar(&debug, "debug", false, "pass in order to run wings in debug mode") Run: func(cmd *cobra.Command, _ []string) {
root.PersistentFlags().BoolVar(&shouldRunProfiler, "profile", false, "pass in order to profile wings") fmt.Printf("wings v%s\nCopyright © 2018 - 2021 Dane Everitt & Contributors\n", system.Version)
root.PersistentFlags().BoolVar(&useAutomaticTls, "auto-tls", false, "pass in order to have wings generate and manage it's own SSL certificates using Let's Encrypt") },
root.PersistentFlags().StringVar(&tlsHostname, "tls-hostname", "", "required with --auto-tls, the FQDN for the generated SSL certificate") }
root.AddCommand(configureCmd) func Execute() {
root.AddCommand(diagnosticsCmd) if err := rootCommand.Execute(); err != nil {
log2.Fatalf("failed to execute command: %s", err)
}
}
func init() {
rootCommand.PersistentFlags().StringVar(&configPath, "config", config.DefaultLocation, "set the location for the configuration file")
rootCommand.PersistentFlags().BoolVar(&debug, "debug", false, "pass in order to run wings in debug mode")
// Flags specifically used when running the API.
rootCommand.Flags().String("profiler", "", "the profiler to run for this instance")
rootCommand.Flags().Bool("auto-tls", false, "pass in order to have wings generate and manage it's own SSL certificates using Let's Encrypt")
rootCommand.Flags().String("tls-hostname", "", "required with --auto-tls, the FQDN for the generated SSL certificate")
rootCommand.Flags().Bool("ignore-certificate-errors", false, "ignore certificate verification errors when executing API calls")
rootCommand.AddCommand(versionCommand)
rootCommand.AddCommand(configureCmd)
rootCommand.AddCommand(diagnosticsCmd)
} }
// Get the configuration path based on the arguments provided. // Get the configuration path based on the arguments provided.
func readConfiguration() (*config.Configuration, error) { func readConfiguration() (*config.Configuration, error) {
var p = configPath p := configPath
if !strings.HasPrefix(p, "/") { if !strings.HasPrefix(p, "/") {
d, err := os.Getwd() d, err := os.Getwd()
if err != nil { if err != nil {
@@ -75,7 +91,7 @@ func readConfiguration() (*config.Configuration, error) {
} }
if s, err := os.Stat(p); err != nil { if s, err := os.Stat(p); err != nil {
return nil, errors.WithStack(err) return nil, err
} else if s.IsDir() { } else if s.IsDir() {
return nil, errors.New("cannot use directory as configuration file path") return nil, errors.New("cannot use directory as configuration file path")
} }
@@ -83,24 +99,33 @@ func readConfiguration() (*config.Configuration, error) {
return config.ReadConfiguration(p) return config.ReadConfiguration(p)
} }
func rootCmdRun(*cobra.Command, []string) { func rootCmdRun(cmd *cobra.Command, _ []string) {
if showVersion { switch cmd.Flag("profiler").Value.String() {
fmt.Println(system.Version) case "cpu":
os.Exit(0) defer profile.Start(profile.CPUProfile).Stop()
} case "mem":
defer profile.Start(profile.MemProfile).Stop()
if shouldRunProfiler { case "alloc":
defer profile.Start().Stop() defer profile.Start(profile.MemProfile, profile.MemProfileAllocs()).Stop()
case "heap":
defer profile.Start(profile.MemProfile, profile.MemProfileHeap()).Stop()
case "routines":
defer profile.Start(profile.GoroutineProfile).Stop()
case "mutex":
defer profile.Start(profile.MutexProfile).Stop()
case "threads":
defer profile.Start(profile.ThreadcreationProfile).Stop()
case "block":
defer profile.Start(profile.BlockProfile).Stop()
} }
// Only attempt configuration file relocation if a custom location has not // Only attempt configuration file relocation if a custom location has not
// been specified in the command startup. // been specified in the command startup.
if configPath == config.DefaultLocation { if configPath == config.DefaultLocation {
if err := RelocateConfiguration(); err != nil { if err := RelocateConfiguration(); err != nil {
if os.IsNotExist(err) { if errors.Is(err, os.ErrNotExist) {
exitWithConfigurationNotice() exitWithConfigurationNotice()
} }
panic(err) panic(err)
} }
} }
@@ -122,8 +147,10 @@ func rootCmdRun(*cobra.Command, []string) {
log.WithField("path", c.GetPath()).Info("loading configuration from path") log.WithField("path", c.GetPath()).Info("loading configuration from path")
if c.Debug { if c.Debug {
log.Debug("running in debug mode") log.Debug("running in debug mode")
log.Warn("certificate checking is disabled") }
if ok, _ := cmd.Flags().GetBool("ignore-certificate-errors"); ok {
log.Warn("running with --ignore-certificate-errors: TLS certificate host chains and name will not be verified")
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{ http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: true,
} }
@@ -132,16 +159,26 @@ func rootCmdRun(*cobra.Command, []string) {
config.Set(c) config.Set(c)
config.SetDebugViaFlag(debug) config.SetDebugViaFlag(debug)
if err := c.System.ConfigureTimezone(); err != nil {
log.WithField("error", err).Fatal("failed to detect system timezone or use supplied configuration value")
return
}
log.WithField("timezone", c.System.Timezone).Info("configured wings with system timezone")
if err := c.System.ConfigureDirectories(); err != nil { if err := c.System.ConfigureDirectories(); err != nil {
log.WithError(err).Fatal("failed to configure system directories for pterodactyl") log.WithField("error", err).Fatal("failed to configure system directories for pterodactyl")
os.Exit(1) return
}
if err := c.System.EnableLogRotation(); err != nil {
log.WithField("error", err).Fatal("failed to configure log rotation on the system")
return return
} }
log.WithField("username", c.System.Username).Info("checking for pterodactyl system user") log.WithField("username", c.System.Username).Info("checking for pterodactyl system user")
if su, err := c.EnsurePterodactylUser(); err != nil { if su, err := c.EnsurePterodactylUser(); err != nil {
log.WithError(err).Error("failed to create pterodactyl system user") log.WithField("error", err).Fatal("failed to create pterodactyl system user")
os.Exit(1)
return return
} else { } else {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
@@ -158,7 +195,7 @@ func rootCmdRun(*cobra.Command, []string) {
if err := environment.ConfigureDocker(&c.Docker); err != nil { if err := environment.ConfigureDocker(&c.Docker); err != nil {
log.WithField("error", err).Fatal("failed to configure docker environment") log.WithField("error", err).Fatal("failed to configure docker environment")
os.Exit(1) return
} }
if err := c.WriteToDisk(); err != nil { if err := c.WriteToDisk(); err != nil {
@@ -170,6 +207,11 @@ func rootCmdRun(*cobra.Command, []string) {
log.WithField("server", s.Id()).Info("loaded configuration for server") log.WithField("server", s.Id()).Info("loaded configuration for server")
} }
states, err := server.CachedServerStates()
if err != nil {
log.WithField("error", err).Error("failed to retrieve locally cached server states from disk, assuming all servers in offline state")
}
// Create a new workerpool that limits us to 4 servers being bootstrapped at a time // Create a new workerpool that limits us to 4 servers being bootstrapped at a time
// on Wings. This allows us to ensure the environment exists, write configurations, // on Wings. This allows us to ensure the environment exists, write configurations,
// and reboot processes without causing a slow-down due to sequential booting. // and reboot processes without causing a slow-down due to sequential booting.
@@ -179,38 +221,51 @@ func rootCmdRun(*cobra.Command, []string) {
s := serv s := serv
pool.Submit(func() { pool.Submit(func() {
s.Log().Info("ensuring server environment exists") s.Log().Info("configuring server environment and restoring to previous state")
// Create a server environment if none exists currently. This allows us to recover from Docker
// being reinstalled on the host system for example. var st string
if err := s.Environment.Create(); err != nil { if state, exists := states[s.Id()]; exists {
s.Log().WithField("error", err).Error("failed to process environment") st = state
} }
r, err := s.Environment.IsRunning() r, err := s.Environment.IsRunning()
if err != nil { // We ignore missing containers because we don't want to actually block booting of wings at this
// point. If we didn't do this and you pruned all of the images and then started wings you could
// end up waiting a long period of time for all of the images to be re-pulled on Wings boot rather
// than when the server itself is started.
if err != nil && !client.IsErrNotFound(err) {
s.Log().WithField("error", err).Error("error checking server environment status") s.Log().WithField("error", err).Error("error checking server environment status")
} }
// If the server is currently running on Docker, mark the process as being in that state. // Check if the server was previously running. If so, attempt to start the server now so that Wings
// We never want to stop an instance that is currently running external from Wings since // can pick up where it left off. If the environment does not exist at all, just create it and then allow
// that is a good way of keeping things running even if Wings gets in a very corrupted state. // the normal flow to execute.
// //
// This will also validate that a server process is running if the last tracked state we have // This does mean that booting wings after a catastrophic machine crash and wiping out the Docker images
// is that it was running, but we see that the container process is not currently running. // as a result will result in a slow boot.
if r || (!r && s.IsRunning()) { if !r && (st == environment.ProcessRunningState || st == environment.ProcessStartingState) {
if err := s.HandlePowerAction(server.PowerActionStart); err != nil {
s.Log().WithField("error", err).Warn("failed to return server to running state")
}
} else if r || (!r && s.IsRunning()) {
// If the server is currently running on Docker, mark the process as being in that state.
// We never want to stop an instance that is currently running external from Wings since
// that is a good way of keeping things running even if Wings gets in a very corrupted state.
//
// This will also validate that a server process is running if the last tracked state we have
// is that it was running, but we see that the container process is not currently running.
s.Log().Info("detected server is running, re-attaching to process...") s.Log().Info("detected server is running, re-attaching to process...")
s.SetState(environment.ProcessRunningState) s.Environment.SetState(environment.ProcessRunningState)
if err := s.Environment.Attach(); err != nil { if err := s.Environment.Attach(); err != nil {
s.Log().WithField("error", errors.WithStack(err)).Warn("failed to attach to running server environment") s.Log().WithField("error", err).Warn("failed to attach to running server environment")
} }
} else {
return // At this point we've determined that the server should indeed be in an offline state, so we'll
// make a call to set that state just to ensure we don't ever accidentally end up with some invalid
// state being tracked.
s.Environment.SetState(environment.ProcessOfflineState)
} }
// Addresses potentially invalid data in the stored file that can cause Wings to lose
// track of what the actual server state is.
_ = s.SetState(environment.ProcessOfflineState)
}) })
} }
@@ -218,9 +273,8 @@ func rootCmdRun(*cobra.Command, []string) {
pool.StopWait() pool.StopWait()
// Initialize the SFTP server. // Initialize the SFTP server.
if err := sftp.Initialize(c); err != nil { if err := sftp.Initialize(c.System); err != nil {
log.WithError(err).Error("failed to initialize the sftp server") log.WithError(err).Fatal("failed to initialize the sftp server")
os.Exit(1)
return return
} }
@@ -234,9 +288,15 @@ func rootCmdRun(*cobra.Command, []string) {
log.WithField("error", err).Error("failed to create backup directory") log.WithField("error", err).Error("failed to create backup directory")
} }
autotls, _ := cmd.Flags().GetBool("auto-tls")
tlshostname, _ := cmd.Flags().GetString("tls-hostname")
if autotls && tlshostname == "" {
autotls = false
}
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"use_ssl": c.Api.Ssl.Enabled, "use_ssl": c.Api.Ssl.Enabled,
"use_auto_tls": useAutomaticTls && len(tlsHostname) > 0, "use_auto_tls": autotls,
"host_address": c.Api.Host, "host_address": c.Api.Host,
"host_port": c.Api.Port, "host_port": c.Api.Port,
}).Info("configuring internal webserver") }).Info("configuring internal webserver")
@@ -247,48 +307,33 @@ func rootCmdRun(*cobra.Command, []string) {
s := &http.Server{ s := &http.Server{
Addr: fmt.Sprintf("%s:%d", c.Api.Host, c.Api.Port), Addr: fmt.Sprintf("%s:%d", c.Api.Host, c.Api.Port),
Handler: r, Handler: r,
TLSConfig: &tls.Config{ TLSConfig: &tls.Config{
NextProtos: []string{ NextProtos: []string{"h2", "http/1.1"},
"h2", // enable HTTP/2 // @see https://blog.cloudflare.com/exposing-go-on-the-internet
"http/1.1",
},
// https://blog.cloudflare.com/exposing-go-on-the-internet
CipherSuites: []uint16{ CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
}, },
PreferServerCipherSuites: true, PreferServerCipherSuites: true,
MinVersion: tls.VersionTLS12,
MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS13,
MaxVersion: tls.VersionTLS13, CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
CurvePreferences: []tls.CurveID{
tls.X25519,
tls.CurveP256,
},
// END https://blog.cloudflare.com/exposing-go-on-the-internet
}, },
} }
// Check if the server should run with TLS but using autocert. // Check if the server should run with TLS but using autocert.
if useAutomaticTls && len(tlsHostname) > 0 { if autotls {
m := autocert.Manager{ m := autocert.Manager{
Prompt: autocert.AcceptTOS, Prompt: autocert.AcceptTOS,
Cache: autocert.DirCache(path.Join(c.System.RootDirectory, "/.tls-cache")), Cache: autocert.DirCache(path.Join(c.System.RootDirectory, "/.tls-cache")),
HostPolicy: autocert.HostWhitelist(tlsHostname), HostPolicy: autocert.HostWhitelist(tlshostname),
} }
log.WithField("hostname", tlsHostname). log.WithField("hostname", tlshostname).
Info("webserver is now listening with auto-TLS enabled; certificates will be automatically generated by Let's Encrypt") Info("webserver is now listening with auto-TLS enabled; certificates will be automatically generated by Let's Encrypt")
// Hook autocert into the main http server. // Hook autocert into the main http server.
@@ -304,9 +349,8 @@ func rootCmdRun(*cobra.Command, []string) {
// Start the main http server with TLS using autocert. // Start the main http server with TLS using autocert.
if err := s.ListenAndServeTLS("", ""); err != nil { if err := s.ListenAndServeTLS("", ""); err != nil {
log.WithFields(log.Fields{"auto_tls": true, "tls_hostname": tlsHostname, "error": err}). log.WithFields(log.Fields{"auto_tls": true, "tls_hostname": tlshostname, "error": err}).
Fatal("failed to configure HTTP server using auto-tls") Fatal("failed to configure HTTP server using auto-tls")
os.Exit(1)
} }
return return
@@ -314,9 +358,8 @@ func rootCmdRun(*cobra.Command, []string) {
// Check if main http server should run with TLS. // Check if main http server should run with TLS.
if c.Api.Ssl.Enabled { if c.Api.Ssl.Enabled {
if err := s.ListenAndServeTLS(c.Api.Ssl.CertificateFile, c.Api.Ssl.KeyFile); err != nil { if err := s.ListenAndServeTLS(strings.ToLower(c.Api.Ssl.CertificateFile), strings.ToLower(c.Api.Ssl.KeyFile)); err != nil {
log.WithFields(log.Fields{"auto_tls": false, "error": err}).Fatal("failed to configure HTTPS server") log.WithFields(log.Fields{"auto_tls": false, "error": err}).Fatal("failed to configure HTTPS server")
os.Exit(1)
} }
return return
} }
@@ -325,47 +368,34 @@ func rootCmdRun(*cobra.Command, []string) {
s.TLSConfig = nil s.TLSConfig = nil
if err := s.ListenAndServe(); err != nil { if err := s.ListenAndServe(); err != nil {
log.WithField("error", err).Fatal("failed to configure HTTP server") log.WithField("error", err).Fatal("failed to configure HTTP server")
os.Exit(1)
} }
}
// Execute calls cobra to handle cli commands // Cancel the context on all of the running servers at this point, even though the
func Execute() error { // program is just shutting down.
return root.Execute() for _, s := range server.GetServers().All() {
s.CtxCancel()
}
} }
// Configures the global logger for Zap so that we can call it from any location // Configures the global logger for Zap so that we can call it from any location
// in the code without having to pass around a logger instance. // in the code without having to pass around a logger instance.
func configureLogging(logDir string, debug bool) error { func configureLogging(logDir string, debug bool) error {
cfg := zap.NewProductionConfig() if err := os.MkdirAll(path.Join(logDir, "/install"), 0700); err != nil {
if debug {
cfg = zap.NewDevelopmentConfig()
}
cfg.Encoding = "console"
cfg.OutputPaths = []string{
"stdout",
}
logger, err := cfg.Build()
if err != nil {
return err return err
} }
zap.ReplaceGlobals(logger)
p := filepath.Join(logDir, "/wings.log") p := filepath.Join(logDir, "/wings.log")
w, err := logrotate.NewFile(p) w, err := logrotate.NewFile(p)
if err != nil { if err != nil {
panic(errors.Wrap(err, "failed to open process log file")) return err
} }
log.SetLevel(log.DebugLevel) log.SetLevel(log.InfoLevel)
log.SetHandler(multi.New( if debug {
cli.Default, log.SetLevel(log.DebugLevel)
cli.New(w.File, false), }
))
log.SetHandler(multi.New(cli.Default, cli.New(w.File, false)))
log.WithField("path", p).Info("writing log files to disk") log.WithField("path", p).Info("writing log files to disk")
return nil return nil
@@ -379,9 +409,9 @@ __ [blue][bold]Pterodactyl[reset] _____/___/_______ _______ ______
\_____\ \/\/ / / / __ / ___/ \_____\ \/\/ / / / __ / ___/
\___\ / / / / /_/ /___ / \___\ / / / / /_/ /___ /
\___/\___/___/___/___/___ /______/ \___/\___/___/___/___/___ /______/
/_______/ [bold]v%s[reset] /_______/ [bold]%s[reset]
Copyright © 2018 - 2020 Dane Everitt & Contributors Copyright © 2018 - 2021 Dane Everitt & Contributors
Website: https://pterodactyl.io Website: https://pterodactyl.io
Source: https://github.com/pterodactyl/wings Source: https://github.com/pterodactyl/wings

View File

@@ -2,11 +2,6 @@ package config
import ( import (
"fmt" "fmt"
"github.com/cobaugh/osrelease"
"github.com/creasty/defaults"
"github.com/gbrlsnchs/jwt/v3"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
@@ -14,6 +9,12 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"emperror.dev/errors"
"github.com/cobaugh/osrelease"
"github.com/creasty/defaults"
"github.com/gbrlsnchs/jwt/v3"
"gopkg.in/yaml.v2"
) )
const DefaultLocation = "/etc/pterodactyl/config.yml" const DefaultLocation = "/etc/pterodactyl/config.yml"
@@ -47,23 +48,18 @@ type Configuration struct {
System SystemConfiguration `json:"system" yaml:"system"` System SystemConfiguration `json:"system" yaml:"system"`
Docker DockerConfiguration `json:"docker" yaml:"docker"` Docker DockerConfiguration `json:"docker" yaml:"docker"`
// The amount of time in seconds that should elapse between disk usage checks
// run by the daemon. Setting a higher number can result in better IO performance
// at an increased risk of a malicious user creating a process that goes over
// the assigned disk limits.
DiskCheckTimeout int `yaml:"disk_check_timeout"`
// Defines internal throttling configurations for server processes to prevent // Defines internal throttling configurations for server processes to prevent
// someone from running an endless loop that spams data to logs. // someone from running an endless loop that spams data to logs.
Throttles ConsoleThrottles Throttles ConsoleThrottles
// The location where the panel is running that this daemon should connect to // The location where the panel is running that this daemon should connect to
// to collect data and send events. // to collect data and send events.
PanelLocation string `json:"remote" yaml:"remote"` PanelLocation string `json:"remote" yaml:"remote"`
RemoteQuery RemoteQueryConfiguration `json:"remote_query" yaml:"remote_query"`
// AllowedMounts is a list of allowed host-system mount points. // AllowedMounts is a list of allowed host-system mount points.
// This is required to have the "Server Mounts" feature work properly. // This is required to have the "Server Mounts" feature work properly.
AllowedMounts []string `json:"allowed_mounts" yaml:"allowed_mounts"` AllowedMounts []string `json:"-" yaml:"allowed_mounts"`
// AllowedOrigins is a list of allowed request origins. // AllowedOrigins is a list of allowed request origins.
// The Panel URL is automatically allowed, this is only needed for adding // The Panel URL is automatically allowed, this is only needed for adding
@@ -73,9 +69,6 @@ type Configuration struct {
// Defines the configuration of the internal SFTP server. // Defines the configuration of the internal SFTP server.
type SftpConfiguration struct { type SftpConfiguration struct {
// If set to true disk checking will not be performed. This will prevent the SFTP
// server from checking the total size of a directory when uploading files.
DisableDiskChecking bool `default:"false" yaml:"disable_disk_checking"`
// The bind address of the SFTP server. // The bind address of the SFTP server.
Address string `default:"0.0.0.0" json:"bind_address" yaml:"bind_address"` Address string `default:"0.0.0.0" json:"bind_address" yaml:"bind_address"`
// The bind port of the SFTP server. // The bind port of the SFTP server.
@@ -95,15 +88,41 @@ type ApiConfiguration struct {
// SSL configuration for the daemon. // SSL configuration for the daemon.
Ssl struct { Ssl struct {
Enabled bool `default:"false"` Enabled bool `json:"enabled" yaml:"enabled"`
CertificateFile string `json:"cert" yaml:"cert"` CertificateFile string `json:"cert" yaml:"cert"`
KeyFile string `json:"key" yaml:"key"` KeyFile string `json:"key" yaml:"key"`
} }
// Determines if functionality for allowing remote download of files into server directories
// is enabled on this instance. If set to "true" remote downloads will not be possible for
// servers.
DisableRemoteDownload bool `json:"disable_remote_download" yaml:"disable_remote_download"`
// The maximum size for files uploaded through the Panel in bytes. // The maximum size for files uploaded through the Panel in bytes.
UploadLimit int `default:"100" json:"upload_limit" yaml:"upload_limit"` UploadLimit int `default:"100" json:"upload_limit" yaml:"upload_limit"`
} }
// Defines the configuration settings for remote requests from Wings to the Panel.
type RemoteQueryConfiguration struct {
// The amount of time in seconds that Wings should allow for a request to the Panel API
// to complete. If this time passes the request will be marked as failed. If your requests
// are taking longer than 30 seconds to complete it is likely a performance issue that
// should be resolved on the Panel, and not something that should be resolved by upping this
// number.
Timeout uint `default:"30" yaml:"timeout"`
// The number of servers to load in a single request to the Panel API when booting the
// Wings instance. A single request is initially made to the Panel to get this number
// of servers, and then the pagination status is checked and additional requests are
// fired off in parallel to request the remaining pages.
//
// It is not recommended to change this from the default as you will likely encounter
// memory limits on your Panel instance. In the grand scheme of things 4 requests for
// 50 servers is likely just as quick as two for 100 or one for 400, and will certainly
// be less likely to cause performance issues on the Panel.
BootServersPerPage uint `default:"50" yaml:"boot_servers_per_page"`
}
// Reads the configuration from the provided file and returns the configuration // Reads the configuration from the provided file and returns the configuration
// object that can then be used. // object that can then be used.
func ReadConfiguration(path string) (*Configuration, error) { func ReadConfiguration(path string) (*Configuration, error) {
@@ -179,7 +198,7 @@ func GetJwtAlgorithm() *jwt.HMACSHA {
func NewFromPath(path string) (*Configuration, error) { func NewFromPath(path string) (*Configuration, error) {
c := new(Configuration) c := new(Configuration)
if err := defaults.Set(c); err != nil { if err := defaults.Set(c); err != nil {
return c, errors.WithStack(err) return c, err
} }
c.unsafeSetPath(path) c.unsafeSetPath(path)
@@ -188,7 +207,7 @@ func NewFromPath(path string) (*Configuration, error) {
} }
// Sets the path where the configuration file is located on the server. This function should // Sets the path where the configuration file is located on the server. This function should
// not be called except by processes that are generating the configuration such as the configration // not be called except by processes that are generating the configuration such as the configuration
// command shipped with this software. // command shipped with this software.
func (c *Configuration) unsafeSetPath(path string) { func (c *Configuration) unsafeSetPath(path string) {
c.Lock() c.Lock()
@@ -210,6 +229,36 @@ func (c *Configuration) GetPath() string {
// If files are not owned by this user there will be issues with permissions on Docker // If files are not owned by this user there will be issues with permissions on Docker
// mount points. // mount points.
func (c *Configuration) EnsurePterodactylUser() (*user.User, error) { func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
sysName, err := getSystemName()
if err != nil {
return nil, err
}
// Our way of detecting if wings is running inside of Docker.
if sysName == "busybox" {
uid := os.Getenv("WINGS_UID")
if uid == "" {
uid = "988"
}
gid := os.Getenv("WINGS_GID")
if gid == "" {
gid = "988"
}
username := os.Getenv("WINGS_USERNAME")
if username == "" {
username = "pterodactyl"
}
u := &user.User{
Uid: uid,
Gid: gid,
Username: username,
}
return u, c.setSystemUser(u)
}
u, err := user.Lookup(c.System.Username) u, err := user.Lookup(c.System.Username)
// If an error is returned but it isn't the unknown user error just abort // If an error is returned but it isn't the unknown user error just abort
@@ -217,35 +266,30 @@ func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
if err == nil { if err == nil {
return u, c.setSystemUser(u) return u, c.setSystemUser(u)
} else if _, ok := err.(user.UnknownUserError); !ok { } else if _, ok := err.(user.UnknownUserError); !ok {
return nil, errors.WithStack(err) return nil, err
} }
sysName, err := getSystemName() command := fmt.Sprintf("useradd --system --no-create-home --shell /usr/sbin/nologin %s", c.System.Username)
if err != nil {
return nil, errors.WithStack(err)
}
var command = fmt.Sprintf("useradd --system --no-create-home --shell /bin/false %s", c.System.Username)
// Alpine Linux is the only OS we currently support that doesn't work with the useradd command, so // Alpine Linux is the only OS we currently support that doesn't work with the useradd command, so
// in those cases we just modify the command a bit to work as expected. // in those cases we just modify the command a bit to work as expected.
if strings.HasPrefix(sysName, "alpine") { if strings.HasPrefix(sysName, "alpine") {
command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /bin/false %[1]s", c.System.Username) command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /sbin/nologin %[1]s", c.System.Username)
// We have to create the group first on Alpine, so do that here before continuing on // We have to create the group first on Alpine, so do that here before continuing on
// to the user creation process. // to the user creation process.
if _, err := exec.Command("addgroup", "-S", c.System.Username).Output(); err != nil { if _, err := exec.Command("addgroup", "-S", c.System.Username).Output(); err != nil {
return nil, errors.WithStack(err) return nil, err
} }
} }
split := strings.Split(command, " ") split := strings.Split(command, " ")
if _, err := exec.Command(split[0], split[1:]...).Output(); err != nil { if _, err := exec.Command(split[0], split[1:]...).Output(); err != nil {
return nil, errors.WithStack(err) return nil, err
} }
if u, err := user.Lookup(c.System.Username); err != nil { if u, err := user.Lookup(c.System.Username); err != nil {
return nil, errors.WithStack(err) return nil, err
} else { } else {
return u, c.setSystemUser(u) return u, c.setSystemUser(u)
} }
@@ -254,8 +298,15 @@ func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
// Set the system user into the configuration and then write it to the disk so that // Set the system user into the configuration and then write it to the disk so that
// it is persisted on boot. // it is persisted on boot.
func (c *Configuration) setSystemUser(u *user.User) error { func (c *Configuration) setSystemUser(u *user.User) error {
uid, _ := strconv.Atoi(u.Uid) uid, err := strconv.Atoi(u.Uid)
gid, _ := strconv.Atoi(u.Gid) if err != nil {
return err
}
gid, err := strconv.Atoi(u.Gid)
if err != nil {
return err
}
c.Lock() c.Lock()
c.System.Username = u.Username c.System.Username = u.Username
@@ -287,11 +338,11 @@ func (c *Configuration) WriteToDisk() error {
b, err := yaml.Marshal(&ccopy) b, err := yaml.Marshal(&ccopy)
if err != nil { if err != nil {
return errors.WithStack(err) return err
} }
if err := ioutil.WriteFile(c.GetPath(), b, 0644); err != nil { if err := ioutil.WriteFile(c.GetPath(), b, 0644); err != nil {
return errors.WithStack(err) return err
} }
return nil return nil
@@ -301,7 +352,7 @@ func (c *Configuration) WriteToDisk() error {
func getSystemName() (string, error) { func getSystemName() (string, error) {
// use osrelease to get release version and ID // use osrelease to get release version and ID
if release, err := osrelease.Read(); err != nil { if release, err := osrelease.Read(); err != nil {
return "", errors.WithStack(err) return "", err
} else { } else {
return release["ID"], nil return release["ID"], nil
} }

View File

@@ -3,8 +3,8 @@ package config
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/pkg/errors"
) )
type dockerNetworkInterfaces struct { type dockerNetworkInterfaces struct {
@@ -49,18 +49,6 @@ type DockerConfiguration struct {
// Domainname is the Docker domainname for all containers. // Domainname is the Docker domainname for all containers.
Domainname string `default:"" json:"domainname" yaml:"domainname"` Domainname string `default:"" json:"domainname" yaml:"domainname"`
// If true, container images will be updated when a server starts if there
// is an update available. If false the daemon will not attempt updates and will
// defer to the host system to manage image updates.
UpdateImages bool `default:"true" json:"update_images" yaml:"update_images"`
// The location of the Docker socket.
Socket string `default:"/var/run/docker.sock" json:"socket" yaml:"socket"`
// Defines the location of the timezone file on the host system that should
// be mounted into the created containers so that they all use the same time.
TimezonePath string `default:"/etc/timezone" json:"timezone_path" yaml:"timezone_path"`
// Registries . // Registries .
Registries map[string]RegistryConfiguration `json:"registries" yaml:"registries"` Registries map[string]RegistryConfiguration `json:"registries" yaml:"registries"`
@@ -85,7 +73,7 @@ func (c RegistryConfiguration) Base64() (string, error) {
b, err := json.Marshal(authConfig) b, err := json.Marshal(authConfig)
if err != nil { if err != nil {
return "", errors.WithStack(err) return "", err
} }
return base64.URLEncoding.EncodeToString(b), nil return base64.URLEncoding.EncodeToString(b), nil

View File

@@ -1,9 +1,19 @@
package config package config
import ( import (
"github.com/apex/log" "context"
"fmt"
"html/template"
"io/ioutil"
"os" "os"
"os/exec"
"path" "path"
"path/filepath"
"regexp"
"time"
"emperror.dev/errors"
"github.com/apex/log"
) )
// Defines basic system configuration settings. // Defines basic system configuration settings.
@@ -26,6 +36,13 @@ type SystemConfiguration struct {
// The user that should own all of the server files, and be used for containers. // The user that should own all of the server files, and be used for containers.
Username string `default:"pterodactyl" yaml:"username"` Username string `default:"pterodactyl" yaml:"username"`
// The timezone for this Wings instance. This is detected by Wings automatically if possible,
// and falls back to UTC if not able to be detected. If you need to set this manually, that
// can also be done.
//
// This timezone value is passed into all containers created by Wings.
Timezone string `yaml:"timezone"`
// Definitions for the user that gets created to ensure that we can quickly access // Definitions for the user that gets created to ensure that we can quickly access
// this information without constantly having to do a system lookup. // this information without constantly having to do a system lookup.
User struct { User struct {
@@ -33,12 +50,70 @@ type SystemConfiguration struct {
Gid int Gid int
} }
// The amount of time in seconds that can elapse before a server's disk space calculation is
// considered stale and a re-check should occur. DANGER: setting this value too low can seriously
// impact system performance and cause massive I/O bottlenecks and high CPU usage for the Wings
// process.
//
// Set to 0 to disable disk checking entirely. This will always return 0 for the disk space used
// by a server and should only be set in extreme scenarios where performance is critical and
// disk usage is not a concern.
DiskCheckInterval int64 `default:"150" yaml:"disk_check_interval"`
// If set to true, file permissions for a server will be checked when the process is
// booted. This can cause boot delays if the server has a large amount of files. In most
// cases disabling this should not have any major impact unless external processes are
// frequently modifying a servers' files.
CheckPermissionsOnBoot bool `default:"true" yaml:"check_permissions_on_boot"`
// If set to false Wings will not attempt to write a log rotate configuration to the disk
// when it boots and one is not detected.
EnableLogRotate bool `default:"true" yaml:"enable_log_rotate"`
// The number of lines to send when a server connects to the websocket.
WebsocketLogCount int `default:"150" yaml:"websocket_log_count"`
Sftp SftpConfiguration `yaml:"sftp"`
CrashDetection CrashDetection `yaml:"crash_detection"`
Backups Backups `yaml:"backups"`
Transfers Transfers `yaml:"transfers"`
}
type CrashDetection struct {
// Determines if Wings should detect a server that stops with a normal exit code of // Determines if Wings should detect a server that stops with a normal exit code of
// "0" as being crashed if the process stopped without any Wings interaction. E.g. // "0" as being crashed if the process stopped without any Wings interaction. E.g.
// the user did not press the stop button, but the process stopped cleanly. // the user did not press the stop button, but the process stopped cleanly.
DetectCleanExitAsCrash bool `default:"true" yaml:"detect_clean_exit_as_crash"` DetectCleanExitAsCrash bool `default:"true" yaml:"detect_clean_exit_as_crash"`
Sftp SftpConfiguration `yaml:"sftp"` // Timeout specifies the timeout between crashes that will not cause the server
// to be automatically restarted, this value is used to prevent servers from
// becoming stuck in a boot-loop after multiple consecutive crashes.
Timeout int `default:"60" json:"timeout"`
}
type Backups struct {
// WriteLimit imposes a Disk I/O write limit on backups to the disk, this affects all
// backup drivers as the archiver must first write the file to the disk in order to
// upload it to any external storage provider.
//
// If the value is less than 1, the write speed is unlimited,
// if the value is greater than 0, the write speed is the value in MiB/s.
//
// Defaults to 0 (unlimited)
WriteLimit int `default:"0" yaml:"write_limit"`
}
type Transfers struct {
// DownloadLimit imposes a Network I/O read limit when downloading a transfer archive.
//
// If the value is less than 1, the write speed is unlimited,
// if the value is greater than 0, the write speed is the value in MiB/s.
//
// Defaults to 0 (unlimited)
DownloadLimit int `default:"0" yaml:"download_limit"`
} }
// Ensures that all of the system directories exist on the system. These directories are // Ensures that all of the system directories exist on the system. These directories are
@@ -49,9 +124,20 @@ func (sc *SystemConfiguration) ConfigureDirectories() error {
return err return err
} }
log.WithField("path", sc.LogDirectory).Debug("ensuring log directory exists") // There are a non-trivial number of users out there whose data directories are actually a
if err := os.MkdirAll(path.Join(sc.LogDirectory, "/install"), 0700); err != nil { // symlink to another location on the disk. If we do not resolve that final destination at this
return err // point things will appear to work, but endless errors will be encountered when we try to
// verify accessed paths since they will all end up resolving outside the expected data directory.
//
// For the sake of automating away as much of this as possible, see if the data directory is a
// symlink, and if so resolve to its final real path, and then update the configuration to use
// that.
if d, err := filepath.EvalSymlinks(sc.Data); err != nil {
if !os.IsNotExist(err) {
return err
}
} else if d != sc.Data {
sc.Data = d
} }
log.WithField("path", sc.Data).Debug("ensuring server data directory exists") log.WithField("path", sc.Data).Debug("ensuring server data directory exists")
@@ -72,6 +158,61 @@ func (sc *SystemConfiguration) ConfigureDirectories() error {
return nil return nil
} }
// Writes a logrotate file for wings to the system logrotate configuration directory if one
// exists and a logrotate file is not found. This allows us to basically automate away the log
// rotation for most installs, but also enable users to make modifications on their own.
func (sc *SystemConfiguration) EnableLogRotation() error {
// Do nothing if not enabled.
if sc.EnableLogRotate == false {
log.Info("skipping log rotate configuration, disabled in wings config file")
return nil
}
if st, err := os.Stat("/etc/logrotate.d"); err != nil && !os.IsNotExist(err) {
return err
} else if (err != nil && os.IsNotExist(err)) || !st.IsDir() {
return nil
}
if _, err := os.Stat("/etc/logrotate.d/wings"); err != nil && !os.IsNotExist(err) {
return err
} else if err == nil {
return nil
}
log.Info("no log rotation configuration found, system is configured to support it, adding file now")
// If we've gotten to this point it means the logrotate directory exists on the system
// but there is not a file for wings already. In that case, let us write a new file to
// it so files can be rotated easily.
f, err := os.Create("/etc/logrotate.d/wings")
if err != nil {
return err
}
defer f.Close()
t, err := template.New("logrotate").Parse(`
{{.LogDirectory}}/wings.log {
size 10M
compress
delaycompress
dateext
maxage 7
missingok
notifempty
create 0640 {{.User.Uid}} {{.User.Gid}}
postrotate
killall -SIGHUP wings
endscript
}`)
if err != nil {
return err
}
return errors.WithMessage(t.Execute(f, sc), "failed to write logrotate file to disk")
}
// Returns the location of the JSON file that tracks server states. // Returns the location of the JSON file that tracks server states.
func (sc *SystemConfiguration) GetStatesPath() string { func (sc *SystemConfiguration) GetStatesPath() string {
return path.Join(sc.RootDirectory, "states.json") return path.Join(sc.RootDirectory, "states.json")
@@ -81,3 +222,47 @@ func (sc *SystemConfiguration) GetStatesPath() string {
func (sc *SystemConfiguration) GetInstallLogPath() string { func (sc *SystemConfiguration) GetInstallLogPath() string {
return path.Join(sc.LogDirectory, "install/") return path.Join(sc.LogDirectory, "install/")
} }
// Configures the timezone data for the configuration if it is currently missing. If
// a value has been set, this functionality will only run to validate that the timezone
// being used is valid.
func (sc *SystemConfiguration) ConfigureTimezone() error {
if sc.Timezone == "" {
if b, err := ioutil.ReadFile("/etc/timezone"); err != nil {
if !os.IsNotExist(err) {
return errors.WithMessage(err, "failed to open /etc/timezone for automatic server timezone calibration")
}
ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
// Okay, file isn't found on this OS, we will try using timedatectl to handle this. If this
// command fails, exit, but if it returns a value use that. If no value is returned we will
// fall through to UTC to get Wings booted at least.
out, err := exec.CommandContext(ctx, "timedatectl").Output()
if err != nil {
log.WithField("error", err).Warn("failed to execute \"timedatectl\" to determine system timezone, falling back to UTC")
sc.Timezone = "UTC"
return nil
}
r := regexp.MustCompile(`Time zone: ([\w/]+)`)
matches := r.FindSubmatch(out)
if len(matches) != 2 || string(matches[1]) == "" {
log.Warn("failed to parse timezone from \"timedatectl\" output, falling back to UTC")
sc.Timezone = "UTC"
return nil
}
sc.Timezone = string(matches[1])
} else {
sc.Timezone = string(b)
}
}
sc.Timezone = regexp.MustCompile(`(?i)[^a-z_/]+`).ReplaceAllString(sc.Timezone, "")
_, err := time.LoadLocation(sc.Timezone)
return errors.WithMessage(err, fmt.Sprintf("the supplied timezone %s is invalid", sc.Timezone))
}

View File

@@ -1,23 +1,27 @@
package config package config
type ConsoleThrottles struct { type ConsoleThrottles struct {
// Wether or not the throttler is enabled for this instance. // Whether or not the throttler is enabled for this instance.
Enabled bool `json:"enabled" yaml:"enabled" default:"true"` Enabled bool `json:"enabled" yaml:"enabled" default:"true"`
// The total number of throttle activations that must accumulate before a server is // The total number of lines that can be output in a given LineResetInterval period before
// forcibly stopped for violating these limits.
KillAtCount uint64 `json:"kill_at_count" yaml:"kill_at_count" default:"5"`
// The amount of time in milliseconds that a server process must go through without
// triggering an output warning before the throttle activation count begins decreasing.
// This time is measured in milliseconds.
Decay uint64 `json:"decay" yaml:"decay" default:"10000"`
// The total number of lines that can be output in a given CheckInterval period before
// a warning is triggered and counted against the server. // a warning is triggered and counted against the server.
Lines uint64 `json:"lines" yaml:"lines" default:"1000"` Lines uint64 `json:"lines" yaml:"lines" default:"2000"`
// The amount of time that must pass between intervals before the count is reset. This // The total number of throttle activations that can accumulate before a server is considered
// value is in milliseconds. // to be breaching and will be stopped. This value is decremented by one every DecayInterval.
CheckInterval uint64 `json:"check_interval" yaml:"check_interval" default:"100"` MaximumTriggerCount uint64 `json:"maximum_trigger_count" yaml:"maximum_trigger_count" default:"5"`
}
// The amount of time after which the number of lines processed is reset to 0. This runs in
// a constant loop and is not affected by the current console output volumes. By default, this
// will reset the processed line count back to 0 every 100ms.
LineResetInterval uint64 `json:"line_reset_interval" yaml:"line_reset_interval" default:"100"`
// The amount of time in milliseconds that must pass without an output warning being triggered
// before a throttle activation is decremented.
DecayInterval uint64 `json:"decay_interval" yaml:"decay_interval" default:"10000"`
// The amount of time that a server is allowed to be stopping for before it is terminated
// forcefully if it triggers output throttles.
StopGracePeriod uint `json:"stop_grace_period" yaml:"stop_grace_period" default:"15"`
}

View File

@@ -1,26 +1,38 @@
version: '3' version: '3.8'
services: services:
daemon: wings:
build: . image: ghcr.io/pterodactyl/wings:latest
restart: always restart: always
hostname: daemon networks:
- wings0
ports: ports:
- "8080:8080" - "8080:8080"
- "2022:2022" - "2022:2022"
tty: true tty: true
environment: environment:
- "DEBUG=false" TZ: "UTC"
WINGS_UID: 988
WINGS_GID: 988
WINGS_USERNAME: pterodactyl
volumes: volumes:
- "/var/run/docker.sock:/var/run/docker.sock" - "/var/run/docker.sock:/var/run/docker.sock"
- "/var/lib/docker/containers/:/var/lib/docker/containers/" - "/var/lib/docker/containers/:/var/lib/docker/containers/"
- "/etc/pterodactyl/:/etc/pterodactyl/"
- "/var/lib/pterodactyl/:/var/lib/pterodactyl/" - "/var/lib/pterodactyl/:/var/lib/pterodactyl/"
- "/srv/daemon-data/:/srv/daemon-data/" - "/var/log/pterodactyl/:/var/log/pterodactyl/"
- "/tmp/pterodactyl/:/tmp/pterodactyl/" - "/tmp/pterodactyl/:/tmp/pterodactyl/"
- "/etc/timezone:/etc/timezone:ro" # you may need /srv/daemon-data if you are upgrading from an old daemon
## Required for ssl if you user let's encrypt. uncomment to use. #- "/srv/daemon-data/:/srv/daemon-data/"
## - "/etc/letsencrypt/:/etc/letsencrypt/" # Required for ssl if you use let's encrypt. uncomment to use.
#- "/etc/letsencrypt/:/etc/letsencrypt/"
networks: networks:
default: wings0:
name: wings0
driver: bridge
ipam: ipam:
config: config:
- subnet: 172.21.0.0/16 - subnet: "172.21.0.0/16"
driver_opts:
com.docker.network.bridge.name: wings0

View File

@@ -2,8 +2,10 @@ package environment
import ( import (
"fmt" "fmt"
"github.com/docker/go-connections/nat"
"strconv" "strconv"
"github.com/docker/go-connections/nat"
"github.com/pterodactyl/wings/config"
) )
// Defines the allocations available for a given server. When using the Docker environment // Defines the allocations available for a given server. When using the Docker environment
@@ -25,6 +27,8 @@ type Allocations struct {
// Converts the server allocation mappings into a format that can be understood by Docker. While // Converts the server allocation mappings into a format that can be understood by Docker. While
// we do strive to support multiple environments, using Docker's standardized format for the // we do strive to support multiple environments, using Docker's standardized format for the
// bindings certainly makes life a little easier for managing things. // bindings certainly makes life a little easier for managing things.
//
// You'll want to use DockerBindings() if you need to re-map 127.0.0.1 to the Docker interface.
func (a *Allocations) Bindings() nat.PortMap { func (a *Allocations) Bindings() nat.PortMap {
var out = nat.PortMap{} var out = nat.PortMap{}
@@ -35,15 +39,47 @@ func (a *Allocations) Bindings() nat.PortMap {
continue continue
} }
binding := []nat.PortBinding{ binding := nat.PortBinding{
{ HostIP: ip,
HostIP: ip, HostPort: strconv.Itoa(port),
HostPort: strconv.Itoa(port),
},
} }
out[nat.Port(fmt.Sprintf("%d/tcp", port))] = binding tcp := nat.Port(fmt.Sprintf("%d/tcp", port))
out[nat.Port(fmt.Sprintf("%d/udp", port))] = binding udp := nat.Port(fmt.Sprintf("%d/udp", port))
out[tcp] = append(out[tcp], binding)
out[udp] = append(out[udp], binding)
}
}
return out
}
// Returns the bindings for the server in a way that is supported correctly by Docker. This replaces
// any reference to 127.0.0.1 with the IP of the pterodactyl0 network interface which will allow the
// server to operate on a local address while still being accessible by other containers.
func (a *Allocations) DockerBindings() nat.PortMap {
iface := config.Get().Docker.Network.Interface
out := a.Bindings()
// Loop over all of the bindings for this container, and convert any that reference 127.0.0.1
// to use the pterodactyl0 network interface IP, as that is the true local for what people are
// trying to do when creating servers.
for p, binds := range out {
for i, alloc := range binds {
if alloc.HostIP != "127.0.0.1" {
continue
}
// If using ISPN just delete the local allocation from the server.
if config.Get().Docker.Network.ISPN {
out[p] = append(out[p][:i], out[p][i+1:]...)
} else {
out[p][i] = nat.PortBinding{
HostIP: iface,
HostPort: alloc.HostPort,
}
}
} }
} }
@@ -54,14 +90,14 @@ func (a *Allocations) Bindings() nat.PortMap {
// by Docker. This formatting is slightly different than "Bindings" as it should // by Docker. This formatting is slightly different than "Bindings" as it should
// return an empty struct rather than a binding. // return an empty struct rather than a binding.
// //
// To accomplish this, we'll just get the values from "Bindings" and then set them // To accomplish this, we'll just get the values from "DockerBindings" and then set them
// to empty structs. Because why not. // to empty structs. Because why not.
func (a *Allocations) Exposed() nat.PortSet { func (a *Allocations) Exposed() nat.PortSet {
var out = nat.PortSet{} var out = nat.PortSet{}
for port := range a.Bindings() { for port := range a.DockerBindings() {
out[port] = struct{}{} out[port] = struct{}{}
} }
return out return out
} }

View File

@@ -1,17 +1,13 @@
package environment package environment
import ( import (
"fmt"
"strings"
"sync" "sync"
"time"
) )
type configurationSettings struct { type Settings struct {
Mounts []Mount Mounts []Mount
Allocations Allocations Allocations Allocations
Limits Limits Limits Limits
Variables Variables
} }
// Defines the actual configuration struct for the environment with all of the settings // Defines the actual configuration struct for the environment with all of the settings
@@ -19,20 +15,36 @@ type configurationSettings struct {
type Configuration struct { type Configuration struct {
mu sync.RWMutex mu sync.RWMutex
settings configurationSettings environmentVariables []string
settings Settings
} }
func NewConfiguration(m []Mount, a Allocations, l Limits, v Variables) *Configuration { // Returns a new environment configuration with the given settings and environment variables
// defined within it.
func NewConfiguration(s Settings, envVars []string) *Configuration {
return &Configuration{ return &Configuration{
settings: configurationSettings{ environmentVariables: envVars,
Mounts: m, settings: s,
Allocations: a,
Limits: l,
Variables: v,
},
} }
} }
// Updates the settings struct for this environment on the fly. This allows modified servers to
// automatically push those changes to the environment.
func (c *Configuration) SetSettings(s Settings) {
c.mu.Lock()
c.settings = s
c.mu.Unlock()
}
// Updates the environment variables associated with this environment by replacing the entire
// array of them with a new one.
func (c *Configuration) SetEnvironmentVariables(ev []string) {
c.mu.Lock()
c.environmentVariables = ev
c.mu.Unlock()
}
// Returns the limits assigned to this environment.
func (c *Configuration) Limits() Limits { func (c *Configuration) Limits() Limits {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
@@ -40,6 +52,7 @@ func (c *Configuration) Limits() Limits {
return c.settings.Limits return c.settings.Limits
} }
// Returns the allocations associated with this environment.
func (c *Configuration) Allocations() Allocations { func (c *Configuration) Allocations() Allocations {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
@@ -47,6 +60,7 @@ func (c *Configuration) Allocations() Allocations {
return c.settings.Allocations return c.settings.Allocations
} }
// Returns all of the mounts associated with this environment.
func (c *Configuration) Mounts() []Mount { func (c *Configuration) Mounts() []Mount {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
@@ -54,31 +68,10 @@ func (c *Configuration) Mounts() []Mount {
return c.settings.Mounts return c.settings.Mounts
} }
// Returns all of the environment variables that should be assigned to a running // Returns the environment variables associated with this instance.
// server instance.
func (c *Configuration) EnvironmentVariables() []string { func (c *Configuration) EnvironmentVariables() []string {
c.mu.RLock() c.mu.RLock()
c.mu.RUnlock() defer c.mu.RUnlock()
zone, _ := time.Now().In(time.Local).Zone() return c.environmentVariables
var out = []string{
fmt.Sprintf("TZ=%s", zone),
fmt.Sprintf("SERVER_MEMORY=%d", c.settings.Limits.MemoryLimit),
fmt.Sprintf("SERVER_IP=%s", c.settings.Allocations.DefaultMapping.Ip),
fmt.Sprintf("SERVER_PORT=%d", c.settings.Allocations.DefaultMapping.Port),
}
eloop:
for k := range c.settings.Variables {
for _, e := range out {
if strings.HasPrefix(e, strings.ToUpper(k)) {
continue eloop
}
}
out = append(out, fmt.Sprintf("%s=%s", strings.ToUpper(k), c.settings.Variables.Get(k)))
}
return out
} }

View File

@@ -2,6 +2,9 @@ package environment
import ( import (
"context" "context"
"strconv"
"sync"
"github.com/apex/log" "github.com/apex/log"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@@ -10,10 +13,28 @@ import (
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
) )
var _cmu sync.Mutex
var _client *client.Client
// Return a Docker client to be used throughout the codebase. Once a client has been created it
// will be returned for all subsequent calls to this function.
func DockerClient() (*client.Client, error) {
_cmu.Lock()
defer _cmu.Unlock()
if _client != nil {
return _client, nil
}
_client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
return _client, err
}
// Configures the required network for the docker environment. // Configures the required network for the docker environment.
func ConfigureDocker(c *config.DockerConfiguration) error { func ConfigureDocker(c *config.DockerConfiguration) error {
// Ensure the required docker network exists on the system. // Ensure the required docker network exists on the system.
cli, err := client.NewClientWithOpts(client.FromEnv) cli, err := DockerClient()
if err != nil { if err != nil {
return err return err
} }
@@ -64,7 +85,7 @@ func createDockerNetwork(cli *client.Client, c *config.DockerConfiguration) erro
Options: map[string]string{ Options: map[string]string{
"encryption": "false", "encryption": "false",
"com.docker.network.bridge.default_bridge": "false", "com.docker.network.bridge.default_bridge": "false",
"com.docker.network.bridge.enable_icc": "true", "com.docker.network.bridge.enable_icc": strconv.FormatBool(c.Network.EnableICC),
"com.docker.network.bridge.enable_ip_masquerade": "true", "com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "pterodactyl0", "com.docker.network.bridge.name": "pterodactyl0",

View File

@@ -1,20 +0,0 @@
package docker
import "io"
type Console struct {
HandlerFunc *func(string)
}
var _ io.Writer = Console{}
func (c Console) Write(b []byte) (int, error) {
if c.HandlerFunc != nil {
l := make([]byte, len(b))
copy(l, b)
(*c.HandlerFunc)(string(l))
}
return len(b), nil
}

View File

@@ -3,6 +3,8 @@ package docker
import ( import (
"bufio" "bufio"
"context" "context"
"emperror.dev/errors"
"encoding/json"
"fmt" "fmt"
"github.com/apex/log" "github.com/apex/log"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@@ -10,26 +12,47 @@ import (
"github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/docker/daemon/logger/jsonfilelog" "github.com/docker/docker/daemon/logger/jsonfilelog"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/system"
"io" "io"
"strconv" "strconv"
"strings" "strings"
"time" "time"
) )
type imagePullStatus struct {
Status string `json:"status"`
Progress string `json:"progress"`
}
// A custom console writer that allows us to keep a function blocked until the
// given stream is properly closed. This does nothing special, only exists to
// make a noop io.Writer.
type noopWriter struct{}
var _ io.Writer = noopWriter{}
// Implement the required Write function to satisfy the io.Writer interface.
func (nw noopWriter) Write(b []byte) (int, error) {
return len(b), nil
}
// Attaches to the docker container itself and ensures that we can pipe data in and out // Attaches to the docker container itself and ensures that we can pipe data in and out
// of the process stream. This should not be used for reading console data as you *will* // of the process stream. This should not be used for reading console data as you *will*
// miss important output at the beginning because of the time delay with attaching to the // miss important output at the beginning because of the time delay with attaching to the
// output. // output.
//
// Calling this function will poll resources for the container in the background until the
// provided context is canceled by the caller. Failure to cancel said context will cause
// background memory leaks as the goroutine will not exit.
func (e *Environment) Attach() error { func (e *Environment) Attach() error {
if e.IsAttached() { if e.IsAttached() {
return nil return nil
} }
if err := e.followOutput(); err != nil { if err := e.followOutput(); err != nil {
return errors.WithStack(err) return err
} }
opts := types.ContainerAttachOptions{ opts := types.ContainerAttachOptions{
@@ -41,32 +64,46 @@ func (e *Environment) Attach() error {
// Set the stream again with the container. // Set the stream again with the container.
if st, err := e.client.ContainerAttach(context.Background(), e.Id, opts); err != nil { if st, err := e.client.ContainerAttach(context.Background(), e.Id, opts); err != nil {
return errors.WithStack(err) return err
} else { } else {
e.SetStream(&st) e.SetStream(&st)
} }
c := new(Console) go func() {
go func(console *Console) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
defer e.stream.Close() defer e.stream.Close()
defer func() { defer func() {
e.setState(environment.ProcessOfflineState) e.SetState(environment.ProcessOfflineState)
e.SetStream(nil) e.SetStream(nil)
}() }()
// Poll resources in a seperate thread since this will block the copy call below go func() {
// from being reached until it is completed if not run in a seperate process. However, if err := e.pollResources(ctx); err != nil {
// we still want it to be stopped when the copy operation below is finished running which if !errors.Is(err, context.Canceled) {
// indicates that the container is no longer running. e.log().WithField("error", err).Error("error during environment resource polling")
go e.pollResources(ctx) } else {
e.log().Warn("stopping server resource polling: context canceled")
}
}
}()
// Stream the reader output to the console which will then fire off events and handle console // Block the completion of this routine until the container is no longer running. This allows
// throttling and sending the output to the user. // the pollResources function to run until it needs to be stopped. Because the container
_, _ = io.Copy(console, e.stream.Reader) // can be polled for resource usage, even when stopped, we need to have this logic present
}(c) // in order to cancel the context and therefore stop the routine that is spawned.
//
// For now, DO NOT use client#ContainerWait from the Docker package. There is a nasty
// bug causing containers to hang on deletion and cause servers to lock up on the system.
//
// This weird code isn't intuitive, but it keeps the function from ending until the container
// is stopped and therefore the stream reader ends up closed.
// @see https://github.com/moby/moby/issues/41827
c := new(noopWriter)
if _, err := io.Copy(c, e.stream.Reader); err != nil {
e.log().WithField("error", err).Error("could not copy from environment stream to noop writer")
}
}()
return nil return nil
} }
@@ -102,7 +139,7 @@ func (e *Environment) InSituUpdate() error {
return nil return nil
} }
return errors.WithStack(err) return err
} }
u := container.UpdateConfig{ u := container.UpdateConfig{
@@ -112,7 +149,7 @@ func (e *Environment) InSituUpdate() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel() defer cancel()
if _, err := e.client.ContainerUpdate(ctx, e.Id, u); err != nil { if _, err := e.client.ContainerUpdate(ctx, e.Id, u); err != nil {
return errors.WithStack(err) return err
} }
return nil return nil
@@ -127,16 +164,25 @@ func (e *Environment) Create() error {
if _, err := e.client.ContainerInspect(context.Background(), e.Id); err == nil { if _, err := e.client.ContainerInspect(context.Background(), e.Id); err == nil {
return nil return nil
} else if !client.IsErrNotFound(err) { } else if !client.IsErrNotFound(err) {
return errors.WithStack(err) return err
} }
// Try to pull the requested image before creating the container. // Try to pull the requested image before creating the container.
if err := e.ensureImageExists(e.meta.Image); err != nil { if err := e.ensureImageExists(e.meta.Image); err != nil {
return errors.WithStack(err) return err
} }
a := e.Configuration.Allocations() a := e.Configuration.Allocations()
evs := e.Configuration.EnvironmentVariables()
for i, v := range evs {
// Convert 127.0.0.1 to the pterodactyl0 network interface if the environment is Docker
// so that the server operates as expected.
if v == "SERVER_IP=127.0.0.1" {
evs[i] = "SERVER_IP=" + config.Get().Docker.Network.Interface
}
}
conf := &container.Config{ conf := &container.Config{
Hostname: e.Id, Hostname: e.Id,
Domainname: config.Get().Docker.Domainname, Domainname: config.Get().Docker.Domainname,
@@ -148,7 +194,7 @@ func (e *Environment) Create() error {
Tty: true, Tty: true,
ExposedPorts: a.Exposed(), ExposedPorts: a.Exposed(),
Image: e.meta.Image, Image: e.meta.Image,
Env: e.variables(), Env: e.Configuration.EnvironmentVariables(),
Labels: map[string]string{ Labels: map[string]string{
"Service": "Pterodactyl", "Service": "Pterodactyl",
"ContainerType": "server_process", "ContainerType": "server_process",
@@ -158,16 +204,16 @@ func (e *Environment) Create() error {
tmpfsSize := strconv.Itoa(int(config.Get().Docker.TmpfsSize)) tmpfsSize := strconv.Itoa(int(config.Get().Docker.TmpfsSize))
hostConf := &container.HostConfig{ hostConf := &container.HostConfig{
PortBindings: a.Bindings(), PortBindings: a.DockerBindings(),
// Configure the mounts for this container. First mount the server data directory // Configure the mounts for this container. First mount the server data directory
// into the container as a r/w bine. // into the container as a r/w bind.
Mounts: e.convertMounts(), Mounts: e.convertMounts(),
// Configure the /tmp folder mapping in containers. This is necessary for some // Configure the /tmp folder mapping in containers. This is necessary for some
// games that need to make use of it for downloads and other installation processes. // games that need to make use of it for downloads and other installation processes.
Tmpfs: map[string]string{ Tmpfs: map[string]string{
"/tmp": "rw,exec,nosuid,size="+tmpfsSize+"M", "/tmp": "rw,exec,nosuid,size=" + tmpfsSize + "M",
}, },
// Define resource limits for the container based on the data passed through // Define resource limits for the container based on the data passed through
@@ -185,6 +231,7 @@ func (e *Environment) Create() error {
Config: map[string]string{ Config: map[string]string{
"max-size": "5m", "max-size": "5m",
"max-file": "1", "max-file": "1",
"compress": "false",
}, },
}, },
@@ -197,19 +244,13 @@ func (e *Environment) Create() error {
NetworkMode: container.NetworkMode(config.Get().Docker.Network.Mode), NetworkMode: container.NetworkMode(config.Get().Docker.Network.Mode),
} }
if _, err := e.client.ContainerCreate(context.Background(), conf, hostConf, nil, e.Id); err != nil { if _, err := e.client.ContainerCreate(context.Background(), conf, hostConf, nil, nil, e.Id); err != nil {
return errors.WithStack(err) return err
} }
return nil return nil
} }
func (e *Environment) variables() []string {
v := e.Configuration.EnvironmentVariables()
return append(v, fmt.Sprintf("STARTUP=%s", e.meta.Invocation))
}
func (e *Environment) convertMounts() []mount.Mount { func (e *Environment) convertMounts() []mount.Mount {
var out []mount.Mount var out []mount.Mount
@@ -228,8 +269,8 @@ func (e *Environment) convertMounts() []mount.Mount {
// Remove the Docker container from the machine. If the container is currently running // Remove the Docker container from the machine. If the container is currently running
// it will be forcibly stopped by Docker. // it will be forcibly stopped by Docker.
func (e *Environment) Destroy() error { func (e *Environment) Destroy() error {
// We set it to stopping than offline to prevent crash detection from being triggeree. // We set it to stopping than offline to prevent crash detection from being triggered.
e.setState(environment.ProcessStoppingState) e.SetState(environment.ProcessStoppingState)
err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{ err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{
RemoveVolumes: true, RemoveVolumes: true,
@@ -237,6 +278,8 @@ func (e *Environment) Destroy() error {
Force: true, Force: true,
}) })
e.SetState(environment.ProcessOfflineState)
// Don't trigger a destroy failure if we try to delete a container that does not // Don't trigger a destroy failure if we try to delete a container that does not
// exist on the system. We're just a step ahead of ourselves in that case. // exist on the system. We're just a step ahead of ourselves in that case.
// //
@@ -245,20 +288,17 @@ func (e *Environment) Destroy() error {
return nil return nil
} }
e.setState(environment.ProcessOfflineState)
return err return err
} }
// Attaches to the log for the container. This avoids us missing cruicial output that // Attaches to the log for the container. This avoids us missing crucial output that
// happens in the split seconds before the code moves from 'Starting' to 'Attaching' // happens in the split seconds before the code moves from 'Starting' to 'Attaching'
// on the process. // on the process.
func (e *Environment) followOutput() error { func (e *Environment) followOutput() error {
if exists, err := e.Exists(); !exists { if exists, err := e.Exists(); !exists {
if err != nil { if err != nil {
return errors.WithStack(err) return err
} }
return errors.New(fmt.Sprintf("no such container: %s", e.Id)) return errors.New(fmt.Sprintf("no such container: %s", e.Id))
} }
@@ -270,21 +310,40 @@ func (e *Environment) followOutput() error {
} }
reader, err := e.client.ContainerLogs(context.Background(), e.Id, opts) reader, err := e.client.ContainerLogs(context.Background(), e.Id, opts)
if err != nil {
return err
}
go func(r io.ReadCloser) { go e.scanOutput(reader)
defer r.Close()
s := bufio.NewScanner(r) return nil
for s.Scan() { }
e.Events().Publish(environment.ConsoleOutputEvent, s.Text())
}
if err := s.Err(); err != nil { func (e *Environment) scanOutput(reader io.ReadCloser) {
log.WithField("error", err).WithField("container_id", e.Id).Warn("error processing scanner line in console output") defer reader.Close()
}
}(reader)
return errors.WithStack(err) events := e.Events()
err := system.ScanReader(reader, func(line string) {
events.Publish(environment.ConsoleOutputEvent, line)
})
if err != nil && err != io.EOF {
log.WithField("error", err).WithField("container_id", e.Id).Warn("error processing scanner line in console output")
return
}
// Return here if the server is offline or currently stopping.
if e.State() == environment.ProcessStoppingState || e.State() == environment.ProcessOfflineState {
return
}
// Close the current reader before starting a new one, the defer will still run
// but it will do nothing if we already closed the stream.
_ = reader.Close()
// Start following the output of the server again.
go e.followOutput()
} }
// Pulls the image from Docker. If there is an error while pulling the image from the source // Pulls the image from Docker. If there is an error while pulling the image from the source
@@ -295,9 +354,15 @@ func (e *Environment) followOutput() error {
// need to block all of the servers from booting just because of that. I'd imagine in a lot of // need to block all of the servers from booting just because of that. I'd imagine in a lot of
// cases an outage shouldn't affect users too badly. It'll at least keep existing servers working // cases an outage shouldn't affect users too badly. It'll at least keep existing servers working
// correctly if anything. // correctly if anything.
//
// TODO: handle authorization & local images
func (e *Environment) ensureImageExists(image string) error { func (e *Environment) ensureImageExists(image string) error {
e.Events().Publish(environment.DockerImagePullStarted, "")
defer e.Events().Publish(environment.DockerImagePullCompleted, "")
// Images prefixed with a ~ are local images that we do not need to try and pull.
if strings.HasPrefix(image, "~") {
return nil
}
// Give it up to 15 minutes to pull the image. I think this should cover 99.8% of cases where an // Give it up to 15 minutes to pull the image. I think this should cover 99.8% of cases where an
// image pull might fail. I can't imagine it will ever take more than 15 minutes to fully pull // image pull might fail. I can't imagine it will ever take more than 15 minutes to fully pull
// an image. Let me know when I am inevitably wrong here... // an image. Let me know when I am inevitably wrong here...
@@ -346,7 +411,7 @@ func (e *Environment) ensureImageExists(image string) error {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"image": image, "image": image,
"container_id": e.Id, "container_id": e.Id,
"error": errors.New(err.Error()), "err": err.Error(),
}).Warn("unable to pull requested image from remote source, however the image exists locally") }).Warn("unable to pull requested image from remote source, however the image exists locally")
// Okay, we found a matching container image, in that case just go ahead and return // Okay, we found a matching container image, in that case just go ahead and return
@@ -362,15 +427,23 @@ func (e *Environment) ensureImageExists(image string) error {
log.WithField("image", image).Debug("pulling docker image... this could take a bit of time") log.WithField("image", image).Debug("pulling docker image... this could take a bit of time")
// I'm not sure what the best approach here is, but this will block execution until the image // I'm not sure what the best approach here is, but this will block execution until the image
// is done being pulled, which is what we neee. // is done being pulled, which is what we need.
scanner := bufio.NewScanner(out) scanner := bufio.NewScanner(out)
for scanner.Scan() { for scanner.Scan() {
continue s := imagePullStatus{}
fmt.Println(scanner.Text())
if err := json.Unmarshal(scanner.Bytes(), &s); err == nil {
e.Events().Publish(environment.DockerImagePullStatus, s.Status+" "+s.Progress)
}
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return err return err
} }
log.WithField("image", image).Debug("completed docker image pull")
return nil return nil
} }

View File

@@ -2,20 +2,20 @@ package docker
import ( import (
"context" "context"
"github.com/apex/log"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/events" "github.com/pterodactyl/wings/events"
"github.com/pterodactyl/wings/system"
"io" "io"
"sync" "sync"
) )
type Metadata struct { type Metadata struct {
Invocation string Image string
Image string Stop api.ProcessStopConfiguration
Stop *api.ProcessStopConfiguration
} }
// Ensure that the Docker environment is always implementing all of the methods // Ensure that the Docker environment is always implementing all of the methods
@@ -24,7 +24,7 @@ var _ environment.ProcessEnvironment = (*Environment)(nil)
type Environment struct { type Environment struct {
mu sync.RWMutex mu sync.RWMutex
eventMu sync.Mutex eventMu sync.Once
// The public identifier for this environment. In this case it is the Docker container // The public identifier for this environment. In this case it is the Docker container
// name that will be used for all instances created under it. // name that will be used for all instances created under it.
@@ -48,15 +48,14 @@ type Environment struct {
emitter *events.EventBus emitter *events.EventBus
// Tracks the environment state. // Tracks the environment state.
st string st *system.AtomicString
stMu sync.RWMutex
} }
// Creates a new base Docker environment. The ID passed through will be the ID that is used to // Creates a new base Docker environment. The ID passed through will be the ID that is used to
// reference the container from here on out. This should be unique per-server (we use the UUID // reference the container from here on out. This should be unique per-server (we use the UUID
// by default). The container does not need to exist at this point. // by default). The container does not need to exist at this point.
func New(id string, m *Metadata, c *environment.Configuration) (*Environment, error) { func New(id string, m *Metadata, c *environment.Configuration) (*Environment, error) {
cli, err := client.NewClientWithOpts(client.FromEnv) cli, err := environment.DockerClient()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -66,15 +65,14 @@ func New(id string, m *Metadata, c *environment.Configuration) (*Environment, er
Configuration: c, Configuration: c,
meta: m, meta: m,
client: cli, client: cli,
st: system.NewAtomicString(environment.ProcessOfflineState),
} }
return e, nil return e, nil
} }
func (e *Environment) SetStopConfiguration(c *api.ProcessStopConfiguration) { func (e *Environment) log() *log.Entry {
e.mu.Lock() return log.WithField("environment", e.Type()).WithField("container_id", e.Id)
e.meta.Stop = c
e.mu.Unlock()
} }
func (e *Environment) Type() string { func (e *Environment) Type() string {
@@ -84,8 +82,9 @@ func (e *Environment) Type() string {
// Set if this process is currently attached to the process. // Set if this process is currently attached to the process.
func (e *Environment) SetStream(s *types.HijackedResponse) { func (e *Environment) SetStream(s *types.HijackedResponse) {
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock()
e.stream = s e.stream = s
e.mu.Unlock()
} }
// Determine if the this process is currently attached to the container. // Determine if the this process is currently attached to the container.
@@ -97,12 +96,9 @@ func (e *Environment) IsAttached() bool {
} }
func (e *Environment) Events() *events.EventBus { func (e *Environment) Events() *events.EventBus {
e.eventMu.Lock() e.eventMu.Do(func() {
defer e.eventMu.Unlock()
if e.emitter == nil {
e.emitter = events.New() e.emitter = events.New()
} })
return e.emitter return e.emitter
} }
@@ -110,7 +106,7 @@ func (e *Environment) Events() *events.EventBus {
// Determines if the container exists in this environment. The ID passed through should be the // Determines if the container exists in this environment. The ID passed through should be the
// server UUID since containers are created utilizing the server UUID as the name and docker // server UUID since containers are created utilizing the server UUID as the name and docker
// will work fine when using the container name as the lookup parameter in addition to the longer // will work fine when using the container name as the lookup parameter in addition to the longer
// ID auto-assigned when the container is createe. // ID auto-assigned when the container is created.
func (e *Environment) Exists() (bool, error) { func (e *Environment) Exists() (bool, error) {
_, err := e.client.ContainerInspect(context.Background(), e.Id) _, err := e.client.ContainerInspect(context.Background(), e.Id)
@@ -144,7 +140,7 @@ func (e *Environment) IsRunning() (bool, error) {
return c.State.Running, nil return c.State.Running, nil
} }
// Determine the container exit state and return the exit code and wether or not // Determine the container exit state and return the exit code and whether or not
// the container was killed by the OOM killer. // the container was killed by the OOM killer.
func (e *Environment) ExitState() (uint32, bool, error) { func (e *Environment) ExitState() (uint32, bool, error) {
c, err := e.client.ContainerInspect(context.Background(), e.Id) c, err := e.client.ContainerInspect(context.Background(), e.Id)
@@ -155,15 +151,39 @@ func (e *Environment) ExitState() (uint32, bool, error) {
// //
// However, someone reported an error in Discord about this scenario happening, // However, someone reported an error in Discord about this scenario happening,
// so I guess this should prevent it? They didn't tell me how they caused it though // so I guess this should prevent it? They didn't tell me how they caused it though
// so that's a mystery that will have to go unsolvee. // so that's a mystery that will have to go unsolved.
// //
// @see https://github.com/pterodactyl/panel/issues/2003 // @see https://github.com/pterodactyl/panel/issues/2003
if client.IsErrNotFound(err) { if client.IsErrNotFound(err) {
return 1, false, nil return 1, false, nil
} }
return 0, false, errors.WithStack(err) return 0, false, err
} }
return uint32(c.State.ExitCode), c.State.OOMKilled, nil return uint32(c.State.ExitCode), c.State.OOMKilled, nil
} }
// Returns the environment configuration allowing a process to make modifications of the
// environment on the fly.
func (e *Environment) Config() *environment.Configuration {
e.mu.RLock()
defer e.mu.RUnlock()
return e.Configuration
}
// Sets the stop configuration for the environment.
func (e *Environment) SetStopConfiguration(c api.ProcessStopConfiguration) {
e.mu.Lock()
defer e.mu.Unlock()
e.meta.Stop = c
}
func (e *Environment) SetImage(i string) {
e.mu.Lock()
defer e.mu.Unlock()
e.meta.Image = i
}

View File

@@ -2,11 +2,11 @@ package docker
import ( import (
"context" "context"
"emperror.dev/errors"
"github.com/apex/log" "github.com/apex/log"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"os" "os"
@@ -20,13 +20,12 @@ import (
// //
// This process will also confirm that the server environment exists and is in a bootable // This process will also confirm that the server environment exists and is in a bootable
// state. This ensures that unexpected container deletion while Wings is running does // state. This ensures that unexpected container deletion while Wings is running does
// not result in the server becoming unbootable. // not result in the server becoming un-bootable.
func (e *Environment) OnBeforeStart() error { func (e *Environment) OnBeforeStart() error {
// Always destroy and re-create the server container to ensure that synced data from // Always destroy and re-create the server container to ensure that synced data from the Panel is used.
// the Panel is usee.
if err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil { if err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil {
if !client.IsErrNotFound(err) { if !client.IsErrNotFound(err) {
return err return errors.WithMessage(err, "failed to remove server docker container during pre-boot")
} }
} }
@@ -35,7 +34,7 @@ func (e *Environment) OnBeforeStart() error {
// container and data storage directory. // container and data storage directory.
// //
// This won't actually run an installation process however, it is just here to ensure the // This won't actually run an installation process however, it is just here to ensure the
// environment gets created properly if it is missing and the server is startee. We're making // environment gets created properly if it is missing and the server is started. We're making
// an assumption that all of the files will still exist at this point. // an assumption that all of the files will still exist at this point.
if err := e.Create(); err != nil { if err := e.Create(); err != nil {
return err return err
@@ -49,6 +48,7 @@ func (e *Environment) OnBeforeStart() error {
// call to OnBeforeStart(). // call to OnBeforeStart().
func (e *Environment) Start() error { func (e *Environment) Start() error {
sawError := false sawError := false
// If sawError is set to true there was an error somewhere in the pipeline that // If sawError is set to true there was an error somewhere in the pipeline that
// got passed up, but we also want to ensure we set the server to be offline at // got passed up, but we also want to ensure we set the server to be offline at
// that point. // that point.
@@ -57,24 +57,24 @@ func (e *Environment) Start() error {
// If we don't set it to stopping first, you'll trigger crash detection which // If we don't set it to stopping first, you'll trigger crash detection which
// we don't want to do at this point since it'll just immediately try to do the // we don't want to do at this point since it'll just immediately try to do the
// exact same action that lead to it crashing in the first place... // exact same action that lead to it crashing in the first place...
e.setState(environment.ProcessStoppingState) e.SetState(environment.ProcessStoppingState)
e.setState(environment.ProcessOfflineState) e.SetState(environment.ProcessOfflineState)
} }
}() }()
if c, err := e.client.ContainerInspect(context.Background(), e.Id); err != nil { if c, err := e.client.ContainerInspect(context.Background(), e.Id); err != nil {
// Do nothing if the container is not found, we just don't want to continue // Do nothing if the container is not found, we just don't want to continue
// to the next block of code here. This check was inlined here to guard againt // to the next block of code here. This check was inlined here to guard against
// a nil-pointer when checking c.State below. // a nil-pointer when checking c.State below.
// //
// @see https://github.com/pterodactyl/panel/issues/2000 // @see https://github.com/pterodactyl/panel/issues/2000
if !client.IsErrNotFound(err) { if !client.IsErrNotFound(err) {
return errors.WithStack(err) return err
} }
} else { } else {
// If the server is running update our internal state and continue on with the attach. // If the server is running update our internal state and continue on with the attach.
if c.State.Running { if c.State.Running {
e.setState(environment.ProcessRunningState) e.SetState(environment.ProcessRunningState)
return e.Attach() return e.Attach()
} }
@@ -84,12 +84,12 @@ func (e *Environment) Start() error {
// to truncate them. // to truncate them.
if _, err := os.Stat(c.LogPath); err == nil { if _, err := os.Stat(c.LogPath); err == nil {
if err := os.Truncate(c.LogPath, 0); err != nil { if err := os.Truncate(c.LogPath, 0); err != nil {
return errors.WithStack(err) return err
} }
} }
} }
e.setState(environment.ProcessStartingState) e.SetState(environment.ProcessStartingState)
// Set this to true for now, we will set it to false once we reach the // Set this to true for now, we will set it to false once we reach the
// end of this chain. // end of this chain.
@@ -99,14 +99,14 @@ func (e *Environment) Start() error {
// exists on the system, and rebuild the container if that is required for server booting to // exists on the system, and rebuild the container if that is required for server booting to
// occur. // occur.
if err := e.OnBeforeStart(); err != nil { if err := e.OnBeforeStart(); err != nil {
return errors.WithStack(err) return err
} }
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel() defer cancel()
if err := e.client.ContainerStart(ctx, e.Id, types.ContainerStartOptions{}); err != nil { if err := e.client.ContainerStart(ctx, e.Id, types.ContainerStartOptions{}); err != nil {
return errors.WithStack(err) return err
} }
// No errors, good to continue through. // No errors, good to continue through.
@@ -126,9 +126,9 @@ func (e *Environment) Stop() error {
s := e.meta.Stop s := e.meta.Stop
e.mu.RUnlock() e.mu.RUnlock()
if s == nil || s.Type == api.ProcessStopSignal { if s.Type == "" || s.Type == api.ProcessStopSignal {
if s == nil { if s.Type == "" {
log.WithField("container_id", e.Id).Warn("no stop configuration detected for environment, using termination proceedure") log.WithField("container_id", e.Id).Warn("no stop configuration detected for environment, using termination procedure")
} }
return e.Terminate(os.Kill) return e.Terminate(os.Kill)
@@ -136,8 +136,8 @@ func (e *Environment) Stop() error {
// If the process is already offline don't switch it back to stopping. Just leave it how // If the process is already offline don't switch it back to stopping. Just leave it how
// it is and continue through to the stop handling for the process. // it is and continue through to the stop handling for the process.
if e.State() != environment.ProcessOfflineState { if e.st.Load() != environment.ProcessOfflineState {
e.setState(environment.ProcessStoppingState) e.SetState(environment.ProcessStoppingState)
} }
// Only attempt to send the stop command to the instance if we are actually attached to // Only attempt to send the stop command to the instance if we are actually attached to
@@ -147,13 +147,13 @@ func (e *Environment) Stop() error {
} }
t := time.Second * 30 t := time.Second * 30
err := e.client.ContainerStop(context.Background(), e.Id, &t)
if err != nil { if err := e.client.ContainerStop(context.Background(), e.Id, &t); err != nil {
// If the container does not exist just mark the process as stopped and return without // If the container does not exist just mark the process as stopped and return without
// an error. // an error.
if client.IsErrNotFound(err) { if client.IsErrNotFound(err) {
e.SetStream(nil) e.SetStream(nil)
e.setState(environment.ProcessOfflineState) e.SetState(environment.ProcessOfflineState)
return nil return nil
} }
@@ -169,7 +169,7 @@ func (e *Environment) Stop() error {
// will be terminated forcefully depending on the value of the second argument. // will be terminated forcefully depending on the value of the second argument.
func (e *Environment) WaitForStop(seconds uint, terminate bool) error { func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
if err := e.Stop(); err != nil { if err := e.Stop(); err != nil {
return errors.WithStack(err) return err
} }
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(seconds)*time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(seconds)*time.Second)
@@ -183,14 +183,27 @@ func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
case <-ctx.Done(): case <-ctx.Done():
if ctxErr := ctx.Err(); ctxErr != nil { if ctxErr := ctx.Err(); ctxErr != nil {
if terminate { if terminate {
log.WithField("container_id", e.Id).Info("server did not stop in time, executing process termination")
return e.Terminate(os.Kill) return e.Terminate(os.Kill)
} }
return errors.WithStack(ctxErr) return ctxErr
} }
case err := <-errChan: case err := <-errChan:
if err != nil { if err != nil {
return errors.WithStack(err) if terminate {
l := log.WithField("container_id", e.Id)
if errors.Is(err, context.DeadlineExceeded) {
l.Warn("deadline exceeded for container stop; terminating process")
} else {
l.WithField("error", err).Warn("error while waiting for container stop; terminating process")
}
return e.Terminate(os.Kill)
}
return err
} }
case <-ok: case <-ok:
} }
@@ -202,31 +215,31 @@ func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
func (e *Environment) Terminate(signal os.Signal) error { func (e *Environment) Terminate(signal os.Signal) error {
c, err := e.client.ContainerInspect(context.Background(), e.Id) c, err := e.client.ContainerInspect(context.Background(), e.Id)
if err != nil { if err != nil {
return errors.WithStack(err) return err
} }
if !c.State.Running { if !c.State.Running {
// If the container is not running but we're not already in a stopped state go ahead // If the container is not running but we're not already in a stopped state go ahead
// and update things to indicate we should be completely stopped now. Set to stopping // and update things to indicate we should be completely stopped now. Set to stopping
// first so crash detection is not triggered. // first so crash detection is not triggered.
if e.State() != environment.ProcessOfflineState { if e.st.Load() != environment.ProcessOfflineState {
e.setState(environment.ProcessStoppingState) e.SetState(environment.ProcessStoppingState)
e.setState(environment.ProcessOfflineState) e.SetState(environment.ProcessOfflineState)
} }
return nil return nil
} }
// We set it to stopping than offline to prevent crash detection from being triggeree. // We set it to stopping than offline to prevent crash detection from being triggered.
e.setState(environment.ProcessStoppingState) e.SetState(environment.ProcessStoppingState)
sig := strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed") sig := strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed")
if err := e.client.ContainerKill(context.Background(), e.Id, sig); err != nil { if err := e.client.ContainerKill(context.Background(), e.Id, sig); err != nil && !client.IsErrNotFound(err) {
return err return err
} }
e.setState(environment.ProcessOfflineState) e.SetState(environment.ProcessOfflineState)
return nil return nil
} }

View File

@@ -1,41 +1,29 @@
package docker package docker
import ( import (
"emperror.dev/errors"
"fmt" "fmt"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
) )
// Returns the current environment state.
func (e *Environment) State() string { func (e *Environment) State() string {
e.stMu.RLock() return e.st.Load()
defer e.stMu.RUnlock()
return e.st
} }
// Sets the state of the environment. This emits an event that server's can hook into to // Sets the state of the environment. This emits an event that server's can hook into to
// take their own actions and track their own state based on the environment. // take their own actions and track their own state based on the environment.
func (e *Environment) setState(state string) error { func (e *Environment) SetState(state string) {
if state != environment.ProcessOfflineState && if state != environment.ProcessOfflineState &&
state != environment.ProcessStartingState && state != environment.ProcessStartingState &&
state != environment.ProcessRunningState && state != environment.ProcessRunningState &&
state != environment.ProcessStoppingState { state != environment.ProcessStoppingState {
return errors.New(fmt.Sprintf("invalid server state received: %s", state)) panic(errors.New(fmt.Sprintf("invalid server state received: %s", state)))
} }
// Get the current state of the environment before changing it. // Emit the event to any listeners that are currently registered.
prevState := e.State() if e.State() != state {
// Emit the event to any listeners that are currently registeree.
if prevState != state {
// If the state changed make sure we update the internal tracking to note that. // If the state changed make sure we update the internal tracking to note that.
e.stMu.Lock() e.st.Store(state)
e.st = state e.Events().Publish(environment.StateChangeEvent, state)
e.stMu.Unlock()
e.Events().Publish(environment.StateChangeEvent, e.State())
} }
return nil
} }

View File

@@ -2,72 +2,69 @@ package docker
import ( import (
"context" "context"
"emperror.dev/errors"
"encoding/json" "encoding/json"
"github.com/apex/log"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"io" "io"
"math" "math"
"sync/atomic"
) )
// Attach to the instance and then automatically emit an event whenever the resource usage for the // Attach to the instance and then automatically emit an event whenever the resource usage for the
// server process changes. // server process changes.
func (e *Environment) pollResources(ctx context.Context) error { func (e *Environment) pollResources(ctx context.Context) error {
if e.State() == environment.ProcessOfflineState { if e.st.Load() == environment.ProcessOfflineState {
return errors.New("attempting to enable resource polling on a stopped server instance") return errors.New("cannot enable resource polling on a stopped server")
} }
stats, err := e.client.ContainerStats(context.Background(), e.Id, true) e.log().Info("starting resource polling for container")
defer e.log().Debug("stopped resource polling for container")
stats, err := e.client.ContainerStats(ctx, e.Id, true)
if err != nil { if err != nil {
return errors.WithStack(err) return err
} }
defer stats.Body.Close()
dec := json.NewDecoder(stats.Body) dec := json.NewDecoder(stats.Body)
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
default: default:
var v *types.StatsJSON var v types.StatsJSON
if err := dec.Decode(&v); err != nil { if err := dec.Decode(&v); err != nil {
if err != io.EOF { if err != io.EOF && !errors.Is(err, context.Canceled) {
log.WithField("container_id", e.Id).Warn("encountered error processing docker stats output, stopping collection") e.log().WithField("error", err).Warn("error while processing Docker stats output for container")
} else {
e.log().Debug("io.EOF encountered during stats decode, stopping polling...")
} }
return nil return nil
} }
// Disable collection if the server is in an offline state and this process is still running. // Disable collection if the server is in an offline state and this process is still running.
if e.State() == environment.ProcessOfflineState { if e.st.Load() == environment.ProcessOfflineState {
e.log().Debug("process in offline state while resource polling is still active; stopping poll")
return nil return nil
} }
var rx uint64 st := environment.Stats{
var tx uint64
for _, nw := range v.Networks {
atomic.AddUint64(&rx, nw.RxBytes)
atomic.AddUint64(&tx, nw.RxBytes)
}
st := &environment.Stats{
Memory: calculateDockerMemory(v.MemoryStats), Memory: calculateDockerMemory(v.MemoryStats),
MemoryLimit: v.MemoryStats.Limit, MemoryLimit: v.MemoryStats.Limit,
CpuAbsolute: calculateDockerAbsoluteCpu(&v.PreCPUStats, &v.CPUStats), CpuAbsolute: calculateDockerAbsoluteCpu(v.PreCPUStats, v.CPUStats),
Network: struct { Network: environment.NetworkStats{},
RxBytes uint64 `json:"rx_bytes"`
TxBytes uint64 `json:"tx_bytes"`
}{
RxBytes: rx,
TxBytes: tx,
},
} }
b, _ := json.Marshal(st) for _, nw := range v.Networks {
e.Events().Publish(environment.ResourceEvent, string(b)) st.Network.RxBytes += nw.RxBytes
st.Network.TxBytes += nw.TxBytes
}
if b, err := json.Marshal(st); err != nil {
e.log().WithField("error", err).Warn("error while marshaling stats object for environment")
} else {
e.Events().Publish(environment.ResourceEvent, string(b))
}
} }
} }
} }
@@ -97,7 +94,7 @@ func calculateDockerMemory(stats types.MemoryStats) uint64 {
// by the defined CPU limits on the container. // by the defined CPU limits on the container.
// //
// @see https://github.com/docker/cli/blob/aa097cf1aa19099da70930460250797c8920b709/cli/command/container/stats_helpers.go#L166 // @see https://github.com/docker/cli/blob/aa097cf1aa19099da70930460250797c8920b709/cli/command/container/stats_helpers.go#L166
func calculateDockerAbsoluteCpu(pStats *types.CPUStats, stats *types.CPUStats) float64 { func calculateDockerAbsoluteCpu(pStats types.CPUStats, stats types.CPUStats) float64 {
// Calculate the change in CPU usage between the current and previous reading. // Calculate the change in CPU usage between the current and previous reading.
cpuDelta := float64(stats.CPUUsage.TotalUsage) - float64(pStats.CPUUsage.TotalUsage) cpuDelta := float64(stats.CPUUsage.TotalUsage) - float64(pStats.CPUUsage.TotalUsage)

View File

@@ -4,89 +4,81 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"context" "context"
"emperror.dev/errors"
"encoding/json" "encoding/json"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/pkg/errors" "github.com/pterodactyl/wings/environment"
"io" "strconv"
"os"
) )
type dockerLogLine struct { type dockerLogLine struct {
Log string `json:"log"` Log string `json:"log"`
} }
var ErrNotAttached = errors.New("not attached to instance")
func (e *Environment) setStream(s *types.HijackedResponse) { func (e *Environment) setStream(s *types.HijackedResponse) {
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock()
e.stream = s e.stream = s
e.mu.Unlock()
} }
// Sends the specified command to the stdin of the running container instance. There is no // Sends the specified command to the stdin of the running container instance. There is no
// confirmation that this data is sent successfully, only that it gets pushed into the stdin. // confirmation that this data is sent successfully, only that it gets pushed into the stdin.
func (e *Environment) SendCommand(c string) error { func (e *Environment) SendCommand(c string) error {
if !e.IsAttached() {
return ErrNotAttached
}
e.mu.RLock() e.mu.RLock()
defer e.mu.RUnlock() defer e.mu.RUnlock()
if !e.IsAttached() { // If the command being processed is the same as the process stop command then we want to mark
return errors.New("attempting to send command to non-attached instance") // the server as entering the stopping state otherwise the process will stop and Wings will think
// it has crashed and attempt to restart it.
if e.meta.Stop.Type == "command" && c == e.meta.Stop.Value {
e.SetState(environment.ProcessStoppingState)
} }
_, err := e.stream.Conn.Write([]byte(c + "\n")) _, err := e.stream.Conn.Write([]byte(c + "\n"))
return errors.WithStack(err) return err
} }
// Reads the log file for the server. This does not care if the server is running or not, it will // Reads the log file for the server. This does not care if the server is running or not, it will
// simply try to read the last X bytes of the file and return them. // simply try to read the last X bytes of the file and return them.
func (e *Environment) Readlog(len int64) ([]string, error) { func (e *Environment) Readlog(lines int) ([]string, error) {
j, err := e.client.ContainerInspect(context.Background(), e.Id) r, err := e.client.ContainerLogs(context.Background(), e.Id, types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Tail: strconv.Itoa(lines),
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer r.Close()
if j.LogPath == "" { var out []string
return nil, errors.New("empty log path defined for server")
scanner := bufio.NewScanner(r)
for scanner.Scan() {
out = append(out, scanner.Text())
} }
f, err := os.Open(j.LogPath) return out, nil
if err != nil {
return nil, err
}
defer f.Close()
// Check if the length of the file is smaller than the amount of data that was requested
// for reading. If so, adjust the length to be the total length of the file. If this is not
// done an error is thrown since we're reading backwards, and not forwards.
if stat, err := os.Stat(j.LogPath); err != nil {
return nil, err
} else if stat.Size() < len {
len = stat.Size()
}
// Seed to the end of the file and then move backwards until the length is met to avoid
// reading the entirety of the file into memory.
if _, err := f.Seek(-len, io.SeekEnd); err != nil {
return nil, err
}
b := make([]byte, len)
if _, err := f.Read(b); err != nil && err != io.EOF {
return nil, err
}
return e.parseLogToStrings(b)
} }
// Docker stores the logs for server output in a JSON format. This function will iterate over the JSON // Docker stores the logs for server output in a JSON format. This function will iterate over the JSON
// that was read from the log file and parse it into a more human readable format. // that was read from the log file and parse it into a more human readable format.
func (e *Environment) parseLogToStrings(b []byte) ([]string, error) { func (e *Environment) parseLogToStrings(b []byte) ([]string, error) {
var hasError = false hasError := false
var out []string var out []string
scanner := bufio.NewScanner(bytes.NewReader(b)) scanner := bufio.NewScanner(bytes.NewReader(b))
for scanner.Scan() { for scanner.Scan() {
var l dockerLogLine var l dockerLogLine
// Unmarshal the contents and allow up to a single error before bailing out of the process. We // Unmarshal the contents and allow up to a single error before bailing out of the process. We
// do this because if you're arbitrarily reading a length of the file you'll likely end up // do this because if you're arbitrarily reading a length of the file you'll likely end up
// with the first line in the output being improperly formatted JSON. In those cases we want to // with the first line in the output being improperly formatted JSON. In those cases we want to

View File

@@ -1,14 +1,18 @@
package environment package environment
import ( import (
"github.com/pterodactyl/wings/events"
"os" "os"
"github.com/pterodactyl/wings/events"
) )
const ( const (
ConsoleOutputEvent = "console output" ConsoleOutputEvent = "console output"
StateChangeEvent = "state change" StateChangeEvent = "state change"
ResourceEvent = "resources" ResourceEvent = "resources"
DockerImagePullStarted = "docker image pull started"
DockerImagePullStatus = "docker image pull status"
DockerImagePullCompleted = "docker image pull completed"
) )
const ( const (
@@ -24,6 +28,9 @@ type ProcessEnvironment interface {
// Returns the name of the environment. // Returns the name of the environment.
Type() string Type() string
// Returns the environment configuration to the caller.
Config() *Configuration
// Returns an event emitter instance that can be hooked into to listen for different // Returns an event emitter instance that can be hooked into to listen for different
// events that are fired by the environment. This should not allow someone to publish // events that are fired by the environment. This should not allow someone to publish
// events, only subscribe to them. // events, only subscribe to them.
@@ -86,6 +93,14 @@ type ProcessEnvironment interface {
SendCommand(string) error SendCommand(string) error
// Reads the log file for the process from the end backwards until the provided // Reads the log file for the process from the end backwards until the provided
// number of bytes is met. // number of lines is met.
Readlog(int64) ([]string, error) Readlog(int) ([]string, error)
// Returns the current state of the environment.
State() string
// Sets the current state of the environment. In general you should let the environment
// handle this itself, but there are some scenarios where it is helpful for the server
// to update the state externally (e.g. starting -> started).
SetState(string)
} }

View File

@@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"math" "math"
"strconv" "strconv"
"github.com/apex/log"
) )
type Mount struct { type Mount struct {
@@ -21,7 +23,7 @@ type Mount struct {
// that we're mounting into the container at the Target location. // that we're mounting into the container at the Target location.
Source string `json:"source"` Source string `json:"source"`
// Wether or not the directory is being mounted as read-only. It is up to the environment to // Whether or not the directory is being mounted as read-only. It is up to the environment to
// handle this value correctly and ensure security expectations are met with its usage. // handle this value correctly and ensure security expectations are met with its usage.
ReadOnly bool `json:"read_only"` ReadOnly bool `json:"read_only"`
} }
@@ -118,7 +120,13 @@ func (v Variables) Get(key string) string {
return fmt.Sprintf("%f", val.(float64)) return fmt.Sprintf("%f", val.(float64))
case bool: case bool:
return strconv.FormatBool(val.(bool)) return strconv.FormatBool(val.(bool))
case string:
return val.(string)
} }
return val.(string) // TODO: I think we can add a check for val == nil and return an empty string for those
// and this warning should theoretically never happen?
log.Warn(fmt.Sprintf("failed to marshal environment variable \"%s\" of type %+v into string", key, val))
return ""
} }

View File

@@ -1,13 +1,9 @@
package environment package environment
import "sync"
// Defines the current resource usage for a given server instance. If a server is offline you // Defines the current resource usage for a given server instance. If a server is offline you
// should obviously expect memory and CPU usage to be 0. However, disk will always be returned // should obviously expect memory and CPU usage to be 0. However, disk will always be returned
// since that is not dependent on the server being running to collect that data. // since that is not dependent on the server being running to collect that data.
type Stats struct { type Stats struct {
mu sync.RWMutex
// The total amount of memory, in bytes, that this server instance is consuming. This is // The total amount of memory, in bytes, that this server instance is consuming. This is
// calculated slightly differently than just using the raw Memory field that the stats // calculated slightly differently than just using the raw Memory field that the stats
// return from the container, so please check the code setting this value for how that // return from the container, so please check the code setting this value for how that
@@ -28,20 +24,10 @@ type Stats struct {
// Disk int64 `json:"disk_bytes"` // Disk int64 `json:"disk_bytes"`
// Current network transmit in & out for a container. // Current network transmit in & out for a container.
Network struct { Network NetworkStats `json:"network"`
RxBytes uint64 `json:"rx_bytes"`
TxBytes uint64 `json:"tx_bytes"`
} `json:"network"`
} }
// Resets the usages values to zero, used when a server is stopped to ensure we don't hold type NetworkStats struct {
// onto any values incorrectly. RxBytes uint64 `json:"rx_bytes"`
func (s *Stats) Empty() { TxBytes uint64 `json:"tx_bytes"`
s.mu.Lock()
defer s.mu.Unlock()
s.Memory = 0
s.CpuAbsolute = 0
s.Network.TxBytes = 0
s.Network.RxBytes = 0
} }

View File

@@ -4,6 +4,8 @@ import (
"encoding/json" "encoding/json"
"strings" "strings"
"sync" "sync"
"github.com/gammazero/workerpool"
) )
type Event struct { type Event struct {
@@ -12,14 +14,13 @@ type Event struct {
} }
type EventBus struct { type EventBus struct {
sync.RWMutex mu sync.RWMutex
pools map[string]*CallbackPool
subscribers map[string]map[chan Event]struct{}
} }
func New() *EventBus { func New() *EventBus {
return &EventBus{ return &EventBus{
subscribers: make(map[string]map[chan Event]struct{}), pools: make(map[string]*CallbackPool),
} }
} }
@@ -39,21 +40,32 @@ func (e *EventBus) Publish(topic string, data string) {
} }
} }
e.mu.RLock()
defer e.mu.RUnlock()
// Acquire a read lock and loop over all of the channels registered for the topic. This // Acquire a read lock and loop over all of the channels registered for the topic. This
// avoids a panic crash if the process tries to unregister the channel while this routine // avoids a panic crash if the process tries to unregister the channel while this routine
// is running. // is running.
go func() { if cp, ok := e.pools[t]; ok {
e.RLock() for _, callback := range cp.callbacks {
defer e.RUnlock() c := *callback
evt := Event{Data: data, Topic: topic}
if ch, ok := e.subscribers[t]; ok { // Using the workerpool with one worker allows us to execute events in a FIFO manner. Running
for channel := range ch { // this using goroutines would cause things such as console output to just output in random order
channel <- Event{Data: data, Topic: topic} // if more than one event is fired at the same time.
} //
// However, the pool submission does not block the execution of this function itself, allowing
// us to call publish without blocking any of the other pathways.
//
// @see https://github.com/pterodactyl/panel/issues/2303
cp.pool.Submit(func() {
c(evt)
})
} }
}() }
} }
// Publishes a JSON message to a given topic.
func (e *EventBus) PublishJson(topic string, data interface{}) error { func (e *EventBus) PublishJson(topic string, data interface{}) error {
b, err := json.Marshal(data) b, err := json.Marshal(data)
if err != nil { if err != nil {
@@ -65,41 +77,46 @@ func (e *EventBus) PublishJson(topic string, data interface{}) error {
return nil return nil
} }
// Subscribe to an emitter topic using a channel. // Register a callback function that will be executed each time one of the events using the topic
func (e *EventBus) Subscribe(topic string, ch chan Event) { // name is called.
e.Lock() func (e *EventBus) On(topic string, callback *func(Event)) {
defer e.Unlock() e.mu.Lock()
defer e.mu.Unlock()
if _, exists := e.subscribers[topic]; !exists { // Check if this topic has been registered at least once for the event listener, and if
e.subscribers[topic] = make(map[chan Event]struct{}) // not create an empty struct for the topic.
if _, exists := e.pools[topic]; !exists {
e.pools[topic] = &CallbackPool{
callbacks: make([]*func(Event), 0),
pool: workerpool.New(1),
}
} }
// Only set the channel if there is not currently a matching one for this topic. This // If this callback is not already registered as an event listener, go ahead and append
// avoids registering two identical listeners for the same topic and causing pain in // it to the array of callbacks for this topic.
// the unsubscribe functionality as well. e.pools[topic].Add(callback)
if _, exists := e.subscribers[topic][ch]; !exists { }
e.subscribers[topic][ch] = struct{}{}
// Removes an event listener from the bus.
func (e *EventBus) Off(topic string, callback *func(Event)) {
e.mu.Lock()
defer e.mu.Unlock()
if cp, ok := e.pools[topic]; ok {
cp.Remove(callback)
} }
} }
// Unsubscribe a channel from a given topic. // Removes all of the event listeners that have been registered for any topic. Also stops the worker
func (e *EventBus) Unsubscribe(topic string, ch chan Event) { // pool to close that routine.
e.Lock() func (e *EventBus) Destroy() {
defer e.Unlock() e.mu.Lock()
defer e.mu.Unlock()
if _, exists := e.subscribers[topic][ch]; exists { // Stop every pool that exists for a given callback topic.
delete(e.subscribers[topic], ch) for _, cp := range e.pools {
cp.pool.Stop()
} }
}
// Removes all of the event listeners for the server. This is used when a server e.pools = make(map[string]*CallbackPool)
// is being deleted to avoid a bunch of de-reference errors cropping up. Obviously
// should also check elsewhere and handle a server reference going nil, but this
// won't hurt.
func (e *EventBus) UnsubscribeAll() {
e.Lock()
defer e.Unlock()
// Reset the entire struct into an empty map.
e.subscribers = make(map[string]map[chan Event]struct{})
} }

50
events/pool.go Normal file
View File

@@ -0,0 +1,50 @@
package events
import (
"reflect"
"github.com/gammazero/workerpool"
)
type CallbackPool struct {
callbacks []*func(Event)
pool *workerpool.WorkerPool
}
// Pushes a new callback into the array of listeners for the pool.
func (cp *CallbackPool) Add(callback *func(Event)) {
if cp.index(reflect.ValueOf(callback)) < 0 {
cp.callbacks = append(cp.callbacks, callback)
}
}
// Removes a callback from the array of registered callbacks if it exists.
func (cp *CallbackPool) Remove(callback *func(Event)) {
i := cp.index(reflect.ValueOf(callback))
// If i < 0 it means there was no index found for the given callback, meaning it was
// never registered or was already unregistered from the listeners. Also double check
// that we didn't somehow escape the length of the topic callback (not sure how that
// would happen, but lets avoid a panic condition).
if i < 0 || i >= len(cp.callbacks) {
return
}
// We can assume that the topic still exists at this point since we acquire an exclusive
// lock on the process, and the "e.index" function cannot return a value >= 0 if there is
// no topic already existing.
cp.callbacks = append(cp.callbacks[:i], cp.callbacks[i+1:]...)
}
// Finds the index of a given callback in the topic by comparing all of the registered callback
// pointers to the passed function. This function does not aquire a lock as it should only be called
// within the confines of a function that has already acquired a lock for the duration of the lookup.
func (cp *CallbackPool) index(v reflect.Value) int {
for i, handler := range cp.callbacks {
if reflect.ValueOf(handler).Pointer() == v.Pointer() {
return i
}
}
return -1
}

122
go.mod
View File

@@ -2,95 +2,81 @@ module github.com/pterodactyl/wings
go 1.13 go 1.13
// Uncomment this in development environments to make changes to the core SFTP
// server software. This assumes you're using the official Pterodactyl Environment
// otherwise this path will not work.
//
// @see https://github.com/pterodactyl/development
//
// replace github.com/pterodactyl/sftp-server => ../sftp-server
require ( require (
github.com/AlecAivazis/survey/v2 v2.1.0 emperror.dev/errors v0.8.0
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect github.com/AlecAivazis/survey/v2 v2.2.7
github.com/Jeffail/gabs/v2 v2.5.1 github.com/Jeffail/gabs/v2 v2.6.0
github.com/Microsoft/go-winio v0.4.14 // indirect github.com/Microsoft/go-winio v0.4.16 // indirect
github.com/Microsoft/hcsshim v0.8.14 // indirect
github.com/NYTimes/logrotate v1.0.0 github.com/NYTimes/logrotate v1.0.0
github.com/andybalholm/brotli v1.0.0 // indirect github.com/andybalholm/brotli v1.0.1 // indirect
github.com/apex/log v1.8.0 github.com/apex/log v1.9.0
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
github.com/beevik/etree v1.1.0 github.com/beevik/etree v1.1.0
github.com/buger/jsonparser v1.0.0 github.com/buger/jsonparser v1.1.0
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249 github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249
github.com/containerd/containerd v1.3.7 // indirect github.com/containerd/containerd v1.4.3 // indirect
github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b // indirect github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c // indirect
github.com/creasty/defaults v1.5.0 github.com/creasty/defaults v1.5.1
github.com/docker/cli v17.12.1-ce-rc2+incompatible
github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible github.com/docker/docker v20.10.1+incompatible
github.com/docker/go-connections v0.4.0 github.com/docker/go-connections v0.4.0
github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.4.0 // indirect github.com/fatih/color v1.10.0
github.com/fatih/color v1.9.0 github.com/franela/goblin v0.0.0-20200825194134-80c0062ed6cd
github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/gabriel-vasile/mimetype v1.1.1 github.com/gabriel-vasile/mimetype v1.1.2
github.com/gammazero/deque v0.0.0-20200721202602-07291166fe33 // indirect github.com/gammazero/deque v0.0.0-20201010052221-3932da5530cc // indirect
github.com/gammazero/workerpool v1.0.0 github.com/gammazero/workerpool v1.1.1
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.2 github.com/gbrlsnchs/jwt/v3 v3.0.0
github.com/gin-gonic/gin v1.6.3 github.com/gin-gonic/gin v1.6.3
github.com/go-playground/validator/v10 v10.3.0 // indirect github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/gogo/protobuf v1.3.1 // indirect github.com/golang/snappy v0.0.2 // indirect
github.com/golang/gddo v0.0.0-20200715224205-051695c33a3f // indirect github.com/google/go-cmp v0.5.2 // indirect
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.2
github.com/gorilla/mux v1.7.4 // indirect github.com/gorilla/mux v1.7.4 // indirect
github.com/gorilla/websocket v1.4.2 github.com/gorilla/websocket v1.4.2
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 github.com/iancoleman/strcase v0.1.2
github.com/icza/dyno v0.0.0-20200205103839-49cb13720835 github.com/icza/dyno v0.0.0-20200205103839-49cb13720835
github.com/imdario/mergo v0.3.8 github.com/imdario/mergo v0.3.9
github.com/klauspost/compress v1.10.10 // indirect github.com/juju/ratelimit v1.0.1
github.com/klauspost/pgzip v1.2.4 github.com/karrick/godirwalk v1.16.1
github.com/klauspost/compress v1.11.4 // indirect
github.com/klauspost/pgzip v1.2.5
github.com/leodido/go-urn v1.2.1 // indirect
github.com/magefile/mage v1.10.0 // indirect github.com/magefile/mage v1.10.0 // indirect
github.com/magiconair/properties v1.8.1 github.com/magiconair/properties v1.8.4
github.com/mattn/go-colorable v0.1.7 github.com/mattn/go-colorable v0.1.8
github.com/mattn/go-shellwords v1.0.10 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/archiver/v3 v3.3.0 github.com/mholt/archiver/v3 v3.5.0
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/nwaples/rardecode v1.1.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pierrec/lz4 v2.5.2+incompatible // indirect github.com/pierrec/lz4/v4 v4.1.2 // indirect
github.com/pkg/errors v0.9.1
github.com/pkg/profile v1.5.0 github.com/pkg/profile v1.5.0
github.com/pkg/sftp v1.11.0 // indirect github.com/pkg/sftp v1.12.0
github.com/prometheus/common v0.11.1 // indirect github.com/prometheus/client_golang v1.9.0 // indirect
github.com/pterodactyl/sftp-server v1.1.5 github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f
github.com/remeh/sizedwaitgroup v1.0.0 github.com/sirupsen/logrus v1.7.0 // indirect
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 github.com/spf13/cobra v1.1.1
github.com/smartystreets/goconvey v1.6.4 // indirect github.com/spf13/pflag v1.0.5
github.com/spf13/cobra v1.0.0 github.com/ugorji/go v1.2.2 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/ulikunitz/xz v0.5.9 // indirect
github.com/uber-go/zap v1.9.1 // indirect golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
github.com/ulikunitz/xz v0.5.7 // indirect golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect
go.uber.org/zap v1.15.0 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de golang.org/x/sys v0.0.0-20201223074533-0d417f636930 // indirect
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect golang.org/x/text v0.3.4 // indirect
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
golang.org/x/text v0.3.3 // indirect
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5 // indirect
golang.org/x/tools/gopls v0.1.3 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 // indirect google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d // indirect
google.golang.org/grpc v1.31.0 // indirect google.golang.org/grpc v1.34.0 // indirect
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/ini.v1 v1.57.0 gopkg.in/ini.v1 v1.62.0
gopkg.in/yaml.v2 v2.3.0 gopkg.in/yaml.v2 v2.4.0
gotest.tools v2.2.0+incompatible // indirect
honnef.co/go/tools v0.0.1-2020.1.3 // indirect
) )

444
go.sum
View File

@@ -1,23 +1,33 @@
cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/AlecAivazis/survey/v2 v2.0.7 h1:+f825XHLse/hWd2tE/V5df04WFGimk34Eyg/z35w/rc= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
github.com/AlecAivazis/survey/v2 v2.0.7/go.mod h1:mlizQTaPjnR4jcpwRSaSlkbsRfYFEyKgLQvYTzxxiHA= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
github.com/AlecAivazis/survey/v2 v2.1.0 h1:AT4+23hOFopXYZaNGugbk7MWItkz0SfTmH/Hk92KeeE= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
github.com/AlecAivazis/survey/v2 v2.1.0/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
emperror.dev/errors v0.8.0 h1:4lycVEx0sdJkwDUfQ9pdu6SR0x7rgympt5f4+ok8jDk=
emperror.dev/errors v0.8.0/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE=
github.com/AlecAivazis/survey/v2 v2.2.7 h1:5NbxkF4RSKmpywYdcRgUmos1o+roJY8duCLZXbVjoig=
github.com/AlecAivazis/survey/v2 v2.2.7/go.mod h1:9DYvHgXtiXm6nCn+jXnOXLKbH+Yo9u8fAS/SduGdoPk=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Jeffail/gabs/v2 v2.2.0 h1:7touC+WzbQ7LO5+mwgxT44miyTqAVCOlIWLA6PiIB5w= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Jeffail/gabs/v2 v2.2.0/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI= github.com/Jeffail/gabs/v2 v2.6.0 h1:WdCnGaDhNa4LSRTMwhLZzJ7SRDXjABNP13SOKvCpL5w=
github.com/Jeffail/gabs/v2 v2.5.1 h1:ANfZYjpMlfTTKebycu4X1AgkVWumFVDYQl7JwOr4mDk= github.com/Jeffail/gabs/v2 v2.6.0/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI=
github.com/Jeffail/gabs/v2 v2.5.1/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Microsoft/go-winio v0.4.7 h1:vOvDiY/F1avSWlCWiKJjdYKz2jVjTK3pWPHndeG4OAY= github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Microsoft/go-winio v0.4.7/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/hcsshim v0.8.14 h1:lbPVK25c1cu5xTLITwpUcxoA9vKrKErASPYygvouJns=
github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg=
github.com/NYTimes/logrotate v1.0.0 h1:6jFGbon6jOtpy3t3kwZZKS4Gdmf1C/Wv5J4ll4Xn5yk= github.com/NYTimes/logrotate v1.0.0 h1:6jFGbon6jOtpy3t3kwZZKS4Gdmf1C/Wv5J4ll4Xn5yk=
github.com/NYTimes/logrotate v1.0.0/go.mod h1:GxNz1cSw1c6t99PXoZlw+nm90H6cyQyrH66pjVv7x88= github.com/NYTimes/logrotate v1.0.0/go.mod h1:GxNz1cSw1c6t99PXoZlw+nm90H6cyQyrH66pjVv7x88=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
@@ -32,29 +42,23 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6 h1:bZ28Hqta7TFAK3Q08CMvv8y3/8ATaEqv2nGoc6yff6c=
github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6/go.mod h1:+lx6/Aqd1kLJ1GQfkvOnaZ1WGmLpMpbprPuIOOZX30U=
github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4=
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apex/log v1.3.0 h1:1fyfbPvUwD10nMoh3hY6MXzvZShJQn9/ck7ATgAt5pA= github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0=
github.com/apex/log v1.3.0/go.mod h1:jd8Vpsr46WAe3EZSQ/IUMs2qQD/GOycT5rPWCO1yGcs= github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA=
github.com/apex/log v1.8.0 h1:+W4j+dttibFvynPLlctdnYFUn1eLKT37BZWWW2iMfEM=
github.com/apex/log v1.8.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA=
github.com/apex/logs v0.0.4/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY=
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
@@ -68,11 +72,9 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/buger/jsonparser v0.0.0-20191204142016-1a29609e0929 h1:MW/JDk68Rny52yI0M0N+P8lySNgB+NhpI/uAmhgOhUM= github.com/buger/jsonparser v1.1.0 h1:EPAGdKZgZCON4ZcMD+h4l/NN4ndr6ijSpj4INh8PbUY=
github.com/buger/jsonparser v0.0.0-20191204142016-1a29609e0929/go.mod h1:tgcrVJ81GPSF0mz+0nu1Xaz0fazGPrmmJfJtxjbHhUQ= github.com/buger/jsonparser v1.1.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/buger/jsonparser v1.0.0 h1:etJTGF5ESxjI0Ic2UaLQs2LQQpa8G9ykQScukbh4L8A=
github.com/buger/jsonparser v1.0.0/go.mod h1:tgcrVJ81GPSF0mz+0nu1Xaz0fazGPrmmJfJtxjbHhUQ=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -80,52 +82,55 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249 h1:R0IDH8daQ3lODvu8YtxnIqqth5qMGCJyADoUQvmLx4o= github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249 h1:R0IDH8daQ3lODvu8YtxnIqqth5qMGCJyADoUQvmLx4o=
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249/go.mod h1:EHKW9yNEYSBpTKzuu7Y9oOrft/UlzH57rMIB03oev6M= github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249/go.mod h1:EHKW9yNEYSBpTKzuu7Y9oOrft/UlzH57rMIB03oev6M=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/containerd/containerd v1.3.6 h1:SMfcKoQyWhaRsYq7290ioC6XFcHDNcHvcEMjF6ORpac= github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=
github.com/containerd/containerd v1.3.6/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
github.com/containerd/containerd v1.3.7 h1:eFSOChY8TTcxvkzp8g+Ov1RL0MYww7XEeK0y+zqGpVc= github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.3.7/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.4.3 h1:ijQT13JedHSHrQGWFcGEwzcNKrAGIiZ+jSD5QQG07SY=
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448 h1:PUD50EuOMkXVcpBIA/R95d56duJR9VxhwncsFbNnxW4= github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b h1:qUtCegLdOUVfVJOw+KDg6eJyE1TGvLlkGEd1091kSSQ= github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c h1:1c6xmkNiu6Jnr6AKGM91GGNsfU+nPNFvw9BZFSo0E+c=
github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creasty/defaults v1.3.0 h1:uG+RAxYbJgOPCOdKEcec9ZJXeva7Y6mj/8egdzwmLtw= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creasty/defaults v1.3.0/go.mod h1:CIEEvs7oIVZm30R8VxtFJs+4k201gReYyuYHJxZc68I= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creasty/defaults v1.5.0 h1:DW6NAGGaKuNSKkntc8BCBrR2KOUAcXVnfcwu/LmJhaQ= github.com/creasty/defaults v1.5.1 h1:j8WexcS3d/t4ZmllX4GEkl4wIB/trOr035ajcLHCISM=
github.com/creasty/defaults v1.5.0/go.mod h1:FPZ+Y0WNrbqOVw+c6av63eyHUAl6pMHZwqLPvXUZGfY= github.com/creasty/defaults v1.5.1/go.mod h1:FPZ+Y0WNrbqOVw+c6av63eyHUAl6pMHZwqLPvXUZGfY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/docker/cli v17.12.1-ce-rc2+incompatible h1:ESUycEAqvFuLglAHkUW66rCc2djYtd3i1x231svLq9o=
github.com/docker/cli v17.12.1-ce-rc2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:iWPIG7pWIsCwT6ZtHnTUpoVMnete7O/pzd9HFE3+tn8= github.com/docker/docker v20.10.1+incompatible h1:u0HIBLwOJdemyBdTCkoBX34u3lb5KyBo0rQE3a5Yg+E=
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v20.10.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
@@ -139,42 +144,37 @@ github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaB
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goblin v0.0.0-20200825194134-80c0062ed6cd h1:b/30UOB56Rhfe185ZfgvZT0/HOql0OzxuiNOxRKXRXc=
github.com/franela/goblin v0.0.0-20200825194134-80c0062ed6cd/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gabriel-vasile/mimetype v0.1.4 h1:5mcsq3+DXypREUkW+1juhjeKmE/XnWgs+paHMJn7lf8= github.com/gabriel-vasile/mimetype v1.1.2 h1:gaPnPcNor5aZSVCJVSGipcpbgMWiAAj9z182ocSGbHU=
github.com/gabriel-vasile/mimetype v0.1.4/go.mod h1:kMJbg3SlWZCsj4R73F1WDzbT9AyGCOVmUtIxxwO5pmI= github.com/gabriel-vasile/mimetype v1.1.2/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To=
github.com/gabriel-vasile/mimetype v1.1.1 h1:qbN9MPuRf3bstHu9zkI9jDWNfH//9+9kHxr9oRBBBOA=
github.com/gabriel-vasile/mimetype v1.1.1/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To=
github.com/gammazero/deque v0.0.0-20200227231300-1e9af0e52b46 h1:iX4+rD9Fjdx8SkmSO/O5WAIX/j79ll3kuqv5VdYt9J8=
github.com/gammazero/deque v0.0.0-20200227231300-1e9af0e52b46/go.mod h1:D90+MBHVc9Sk1lJAbEVgws0eYEurY4mv2TDso3Nxh3w=
github.com/gammazero/deque v0.0.0-20200721202602-07291166fe33 h1:UG4wNrJX9xSKnm/Gck5yTbxnOhpNleuE4MQRdmcGySo= github.com/gammazero/deque v0.0.0-20200721202602-07291166fe33 h1:UG4wNrJX9xSKnm/Gck5yTbxnOhpNleuE4MQRdmcGySo=
github.com/gammazero/deque v0.0.0-20200721202602-07291166fe33/go.mod h1:D90+MBHVc9Sk1lJAbEVgws0eYEurY4mv2TDso3Nxh3w= github.com/gammazero/deque v0.0.0-20200721202602-07291166fe33/go.mod h1:D90+MBHVc9Sk1lJAbEVgws0eYEurY4mv2TDso3Nxh3w=
github.com/gammazero/workerpool v0.0.0-20200608033439-1a5ca90a5753 h1:oSQ61LxZkz3Z4La0O5cbyVDvLWEfbNgiD43cSPdjPQQ= github.com/gammazero/deque v0.0.0-20201010052221-3932da5530cc h1:F7BbnLACph7UYiz9ZHi6npcROwKaZUyviDjsNERsoMM=
github.com/gammazero/workerpool v0.0.0-20200608033439-1a5ca90a5753/go.mod h1:/XWO2YAUUpPi3smDlFBl0vpX0JHwUomDM/oRMwRmnSs= github.com/gammazero/deque v0.0.0-20201010052221-3932da5530cc/go.mod h1:IlBLfYXnuw9sspy1XS6ctu5exGb6WHGKQsyo4s7bOEA=
github.com/gammazero/workerpool v1.0.0 h1:MfkJc6KL0tAmjrRDS203AZz3F+84Uod9YbL8KjpcQ00= github.com/gammazero/workerpool v1.1.1 h1:MN29GcZtZZAgzTU+Zk54Y+J9XkE54MoXON/NCZvNulo=
github.com/gammazero/workerpool v1.0.0/go.mod h1:/XWO2YAUUpPi3smDlFBl0vpX0JHwUomDM/oRMwRmnSs= github.com/gammazero/workerpool v1.1.1/go.mod h1:5BN0IJVRjSFAypo9QTJCaWdijjNz9Jjl6VFS1PRjCeg=
github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/gbrlsnchs/jwt/v3 v3.0.0 h1:gtPjdT3gAbBLjVckJsgNf+a46sqrCBfRebg2r/NysIo=
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0 h1:7KeiSrO5puFH1+vdAdbpiie2TrNnkvFc/eOQzT60Z2k= github.com/gbrlsnchs/jwt/v3 v3.0.0/go.mod h1:AncDcjXz18xetI3A6STfXq2w+LuTx8pQ8bGEwRN8zVM=
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0/go.mod h1:D1+3UtCYAJ1os1PI+zhTVEj6Tb+IHJvXjXKz83OstmM=
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.2 h1:3t7jvTkeQfk1FdP0noXSNiM6AdBokLz7QmZDmnCHAAA=
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.2/go.mod h1:AncDcjXz18xetI3A6STfXq2w+LuTx8pQ8bGEwRN8zVM=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
@@ -190,11 +190,11 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -202,22 +202,18 @@ github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721 h1:KRMr9A3qfbVM7iV/WcLY/rL5LICqwMHLhwRXKu99fXw=
github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
github.com/golang/gddo v0.0.0-20200715224205-051695c33a3f/go.mod h1:sam69Hju0uq+5uvLJUMDlsKlQ21Vrs1Kd/1YFPNYdOU=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
@@ -226,25 +222,36 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
@@ -253,17 +260,16 @@ github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
@@ -279,7 +285,6 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
@@ -289,15 +294,12 @@ github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDG
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8= github.com/iancoleman/strcase v0.1.2 h1:gnomlvw9tnV3ITTAxzKSgTF+8kFWcU/f+TgttpXGz1U=
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= github.com/iancoleman/strcase v0.1.2/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
github.com/icza/dyno v0.0.0-20200205103839-49cb13720835 h1:f1irK5f03uGGj+FjgQfZ5VhdKNVQVJ4skHsedzVohQ4= github.com/icza/dyno v0.0.0-20200205103839-49cb13720835 h1:f1irK5f03uGGj+FjgQfZ5VhdKNVQVJ4skHsedzVohQ4=
github.com/icza/dyno v0.0.0-20200205103839-49cb13720835/go.mod h1:c1tRKs5Tx7E2+uHGSyyncziFjvGpgv4H2HrqXeUQ/Uk= github.com/icza/dyno v0.0.0-20200205103839-49cb13720835/go.mod h1:c1tRKs5Tx7E2+uHGSyyncziFjvGpgv4H2HrqXeUQ/Uk=
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.10 h1:6q5mVkdH/vYmqngx7kZQTjJ5HRsx+ImorDIEQ+beJgc=
github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
@@ -312,30 +314,34 @@ github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGn
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY=
github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g= github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.9.2 h1:LfVyl+ZlLlLDeQ/d2AqfGIIH4qEDu0Ed2S5GyhCWIWY=
github.com/klauspost/compress v1.9.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I= github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.1 h1:oIPZROsWuPHpOdMVWLuJZXwgjhrW8r1yEX8UqMyeNHM=
github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/pgzip v1.2.3 h1:Ce2to9wvs/cuJ2b86/CKQoTYr9VHfpanYosZ0UBJqdw=
github.com/klauspost/pgzip v1.2.3/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/pgzip v1.2.4 h1:TQ7CNpYKovDOmqzRHKxJh0BeaBI7UdQZYc6p7pMQh1A= github.com/klauspost/pgzip v1.2.4 h1:TQ7CNpYKovDOmqzRHKxJh0BeaBI7UdQZYc6p7pMQh1A=
github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
@@ -351,6 +357,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
@@ -358,38 +366,31 @@ github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE=
github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g= github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g=
github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY=
github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/archiver/v3 v3.3.0 h1:vWjhY8SQp5yzM9P6OJ/eZEkmi3UAbRrxCq48MxjAzig= github.com/mholt/archiver/v3 v3.5.0 h1:nE8gZIrw66cu4osS/U7UW7YDuGMHssxKutU8IfWxwWE=
github.com/mholt/archiver/v3 v3.3.0/go.mod h1:YnQtqsp+94Rwd0D/rk5cnLrxusUBUXg+08Ebtr1Mqao= github.com/mholt/archiver/v3 v3.5.0/go.mod h1:qqTTPUK/HZPFgFQ/TJ3BzvTpF/dPtFVJXdQbCmeMxwc=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
@@ -400,8 +401,9 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -421,8 +423,6 @@ github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ= github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
@@ -434,12 +434,13 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 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/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
@@ -453,15 +454,15 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/pierrec/lz4 v1.0.1 h1:w6GMGWSsCI04fTM8wQRdnW74MuJISakuUU0onU0TYB4=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= github.com/pierrec/lz4/v4 v4.0.3 h1:vNQKSVZNYUEAvRY9FaUXAF1XPbSOHJtDTiP41kzDz2E=
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.0.3/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM=
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
@@ -469,14 +470,10 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pkg/profile v1.4.0 h1:uCmaf4vVbWAOZz36k1hrQD7ijGRzLwaME8Am/7a4jZI=
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
github.com/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug= github.com/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug=
github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
github.com/pkg/sftp v1.8.3 h1:9jSe2SxTM8/3bXZjtqnkgTBW+lA8db0knZJyns7gpBA= github.com/pkg/sftp v1.12.0 h1:/f3b24xrDhkhddlaobPe2JgBqfdt+gC/NYl0QY9IOuI=
github.com/pkg/sftp v1.8.3/go.mod h1:NxmoDg/QLVWluQDUYG7XBZTLUpKeFa8e3aMf1BfjyHk= github.com/pkg/sftp v1.12.0/go.mod h1:fUqqXB5vEgVCZ131L+9say31RAri6aF6KDViawhxKK8=
github.com/pkg/sftp v1.11.0 h1:4Zv0OGbpkg4yNuUtH0s8rvoYxRCNyT29NVUo6pgPmxI=
github.com/pkg/sftp v1.11.0/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@@ -489,6 +486,8 @@ github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQ
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.9.0 h1:Rrch9mh17XcxvEu9D9DEpb4isxjGBtcevQjKvxPRQIU=
github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
@@ -505,8 +504,9 @@ github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkp
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.11.1 h1:0ZISXCMRuCZcxF77aT1BXY5m74mX2vrGYl1dSwBI0Jo= github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM=
github.com/prometheus/common v0.11.1/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@@ -516,23 +516,17 @@ github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/pterodactyl/sftp-server v1.1.4 h1:JESuEuZ+d2tajMjuQblPOlGISM9Uc2xOzk7irVF9PQ0=
github.com/pterodactyl/sftp-server v1.1.4/go.mod h1:KjSONrenRr1oCh94QIVAU6yEzMe+Hd7r/JHrh5/oQHs=
github.com/pterodactyl/sftp-server v1.1.5 h1:r5RIfCDVLpn6MsfD8zcCQLtviy14GJ9E+9HzidjgAGw=
github.com/pterodactyl/sftp-server v1.1.5/go.mod h1:YVx5g2gjln7fYFO7+c3iDRTwNyA5GuJtkKME0UDB8co=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce h1:aP+C+YbHZfOQlutA4p4soHi7rVUqHQdWEVMSkHfDTqY=
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E=
github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 h1:G04eS0JkAIVZfaJLjla9dNxkJCPiKIGZlw9AfOhzOD0= github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f h1:8P2MkG70G76gnZBOPGwmMIgwBb/rESQuwsJ7K8ds4NE=
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94/go.mod h1:b18R55ulyQ/h3RaWyloPyER7fWQVZvimKKhnI5OfrJQ= github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
@@ -543,6 +537,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8= github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8=
@@ -554,25 +550,18 @@ github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4S
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU= github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4=
github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v0.0.0-20170901151539-12bd96e66386/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
@@ -583,11 +572,13 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0 h1:Rw8kxzWo1mr6FSaYXjQELRe88y2KdfynXdnK72rdjtA= github.com/tj/assert v0.0.0-20171129193455-018094318fb0 h1:Rw8kxzWo1mr6FSaYXjQELRe88y2KdfynXdnK72rdjtA=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc= github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc=
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
@@ -596,66 +587,70 @@ github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds=
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/uber-go/zap v1.9.1/go.mod h1:GY+83l3yxBcBw2kmHu/sAWwItnTn+ynxHCRo+WiIQOY=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.2 h1:60ZHIOcsJlo3bJm9CbTVu7OSqT2mxaEmyQbK2NwCkn0=
github.com/ugorji/go v1.2.2/go.mod h1:bitgyERdV7L7Db/Z5gfd5v2NQMNhhiFiZwpgMw2SP7k=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.2 h1:08Gah8d+dXj4cZNUHhtuD/S4PXD5WpVbj5B8/ClELAQ=
github.com/ugorji/go/codec v1.2.2/go.mod h1:OM8g7OAy52uYl3Yk+RE/3AS1nXFn1Wh4PPLtupCxbuU=
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8= github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4= github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4=
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88=
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -664,12 +659,11 @@ golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -682,21 +676,19 @@ golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U=
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -705,10 +697,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -719,41 +709,58 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f h1:mOhmO9WsBaJCNmaZHPtHs9wOcdqdKCjF6OPJlmDM3KI=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200806125547-5acd03effb82 h1:6cBnXxYO+CiRVrChvCosSv7magqTPbyAgz1M8iOv5wM= golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200806125547-5acd03effb82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201223074533-0d417f636930 h1:vRgIt+nup/B/BwIS0g2oC0haq0iqbV3ZA+u6+0TlNCo=
golang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -761,22 +768,25 @@ golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190710153321-831012c29e42/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5 h1:MeC2gMlMdkd67dn17MEby3rGXRxZtWeiRXOnISfTQ74=
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools/gopls v0.1.3/go.mod h1:vrCQzOKxvuiZLjCKSmbbov04oeBQQOb4VQqwYK2PWIY=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -784,37 +794,46 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 h1:LCO0fg4kb6WwkXQXRQQgUYsFeFb5taTX5WAx5O/Vt28= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d h1:HV9Z9qMhQEsdlvxNFELgQ11RkMzO3CMkjEySjCtuLes=
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0= google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= google.golang.org/grpc v1.34.0 h1:raiipEjMOIC/TO2AvyTxP25XFdLxNIBwzDh3FM3XztI=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -838,10 +857,9 @@ gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qS
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
@@ -855,16 +873,22 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=

View File

@@ -2,16 +2,13 @@ package installer
import ( import (
"encoding/json" "encoding/json"
"github.com/apex/log"
"emperror.dev/errors"
"github.com/asaskevich/govalidator" "github.com/asaskevich/govalidator"
"github.com/buger/jsonparser" "github.com/buger/jsonparser"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server"
"os"
"path"
) )
type Installer struct { type Installer struct {
@@ -26,14 +23,11 @@ func New(data []byte) (*Installer, error) {
return nil, NewValidationError("uuid provided was not in a valid format") return nil, NewValidationError("uuid provided was not in a valid format")
} }
if !govalidator.IsUUIDv4(getString(data, "service", "egg")) {
return nil, NewValidationError("service egg provided was not in a valid format")
}
cfg := &server.Configuration{ cfg := &server.Configuration{
Uuid: getString(data, "uuid"), Uuid: getString(data, "uuid"),
Suspended: false, Suspended: false,
Invocation: getString(data, "invocation"), Invocation: getString(data, "invocation"),
SkipEggScripts: getBoolean(data, "skip_egg_scripts"),
Build: environment.Limits{ Build: environment.Limits{
MemoryLimit: getInt(data, "build", "memory"), MemoryLimit: getInt(data, "build", "memory"),
Swap: getInt(data, "build", "swap"), Swap: getInt(data, "build", "swap"),
@@ -50,33 +44,33 @@ func New(data []byte) (*Installer, error) {
// Unmarshal the environment variables from the request into the server struct. // Unmarshal the environment variables from the request into the server struct.
if b, _, _, err := jsonparser.Get(data, "environment"); err != nil { if b, _, _, err := jsonparser.Get(data, "environment"); err != nil {
return nil, errors.WithStack(err) return nil, err
} else { } else {
cfg.EnvVars = make(environment.Variables) cfg.EnvVars = make(environment.Variables)
if err := json.Unmarshal(b, &cfg.EnvVars); err != nil { if err := json.Unmarshal(b, &cfg.EnvVars); err != nil {
return nil, errors.WithStack(err) return nil, err
} }
} }
// Unmarshal the allocation mappings from the request into the server struct. // Unmarshal the allocation mappings from the request into the server struct.
if b, _, _, err := jsonparser.Get(data, "allocations", "mappings"); err != nil { if b, _, _, err := jsonparser.Get(data, "allocations", "mappings"); err != nil {
return nil, errors.WithStack(err) return nil, err
} else { } else {
cfg.Allocations.Mappings = make(map[string][]int) cfg.Allocations.Mappings = make(map[string][]int)
if err := json.Unmarshal(b, &cfg.Allocations.Mappings); err != nil { if err := json.Unmarshal(b, &cfg.Allocations.Mappings); err != nil {
return nil, errors.WithStack(err) return nil, err
} }
} }
cfg.Container.Image = getString(data, "container", "image") cfg.Container.Image = getString(data, "container", "image")
c, rerr, err := api.NewRequester().GetServerConfiguration(cfg.Uuid) c, err := api.New().GetServerConfiguration(cfg.Uuid)
if err != nil || rerr != nil { if err != nil {
if err != nil { if !api.IsRequestError(err) {
return nil, errors.WithStack(err) return nil, err
} }
return nil, errors.New(rerr.String()) return nil, errors.New(err.Error())
} }
// Create a new server instance using the configuration we wrote to the disk // Create a new server instance using the configuration we wrote to the disk
@@ -98,34 +92,6 @@ func (i *Installer) Server() *server.Server {
return i.server return i.server
} }
// Executes the installer process, creating the server and running through the
// associated installation process based on the parameters passed through for
// the server instance.
func (i *Installer) Execute() {
p := path.Join(config.Get().System.Data, i.Uuid())
l := log.WithFields(log.Fields{"server": i.Uuid(), "process": "installer"})
l.WithField("path", p).Debug("creating required server data directory")
if err := os.MkdirAll(p, 0755); err != nil {
l.WithFields(log.Fields{"path": p, "error": errors.WithStack(err)}).Error("failed to create server data directory")
return
}
if err := os.Chown(p, config.Get().System.User.Uid, config.Get().System.User.Gid); err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to chown server data directory")
return
}
l.Debug("creating required environment for server instance")
// TODO: ensure data directory exists.
if err := i.server.Environment.Create(); err != nil {
l.WithField("error", err).Error("failed to create environment for server")
return
}
l.Info("successfully created environment for server during install process")
}
// Returns a string value from the JSON data provided. // Returns a string value from the JSON data provided.
func getString(data []byte, key ...string) string { func getString(data []byte, key ...string) string {
value, _ := jsonparser.GetString(data, key...) value, _ := jsonparser.GetString(data, key...)
@@ -139,3 +105,9 @@ func getInt(data []byte, key ...string) int64 {
return value return value
} }
func getBoolean(data []byte, key ...string) bool {
value, _ := jsonparser.GetBoolean(data, key...)
return value
}

View File

@@ -1,21 +1,22 @@
package cli package cli
import ( import (
"emperror.dev/errors"
"fmt" "fmt"
"github.com/apex/log" "github.com/apex/log"
"github.com/apex/log/handlers/cli" "github.com/apex/log/handlers/cli"
color2 "github.com/fatih/color" color2 "github.com/fatih/color"
"github.com/mattn/go-colorable" "github.com/mattn/go-colorable"
"github.com/pkg/errors"
"io" "io"
"os" "os"
"strings"
"sync" "sync"
"time" "time"
) )
var Default = New(os.Stderr, true) var Default = New(os.Stderr, true)
var bold = color2.New(color2.Bold) var bold = color2.New(color2.Bold)
var boldred = color2.New(color2.Bold, color2.FgRed)
var Strings = [...]string{ var Strings = [...]string{
log.DebugLevel: "DEBUG", log.DebugLevel: "DEBUG",
@@ -41,10 +42,6 @@ func New(w io.Writer, useColors bool) *Handler {
return &Handler{Writer: colorable.NewNonColorable(w), Padding: 2} return &Handler{Writer: colorable.NewNonColorable(w), Padding: 2}
} }
type tracer interface {
StackTrace() errors.StackTrace
}
// HandleLog implements log.Handler. // HandleLog implements log.Handler.
func (h *Handler) HandleLog(e *log.Entry) error { func (h *Handler) HandleLog(e *log.Entry) error {
color := cli.Colors[e.Level] color := cli.Colors[e.Level]
@@ -60,7 +57,6 @@ func (h *Handler) HandleLog(e *log.Entry) error {
if name == "source" { if name == "source" {
continue continue
} }
fmt.Fprintf(h.Writer, " %s=%v", color.Sprint(name), e.Fields.Get(name)) fmt.Fprintf(h.Writer, " %s=%v", color.Sprint(name), e.Fields.Get(name))
} }
@@ -71,44 +67,63 @@ func (h *Handler) HandleLog(e *log.Entry) error {
continue continue
} }
var br = color2.New(color2.Bold, color2.FgRed)
if err, ok := e.Fields.Get("error").(error); ok { if err, ok := e.Fields.Get("error").(error); ok {
fmt.Fprintf(h.Writer, "\n%s%+v\n\n", br.Sprintf("Stacktrace:"), getErrorStack(err, false)) // Attach the stacktrace if it is missing at this point, but don't point
} else { // it specifically to this line since that is irrelevant.
fmt.Fprintf(h.Writer, "\n%s%+v\n\n", br.Sprintf("Invalid Error:"), err) err = errors.WithStackDepthIf(err, 4)
formatted := fmt.Sprintf("\n%s\n%+v\n\n", boldred.Sprintf("Stacktrace:"), err)
if !strings.Contains(formatted, "runtime.goexit") {
_, _ = fmt.Fprint(h.Writer, formatted)
break
}
// Inserts a new-line between sections of a stack.
// When wrapping errors, you get multiple separate stacks that start with their message,
// this allows us to separate them with a new-line and view them more easily.
//
// For example:
//
// Stacktrace:
// readlink test: no such file or directory
// failed to read symlink target for 'test'
// github.com/pterodactyl/wings/server/filesystem.(*Archive).addToArchive
// github.com/pterodactyl/wings/server/filesystem/archive.go:166
// ... (Truncated the stack for easier reading)
// runtime.goexit
// runtime/asm_amd64.s:1374
// **NEW LINE INSERTED HERE**
// backup: error while generating server backup
// github.com/pterodactyl/wings/server.(*Server).Backup
// github.com/pterodactyl/wings/server/backup.go:84
// ... (Truncated the stack for easier reading)
// runtime.goexit
// runtime/asm_amd64.s:1374
//
var b strings.Builder
var endOfStack bool
for _, s := range strings.Split(formatted, "\n") {
b.WriteString(s + "\n")
if s == "runtime.goexit" {
endOfStack = true
continue
}
if !endOfStack {
continue
}
b.WriteString("\n")
endOfStack = false
}
_, _ = fmt.Fprint(h.Writer, b.String())
} }
// Only one key with the name "error" can be in the map.
break
} }
return nil return nil
} }
func getErrorStack(err error, i bool) errors.StackTrace {
e, ok := err.(tracer)
if !ok {
if i {
// Just abort out of this and return a stacktrace leading up to this point. It isn't perfect
// but it'll at least include what function lead to this being called which we can then handle.
return errors.Wrap(err, "failed to generate stacktrace for caught error").(tracer).StackTrace()
}
return getErrorStack(errors.Wrap(err, err.Error()), true)
}
st := e.StackTrace()
l := len(st)
// If this was an internal stack generation we're going to skip over the top four items in the stack
// trace since they'll point to the error that was generated by this function.
f := 0
if i {
f = 4
}
if i && l > 9 {
l = 9
} else if !i && l > 5 {
l = 5
}
return st[f:l]
}

View File

View File

@@ -2,16 +2,17 @@ package parser
import ( import (
"bytes" "bytes"
"github.com/Jeffail/gabs/v2"
"github.com/apex/log"
"github.com/buger/jsonparser"
"github.com/iancoleman/strcase"
"github.com/pkg/errors"
"io/ioutil" "io/ioutil"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"emperror.dev/errors"
"github.com/Jeffail/gabs/v2"
"github.com/apex/log"
"github.com/buger/jsonparser"
"github.com/iancoleman/strcase"
) )
// Regex to match anything that has a value matching the format of {{ config.$1 }} which // Regex to match anything that has a value matching the format of {{ config.$1 }} which
@@ -97,12 +98,20 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error
// time this code is being written. // time this code is being written.
for _, child := range parsed.Path(strings.Trim(parts[0], ".")).Children() { for _, child := range parsed.Path(strings.Trim(parts[0], ".")).Children() {
if err := v.SetAtPathway(child, strings.Trim(parts[1], "."), []byte(value)); err != nil { if err := v.SetAtPathway(child, strings.Trim(parts[1], "."), []byte(value)); err != nil {
return nil, err if errors.Is(err, gabs.ErrNotFound) {
continue
}
return nil, errors.WithMessage(err, "failed to set config value of array child")
} }
} }
} else { } else {
if err = v.SetAtPathway(parsed, v.Match, []byte(value)); err != nil { if err = v.SetAtPathway(parsed, v.Match, []byte(value)); err != nil {
return nil, err if errors.Is(err, gabs.ErrNotFound) {
continue
}
return nil, errors.WithMessage(err, "unable to set config value at pathway: "+v.Match)
} }
} }
} }
@@ -110,42 +119,113 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error
return parsed, nil return parsed, nil
} }
// Sets the value at a specific pathway, but checks if we were looking for a specific // Regex used to check if there is an array element present in the given pathway by looking for something
// value or not before doing it. // along the lines of "something[1]" or "something[1].nestedvalue" as the path.
func (cfr *ConfigurationFileReplacement) SetAtPathway(c *gabs.Container, path string, value []byte) error { var checkForArrayElement = regexp.MustCompile(`^([^\[\]]+)\[([\d]+)](\..+)?$`)
if cfr.IfValue != "" {
// If this is a regex based matching, we need to get a little more creative since
// we're only going to replacing part of the string, and not the whole thing.
if c.Exists(path) && strings.HasPrefix(cfr.IfValue, "regex:") {
// We're doing some regex here.
r, err := regexp.Compile(strings.TrimPrefix(cfr.IfValue, "regex:"))
if err != nil {
log.WithFields(log.Fields{"if_value": strings.TrimPrefix(cfr.IfValue, "regex:"), "error": err}).
Warn("configuration if_value using invalid regexp, cannot perform replacement")
return nil // Attempt to set the value of the path depending on if it is an array or not. Gabs cannot handle array
} // values as "something[1]" but can parse them just fine. This is basically just overly complex code
// to handle that edge case and ensure the value gets set correctly.
//
// Bless thee who has to touch these most unholy waters.
func setValueAtPath(c *gabs.Container, path string, value interface{}) error {
var err error
// If the path exists and there is a regex match, go ahead and attempt the replacement matches := checkForArrayElement.FindStringSubmatch(path)
// using the value we got from the key. This will only replace the one match. if len(matches) < 3 {
v := strings.Trim(string(c.Path(path).Bytes()), "\"") // Only update the value if the pathway actually exists in the configuration, otherwise
if r.Match([]byte(v)) { // do nothing.
_, err := c.SetP(r.ReplaceAllString(v, string(value)), path) if c.ExistsP(path) {
_, err = c.SetP(value, path)
}
return err return err
} }
return nil i, _ := strconv.Atoi(matches[2])
} else { // Find the array element "i" or try to create it if "i" is equal to 0 and is not found
if !c.Exists(path) || (c.Exists(path) && !bytes.Equal(c.Bytes(), []byte(cfr.IfValue))) { // at the given path.
return nil ct, err := c.ArrayElementP(i, matches[1])
} if err != nil {
if i != 0 || (!errors.Is(err, gabs.ErrNotArray) && !errors.Is(err, gabs.ErrNotFound)) {
return errors.WithMessage(err, "error while parsing array element at path")
}
t := make([]interface{}, 1)
// If the length of matches is 4 it means we're trying to access an object down in this array
// key, so make sure we generate the array as an array of objects, and not just a generic nil
// array.
if len(matches) == 4 {
t = []interface{}{map[string]interface{}{}}
}
// If the error is because this isn't an array or isn't found go ahead and create the array with
// an empty object if we have additional things to set on the array, or just an empty array type
// if there is not an object structure detected (no matches[3] available).
if _, err = c.SetP(t, matches[1]); err != nil {
return errors.WithMessage(err, "failed to create empty array for missing element")
}
// Set our cursor to be the array element we expect, which in this case is just the first element
// since we won't run this code unless the array element is 0. There is too much complexity in trying
// to match additional elements. In those cases the server will just have to be rebooted or something.
ct, err = c.ArrayElementP(0, matches[1])
if err != nil {
return errors.WithMessage(err, "failed to find array element at path")
} }
} }
_, err := c.SetP(cfr.getKeyValue(value), path) // Try to set the value. If the path does not exist an error will be raised to the caller which will
// then check if the error is because the path is missing. In those cases we just ignore the error since
// we don't want to do anything specifically when that happens.
//
// If there are four matches in the regex it means that we managed to also match a trailing pathway
// for the key, which should be found in the given array key item and modified further.
if len(matches) == 4 {
_, err = ct.SetP(value, strings.TrimPrefix(matches[3], "."))
} else {
_, err = ct.Set(value)
}
return err if err != nil {
return errors.WithMessage(err, "failed to set value at config path: "+path)
}
return nil
}
// Sets the value at a specific pathway, but checks if we were looking for a specific
// value or not before doing it.
func (cfr *ConfigurationFileReplacement) SetAtPathway(c *gabs.Container, path string, value []byte) error {
if cfr.IfValue == "" {
return setValueAtPath(c, path, cfr.getKeyValue(value))
}
// If this is a regex based matching, we need to get a little more creative since
// we're only going to replacing part of the string, and not the whole thing.
if c.ExistsP(path) && strings.HasPrefix(cfr.IfValue, "regex:") {
// We're doing some regex here.
r, err := regexp.Compile(strings.TrimPrefix(cfr.IfValue, "regex:"))
if err != nil {
log.WithFields(log.Fields{"if_value": strings.TrimPrefix(cfr.IfValue, "regex:"), "error": err}).
Warn("configuration if_value using invalid regexp, cannot perform replacement")
return nil
}
// If the path exists and there is a regex match, go ahead and attempt the replacement
// using the value we got from the key. This will only replace the one match.
v := strings.Trim(string(c.Path(path).Bytes()), "\"")
if r.Match([]byte(v)) {
return setValueAtPath(c, path, r.ReplaceAllString(v, string(value)))
}
return nil
} else if !c.ExistsP(path) || (c.ExistsP(path) && !bytes.Equal(c.Bytes(), []byte(cfr.IfValue))) {
return nil
}
return setValueAtPath(c, path, cfr.getKeyValue(value))
} }
// Looks up a configuration value on the Daemon given a dot-notated syntax. // Looks up a configuration value on the Daemon given a dot-notated syntax.
@@ -174,7 +254,7 @@ func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplac
match, _, _, err := jsonparser.Get(f.configuration, path...) match, _, _, err := jsonparser.Get(f.configuration, path...)
if err != nil { if err != nil {
if err != jsonparser.KeyPathNotFoundError { if err != jsonparser.KeyPathNotFoundError {
return string(match), errors.WithStack(err) return string(match), err
} }
log.WithFields(log.Fields{"path": path, "filename": f.FileName}).Debug("attempted to load a configuration value that does not exist") log.WithFields(log.Fields{"path": path, "filename": f.FileName}).Debug("attempted to load a configuration value that does not exist")

View File

@@ -3,20 +3,21 @@ package parser
import ( import (
"bufio" "bufio"
"encoding/json" "encoding/json"
"fmt" "io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"emperror.dev/errors"
"github.com/apex/log" "github.com/apex/log"
"github.com/beevik/etree" "github.com/beevik/etree"
"github.com/buger/jsonparser" "github.com/buger/jsonparser"
"github.com/icza/dyno" "github.com/icza/dyno"
"github.com/magiconair/properties" "github.com/magiconair/properties"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"io/ioutil"
"os"
"regexp"
"strings"
) )
// The file parsing options that are available for a server configuration file. // The file parsing options that are available for a server configuration file.
@@ -76,11 +77,6 @@ func (f *ConfigurationFile) UnmarshalJSON(data []byte) error {
return nil return nil
} }
// Regex to match paths such as foo[1].bar[2] and convert them into a format that
// gabs can work with, such as foo.1.bar.2 in this case. This is applied when creating
// the struct for the configuration file replacements.
var cfrMatchReplacement = regexp.MustCompile(`\[(\d+)]`)
// Defines a single find/replace instance for a given server configuration file. // Defines a single find/replace instance for a given server configuration file.
type ConfigurationFileReplacement struct { type ConfigurationFileReplacement struct {
Match string `json:"match"` Match string `json:"match"`
@@ -96,8 +92,7 @@ func (cfr *ConfigurationFileReplacement) UnmarshalJSON(data []byte) error {
return err return err
} }
// See comment on the replacement regex to understand what exactly this is doing. cfr.Match = m
cfr.Match = cfrMatchReplacement.ReplaceAllString(m, ".$1")
iv, err := jsonparser.GetString(data, "if_value") iv, err := jsonparser.GetString(data, "if_value")
// We only check keypath here since match & replace_with should be present on all of // We only check keypath here since match & replace_with should be present on all of
@@ -163,21 +158,26 @@ func (f *ConfigurationFile) Parse(path string, internal bool) error {
break break
} }
if os.IsNotExist(err) { if errors.Is(err, os.ErrNotExist) {
// File doesn't exist, we tried creating it, and same error is returned? Pretty // File doesn't exist, we tried creating it, and same error is returned? Pretty
// sure this pathway is impossible, but if not, abort here. // sure this pathway is impossible, but if not, abort here.
if internal { if internal {
return nil return nil
} }
if _, err := os.Create(path); err != nil { b := strings.TrimSuffix(path, filepath.Base(path))
return errors.WithStack(err) if err := os.MkdirAll(b, 0755); err != nil {
return errors.WithMessage(err, "failed to create base directory for missing configuration file")
} else {
if _, err := os.Create(path); err != nil {
return errors.WithMessage(err, "failed to create missing configuration file")
}
} }
return f.Parse(path, true) return f.Parse(path, true)
} }
return errors.WithStack(err) return err
} }
// Parses an xml file. // Parses an xml file.
@@ -220,7 +220,7 @@ func (f *ConfigurationFile) parseXmlFile(path string) error {
parts := strings.Split(replacement.Match, ".") parts := strings.Split(replacement.Match, ".")
// Set the initial element to be the root element, and then work from there. // Set the initial element to be the root element, and then work from there.
var element = doc.Root() element := doc.Root()
// Iterate over the path to create the required structure for the given element's path. // Iterate over the path to create the required structure for the given element's path.
// This does not set a value, only ensures that the base structure exists. We start at index // This does not set a value, only ensures that the base structure exists. We start at index
@@ -359,7 +359,7 @@ func (f *ConfigurationFile) parseYamlFile(path string) error {
// Unmarshal the yaml data into a JSON interface such that we can work with // Unmarshal the yaml data into a JSON interface such that we can work with
// any arbitrary data structure. If we don't do this, I can't use gabs which // any arbitrary data structure. If we don't do this, I can't use gabs which
// makes working with unknown JSON signficiantly easier. // makes working with unknown JSON significantly easier.
jsonBytes, err := json.Marshal(dyno.ConvertMapI2MapS(i)) jsonBytes, err := json.Marshal(dyno.ConvertMapI2MapS(i))
if err != nil { if err != nil {
return err return err
@@ -385,38 +385,25 @@ func (f *ConfigurationFile) parseYamlFile(path string) error {
// scanning a file and performing a replacement. You should attempt to use anything other // scanning a file and performing a replacement. You should attempt to use anything other
// than this function where possible. // than this function where possible.
func (f *ConfigurationFile) parseTextFile(path string) error { func (f *ConfigurationFile) parseTextFile(path string) error {
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644) input, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
return err return err
} }
defer file.Close()
scanner := bufio.NewScanner(file) lines := strings.Split(string(input), "\n")
for scanner.Scan() { for i, line := range lines {
hasReplaced := false
t := scanner.Text()
// Iterate over the potential replacements for the line and check if there are
// any matches.
for _, replace := range f.Replace { for _, replace := range f.Replace {
if !strings.HasPrefix(t, replace.Match) { // If this line doesn't match what we expect for the replacement, move on to the next
// line. Otherwise, update the line to have the replacement value.
if !strings.HasPrefix(line, replace.Match) {
continue continue
} }
hasReplaced = true lines[i] = replace.ReplaceWith.String()
t = strings.Replace(t, replace.Match, replace.ReplaceWith.String(), 1)
}
// If there was a replacement that occurred on this specific line, do a write to the file
// immediately to write that modified content to the disk.
if hasReplaced {
if _, err := file.WriteAt([]byte(t), int64(len(scanner.Bytes()))); err != nil {
return err
}
} }
} }
if err := scanner.Err(); err != nil { if err := ioutil.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644); err != nil {
return err return err
} }
@@ -426,11 +413,41 @@ func (f *ConfigurationFile) parseTextFile(path string) error {
// Parses a properties file and updates the values within it to match those that // Parses a properties file and updates the values within it to match those that
// are passed. Writes the file once completed. // are passed. Writes the file once completed.
func (f *ConfigurationFile) parsePropertiesFile(path string) error { func (f *ConfigurationFile) parsePropertiesFile(path string) error {
// Open the file.
f2, err := os.Open(path)
if err != nil {
return err
}
var s strings.Builder
// Get any header comments from the file.
scanner := bufio.NewScanner(f2)
for scanner.Scan() {
text := scanner.Text()
if len(text) > 0 && text[0] != '#' {
break
}
s.WriteString(text)
s.WriteString("\n")
}
// Close the file.
_ = f2.Close()
// Handle any scanner errors.
if err := scanner.Err(); err != nil {
return err
}
// Decode the properties file.
p, err := properties.LoadFile(path, properties.UTF8) p, err := properties.LoadFile(path, properties.UTF8)
if err != nil { if err != nil {
return err return err
} }
// Replace any values that need to be replaced.
for _, replace := range f.Replace { for _, replace := range f.Replace {
data, err := f.LookupConfigurationValue(replace) data, err := f.LookupConfigurationValue(replace)
if err != nil { if err != nil {
@@ -450,23 +467,28 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
} }
} }
// Add the new file content to the string builder.
for _, key := range p.Keys() {
value, ok := p.Get(key)
if !ok {
continue
}
s.WriteString(key)
s.WriteByte('=')
s.WriteString(strings.Trim(strconv.QuoteToASCII(value), `"`))
s.WriteString("\n")
}
// Open the file for writing.
w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil { if err != nil {
return err return err
} }
defer w.Close()
var s string // Write the data to the file.
// This is a copy of the properties.String() func except we don't plop spaces around if _, err := w.Write([]byte(s.String())); err != nil {
// the key=value configurations since people like to complain about that.
// func (p *Properties) String() string
for _, key := range p.Keys() {
value, _ := p.Get(key)
s = fmt.Sprintf("%s%s=%s\n", s, key, value)
}
// Can't use the properties.Write() function since that doesn't apply our nicer formatting.
if _, err := w.Write([]byte(s)); err != nil {
return err return err
} }

View File

@@ -21,4 +21,4 @@ func (cv *ReplaceValue) String() string {
func (cv *ReplaceValue) Type() jsonparser.ValueType { func (cv *ReplaceValue) Type() jsonparser.ValueType {
return cv.valueType return cv.valueType
} }

View File

@@ -0,0 +1,319 @@
package downloader
import (
"context"
"emperror.dev/errors"
"encoding/json"
"fmt"
"github.com/google/uuid"
"github.com/pterodactyl/wings/server"
"io"
"net"
"net/http"
"net/url"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
)
var client = &http.Client{
Timeout: time.Hour * 12,
// Disallow any redirect on a HTTP call. This is a security requirement: do not modify
// this logic without first ensuring that the new target location IS NOT within the current
// instance's local network.
//
// This specific error response just causes the client to not follow the redirect and
// returns the actual redirect response to the caller. Not perfect, but simple and most
// people won't be using URLs that redirect anyways hopefully?
//
// We'll re-evaluate this down the road if needed.
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
var instance = &Downloader{
// Tracks all of the active downloads.
downloadCache: make(map[string]*Download),
// Tracks all of the downloads active for a given server instance. This is
// primarily used to make things quicker and keep the code a little more
// legible throughout here.
serverCache: make(map[string][]string),
}
// Regex to match the end of an IPv4/IPv6 address. This allows the port to be removed
// so that we are just working with the raw IP address in question.
var ipMatchRegex = regexp.MustCompile(`(:\d+)$`)
// Internal IP ranges that should be blocked if the resource requested resolves within.
var internalRanges = []*net.IPNet{
mustParseCIDR("127.0.0.1/8"),
mustParseCIDR("10.0.0.0/8"),
mustParseCIDR("172.16.0.0/12"),
mustParseCIDR("192.168.0.0/16"),
mustParseCIDR("169.254.0.0/16"),
mustParseCIDR("::1/128"),
mustParseCIDR("fe80::/10"),
mustParseCIDR("fc00::/7"),
}
const ErrInternalResolution = errors.Sentinel("downloader: destination resolves to internal network location")
const ErrInvalidIPAddress = errors.Sentinel("downloader: invalid IP address")
const ErrDownloadFailed = errors.Sentinel("downloader: download request failed")
type Counter struct {
total int
onWrite func(total int)
}
func (c *Counter) Write(p []byte) (int, error) {
n := len(p)
c.total += n
c.onWrite(c.total)
return n, nil
}
type DownloadRequest struct {
URL *url.URL
Directory string
}
type Download struct {
Identifier string
mu sync.RWMutex
req DownloadRequest
server *server.Server
progress float64
cancelFunc *context.CancelFunc
}
// Starts a new tracked download which allows for cancelation later on by calling
// the Downloader.Cancel function.
func New(s *server.Server, r DownloadRequest) *Download {
dl := Download{
Identifier: uuid.Must(uuid.NewRandom()).String(),
req: r,
server: s,
}
instance.track(&dl)
return &dl
}
// Returns all of the tracked downloads for a given server instance.
func ByServer(sid string) []*Download {
instance.mu.Lock()
defer instance.mu.Unlock()
var downloads []*Download
if v, ok := instance.serverCache[sid]; ok {
for _, id := range v {
if dl, dlok := instance.downloadCache[id]; dlok {
downloads = append(downloads, dl)
}
}
}
return downloads
}
// Returns a single Download matching a given identifier. If no download is found
// the second argument in the response will be false.
func ByID(dlid string) *Download {
return instance.find(dlid)
}
//goland:noinspection GoVetCopyLock
func (dl Download) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Identifier string
Progress float64
}{
Identifier: dl.Identifier,
Progress: dl.Progress(),
})
}
// Executes a given download for the server and begins writing the file to the disk. Once
// completed the download will be removed from the cache.
func (dl *Download) Execute() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Hour*12)
dl.cancelFunc = &cancel
defer dl.Cancel()
// Always ensure that we're checking the destination for the download to avoid a malicious
// user from accessing internal network resources.
if err := dl.isExternalNetwork(ctx); err != nil {
return err
}
// At this point we have verified the destination is not within the local network, so we can
// now make a request to that URL and pull down the file, saving it to the server's data
// directory.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, dl.req.URL.String(), nil)
if err != nil {
return errors.WrapIf(err, "downloader: failed to create request")
}
req.Header.Set("User-Agent", "Pterodactyl Panel (https://pterodactyl.io)")
res, err := client.Do(req)
if err != nil {
return ErrDownloadFailed
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return errors.New("downloader: got bad response status from endpoint: " + res.Status)
}
// If there is a Content-Length header on this request go ahead and check that we can
// even write the whole file before beginning this process. If there is no header present
// we'll just have to give it a spin and see how it goes.
if res.ContentLength > 0 {
if err := dl.server.Filesystem().HasSpaceFor(res.ContentLength); err != nil {
return errors.WrapIf(err, "downloader: failed to write file: not enough space")
}
}
fnameparts := strings.Split(dl.req.URL.Path, "/")
p := filepath.Join(dl.req.Directory, fnameparts[len(fnameparts)-1])
dl.server.Log().WithField("path", p).Debug("writing remote file to disk")
r := io.TeeReader(res.Body, dl.counter(res.ContentLength))
if err := dl.server.Filesystem().Writefile(p, r); err != nil {
return errors.WrapIf(err, "downloader: failed to write file to server directory")
}
return nil
}
// Cancels a running download and frees up the associated resources. If a file is being
// written a partial file will remain present on the disk.
func (dl *Download) Cancel() {
if dl.cancelFunc != nil {
(*dl.cancelFunc)()
}
instance.remove(dl.Identifier)
}
// Checks if the given download belongs to the provided server.
func (dl *Download) BelongsTo(s *server.Server) bool {
return dl.server.Id() == s.Id()
}
// Returns the current progress of the download as a float value between 0 and 1 where
// 1 indicates that the download is completed.
func (dl *Download) Progress() float64 {
dl.mu.RLock()
defer dl.mu.RUnlock()
return dl.progress
}
// Handles a write event by updating the progress completed percentage and firing off
// events to the server websocket as needed.
func (dl *Download) counter(contentLength int64) *Counter {
onWrite := func(t int) {
dl.mu.Lock()
defer dl.mu.Unlock()
dl.progress = float64(t) / float64(contentLength)
}
return &Counter{
onWrite: onWrite,
}
}
// Verifies that a given download resolves to a location not within the current local
// network for the machine. If the final destination of a resource is within the local
// network an ErrInternalResolution error is returned.
func (dl *Download) isExternalNetwork(ctx context.Context) error {
dialer := &net.Dialer{
LocalAddr: nil,
}
host := dl.req.URL.Host
if !ipMatchRegex.MatchString(host) {
if dl.req.URL.Scheme == "https" {
host = host + ":443"
} else {
host = host + ":80"
}
}
c, err := dialer.DialContext(ctx, "tcp", host)
if err != nil {
return errors.WithStack(err)
}
c.Close()
ip := net.ParseIP(ipMatchRegex.ReplaceAllString(c.RemoteAddr().String(), ""))
if ip == nil {
return errors.WithStack(ErrInvalidIPAddress)
}
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsInterfaceLocalMulticast() {
return errors.WithStack(ErrInternalResolution)
}
for _, block := range internalRanges {
if block.Contains(ip) {
return errors.WithStack(ErrInternalResolution)
}
}
return nil
}
// Defines a global downloader struct that keeps track of all currently processing downloads
// for the machine.
type Downloader struct {
mu sync.RWMutex
downloadCache map[string]*Download
serverCache map[string][]string
}
// Tracks a download in the internal cache for this instance.
func (d *Downloader) track(dl *Download) {
d.mu.Lock()
defer d.mu.Unlock()
sid := dl.server.Id()
if _, ok := d.downloadCache[dl.Identifier]; !ok {
d.downloadCache[dl.Identifier] = dl
if _, ok := d.serverCache[sid]; !ok {
d.serverCache[sid] = []string{}
}
d.serverCache[sid] = append(d.serverCache[sid], dl.Identifier)
}
}
// Finds a given download entry using the provided ID and returns it.
func (d *Downloader) find(dlid string) *Download {
d.mu.RLock()
defer d.mu.RUnlock()
if entry, ok := d.downloadCache[dlid]; ok {
return entry
}
return nil
}
// Remove the given download reference from the cache storing them. This also updates
// the slice of active downloads for a given server to not include this download.
func (d *Downloader) remove(dlid string) {
d.mu.Lock()
defer d.mu.Unlock()
if _, ok := d.downloadCache[dlid]; !ok {
return
}
sid := d.downloadCache[dlid].server.Id()
delete(d.downloadCache, dlid)
if tracked, ok := d.serverCache[sid]; ok {
var out []string
for _, k := range tracked {
if k != dlid {
out = append(out, k)
}
}
d.serverCache[sid] = out
}
}
func mustParseCIDR(ip string) *net.IPNet {
_, block, err := net.ParseCIDR(ip)
if err != nil {
panic(fmt.Errorf("downloader: failed to parse CIDR: %s", err))
}
return block
}

View File

@@ -2,55 +2,64 @@ package router
import ( import (
"fmt" "fmt"
"net/http"
"os"
"strings"
"emperror.dev/errors"
"github.com/apex/log" "github.com/apex/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server"
"net/http" "github.com/pterodactyl/wings/server/filesystem"
"os"
) )
type RequestError struct { type RequestError struct {
Err error err error
Uuid string uuid string
Message string message string
server *server.Server server *server.Server
} }
// Attaches an error to the gin.Context object for the request and ensures that it
// has a proper stacktrace associated with it when doing so.
//
// If you just call c.Error(err) without using this function you'll likely end up
// with an error that has no annotated stack on it.
func WithError(c *gin.Context, err error) error {
return c.Error(errors.WithStackDepthIf(err, 1))
}
// Generates a new tracked error, which simply tracks the specific error that // Generates a new tracked error, which simply tracks the specific error that
// is being passed in, and also assigned a UUID to the error so that it can be // is being passed in, and also assigned a UUID to the error so that it can be
// cross referenced in the logs. // cross referenced in the logs.
func TrackedError(err error) *RequestError { func NewTrackedError(err error) *RequestError {
return &RequestError{ return &RequestError{
Err: err, err: err,
Uuid: uuid.Must(uuid.NewRandom()).String(), uuid: uuid.Must(uuid.NewRandom()).String(),
Message: "",
} }
} }
// Same as TrackedError, except this will also attach the server instance that // Same as NewTrackedError, except this will also attach the server instance that
// generated this server for the purposes of logging. // generated this server for the purposes of logging.
func TrackedServerError(err error, s *server.Server) *RequestError { func NewServerError(err error, s *server.Server) *RequestError {
return &RequestError{ return &RequestError{
Err: errors.WithStack(err), err: err,
Uuid: uuid.Must(uuid.NewRandom()).String(), uuid: uuid.Must(uuid.NewRandom()).String(),
Message: "", server: s,
server: s,
} }
} }
func (e *RequestError) logger() *log.Entry { func (e *RequestError) logger() *log.Entry {
if e.server != nil { if e.server != nil {
return e.server.Log().WithField("error_id", e.Uuid) return e.server.Log().WithField("error_id", e.uuid).WithField("error", e.err)
} }
return log.WithField("error_id", e.uuid).WithField("error", e.err)
return log.WithField("error_id", e.Uuid)
} }
// Sets the output message to display to the user in the error. // Sets the output message to display to the user in the error.
func (e *RequestError) SetMessage(msg string) *RequestError { func (e *RequestError) SetMessage(msg string) *RequestError {
e.Message = msg e.message = msg
return e return e
} }
@@ -58,44 +67,86 @@ func (e *RequestError) SetMessage(msg string) *RequestError {
// will also include the error UUID in the output so that the user can report that // will also include the error UUID in the output so that the user can report that
// and link the response to a specific error in the logs. // and link the response to a specific error in the logs.
func (e *RequestError) AbortWithStatus(status int, c *gin.Context) { func (e *RequestError) AbortWithStatus(status int, c *gin.Context) {
// In instances where the status has already been set just use that existing status
// since we cannot change it at this point, and trying to do so will emit a gin warning
// into the program output.
if c.Writer.Status() != 200 {
status = c.Writer.Status()
}
// If this error is because the resource does not exist, we likely do not need to log // If this error is because the resource does not exist, we likely do not need to log
// the error anywhere, just return a 404 and move on with our lives. // the error anywhere, just return a 404 and move on with our lives.
if os.IsNotExist(e.Err) { if errors.Is(e.err, os.ErrNotExist) {
e.logger().WithField("error", e.Err).Debug("encountered os.IsNotExist error while handling request") e.logger().Debug("encountered os.IsNotExist error while handling request")
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on the system.", "error": "The requested resource was not found on the system.",
}) })
return return
} }
if strings.HasPrefix(e.err.Error(), "invalid URL escape") {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Some of the data provided in the request appears to be escaped improperly.",
})
return
}
// If this is a Filesystem error just return it without all of the tracking code nonsense
// since we don't need to be logging it into the logs or anything, its just a normal error
// that the user can solve on their end.
if st, msg := e.getAsFilesystemError(); st != 0 {
c.AbortWithStatusJSON(st, gin.H{"error": msg})
return
}
// Otherwise, log the error to zap, and then report the error back to the user. // Otherwise, log the error to zap, and then report the error back to the user.
if status >= 500 { if status >= 500 {
e.logger().WithField("error", e.Err).Error("encountered HTTP/500 error while handling request") e.logger().Error("unexpected error while handling HTTP request")
c.Error(errors.WithStack(e))
} else { } else {
e.logger().WithField("error", e.Err).Debug("encountered non-HTTP/500 error while handling request") e.logger().Debug("non-server error encountered while handling HTTP request")
} }
msg := "An unexpected error was encountered while processing this request." if e.message == "" {
if e.Message != "" { e.message = "An unexpected error was encountered while processing this request."
msg = e.Message
} }
c.AbortWithStatusJSON(status, gin.H{ c.AbortWithStatusJSON(status, gin.H{"error": e.message, "error_id": e.uuid})
"error": msg,
"error_id": e.Uuid,
})
} }
// Helper function to just abort with an internal server error. This is generally the response // Helper function to just abort with an internal server error. This is generally the response
// from most errors encountered by the API. // from most errors encountered by the API.
func (e *RequestError) AbortWithServerError(c *gin.Context) { func (e *RequestError) Abort(c *gin.Context) {
e.AbortWithStatus(http.StatusInternalServerError, c) e.AbortWithStatus(http.StatusInternalServerError, c)
} }
// Looks at the given RequestError and determines if it is a specific filesystem error that
// we can process and return differently for the user.
func (e *RequestError) getAsFilesystemError() (int, string) {
err := errors.Unwrap(e.err)
if err == nil {
return 0, ""
}
if errors.Is(err, os.ErrNotExist) || filesystem.IsErrorCode(err, filesystem.ErrCodePathResolution) {
return http.StatusNotFound, "The requested resource was not found on the system."
}
if filesystem.IsErrorCode(err, filesystem.ErrCodeDiskSpace) {
return http.StatusConflict, "There is not enough disk space available to perform that action."
}
if strings.HasSuffix(err.Error(), "file name too long") {
return http.StatusBadRequest, "Cannot perform that action: file name is too long."
}
if e, ok := err.(*os.SyscallError); ok && e.Syscall == "readdirent" {
return http.StatusNotFound, "The requested directory does not exist."
}
return 0, ""
}
// Handle specific filesystem errors for a server.
func (e *RequestError) AbortFilesystemError(c *gin.Context) {
e.Abort(c)
}
// Format the error to a string and include the UUID. // Format the error to a string and include the UUID.
func (e *RequestError) Error() string { func (e *RequestError) Error() string {
return fmt.Sprintf("%v (uuid: %s)", e.Err, e.Uuid) return fmt.Sprintf("%v (uuid: %s)", e.err, e.uuid)
} }

View File

@@ -1,65 +1,101 @@
package router package router
import ( import (
"io"
"net/http"
"strings"
"emperror.dev/errors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server"
"net/http"
"strings"
) )
// Set the access request control headers on all of the requests. type Middleware struct{}
func SetAccessControlHeaders(c *gin.Context) {
c.Header("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
o := c.GetHeader("Origin") // A custom handler function allowing for errors bubbled up by c.Error() to be returned in a
if o != config.Get().PanelLocation { // standardized format with tracking UUIDs on them for easier log searching.
for _, origin := range config.Get().AllowedOrigins { func (m *Middleware) ErrorHandler() gin.HandlerFunc {
if o != origin { return func(c *gin.Context) {
continue c.Next()
} err := c.Errors.Last()
if err == nil || err.Err == nil {
c.Header("Access-Control-Allow-Origin", origin)
c.Next()
return return
} }
tracked := NewTrackedError(err.Err)
// If there is a server in the context for this request pull it out so that we can
// track the error specifically for that server.
if s, ok := c.Get("server"); ok {
tracked = NewServerError(err.Err, s.(*server.Server))
}
// This error occurs if you submit invalid JSON data to an endpoint.
if err.Err.Error() == io.EOF.Error() {
c.JSON(c.Writer.Status(), gin.H{"error": "A JSON formatted body is required for this endpoint."})
return
}
tracked.Abort(c)
return
} }
}
c.Header("Access-Control-Allow-Origin", config.Get().PanelLocation) // Set the access request control headers on all of the requests.
c.Next() func (m *Middleware) SetAccessControlHeaders() gin.HandlerFunc {
origins := config.Get().AllowedOrigins
location := config.Get().PanelLocation
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Methods", "GET, POST, PATCH, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Accept, Accept-Encoding, Authorization, Cache-Control, Content-Type, Content-Length, Origin, X-Real-IP, X-CSRF-Token")
o := c.GetHeader("Origin")
if o != location {
for _, origin := range origins {
if origin != "*" && o != origin {
continue
}
c.Header("Access-Control-Allow-Origin", origin)
c.Next()
return
}
}
c.Header("Access-Control-Allow-Origin", location)
c.Next()
}
} }
// Authenticates the request token against the given permission string, ensuring that // Authenticates the request token against the given permission string, ensuring that
// if it is a server permission, the token has control over that server. If it is a global // if it is a server permission, the token has control over that server. If it is a global
// token, this will ensure that the request is using a properly signed global token. // token, this will ensure that the request is using a properly signed global token.
func AuthorizationMiddleware(c *gin.Context) { func (m *Middleware) RequireAuthorization() gin.HandlerFunc {
auth := strings.SplitN(c.GetHeader("Authorization"), " ", 2) token := config.Get().AuthenticationToken
return func(c *gin.Context) {
auth := strings.SplitN(c.GetHeader("Authorization"), " ", 2)
if len(auth) != 2 || auth[0] != "Bearer" {
c.Header("WWW-Authenticate", "Bearer")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "The required authorization heads were not present in the request.",
})
return
}
if len(auth) != 2 || auth[0] != "Bearer" { // All requests to Wings must be authorized with the authentication token present in
c.Header("WWW-Authenticate", "Bearer") // the Wings configuration file. Remeber, all requests to Wings come from the Panel
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ // backend, or using a signed JWT for temporary authentication.
"error": "The required authorization heads were not present in the request.", if auth[1] == token {
c.Next()
return
}
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "You are not authorized to access this endpoint.",
}) })
return
} }
// Try to match the request against the global token for the Daemon, regardless
// of the permission type. If nothing is matched we will fall through to the Panel
// API to try and validate permissions for a server.
if auth[1] == config.Get().AuthenticationToken {
c.Next()
return
}
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "You are not authorized to access this endpoint.",
})
} }
// Helper function to fetch a server out of the servers collection stored in memory. // Helper function to fetch a server out of the servers collection stored in memory.
//
// This function should not be used in new controllers, prefer ExtractServer where
// possible.
func GetServer(uuid string) *server.Server { func GetServer(uuid string) *server.Server {
return server.GetServers().Find(func(s *server.Server) bool { return server.GetServers().Find(func(s *server.Server) bool {
return uuid == s.Id() return uuid == s.Id()
@@ -68,14 +104,43 @@ func GetServer(uuid string) *server.Server {
// Ensure that the requested server exists in this setup. Returns a 404 if we cannot // Ensure that the requested server exists in this setup. Returns a 404 if we cannot
// locate it. // locate it.
func ServerExists(c *gin.Context) { func (m *Middleware) ServerExists() gin.HandlerFunc {
u, err := uuid.Parse(c.Param("server")) return func(c *gin.Context) {
if err != nil || GetServer(u.String()) == nil { u, err := uuid.Parse(c.Param("server"))
if err == nil {
if s := GetServer(u.String()); s != nil {
c.Set("server", s)
c.Next()
return
}
}
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The resource you requested does not exist.", "error": "The resource you requested does not exist.",
}) })
return
} }
}
c.Next()
// Checks if remote file downloading is enabled on this instance before allowing access
// to the given endpoint.
func (m *Middleware) CheckRemoteDownloadEnabled() gin.HandlerFunc {
disabled := config.Get().Api.DisableRemoteDownload
return func(c *gin.Context) {
if disabled {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "This functionality is not currently enabled on this instance.",
})
return
}
c.Next()
}
}
// Returns the server instance from the gin context. If there is no server set in the
// context (e.g. calling from a controller not protected by ServerExists) this function
// will panic.
func ExtractServer(c *gin.Context) *server.Server {
if s, ok := c.Get("server"); ok {
return s.(*server.Server)
}
panic(errors.New("cannot extract server, missing on gin context"))
} }

View File

@@ -9,10 +9,9 @@ import (
func Configure() *gin.Engine { func Configure() *gin.Engine {
gin.SetMode("release") gin.SetMode("release")
m := Middleware{}
router := gin.New() router := gin.New()
router.Use(gin.Recovery(), m.ErrorHandler(), m.SetAccessControlHeaders())
router.Use(gin.Recovery())
router.Use(SetAccessControlHeaders)
// @todo log this into a different file so you can setup IP blocking for abusive requests and such. // @todo log this into a different file so you can setup IP blocking for abusive requests and such.
// This should still dump requests in debug mode since it does help with understanding the request // This should still dump requests in debug mode since it does help with understanding the request
// lifecycle and quickly seeing what was called leading to the logs. However, it isn't feasible to mix // lifecycle and quickly seeing what was called leading to the logs. However, it isn't feasible to mix
@@ -40,16 +39,16 @@ func Configure() *gin.Engine {
// This route is special it sits above all of the other requests because we are // This route is special it sits above all of the other requests because we are
// using a JWT to authorize access to it, therefore it needs to be publicly // using a JWT to authorize access to it, therefore it needs to be publicly
// accessible. // accessible.
router.GET("/api/servers/:server/ws", ServerExists, getServerWebsocket) router.GET("/api/servers/:server/ws", m.ServerExists(), getServerWebsocket)
// This request is called by another daemon when a server is going to be transferred out. // This request is called by another daemon when a server is going to be transferred out.
// This request does not need the AuthorizationMiddleware as the panel should never call it // This request does not need the AuthorizationMiddleware as the panel should never call it
// and requests are authenticated through a JWT the panel issues to the other daemon. // and requests are authenticated through a JWT the panel issues to the other daemon.
router.GET("/api/servers/:server/archive", ServerExists, getServerArchive) router.GET("/api/servers/:server/archive", m.ServerExists(), getServerArchive)
// All of the routes beyond this mount will use an authorization middleware // All of the routes beyond this mount will use an authorization middleware
// and will not be accessible without the correct Authorization header provided. // and will not be accessible without the correct Authorization header provided.
protected := router.Use(AuthorizationMiddleware) protected := router.Use(m.RequireAuthorization())
protected.POST("/api/update", postUpdateConfiguration) protected.POST("/api/update", postUpdateConfiguration)
protected.GET("/api/system", getSystemInformation) protected.GET("/api/system", getSystemInformation)
protected.GET("/api/servers", getAllServers) protected.GET("/api/servers", getAllServers)
@@ -59,7 +58,7 @@ func Configure() *gin.Engine {
// These are server specific routes, and require that the request be authorized, and // These are server specific routes, and require that the request be authorized, and
// that the server exist on the Daemon. // that the server exist on the Daemon.
server := router.Group("/api/servers/:server") server := router.Group("/api/servers/:server")
server.Use(AuthorizationMiddleware, ServerExists) server.Use(m.RequireAuthorization(), m.ServerExists())
{ {
server.GET("", getServer) server.GET("", getServer)
server.PATCH("", patchServer) server.PATCH("", patchServer)
@@ -70,6 +69,7 @@ func Configure() *gin.Engine {
server.POST("/commands", postServerCommands) server.POST("/commands", postServerCommands)
server.POST("/install", postServerInstall) server.POST("/install", postServerInstall)
server.POST("/reinstall", postServerReinstall) server.POST("/reinstall", postServerReinstall)
server.POST("/ws/deny", postServerDenyWSTokens)
// This archive request causes the archive to start being created // This archive request causes the archive to start being created
// this should only be triggered by the panel. // this should only be triggered by the panel.
@@ -86,6 +86,11 @@ func Configure() *gin.Engine {
files.POST("/delete", postServerDeleteFiles) files.POST("/delete", postServerDeleteFiles)
files.POST("/compress", postServerCompressFiles) files.POST("/compress", postServerCompressFiles)
files.POST("/decompress", postServerDecompressFiles) files.POST("/decompress", postServerDecompressFiles)
files.POST("/chmod", postServerChmodFile)
files.GET("/pull", m.CheckRemoteDownloadEnabled(), getServerPullingFiles)
files.POST("/pull", m.CheckRemoteDownloadEnabled(), postServerPullRemoteFile)
files.DELETE("/pull/:download", m.CheckRemoteDownloadEnabled(), deleteServerPullRemoteFile)
} }
backup := server.Group("/backup") backup := server.Group("/backup")

View File

@@ -2,19 +2,21 @@ package router
import ( import (
"bufio" "bufio"
"github.com/gin-gonic/gin" "errors"
"github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server/backup"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server/backup"
) )
// Handle a download request for a server backup. // Handle a download request for a server backup.
func getDownloadBackup(c *gin.Context) { func getDownloadBackup(c *gin.Context) {
token := tokens.BackupPayload{} token := tokens.BackupPayload{}
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil { if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
TrackedError(err).AbortWithServerError(c) NewTrackedError(err).Abort(c)
return return
} }
@@ -28,20 +30,20 @@ func getDownloadBackup(c *gin.Context) {
b, st, err := backup.LocateLocal(token.BackupUuid) b, st, err := backup.LocateLocal(token.BackupUuid)
if err != nil { if err != nil {
if os.IsNotExist(err) { if errors.Is(err, os.ErrNotExist) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested backup was not found on this server.", "error": "The requested backup was not found on this server.",
}) })
return return
} }
TrackedServerError(err, s).AbortWithServerError(c) NewServerError(err, s).Abort(c)
return return
} }
f, err := os.Open(b.Path()) f, err := os.Open(b.Path())
if err != nil { if err != nil {
TrackedServerError(err, s).AbortWithServerError(c) NewServerError(err, s).Abort(c)
return return
} }
defer f.Close() defer f.Close()
@@ -57,7 +59,7 @@ func getDownloadBackup(c *gin.Context) {
func getDownloadFile(c *gin.Context) { func getDownloadFile(c *gin.Context) {
token := tokens.FilePayload{} token := tokens.FilePayload{}
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil { if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
TrackedError(err).AbortWithServerError(c) NewTrackedError(err).Abort(c)
return return
} }
@@ -69,12 +71,12 @@ func getDownloadFile(c *gin.Context) {
return return
} }
p, _ := s.Filesystem.SafePath(token.FilePath) p, _ := s.Filesystem().SafePath(token.FilePath)
st, err := os.Stat(p) st, err := os.Stat(p)
// If there is an error or we're somehow trying to download a directory, just // If there is an error or we're somehow trying to download a directory, just
// respond with the appropriate error. // respond with the appropriate error.
if err != nil { if err != nil {
TrackedServerError(err, s).AbortWithServerError(c) NewServerError(err, s).Abort(c)
return return
} else if st.IsDir() { } else if st.IsDir() {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
@@ -85,7 +87,7 @@ func getDownloadFile(c *gin.Context) {
f, err := os.Open(p) f, err := os.Open(p)
if err != nil { if err != nil {
TrackedServerError(err, s).AbortWithServerError(c) NewServerError(err, s).Abort(c)
return return
} }
@@ -94,4 +96,4 @@ func getDownloadFile(c *gin.Context) {
c.Header("Content-Type", "application/octet-stream") c.Header("Content-Type", "application/octet-stream")
bufio.NewReader(f).WriteTo(c.Writer) bufio.NewReader(f).WriteTo(c.Writer)
} }

View File

@@ -3,36 +3,47 @@ package router
import ( import (
"bytes" "bytes"
"context" "context"
"github.com/apex/log"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/server"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/router/downloader"
"github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server"
) )
type serverProcData struct {
server.ResourceUsage
Suspended bool `json:"suspended"`
}
// Returns a single server from the collection of servers. // Returns a single server from the collection of servers.
func getServer(c *gin.Context) { func getServer(c *gin.Context) {
s := GetServer(c.Param("server")) s := GetServer(c.Param("server"))
p := *s.Proc() c.JSON(http.StatusOK, serverProcData{
ResourceUsage: s.Proc(),
c.JSON(http.StatusOK, p) Suspended: s.IsSuspended(),
})
} }
// Returns the logs for a given server instance. // Returns the logs for a given server instance.
func getServerLogs(c *gin.Context) { func getServerLogs(c *gin.Context) {
s := GetServer(c.Param("server")) s := GetServer(c.Param("server"))
l, _ := strconv.ParseInt(c.DefaultQuery("size", "8192"), 10, 64) l, _ := strconv.Atoi(c.DefaultQuery("size", "100"))
if l <= 0 { if l <= 0 {
l = 2048 l = 100
} else if l > 100 {
l = 100
} }
out, err := s.ReadLogfile(l) out, err := s.ReadLogfile(l)
if err != nil { if err != nil {
TrackedServerError(err, s).AbortWithServerError(c) NewServerError(err, s).Abort(c)
return return
} }
@@ -50,7 +61,7 @@ func getServerLogs(c *gin.Context) {
func postServerPower(c *gin.Context) { func postServerPower(c *gin.Context) {
s := GetServer(c.Param("server")) s := GetServer(c.Param("server"))
var data struct{ var data struct {
Action server.PowerAction `json:"action"` Action server.PowerAction `json:"action"`
} }
@@ -78,7 +89,7 @@ func postServerPower(c *gin.Context) {
return return
} }
// Pass the actual heavy processing off to a seperate thread to handle so that // Pass the actual heavy processing off to a separate thread to handle so that
// we can immediately return a response from the server. Some of these actions // we can immediately return a response from the server. Some of these actions
// can take quite some time, especially stopping or restarting. // can take quite some time, especially stopping or restarting.
go func(s *server.Server) { go func(s *server.Server) {
@@ -101,7 +112,7 @@ func postServerCommands(c *gin.Context) {
s := GetServer(c.Param("server")) s := GetServer(c.Param("server"))
if running, err := s.Environment.IsRunning(); err != nil { if running, err := s.Environment.IsRunning(); err != nil {
TrackedServerError(err, s).AbortWithServerError(c) NewServerError(err, s).Abort(c)
return return
} else if !running { } else if !running {
c.AbortWithStatusJSON(http.StatusBadGateway, gin.H{ c.AbortWithStatusJSON(http.StatusBadGateway, gin.H{
@@ -134,11 +145,13 @@ func patchServer(c *gin.Context) {
buf := bytes.Buffer{} buf := bytes.Buffer{}
buf.ReadFrom(c.Request.Body) buf.ReadFrom(c.Request.Body)
if err := s.UpdateDataStructure(buf.Bytes(), true); err != nil { if err := s.UpdateDataStructure(buf.Bytes()); err != nil {
TrackedServerError(err, s).AbortWithServerError(c) NewServerError(err, s).Abort(c)
return return
} }
s.SyncWithEnvironment()
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)
} }
@@ -159,27 +172,36 @@ func postServerInstall(c *gin.Context) {
func postServerReinstall(c *gin.Context) { func postServerReinstall(c *gin.Context) {
s := GetServer(c.Param("server")) s := GetServer(c.Param("server"))
go func(serv *server.Server) { if s.ExecutingPowerAction() {
if err := serv.Reinstall(); err != nil { c.AbortWithStatusJSON(http.StatusConflict, gin.H{
serv.Log().WithField("error", err).Error("failed to complete server re-install process") "error": "Cannot execute server reinstall event while another power action is running.",
})
return
}
go func(s *server.Server) {
if err := s.Reinstall(); err != nil {
s.Log().WithField("error", err).Error("failed to complete server re-install process")
} }
}(s) }(s)
c.Status(http.StatusAccepted) c.Status(http.StatusAccepted)
} }
// Deletes a server from the wings daemon and deassociates its objects. // Deletes a server from the wings daemon and dissociate it's objects.
func deleteServer(c *gin.Context) { func deleteServer(c *gin.Context) {
s := GetServer(c.Param("server")) s := ExtractServer(c)
// Immediately suspend the server to prevent a user from attempting // Immediately suspend the server to prevent a user from attempting
// to start it while this process is running. // to start it while this process is running.
s.Config().SetSuspended(true) s.Config().SetSuspended(true)
// If the server is currently installing, abort it. // Stop all running background tasks for this server that are using the context on
if s.IsInstalling() { // the server struct. This will cancel any running install processes for the server
s.AbortInstallation() // as well.
} s.CtxCancel()
s.Events().Destroy()
s.Websockets().CancelAll()
// Delete the server's archive if it exists. We intentionally don't return // Delete the server's archive if it exists. We intentionally don't return
// here, if the archive fails to delete, the server can still be removed. // here, if the archive fails to delete, the server can still be removed.
@@ -187,14 +209,17 @@ func deleteServer(c *gin.Context) {
s.Log().WithField("error", err).Warn("failed to delete server archive during deletion process") s.Log().WithField("error", err).Warn("failed to delete server archive during deletion process")
} }
// Unsubscribe all of the event listeners. // Remove any pending remote file downloads for the server.
s.Events().UnsubscribeAll() for _, dl := range downloader.ByServer(s.Id()) {
dl.Cancel()
}
// Destroy the environment; in Docker this will handle a running container and // Destroy the environment; in Docker this will handle a running container and
// forcibly terminate it before removing the container, so we do not need to handle // forcibly terminate it before removing the container, so we do not need to handle
// that here. // that here.
if err := s.Environment.Destroy(); err != nil { if err := s.Environment.Destroy(); err != nil {
TrackedServerError(err, s).AbortWithServerError(c) WithError(c, err)
return
} }
// Once the environment is terminated, remove the server files from the system. This is // Once the environment is terminated, remove the server files from the system. This is
@@ -205,14 +230,11 @@ func deleteServer(c *gin.Context) {
// so we don't want to block the HTTP call while waiting on this. // so we don't want to block the HTTP call while waiting on this.
go func(p string) { go func(p string) {
if err := os.RemoveAll(p); err != nil { if err := os.RemoveAll(p); err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{"path": p, "error": err}).Warn("failed to remove server files during deletion process")
"path": p,
"error": errors.WithStack(err),
}).Warn("failed to remove server files during deletion process")
} }
}(s.Filesystem.Path()) }(s.Filesystem().Path())
var uuid = s.Id() uuid := s.Id()
server.GetServers().Remove(func(s2 *server.Server) bool { server.GetServers().Remove(func(s2 *server.Server) bool {
return s2.Id() == uuid return s2.Id() == uuid
}) })
@@ -222,3 +244,22 @@ func deleteServer(c *gin.Context) {
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)
} }
// Adds any of the JTIs passed through in the body to the deny list for the websocket
// preventing any JWT generated before the current time from being used to connect to
// the socket or send along commands.
func postServerDenyWSTokens(c *gin.Context) {
var data struct {
JTIs []string `json:"jtis"`
}
if err := c.BindJSON(&data); err != nil {
return
}
for _, jti := range data.JTIs {
tokens.DenyJTI(jti)
}
c.Status(http.StatusNoContent)
}

View File

@@ -1,12 +1,14 @@
package router package router
import ( import (
"errors"
"fmt" "fmt"
"net/http"
"os"
"emperror.dev/errors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/backup" "github.com/pterodactyl/wings/server/backup"
"net/http"
) )
// Backs up a server. // Backs up a server.
@@ -33,32 +35,52 @@ func postServerBackup(c *gin.Context) {
} }
if err != nil { if err != nil {
TrackedServerError(err, s).AbortWithServerError(c) NewServerError(err, s).Abort(c)
return return
} }
// Attach the server ID to the backup log output for easier parsing.
adapter.WithLogContext(map[string]interface{}{
"server": s.Id(),
})
go func(b backup.BackupInterface, serv *server.Server) { go func(b backup.BackupInterface, serv *server.Server) {
if err := serv.Backup(b); err != nil { if err := serv.Backup(b); err != nil {
serv.Log().WithField("error", err).Error("failed to generate backup for server") serv.Log().WithField("error", errors.WithStackIf(err)).Error("failed to generate backup for server")
} }
}(adapter, s) }(adapter, s)
c.Status(http.StatusAccepted) c.Status(http.StatusAccepted)
} }
// Deletes a local backup of a server. // Deletes a local backup of a server. If the backup is not found on the machine just return
// a 404 error. The service calling this endpoint can make its own decisions as to how it wants
// to handle that response.
func deleteServerBackup(c *gin.Context) { func deleteServerBackup(c *gin.Context) {
s := GetServer(c.Param("server")) s := GetServer(c.Param("server"))
b, _, err := backup.LocateLocal(c.Param("backup")) b, _, err := backup.LocateLocal(c.Param("backup"))
if err != nil { if err != nil {
TrackedServerError(err, s).AbortWithServerError(c) // Just return from the function at this point if the backup was not located.
if errors.Is(err, os.ErrNotExist) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested backup was not found on this server.",
})
return
}
NewServerError(err, s).Abort(c)
return return
} }
if err := b.Remove(); err != nil { if err := b.Remove(); err != nil {
TrackedServerError(err, s).AbortWithServerError(c) // I'm not entirely sure how likely this is to happen, however if we did manage to locate
return // the backup previously and it is now missing when we go to delete, just treat it as having
// been successful, rather than returning a 404.
if !errors.Is(err, os.ErrNotExist) {
NewServerError(err, s).Abort(c)
return
}
} }
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)

View File

@@ -1,13 +1,7 @@
package router package router
import ( import (
"bufio"
"context" "context"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server"
"golang.org/x/sync/errgroup"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/url" "net/url"
@@ -16,46 +10,27 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/router/downloader"
"github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/filesystem"
"golang.org/x/sync/errgroup"
) )
// Returns the contents of a file on the server. // Returns the contents of a file on the server.
func getServerFileContents(c *gin.Context) { func getServerFileContents(c *gin.Context) {
s := GetServer(c.Param("server")) s := ExtractServer(c)
f := c.Query("file")
p, err := url.QueryUnescape(c.Query("file")) p := "/" + strings.TrimLeft(f, "/")
st, err := s.Filesystem().Stat(p)
if err != nil { if err != nil {
TrackedServerError(err, s).AbortWithServerError(c) WithError(c, err)
return return
} }
p = "/" + strings.TrimLeft(p, "/")
cleaned, err := s.Filesystem.SafePath(p)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The file requested could not be found.",
})
return
}
st, err := s.Filesystem.Stat(cleaned)
if err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
if st.Info.IsDir() {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on the system.",
})
return
}
f, err := os.Open(cleaned)
if err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
defer f.Close()
c.Header("X-Mime-Type", st.Mimetype) c.Header("X-Mime-Type", st.Mimetype)
c.Header("Content-Length", strconv.Itoa(int(st.Info.Size()))) c.Header("Content-Length", strconv.Itoa(int(st.Info.Size())))
@@ -67,33 +42,31 @@ func getServerFileContents(c *gin.Context) {
c.Header("Content-Type", "application/octet-stream") c.Header("Content-Type", "application/octet-stream")
} }
bufio.NewReader(f).WriteTo(c.Writer) // TODO(dane): should probably come up with a different approach here. If an error is encountered
// by this Readfile call you'll end up causing a (recovered) panic in the program because so many
// headers have already been set. We should probably add a RawReadfile that just returns the file
// to be read and then we can stream from that safely without error.
//
// Until that becomes a problem though I'm just going to leave this how it is. The panic is recovered
// and a normal 500 error is returned to the client to my knowledge. It is also very unlikely to
// happen since we're doing so much before this point that would normally throw an error if there
// was a problem with the file.
if err := s.Filesystem().Readfile(p, c.Writer); err != nil {
WithError(c, err)
return
}
c.Writer.Flush()
} }
// Returns the contents of a directory for a server. // Returns the contents of a directory for a server.
func getServerListDirectory(c *gin.Context) { func getServerListDirectory(c *gin.Context) {
s := GetServer(c.Param("server")) s := ExtractServer(c)
dir := c.Query("directory")
d, err := url.QueryUnescape(c.Query("directory")) if stats, err := s.Filesystem().ListDirectory(dir); err != nil {
if err != nil { WithError(c, err)
TrackedServerError(err, s).AbortWithServerError(c) } else {
return c.JSON(http.StatusOK, stats)
} }
stats, err := s.Filesystem.ListDirectory(d)
if err != nil {
if err.Error() == "readdirent: not a directory" {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested directory does not exist.",
})
return
}
TrackedServerError(err, s).AbortWithServerError(c)
return
}
c.JSON(http.StatusOK, stats)
} }
type renameFile struct { type renameFile struct {
@@ -133,7 +106,7 @@ func putServerRenameFiles(c *gin.Context) {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
default: default:
if err := s.Filesystem.Rename(pf, pt); err != nil { if err := s.Filesystem().Rename(pf, pt); err != nil {
// Return nil if the error is an is not exists. // Return nil if the error is an is not exists.
// NOTE: os.IsNotExist() does not work if the error is wrapped. // NOTE: os.IsNotExist() does not work if the error is wrapped.
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
@@ -149,7 +122,14 @@ func putServerRenameFiles(c *gin.Context) {
} }
if err := g.Wait(); err != nil { if err := g.Wait(); err != nil {
TrackedServerError(err, s).AbortWithServerError(c) if errors.Is(err, os.ErrExist) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Cannot move or rename file, destination already exists.",
})
return
}
NewServerError(err, s).AbortFilesystemError(c)
return return
} }
@@ -168,15 +148,8 @@ func postServerCopyFile(c *gin.Context) {
return return
} }
if err := s.Filesystem.Copy(data.Location); err != nil { if err := s.Filesystem().Copy(data.Location); err != nil {
// Check if the file does not exist. NewServerError(err, s).AbortFilesystemError(c)
// NOTE: os.IsNotExist() does not work if the error is wrapped.
if errors.Is(err, os.ErrNotExist) {
c.Status(http.StatusNotFound)
return
}
TrackedServerError(err, s).AbortWithServerError(c)
return return
} }
@@ -215,13 +188,13 @@ func postServerDeleteFiles(c *gin.Context) {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
default: default:
return s.Filesystem.Delete(pi) return s.Filesystem().Delete(pi)
} }
}) })
} }
if err := g.Wait(); err != nil { if err := g.Wait(); err != nil {
TrackedServerError(err, s).AbortWithServerError(c) NewServerError(err, s).Abort(c)
return return
} }
@@ -232,21 +205,99 @@ func postServerDeleteFiles(c *gin.Context) {
func postServerWriteFile(c *gin.Context) { func postServerWriteFile(c *gin.Context) {
s := GetServer(c.Param("server")) s := GetServer(c.Param("server"))
f, err := url.QueryUnescape(c.Query("file")) f := c.Query("file")
if err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
f = "/" + strings.TrimLeft(f, "/") f = "/" + strings.TrimLeft(f, "/")
if err := s.Filesystem.Writefile(f, c.Request.Body); err != nil { if err := s.Filesystem().Writefile(f, c.Request.Body); err != nil {
TrackedServerError(err, s).AbortWithServerError(c) if filesystem.IsErrorCode(err, filesystem.ErrCodeIsDirectory) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Cannot write file, name conflicts with an existing directory by the same name.",
})
return
}
NewServerError(err, s).AbortFilesystemError(c)
return return
} }
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)
} }
// Returns all of the currently in-progress file downloads and their current download
// progress. The progress is also pushed out via a websocket event allowing you to just
// call this once to get current downloads, and then listen to targeted websocket events
// with the current progress for everything.
func getServerPullingFiles(c *gin.Context) {
s := ExtractServer(c)
c.JSON(http.StatusOK, gin.H{
"downloads": downloader.ByServer(s.Id()),
})
}
// Writes the contents of the remote URL to a file on a server.
func postServerPullRemoteFile(c *gin.Context) {
s := ExtractServer(c)
var data struct {
URL string `binding:"required" json:"url"`
Directory string `binding:"required,omitempty" json:"directory"`
}
if err := c.BindJSON(&data); err != nil {
return
}
u, err := url.Parse(data.URL)
if err != nil {
if e, ok := err.(*url.Error); ok {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "An error occurred while parsing that URL: " + e.Err.Error(),
})
return
}
WithError(c, err)
return
}
if err := s.Filesystem().HasSpaceErr(true); err != nil {
WithError(c, err)
return
}
// Do not allow more than three simultaneous remote file downloads at one time.
if len(downloader.ByServer(s.Id())) >= 3 {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "This server has reached its limit of 3 simultaneous remote file downloads at once. Please wait for one to complete before trying again.",
})
return
}
dl := downloader.New(s, downloader.DownloadRequest{
URL: u,
Directory: data.Directory,
})
// Execute this pull in a seperate thread since it may take a long time to complete.
go func() {
s.Log().WithField("download_id", dl.Identifier).WithField("url", u.String()).Info("starting pull of remote file to disk")
if err := dl.Execute(); err != nil {
s.Log().WithField("download_id", dl.Identifier).WithField("error", err).Error("failed to pull remote file")
} else {
s.Log().WithField("download_id", dl.Identifier).Info("completed pull of remote file")
}
}()
c.JSON(http.StatusAccepted, gin.H{
"identifier": dl.Identifier,
})
}
// Stops a remote file download if it exists and belongs to this server.
func deleteServerPullRemoteFile(c *gin.Context) {
s := ExtractServer(c)
if dl := downloader.ByID(c.Param("download")); dl != nil && dl.BelongsTo(s) {
dl.Cancel()
}
c.Status(http.StatusNoContent)
}
// Create a directory on a server. // Create a directory on a server.
func postServerCreateDirectory(c *gin.Context) { func postServerCreateDirectory(c *gin.Context) {
s := GetServer(c.Param("server")) s := GetServer(c.Param("server"))
@@ -260,8 +311,15 @@ func postServerCreateDirectory(c *gin.Context) {
return return
} }
if err := s.Filesystem.CreateDirectory(data.Name, data.Path); err != nil { if err := s.Filesystem().CreateDirectory(data.Name, data.Path); err != nil {
TrackedServerError(err, s).AbortWithServerError(c) if err.Error() == "not a directory" {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Part of the path being created is not a directory (ENOTDIR).",
})
return
}
NewServerError(err, s).Abort(c)
return return
} }
@@ -287,20 +345,20 @@ func postServerCompressFiles(c *gin.Context) {
return return
} }
if !s.Filesystem.HasSpaceAvailable() { if !s.Filesystem().HasSpaceAvailable(true) {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{ c.AbortWithStatusJSON(http.StatusConflict, gin.H{
"error": "This server does not have enough available disk space to generate a compressed archive.", "error": "This server does not have enough available disk space to generate a compressed archive.",
}) })
return return
} }
f, err := s.Filesystem.CompressFiles(data.RootPath, data.Files) f, err := s.Filesystem().CompressFiles(data.RootPath, data.Files)
if err != nil { if err != nil {
TrackedServerError(err, s).AbortWithServerError(c) NewServerError(err, s).AbortFilesystemError(c)
return return
} }
c.JSON(http.StatusOK, &server.Stat{ c.JSON(http.StatusOK, &filesystem.Stat{
Info: f, Info: f,
Mimetype: "application/tar+gzip", Mimetype: "application/tar+gzip",
}) })
@@ -318,9 +376,18 @@ func postServerDecompressFiles(c *gin.Context) {
return return
} }
hasSpace, err := s.Filesystem.SpaceAvailableForDecompression(data.RootPath, data.File) hasSpace, err := s.Filesystem().SpaceAvailableForDecompression(data.RootPath, data.File)
if err != nil { if err != nil {
TrackedServerError(err, s).AbortWithServerError(c) // Handle an unknown format error.
if filesystem.IsErrorCode(err, filesystem.ErrCodeUnknownArchive) {
s.Log().WithField("error", err).Warn("failed to decompress file due to unknown format")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "unknown archive format",
})
return
}
NewServerError(err, s).Abort(c)
return return
} }
@@ -331,15 +398,98 @@ func postServerDecompressFiles(c *gin.Context) {
return return
} }
if err := s.Filesystem.DecompressFile(data.RootPath, data.File); err != nil { if err := s.Filesystem().DecompressFile(data.RootPath, data.File); err != nil {
// Check if the file does not exist.
// NOTE: os.IsNotExist() does not work if the error is wrapped.
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
c.Status(http.StatusNotFound) c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested archive was not found.",
})
return return
} }
TrackedServerError(err, s).AbortWithServerError(c) // If the file is busy for some reason just return a nicer error to the user since there is not
// much we specifically can do. They'll need to stop the running server process in order to overwrite
// a file like this.
if strings.Contains(err.Error(), "text file busy") {
s.Log().WithField("error", err).Warn("failed to decompress file due to busy text file")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "One or more files this archive is attempting to overwrite are currently in use by another process. Please try again.",
})
return
}
NewServerError(err, s).AbortFilesystemError(c)
return
}
c.Status(http.StatusNoContent)
}
type chmodFile struct {
File string `json:"file"`
Mode string `json:"mode"`
}
var errInvalidFileMode = errors.New("invalid file mode")
func postServerChmodFile(c *gin.Context) {
s := GetServer(c.Param("server"))
var data struct {
Root string `json:"root"`
Files []chmodFile `json:"files"`
}
if err := c.BindJSON(&data); err != nil {
log.Debug(err.Error())
return
}
if len(data.Files) == 0 {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"error": "No files to chmod were provided.",
})
return
}
g, ctx := errgroup.WithContext(context.Background())
// Loop over the array of files passed in and perform the move or rename action against each.
for _, p := range data.Files {
g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
default:
mode, err := strconv.ParseUint(p.Mode, 8, 32)
if err != nil {
return errInvalidFileMode
}
if err := s.Filesystem().Chmod(path.Join(data.Root, p.File), os.FileMode(mode)); err != nil {
// Return nil if the error is an is not exists.
// NOTE: os.IsNotExist() does not work if the error is wrapped.
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
return nil
}
})
}
if err := g.Wait(); err != nil {
if errors.Is(err, errInvalidFileMode) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Invalid file mode.",
})
return
}
NewServerError(err, s).AbortFilesystemError(c)
return return
} }
@@ -349,7 +499,7 @@ func postServerDecompressFiles(c *gin.Context) {
func postServerUploadFiles(c *gin.Context) { func postServerUploadFiles(c *gin.Context) {
token := tokens.UploadPayload{} token := tokens.UploadPayload{}
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil { if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
TrackedError(err).AbortWithServerError(c) NewTrackedError(err).Abort(c)
return return
} }
@@ -361,42 +511,40 @@ func postServerUploadFiles(c *gin.Context) {
return return
} }
if !s.Filesystem.HasSpaceAvailable() {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
"error": "This server does not have enough available disk space to accept any file uploads.",
})
return
}
form, err := c.MultipartForm() form, err := c.MultipartForm()
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Failed to get multipart form.", "error": "Failed to get multipart form data from request.",
}) })
return return
} }
headers, ok := form.File["files"] headers, ok := form.File["files"]
if !ok { if !ok {
c.AbortWithStatusJSON(http.StatusNotModified, gin.H{ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "No files were attached to the request.", "error": "No files were found on the request body.",
}) })
return return
} }
directory := c.Query("directory") directory := c.Query("directory")
var totalSize int64
for _, header := range headers { for _, header := range headers {
p, err := s.Filesystem.SafePath(filepath.Join(directory, header.Filename)) totalSize += header.Size
}
for _, header := range headers {
p, err := s.Filesystem().SafePath(filepath.Join(directory, header.Filename))
if err != nil { if err != nil {
c.AbortWithError(http.StatusInternalServerError, err) NewServerError(err, s).AbortFilesystemError(c)
return return
} }
// We run this in a different method so I can use defer without any of // We run this in a different method so I can use defer without any of
// the consequences caused by calling it in a loop. // the consequences caused by calling it in a loop.
if err := handleFileUpload(p, s, header); err != nil { if err := handleFileUpload(p, s, header); err != nil {
c.AbortWithError(http.StatusInternalServerError, err) NewServerError(err, s).AbortFilesystemError(c)
return return
} }
} }
@@ -405,12 +553,12 @@ func postServerUploadFiles(c *gin.Context) {
func handleFileUpload(p string, s *server.Server, header *multipart.FileHeader) error { func handleFileUpload(p string, s *server.Server, header *multipart.FileHeader) error {
file, err := header.Open() file, err := header.Open()
if err != nil { if err != nil {
return errors.WithStack(err) return err
} }
defer file.Close() defer file.Close()
if err := s.Filesystem.Writefile(p, file); err != nil { if err := s.Filesystem().Writefile(p, file); err != nil {
return errors.WithStack(err) return err
} }
return nil return nil

View File

@@ -3,6 +3,8 @@ package router
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
ws "github.com/gorilla/websocket" ws "github.com/gorilla/websocket"
"github.com/pterodactyl/wings/router/websocket" "github.com/pterodactyl/wings/router/websocket"
@@ -13,7 +15,7 @@ func getServerWebsocket(c *gin.Context) {
s := GetServer(c.Param("server")) s := GetServer(c.Param("server"))
handler, err := websocket.GetHandler(s, c.Writer, c.Request) handler, err := websocket.GetHandler(s, c.Writer, c.Request)
if err != nil { if err != nil {
TrackedServerError(err, s).AbortWithServerError(c) NewServerError(err, s).Abort(c)
return return
} }
defer handler.Connection.Close() defer handler.Connection.Close()
@@ -23,6 +25,28 @@ func getServerWebsocket(c *gin.Context) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
// Track this open connection on the server so that we can close them all programmatically
// if the server is deleted.
s.Websockets().Push(handler.Uuid(), &cancel)
defer s.Websockets().Remove(handler.Uuid())
// Listen for the context being canceled and then close the websocket connection. This normally
// just happens because you're disconnecting from the socket in the browser, however in some
// cases we close the connections programmatically (e.g. deleting the server) and need to send
// a close message to the websocket so it disconnects.
go func(ctx context.Context, c *ws.Conn) {
ListenerLoop:
for {
select {
case <-ctx.Done():
handler.Connection.WriteControl(ws.CloseMessage, ws.FormatCloseMessage(ws.CloseGoingAway, "server deleted"), time.Now().Add(time.Second*5))
// A break right here without defining the specific loop would only break the select
// and not actually break the for loop, thus causing this routine to stick around forever.
break ListenerLoop
}
}
}(ctx, handler.Connection)
go handler.ListenForServerEvents(ctx) go handler.ListenForServerEvents(ctx)
go handler.ListenForExpiration(ctx) go handler.ListenForExpiration(ctx)

View File

@@ -2,21 +2,22 @@ package router
import ( import (
"bytes" "bytes"
"net/http"
"strings"
"github.com/apex/log" "github.com/apex/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/installer" "github.com/pterodactyl/wings/installer"
"github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/system" "github.com/pterodactyl/wings/system"
"net/http"
"strings"
) )
// Returns information about the system that wings is running on. // Returns information about the system that wings is running on.
func getSystemInformation(c *gin.Context) { func getSystemInformation(c *gin.Context) {
i, err := system.GetSystemInformation() i, err := system.GetSystemInformation()
if err != nil { if err != nil {
TrackedError(err).AbortWithServerError(c) NewTrackedError(err).Abort(c)
return return
} }
@@ -45,7 +46,7 @@ func postCreateServer(c *gin.Context) {
return return
} }
TrackedError(err).AbortWithServerError(c) NewTrackedError(err).Abort(c)
return return
} }
@@ -57,7 +58,11 @@ func postCreateServer(c *gin.Context) {
// cycle. If there are any errors they will be logged and communicated back // cycle. If there are any errors they will be logged and communicated back
// to the Panel where a reinstall may take place. // to the Panel where a reinstall may take place.
go func(i *installer.Installer) { go func(i *installer.Installer) {
i.Execute() err := i.Server().CreateEnvironment()
if err != nil {
i.Server().Log().WithField("error", err).Error("failed to create server environment during install process")
return
}
if err := i.Server().Install(false); err != nil { if err := i.Server().Install(false); err != nil {
log.WithFields(log.Fields{"server": i.Uuid(), "error": err}).Error("failed to run install process for server") log.WithFields(log.Fields{"server": i.Uuid(), "error": err}).Error("failed to run install process for server")
@@ -71,7 +76,7 @@ func postCreateServer(c *gin.Context) {
func postUpdateConfiguration(c *gin.Context) { func postUpdateConfiguration(c *gin.Context) {
// A backup of the configuration for error purposes. // A backup of the configuration for error purposes.
ccopy := *config.Get() ccopy := *config.Get()
// A copy of the configuration we're using to bind the data recevied into. // A copy of the configuration we're using to bind the data received into.
cfg := *config.Get() cfg := *config.Get()
// BindJSON sends 400 if the request fails, all we need to do is return // BindJSON sends 400 if the request fails, all we need to do is return
@@ -85,8 +90,8 @@ func postUpdateConfiguration(c *gin.Context) {
// //
// If you pass through manual locations in the API call this logic will be skipped. // If you pass through manual locations in the API call this logic will be skipped.
if strings.HasPrefix(cfg.Api.Ssl.KeyFile, "/etc/letsencrypt/live/") { if strings.HasPrefix(cfg.Api.Ssl.KeyFile, "/etc/letsencrypt/live/") {
cfg.Api.Ssl.KeyFile = ccopy.Api.Ssl.KeyFile cfg.Api.Ssl.KeyFile = strings.ToLower(ccopy.Api.Ssl.KeyFile)
cfg.Api.Ssl.CertificateFile = ccopy.Api.Ssl.CertificateFile cfg.Api.Ssl.CertificateFile = strings.ToLower(ccopy.Api.Ssl.CertificateFile)
} }
config.Set(&cfg) config.Set(&cfg)
@@ -95,7 +100,7 @@ func postUpdateConfiguration(c *gin.Context) {
// before this code was run. // before this code was run.
config.Set(&ccopy) config.Set(&ccopy)
TrackedError(err).AbortWithServerError(c) NewTrackedError(err).Abort(c)
return return
} }

View File

@@ -2,29 +2,54 @@ package router
import ( import (
"bufio" "bufio"
"bytes"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"errors" "encoding/json"
"github.com/buger/jsonparser" "fmt"
"github.com/gin-gonic/gin"
"github.com/mholt/archiver/v3"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/installer"
"github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server"
"go.uber.org/zap"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync/atomic"
"time" "time"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/juju/ratelimit"
"github.com/mholt/archiver/v3"
"github.com/mitchellh/colorstring"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/installer"
"github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/system"
) )
// Number of ticks in the progress bar
const ticks = 25
// 100% / number of ticks = percentage represented by each tick
const tickPercentage = 100 / ticks
type downloadProgress struct {
size int64
progress int64
}
// Data passed over to initiate a server transfer.
type serverTransferRequest struct {
ServerID string `binding:"required" json:"server_id"`
URL string `binding:"required" json:"url"`
Token string `binding:"required" json:"token"`
Server json.RawMessage `json:"server"`
}
// Returns the archive for a server so that it can be transferred to a new node.
func getServerArchive(c *gin.Context) { func getServerArchive(c *gin.Context) {
auth := strings.SplitN(c.GetHeader("Authorization"), " ", 2) auth := strings.SplitN(c.GetHeader("Authorization"), " ", 2)
@@ -38,46 +63,37 @@ func getServerArchive(c *gin.Context) {
token := tokens.TransferPayload{} token := tokens.TransferPayload{}
if err := tokens.ParseToken([]byte(auth[1]), &token); err != nil { if err := tokens.ParseToken([]byte(auth[1]), &token); err != nil {
TrackedError(err).AbortWithServerError(c) NewTrackedError(err).Abort(c)
return return
} }
if token.Subject != c.Param("server") { s := ExtractServer(c)
if token.Subject != s.Id() {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "( .. •˘___˘• .. )", "error": "Missing required token subject, or subject is not valid for the requested server.",
}) })
return return
} }
s := GetServer(c.Param("server"))
st, err := s.Archiver.Stat() st, err := s.Archiver.Stat()
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !errors.Is(err, os.ErrNotExist) {
TrackedServerError(err, s).SetMessage("failed to stat archive").AbortWithServerError(c) WithError(c, err)
return return
} }
c.AbortWithStatus(http.StatusNotFound) c.AbortWithStatus(http.StatusNotFound)
return return
} }
checksum, err := s.Archiver.Checksum() checksum, err := s.Archiver.Checksum()
if err != nil { if err != nil {
TrackedServerError(err, s).SetMessage("failed to calculate checksum").AbortWithServerError(c) NewServerError(err, s).SetMessage("failed to calculate checksum").Abort(c)
return return
} }
file, err := os.Open(s.Archiver.ArchivePath()) file, err := os.Open(s.Archiver.Path())
if err != nil { if err != nil {
tserr := TrackedServerError(err, s) WithError(c, err)
if !os.IsNotExist(err) {
tserr.SetMessage("failed to open archive for reading")
} else {
tserr.SetMessage("failed to open archive")
}
tserr.AbortWithServerError(c)
return return
} }
defer file.Close() defer file.Close()
@@ -85,64 +101,28 @@ func getServerArchive(c *gin.Context) {
c.Header("X-Checksum", checksum) c.Header("X-Checksum", checksum)
c.Header("X-Mime-Type", st.Mimetype) c.Header("X-Mime-Type", st.Mimetype)
c.Header("Content-Length", strconv.Itoa(int(st.Info.Size()))) c.Header("Content-Length", strconv.Itoa(int(st.Info.Size())))
c.Header("Content-Disposition", "attachment; filename="+s.Archiver.ArchiveName()) c.Header("Content-Disposition", "attachment; filename="+s.Archiver.Name())
c.Header("Content-Type", "application/octet-stream") c.Header("Content-Type", "application/octet-stream")
bufio.NewReader(file).WriteTo(c.Writer) bufio.NewReader(file).WriteTo(c.Writer)
} }
func postServerArchive(c *gin.Context) { func postServerArchive(c *gin.Context) {
s := GetServer(c.Param("server")) s := ExtractServer(c)
go func(server *server.Server) { go func(s *server.Server) {
start := time.Now() r := api.New()
l := log.WithField("server", s.Id())
if err := server.Archiver.Archive(); err != nil { // This function automatically adds the Source Node prefix and Timestamp to the log
zap.S().Errorw("failed to get archive for server", zap.String("server", server.Id()), zap.Error(err)) // output before sending it over the websocket.
return sendTransferLog := func(data string) {
output := colorstring.Color(fmt.Sprintf("[yellow][bold]%s [Pterodactyl Transfer System] [Source Node]:[default] %s", time.Now().Format(time.RFC1123), data))
s.Events().Publish(server.TransferLogsEvent, output)
} }
zap.S().Debugw( s.Events().Publish(server.TransferStatusEvent, "starting")
"successfully created archive for server", sendTransferLog("Attempting to archive server...")
zap.String("server", server.Id()),
zap.Duration("time", time.Now().Sub(start).Round(time.Microsecond)),
)
r := api.NewRequester()
rerr, err := r.SendArchiveStatus(server.Id(), true)
if rerr != nil || err != nil {
if err != nil {
zap.S().Errorw("failed to notify panel with archive status", zap.String("server", server.Id()), zap.Error(err))
return
}
zap.S().Errorw(
"panel returned an error when sending the archive status",
zap.String("server", server.Id()),
zap.Error(errors.New(rerr.String())),
)
return
}
zap.S().Debugw("successfully notified panel about archive status", zap.String("server", server.Id()))
}(s)
c.Status(http.StatusAccepted)
}
func postTransfer(c *gin.Context) {
zap.S().Debug("incoming transfer from panel")
buf := bytes.Buffer{}
buf.ReadFrom(c.Request.Body)
go func(data []byte) {
serverID, _ := jsonparser.GetString(data, "server_id")
url, _ := jsonparser.GetString(data, "url")
token, _ := jsonparser.GetString(data, "token")
// Create an http client with no timeout.
client := &http.Client{Timeout: 0}
hasError := true hasError := true
defer func() { defer func() {
@@ -150,142 +130,342 @@ func postTransfer(c *gin.Context) {
return return
} }
zap.S().Errorw("server transfer has failed", zap.String("server", serverID)) // Mark the server as not being transferred so it can actually be used.
rerr, err := api.NewRequester().SendTransferFailure(serverID) s.SetTransferring(false)
if rerr != nil || err != nil {
if err != nil { s.Events().Publish(server.TransferStatusEvent, "failure")
zap.S().Errorw("failed to notify panel with transfer failure", zap.String("server", serverID), zap.Error(err))
sendTransferLog("Attempting to notify panel of archive failure..")
if err := r.SendArchiveStatus(s.Id(), false); err != nil {
if !api.IsRequestError(err) {
sendTransferLog("Failed to notify panel of archive failure: " + err.Error())
l.WithField("error", err).Error("failed to notify panel of failed archive status")
return return
} }
zap.S().Errorw("panel returned an error when notifying of a transfer failure", zap.String("server", serverID), zap.Error(errors.New(rerr.String()))) sendTransferLog("Panel returned an error while notifying it of a failed archive: " + err.Error())
l.WithField("error", err.Error()).Error("panel returned an error when notifying it of a failed archive status")
return return
} }
zap.S().Debugw("successfully notified panel about transfer failure", zap.String("server", serverID)) sendTransferLog("Successfully notified panel of failed archive status")
l.Info("successfully notified panel of failed archive status")
}() }()
// Make a new GET request to the URL the panel gave us. // Mark the server as transferring to prevent problems.
req, err := http.NewRequest("GET", url, nil) s.SetTransferring(true)
if err != nil {
zap.S().Errorw("failed to create http request", zap.Error(err)) // Ensure the server is offline. Sometimes a "No such container" error gets through
// which means the server is already stopped. We can ignore that.
if err := s.Environment.WaitForStop(60, false); err != nil && !strings.Contains(strings.ToLower(err.Error()), "no such container") {
sendTransferLog("Failed to stop server, aborting transfer..")
l.WithField("error", err).Error("failed to stop server")
return return
} }
// Add the authorization header. // Attempt to get an archive of the server.
req.Header.Set("Authorization", token) if err := s.Archiver.Archive(); err != nil {
sendTransferLog("An error occurred while archiving the server: " + err.Error())
l.WithField("error", err).Error("failed to get transfer archive for server")
return
}
// Execute the http request. sendTransferLog("Successfully created archive, attempting to notify panel..")
res, err := client.Do(req) l.Info("successfully created server transfer archive, notifying panel..")
if err := r.SendArchiveStatus(s.Id(), true); err != nil {
if !api.IsRequestError(err) {
sendTransferLog("Failed to notify panel of archive success: " + err.Error())
l.WithField("error", err).Error("failed to notify panel of successful archive status")
return
}
sendTransferLog("Panel returned an error while notifying it of a successful archive: " + err.Error())
l.WithField("error", err.Error()).Error("panel returned an error when notifying it of a successful archive status")
return
}
hasError = false
// This log may not be displayed by the client due to the status event being sent before or at the same time.
sendTransferLog("Successfully notified panel of successful archive status")
l.Info("successfully notified panel of successful transfer archive status")
s.Events().Publish(server.TransferStatusEvent, "archived")
}(s)
c.Status(http.StatusAccepted)
}
func (w *downloadProgress) Write(v []byte) (int, error) {
n := len(v)
atomic.AddInt64(&w.progress, int64(n))
return n, nil
}
// Log helper function to attach all errors and info output to a consistently formatted
// log string for easier querying.
func (str serverTransferRequest) log() *log.Entry {
return log.WithField("subsystem", "transfers").WithField("server_id", str.ServerID)
}
// Downloads an archive from the machine that the server currently lives on.
func (str serverTransferRequest) downloadArchive() (*http.Response, error) {
client := http.Client{Timeout: 0}
req, err := http.NewRequest(http.MethodGet, str.URL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", str.Token)
res, err := client.Do(req) // lgtm [go/request-forgery]
if err != nil {
return nil, err
}
return res, nil
}
// Returns the path to the local archive on the system.
func (str serverTransferRequest) path() string {
return filepath.Join(config.Get().System.ArchiveDirectory, str.ServerID+".tar.gz")
}
// Creates the archive location on this machine by first checking that the required file
// does not already exist. If it does exist, the file is deleted and then re-created as
// an empty file.
func (str serverTransferRequest) createArchiveFile() (*os.File, error) {
p := str.path()
if _, err := os.Stat(p); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
} else if err := os.Remove(p); err != nil {
return nil, err
}
return os.Create(p)
}
// Deletes the archive from the local filesystem. This is executed as a deferred function.
func (str serverTransferRequest) removeArchivePath() {
p := str.path()
str.log().Debug("deleting temporary transfer archive")
if err := os.Remove(p); err != nil && !os.IsNotExist(err) {
str.log().WithField("path", p).WithField("error", err).Error("failed to delete temporary transfer archive file")
return
}
str.log().Debug("deleted temporary transfer archive successfully")
}
// Verifies that the SHA-256 checksum of the file on the local filesystem matches the
// expected value from the transfer request. The string value returned is the computed
// checksum on the system.
func (str serverTransferRequest) verifyChecksum(matches string) (bool, string, error) {
file, err := os.Open(str.path())
if err != nil {
return false, "", err
}
defer file.Close()
hash := sha256.New()
buf := make([]byte, 1024*4)
if _, err := io.CopyBuffer(hash, file, buf); err != nil {
return false, "", err
}
checksum := hex.EncodeToString(hash.Sum(nil))
return checksum == matches, checksum, nil
}
// Sends a notification to the Panel letting it know what the status of this transfer is.
func (str serverTransferRequest) sendTransferStatus(successful bool) error {
lg := str.log().WithField("transfer_successful", successful)
lg.Info("notifying Panel of server transfer state")
if err := api.New().SendTransferStatus(str.ServerID, successful); err != nil {
lg.WithField("error", err).Error("error notifying panel of transfer state")
return err
}
lg.Debug("notified panel of transfer state")
return nil
}
// Initiates a transfer between two nodes for a server by downloading an archive from the
// remote node and then applying the server details to this machine.
func postTransfer(c *gin.Context) {
var data serverTransferRequest
if err := c.BindJSON(&data); err != nil {
return
}
u, err := uuid.Parse(data.ServerID)
if err != nil {
WithError(c, err)
return
}
// Force the server ID to be a valid UUID string at this point. If it is not an error
// is returned to the caller. This limits injection vulnerabilities that would cause
// the str.path() function to return a location not within the server archive directory.
data.ServerID = u.String()
data.log().Info("handling incoming server transfer request")
go func(data *serverTransferRequest) {
hasError := true
// Create a new server installer. This will only configure the environment and not
// run the installer scripts.
i, err := installer.New(data.Server)
if err != nil { if err != nil {
zap.S().Errorw("failed to send http request", zap.Error(err)) _ = data.sendTransferStatus(false)
data.log().WithField("error", err).Error("failed to validate received server data")
return
}
// This function automatically adds the Target Node prefix and Timestamp to the log output before sending it
// over the websocket.
sendTransferLog := func(data string) {
output := colorstring.Color(fmt.Sprintf("[yellow][bold]%s [Pterodactyl Transfer System] [Target Node]:[default] %s", time.Now().Format(time.RFC1123), data))
i.Server().Events().Publish(server.TransferLogsEvent, output)
}
// Mark the server as transferring to prevent problems later on during the process and
// then push the server into the global server collection for this instance.
i.Server().SetTransferring(true)
server.GetServers().Add(i.Server())
defer func(s *server.Server) {
// In the event that this transfer call fails, remove the server from the global
// server tracking so that we don't have a dangling instance.
if err := data.sendTransferStatus(!hasError); hasError || err != nil {
sendTransferLog("Server transfer failed, check Wings logs for additional information.")
s.Events().Publish(server.TransferStatusEvent, "failure")
server.GetServers().Remove(func(s2 *server.Server) bool {
return s.Id() == s2.Id()
})
// If the transfer status was successful but the request failed, act like the transfer failed.
if !hasError && err != nil {
// Delete all extracted files.
if err := os.RemoveAll(s.Filesystem().Path()); err != nil && !os.IsNotExist(err) {
data.log().WithField("error", err).Warn("failed to delete local server files directory")
}
}
} else {
s.SetTransferring(false)
s.Events().Publish(server.TransferStatusEvent, "success")
sendTransferLog("Transfer completed.")
}
}(i.Server())
data.log().Info("downloading server archive from current server node")
sendTransferLog("Received incoming transfer from Panel, attempting to download archive from source node...")
res, err := data.downloadArchive()
if err != nil {
sendTransferLog("Failed to retrieve server archive from remote node: " + err.Error())
data.log().WithField("error", err).Error("failed to download archive for server transfer")
return return
} }
defer res.Body.Close() defer res.Body.Close()
// Handle non-200 status codes.
if res.StatusCode != 200 { if res.StatusCode != 200 {
body, err := ioutil.ReadAll(res.Body) data.log().WithField("error", err).WithField("status", res.StatusCode).Error("unexpected error response from transfer endpoint")
if err != nil {
zap.S().Errorw("failed to read response body", zap.Int("status", res.StatusCode), zap.Error(err))
return
}
zap.S().Errorw("failed to request server archive", zap.Int("status", res.StatusCode), zap.String("body", string(body)))
return return
} }
// Get the path to the archive. size := res.ContentLength
archivePath := filepath.Join(config.Get().System.ArchiveDirectory, serverID + ".tar.gz") if size == 0 {
data.log().WithField("error", err).Error("received an archive response with Content-Length of 0")
// Check if the archive already exists and delete it if it does.
_, err = os.Stat(archivePath)
if err != nil {
if !os.IsNotExist(err) {
zap.S().Errorw("failed to stat file", zap.Error(err))
return
}
} else {
if err := os.Remove(archivePath); err != nil {
zap.S().Errorw("failed to delete old file", zap.Error(err))
return
}
}
// Create the file.
file, err := os.Create(archivePath)
if err != nil {
zap.S().Errorw("failed to open file on disk", zap.Error(err))
return return
} }
sendTransferLog("Got server archive response from remote node. (Content-Length: " + strconv.Itoa(int(size)) + ")")
sendTransferLog("Creating local archive file...")
file, err := data.createArchiveFile()
if err != nil {
data.log().WithField("error", err).Error("failed to create archive file on local filesystem")
return
}
sendTransferLog("Writing archive to disk...")
data.log().Info("writing transfer archive to disk...")
// Copy the file. // Copy the file.
buf := make([]byte, 1024 * 4) progress := &downloadProgress{size: size}
_, err = io.CopyBuffer(file, res.Body, buf) ticker := time.NewTicker(3 * time.Second)
if err != nil { go func(progress *downloadProgress, t *time.Ticker) {
zap.S().Errorw("failed to copy file to disk", zap.Error(err)) for range ticker.C {
return // p = 100 (Downloaded)
// size = 1000 (Content-Length)
// p / size = 0.1
// * 100 = 10% (Multiply by 100 to get a percentage of the download)
// 10% / tickPercentage = (10% / (100 / 25)) (Divide by tick percentage to get the number of ticks)
// 2.5 (Number of ticks as a float64)
// 2 (convert to an integer)
p := atomic.LoadInt64(&progress.progress)
// We have to cast these numbers to float in order to get a float result from the division.
width := ((float64(p) / float64(size)) * 100) / tickPercentage
bar := strings.Repeat("=", int(width)) + strings.Repeat(" ", ticks-int(width))
sendTransferLog("Downloading [" + bar + "] " + system.FormatBytes(p) + " / " + system.FormatBytes(progress.size))
}
}(progress, ticker)
var reader io.Reader
downloadLimit := float64(config.Get().System.Transfers.DownloadLimit) * 1024 * 1024
if downloadLimit > 0 {
// Wrap the body with a reader that is limited to the defined download limit speed.
reader = ratelimit.Reader(res.Body, ratelimit.NewBucketWithRate(downloadLimit, int64(downloadLimit)))
} else {
reader = res.Body
} }
// Close the file so it can be opened to verify the checksum. buf := make([]byte, 1024*4)
if _, err := io.CopyBuffer(file, io.TeeReader(reader, progress), buf); err != nil {
ticker.Stop()
_ = file.Close()
sendTransferLog("Failed while writing archive file to disk: " + err.Error())
data.log().WithField("error", err).Error("failed to copy archive file to disk")
return
}
ticker.Stop()
// Show 100% completion.
humanSize := system.FormatBytes(progress.size)
sendTransferLog("Downloading [" + strings.Repeat("=", ticks) + "] " + humanSize + " / " + humanSize)
if err := file.Close(); err != nil { if err := file.Close(); err != nil {
zap.S().Errorw("failed to close archive file", zap.Error(err)) data.log().WithField("error", err).Error("unable to close archive file on local filesystem")
return return
} }
zap.S().Debug("server archive has been downloaded, computing checksum..", zap.String("server", serverID)) data.log().Info("finished writing transfer archive to disk")
sendTransferLog("Successfully wrote archive to disk.")
// Open the archive file for computing a checksum. // Whenever the transfer fails or succeeds, delete the temporary transfer archive that
file, err = os.Open(archivePath) // was created on the disk.
if err != nil { defer data.removeArchivePath()
zap.S().Errorw("failed to open file on disk", zap.Error(err))
sendTransferLog("Verifying checksum of downloaded archive...")
data.log().Info("computing checksum of downloaded archive file")
expected := res.Header.Get("X-Checksum")
if matches, computed, err := data.verifyChecksum(expected); err != nil {
data.log().WithField("error", err).Error("encountered an error while calculating local filesystem archive checksum")
return
} else if !matches {
sendTransferLog("@@@@@ CHECKSUM VERIFICATION FAILED @@@@@")
sendTransferLog(" - Source Checksum: " + expected)
sendTransferLog(" - Computed Checksum: " + computed)
data.log().WithField("expected_sum", expected).WithField("computed_checksum", computed).Error("checksum mismatch when verifying integrity of local archive")
return return
} }
// Compute the sha256 checksum of the file. // Create the server's environment.
hash := sha256.New() sendTransferLog("Creating server environment, this could take a while..")
buf = make([]byte, 1024 * 4) data.log().Info("creating server environment")
if _, err := io.CopyBuffer(hash, file, buf); err != nil { if err := i.Server().CreateEnvironment(); err != nil {
zap.S().Errorw("failed to copy file for checksum verification", zap.Error(err)) data.log().WithField("error", err).Error("failed to create server environment")
return return
} }
// Verify the two checksums. sendTransferLog("Server environment has been created, extracting transfer archive..")
if hex.EncodeToString(hash.Sum(nil)) != res.Header.Get("X-Checksum") { data.log().Info("server environment configured, extracting transfer archive")
zap.S().Errorw("checksum failed verification") if err := archiver.NewTarGz().Unarchive(data.path(), i.Server().Filesystem().Path()); err != nil {
return // Un-archiving failed, delete the server's data directory.
} if err := os.RemoveAll(i.Server().Filesystem().Path()); err != nil && !os.IsNotExist(err) {
data.log().WithField("error", err).Warn("failed to delete local server files directory")
// Close the file. }
if err := file.Close(); err != nil { data.log().WithField("error", err).Error("failed to extract server archive")
zap.S().Errorw("failed to close archive file", zap.Error(err))
return
}
zap.S().Infow("server archive transfer was successful", zap.String("server", serverID))
// Get the server data from the request.
serverData, t, _, _ := jsonparser.Get(data, "server")
if t != jsonparser.Object {
zap.S().Errorw("invalid server data passed in request")
return
}
// Create a new server installer (note this does not execute the install script)
i, err := installer.New(serverData)
if err != nil {
zap.S().Warnw("failed to validate the received server data", zap.Error(err))
return
}
// Add the server to the collection.
server.GetServers().Add(i.Server())
// Create the server's environment (note this does not execute the install script)
i.Execute()
// Un-archive the archive. That sounds weird..
if err := archiver.NewTarGz().Unarchive(archivePath, i.Server().Filesystem.Path()); err != nil {
zap.S().Errorw("failed to extract archive", zap.String("server", serverID), zap.Error(err))
return return
} }
@@ -296,20 +476,9 @@ func postTransfer(c *gin.Context) {
// hiccup or the fix of whatever error causing the success request to fail. // hiccup or the fix of whatever error causing the success request to fail.
hasError = false hasError = false
// Notify the panel that the transfer succeeded. data.log().Info("archive extracted successfully, notifying Panel of status")
rerr, err := api.NewRequester().SendTransferSuccess(serverID) sendTransferLog("Archive extracted successfully.")
if rerr != nil || err != nil { }(&data)
if err != nil {
zap.S().Errorw("failed to notify panel with transfer success", zap.String("server", serverID), zap.Error(err))
return
}
zap.S().Errorw("panel returned an error when notifying of a transfer success", zap.String("server", serverID), zap.Error(errors.New(rerr.String())))
return
}
zap.S().Debugw("successfully notified panel about transfer success", zap.String("server", serverID))
}(buf.Bytes())
c.Status(http.StatusAccepted) c.Status(http.StatusAccepted)
} }

View File

@@ -2,11 +2,40 @@ package tokens
import ( import (
"encoding/json" "encoding/json"
"github.com/apex/log"
"github.com/gbrlsnchs/jwt/v3" "github.com/gbrlsnchs/jwt/v3"
"strings" "strings"
"sync" "sync"
"time"
) )
// The time at which Wings was booted. No JWT's created before this time are allowed to
// connect to the socket since they may have been marked as denied already and therefore
// could be invalid at this point.
//
// By doing this we make it so that a user who gets disconnected from Wings due to a Wings
// reboot just needs to request a new token as if their old token had expired naturally.
var wingsBootTime = time.Now()
// A map that contains any JTI's that have been denied by the Panel and the time at which
// they were marked as denied. Therefore any JWT with the same JTI and an IssuedTime that
// is the same as or before this time should be considered invalid.
//
// This is used to allow the Panel to revoke tokens en-masse for a given user & server
// combination since the JTI for tokens is just MD5(user.id + server.uuid). When a server
// is booted this listing is fetched from the panel and the Websocket is dynamically updated.
var denylist sync.Map
// Adds a JTI to the denylist by marking any JWTs generated before the current time as
// being invalid if they use the same JTI.
func DenyJTI(jti string) {
log.WithField("jti", jti).Debugf("adding \"%s\" to JTI denylist", jti)
denylist.Store(jti, time.Now())
}
// A JWT payload for Websocket connections. This JWT is passed along to the Websocket after
// it has been connected to by sending an "auth" event.
type WebsocketPayload struct { type WebsocketPayload struct {
jwt.Payload jwt.Payload
sync.RWMutex sync.RWMutex
@@ -24,6 +53,7 @@ func (p *WebsocketPayload) GetPayload() *jwt.Payload {
return &p.Payload return &p.Payload
} }
// Returns the UUID of the server associated with this JWT.
func (p *WebsocketPayload) GetServerUuid() string { func (p *WebsocketPayload) GetServerUuid() string {
p.RLock() p.RLock()
defer p.RUnlock() defer p.RUnlock()
@@ -31,6 +61,33 @@ func (p *WebsocketPayload) GetServerUuid() string {
return p.ServerUUID return p.ServerUUID
} }
// Check if the JWT has been marked as denied by the instance due to either being issued
// before Wings was booted, or because we have denied all tokens with the same JTI
// occurring before a set time.
func (p *WebsocketPayload) Denylisted() bool {
// If there is no IssuedAt present for the token, we cannot validate the token so
// just immediately mark it as not valid.
if p.IssuedAt == nil {
return true
}
// If the time that the token was issued is before the time at which Wings was booted
// then the token is invalid for our purposes, even if the token "has permission".
if p.IssuedAt.Time.Before(wingsBootTime) {
return true
}
// Finally, if the token was issued before a time that is currently denied for this
// token instance, ignore the permissions response.
if t, ok := denylist.Load(p.JWTID); ok {
if p.IssuedAt.Time.Before(t.(time.Time)) {
return true
}
}
return false
}
// Checks if the given token payload has a permission string. // Checks if the given token payload has a permission string.
func (p *WebsocketPayload) HasPermission(permission string) bool { func (p *WebsocketPayload) HasPermission(permission string) bool {
p.RLock() p.RLock()
@@ -38,7 +95,7 @@ func (p *WebsocketPayload) HasPermission(permission string) bool {
for _, k := range p.Permissions { for _, k := range p.Permissions {
if k == permission || (!strings.HasPrefix(permission, "admin") && k == "*") { if k == permission || (!strings.HasPrefix(permission, "admin") && k == "*") {
return true return !p.Denylisted()
} }
} }

View File

@@ -28,7 +28,7 @@ func (h *Handler) ListenForExpiration(ctx context.Context) {
if jwt != nil { if jwt != nil {
if jwt.ExpirationTime.Unix()-time.Now().Unix() <= 0 { if jwt.ExpirationTime.Unix()-time.Now().Unix() <= 0 {
_ = h.SendJson(&Message{Event: TokenExpiredEvent}) _ = h.SendJson(&Message{Event: TokenExpiredEvent})
} else if jwt.ExpirationTime.Unix()-time.Now().Unix() <= 180 { } else if jwt.ExpirationTime.Unix()-time.Now().Unix() <= 60 {
_ = h.SendJson(&Message{Event: TokenExpiringEvent}) _ = h.SendJson(&Message{Event: TokenExpiringEvent})
} }
} }
@@ -36,38 +36,42 @@ func (h *Handler) ListenForExpiration(ctx context.Context) {
} }
} }
var e = []string{
server.StatsEvent,
server.StatusEvent,
server.ConsoleOutputEvent,
server.InstallOutputEvent,
server.InstallStartedEvent,
server.InstallCompletedEvent,
server.DaemonMessageEvent,
server.BackupCompletedEvent,
server.TransferLogsEvent,
server.TransferStatusEvent,
}
// Listens for different events happening on a server and sends them along // Listens for different events happening on a server and sends them along
// to the connected websocket. // to the connected websocket.
func (h *Handler) ListenForServerEvents(ctx context.Context) { func (h *Handler) ListenForServerEvents(ctx context.Context) {
e := []string{ h.server.Log().Debug("listening for server events over websocket")
server.StatsEvent, callback := func(e events.Event) {
server.StatusEvent, if err := h.SendJson(&Message{Event: e.Topic, Args: []string{e.Data}}); err != nil {
server.ConsoleOutputEvent, h.server.Log().WithField("error", err).Warn("error while sending server data over websocket")
server.InstallOutputEvent,
server.InstallStartedEvent,
server.InstallCompletedEvent,
server.DaemonMessageEvent,
server.BackupCompletedEvent,
}
eventChannel := make(chan events.Event)
for _, event := range e {
h.server.Events().Subscribe(event, eventChannel)
}
for d := range eventChannel {
select {
case <-ctx.Done():
for _, event := range e {
h.server.Events().Unsubscribe(event, eventChannel)
}
close(eventChannel)
default:
_ = h.SendJson(&Message{
Event: d.Topic,
Args: []string{d.Data},
})
} }
} }
// Subscribe to all of the events with the same callback that will push the data out over the
// websocket for the server.
for _, evt := range e {
h.server.Events().On(evt, &callback)
}
go func(ctx context.Context) {
select {
case <-ctx.Done():
// Once this context is stopped, de-register all of the listeners that have been registered.
for _, evt := range e {
h.server.Events().Off(evt, &callback)
}
}
}(ctx)
} }

View File

@@ -8,16 +8,13 @@ const (
SetStateEvent = "set state" SetStateEvent = "set state"
SendServerLogsEvent = "send logs" SendServerLogsEvent = "send logs"
SendCommandEvent = "send command" SendCommandEvent = "send command"
SendStatsEvent = "send stats"
ErrorEvent = "daemon error" ErrorEvent = "daemon error"
JwtErrorEvent = "jwt error"
) )
type Message struct { type Message struct {
// The event to perform. Should be one of the following that are supported: // The event to perform.
//
// - status : Returns the server's power state.
// - logs : Returns the server log data at the time of the request.
// - power : Performs a power action aganist the server based the data.
// - command : Performs a command on a server using the data field.
Event string `json:"event"` Event string `json:"event"`
// The data to pass along, only used by power/command currently. Other requests // The data to pass along, only used by power/command currently. Other requests

View File

@@ -2,15 +2,16 @@ package websocket
import ( import (
"context" "context"
"emperror.dev/errors"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/apex/log" "github.com/apex/log"
"github.com/gbrlsnchs/jwt/v3" "github.com/gbrlsnchs/jwt/v3"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/environment/docker"
"github.com/pterodactyl/wings/router/tokens" "github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server"
"net/http" "net/http"
@@ -27,14 +28,31 @@ const (
PermissionSendPowerRestart = "control.restart" PermissionSendPowerRestart = "control.restart"
PermissionReceiveErrors = "admin.websocket.errors" PermissionReceiveErrors = "admin.websocket.errors"
PermissionReceiveInstall = "admin.websocket.install" PermissionReceiveInstall = "admin.websocket.install"
PermissionReceiveTransfer = "admin.websocket.transfer"
PermissionReceiveBackups = "backup.read" PermissionReceiveBackups = "backup.read"
) )
type Handler struct { type Handler struct {
sync.RWMutex sync.RWMutex `json:"-"`
Connection *websocket.Conn Connection *websocket.Conn `json:"-"`
jwt *tokens.WebsocketPayload `json:"-"` jwt *tokens.WebsocketPayload
server *server.Server server *server.Server
uuid uuid.UUID
}
var (
ErrJwtNotPresent = errors.New("jwt: no jwt present")
ErrJwtNoConnectPerm = errors.New("jwt: missing connect permission")
ErrJwtUuidMismatch = errors.New("jwt: server uuid mismatch")
ErrJwtOnDenylist = errors.New("jwt: created too far in past (denylist)")
)
func IsJwtError(err error) bool {
return errors.Is(err, ErrJwtNotPresent) ||
errors.Is(err, ErrJwtNoConnectPerm) ||
errors.Is(err, ErrJwtUuidMismatch) ||
errors.Is(err, ErrJwtOnDenylist) ||
errors.Is(err, jwt.ErrExpValidation)
} }
// Parses a JWT into a websocket token payload. // Parses a JWT into a websocket token payload.
@@ -45,8 +63,12 @@ func NewTokenPayload(token []byte) (*tokens.WebsocketPayload, error) {
return nil, err return nil, err
} }
if payload.Denylisted() {
return nil, ErrJwtOnDenylist
}
if !payload.HasPermission(PermissionConnect) { if !payload.HasPermission(PermissionConnect) {
return nil, errors.New("not authorized to connect to this socket") return nil, ErrJwtNoConnectPerm
} }
return &payload, nil return &payload, nil
@@ -62,15 +84,11 @@ func GetHandler(s *server.Server, w http.ResponseWriter, r *http.Request) (*Hand
if o == config.Get().PanelLocation { if o == config.Get().PanelLocation {
return true return true
} }
for _, origin := range config.Get().AllowedOrigins { for _, origin := range config.Get().AllowedOrigins {
if o != origin { if origin == "*" || origin == o {
continue return true
} }
return true
} }
return false return false
}, },
} }
@@ -80,17 +98,30 @@ func GetHandler(s *server.Server, w http.ResponseWriter, r *http.Request) (*Hand
return nil, err return nil, err
} }
u, err := uuid.NewRandom()
if err != nil {
return nil, err
}
return &Handler{ return &Handler{
Connection: conn, Connection: conn,
jwt: nil, jwt: nil,
server: s, server: s,
uuid: u,
}, nil }, nil
} }
func (h *Handler) Uuid() uuid.UUID {
return h.uuid
}
func (h *Handler) SendJson(v *Message) error { func (h *Handler) SendJson(v *Message) error {
// Do not send JSON down the line if the JWT on the connection is not // Do not send JSON down the line if the JWT on the connection is not valid!
// valid!
if err := h.TokenValid(); err != nil { if err := h.TokenValid(); err != nil {
h.unsafeSendJson(Message{
Event: JwtErrorEvent,
Args: []string{err.Error()},
})
return nil return nil
} }
@@ -111,9 +142,32 @@ func (h *Handler) SendJson(v *Message) error {
return nil return nil
} }
} }
// If we are sending transfer output, only send it to the user if they have the required permissions.
if v.Event == server.TransferLogsEvent {
if !j.HasPermission(PermissionReceiveTransfer) {
return nil
}
}
} }
return h.unsafeSendJson(v) if err := h.unsafeSendJson(v); err != nil {
// Not entirely sure how this happens (likely just when there is a ton of console spam)
// but I don't care to fix it right now, so just mask the error and throw a warning into
// the logs for us to look into later.
if errors.Is(err, websocket.ErrCloseSent) {
if h.server != nil {
h.server.Log().WithField("subsystem", "websocket").
WithField("event", v.Event).
Warn("failed to send event to websocket: close already sent")
}
return nil
}
return err
}
return nil
} }
// Sends JSON over the websocket connection, ignoring the authentication state of the // Sends JSON over the websocket connection, ignoring the authentication state of the
@@ -130,19 +184,23 @@ func (h *Handler) unsafeSendJson(v interface{}) error {
func (h *Handler) TokenValid() error { func (h *Handler) TokenValid() error {
j := h.GetJwt() j := h.GetJwt()
if j == nil { if j == nil {
return errors.New("no jwt present") return ErrJwtNotPresent
} }
if err := jwt.ExpirationTimeValidator(time.Now())(&j.Payload); err != nil { if err := jwt.ExpirationTimeValidator(time.Now())(&j.Payload); err != nil {
return err return err
} }
if j.Denylisted() {
return ErrJwtOnDenylist
}
if !j.HasPermission(PermissionConnect) { if !j.HasPermission(PermissionConnect) {
return errors.New("jwt does not have connect permission") return ErrJwtNoConnectPerm
} }
if h.server.Id() != j.GetServerUuid() { if h.server.Id() != j.GetServerUuid() {
return errors.New("jwt server uuid mismatch") return ErrJwtUuidMismatch
} }
return nil return nil
@@ -153,22 +211,26 @@ func (h *Handler) TokenValid() error {
// error message, otherwise we just send back a standard error message. // error message, otherwise we just send back a standard error message.
func (h *Handler) SendErrorJson(msg Message, err error, shouldLog ...bool) error { func (h *Handler) SendErrorJson(msg Message, err error, shouldLog ...bool) error {
j := h.GetJwt() j := h.GetJwt()
isJWTError := IsJwtError(err)
message := "an unexpected error was encountered while handling this request" wsm := Message{
if server.IsSuspendedError(err) || (j != nil && j.HasPermission(PermissionReceiveErrors)) { Event: ErrorEvent,
message = err.Error() Args: []string{"an unexpected error was encountered while handling this request"},
} }
m, u := h.GetErrorMessage(message) if isJWTError || (j != nil && j.HasPermission(PermissionReceiveErrors)) {
if isJWTError {
wsm.Event = JwtErrorEvent
}
wsm.Args = []string{err.Error()}
}
wsm := Message{Event: ErrorEvent} m, u := h.GetErrorMessage(wsm.Args[0])
wsm.Args = []string{m} wsm.Args = []string{m}
if len(shouldLog) == 0 || (len(shouldLog) == 1 && shouldLog[0] == true) { if !isJWTError && (len(shouldLog) == 0 || (len(shouldLog) == 1 && shouldLog[0] == true)) {
if !server.IsSuspendedError(err) { h.server.Log().WithFields(log.Fields{"event": msg.Event, "error_identifier": u.String(), "error": err}).
h.server.Log().WithFields(log.Fields{"event": msg.Event, "error_identifier": u.String(), "error": err}). Errorf("error processing websocket event \"%s\"", msg.Event)
Error("failed to handle websocket process; an error was encountered processing an event")
}
} }
return h.unsafeSendJson(wsm) return h.unsafeSendJson(wsm)
@@ -202,13 +264,10 @@ func (h *Handler) GetJwt() *tokens.WebsocketPayload {
func (h *Handler) HandleInbound(m Message) error { func (h *Handler) HandleInbound(m Message) error {
if m.Event != AuthenticationEvent { if m.Event != AuthenticationEvent {
if err := h.TokenValid(); err != nil { if err := h.TokenValid(); err != nil {
log.WithField("message", err.Error()).Debug("jwt for server websocket is no longer valid")
h.unsafeSendJson(Message{ h.unsafeSendJson(Message{
Event: ErrorEvent, Event: JwtErrorEvent,
Args: []string{"could not authenticate client: " + err.Error()}, Args: []string{err.Error()},
}) })
return nil return nil
} }
} }
@@ -252,7 +311,7 @@ func (h *Handler) HandleInbound(m Message) error {
// On every authentication event, send the current server status back // On every authentication event, send the current server status back
// to the client. :) // to the client. :)
state := h.server.GetState() state := h.server.Environment.State()
h.SendJson(&Message{ h.SendJson(&Message{
Event: server.StatusEvent, Event: server.StatusEvent,
Args: []string{state}, Args: []string{state},
@@ -261,13 +320,15 @@ func (h *Handler) HandleInbound(m Message) error {
// Only send the current disk usage if the server is offline, if docker container is running, // Only send the current disk usage if the server is offline, if docker container is running,
// Environment#EnableResourcePolling() will send this data to all clients. // Environment#EnableResourcePolling() will send this data to all clients.
if state == environment.ProcessOfflineState { if state == environment.ProcessOfflineState {
_ = h.server.Filesystem.HasSpaceAvailable() if !h.server.IsInstalling() && !h.server.IsTransferring() {
_ = h.server.Filesystem().HasSpaceAvailable(false)
b, _ := json.Marshal(h.server.Proc()) b, _ := json.Marshal(h.server.Proc())
h.SendJson(&Message{ h.SendJson(&Message{
Event: server.StatsEvent, Event: server.StatsEvent,
Args: []string{string(b)}, Args: []string{string(b)},
}) })
}
} }
return nil return nil
@@ -309,7 +370,7 @@ func (h *Handler) HandleInbound(m Message) error {
return nil return nil
} }
logs, err := h.server.Environment.Readlog(1024 * 16) logs, err := h.server.Environment.Readlog(config.Get().System.WebsocketLogCount)
if err != nil { if err != nil {
return err return err
} }
@@ -321,6 +382,16 @@ func (h *Handler) HandleInbound(m Message) error {
}) })
} }
return nil
}
case SendStatsEvent:
{
b, _ := json.Marshal(h.server.Proc())
h.SendJson(&Message{
Event: server.StatsEvent,
Args: []string{string(b)},
})
return nil return nil
} }
case SendCommandEvent: case SendCommandEvent:
@@ -329,10 +400,22 @@ func (h *Handler) HandleInbound(m Message) error {
return nil return nil
} }
if h.server.GetState() == environment.ProcessOfflineState { if h.server.Environment.State() == environment.ProcessOfflineState {
return nil return nil
} }
// TODO(dane): should probably add a new process state that is "booting environment" or something
// so that we can better handle this and only set the environment to booted once we're attached.
//
// Or maybe just an IsBooted function?
if h.server.Environment.State() == environment.ProcessStartingState {
if e, ok := h.server.Environment.(*docker.Environment); ok {
if !e.IsAttached() {
return nil
}
}
}
return h.server.Environment.SendCommand(strings.Join(m.Args, "")) return h.server.Environment.SendCommand(strings.Join(m.Args, ""))
} }
} }

View File

@@ -3,12 +3,15 @@ package server
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"github.com/mholt/archiver/v3"
"github.com/pterodactyl/wings/config"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"emperror.dev/errors"
"github.com/mholt/archiver/v3"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/server/filesystem"
) )
// Archiver represents a Server Archiver. // Archiver represents a Server Archiver.
@@ -16,19 +19,19 @@ type Archiver struct {
Server *Server Server *Server
} }
// ArchivePath returns the path to the server's archive. // Path returns the path to the server's archive.
func (a *Archiver) ArchivePath() string { func (a *Archiver) Path() string {
return filepath.Join(config.Get().System.ArchiveDirectory, a.ArchiveName()) return filepath.Join(config.Get().System.ArchiveDirectory, a.Name())
} }
// ArchiveName returns the name of the server's archive. // Name returns the name of the server's archive.
func (a *Archiver) ArchiveName() string { func (a *Archiver) Name() string {
return a.Server.Id() + ".tar.gz" return a.Server.Id() + ".tar.gz"
} }
// Exists returns a boolean based off if the archive exists. // Exists returns a boolean based off if the archive exists.
func (a *Archiver) Exists() bool { func (a *Archiver) Exists() bool {
if _, err := os.Stat(a.ArchivePath()); os.IsNotExist(err) { if _, err := os.Stat(a.Path()); os.IsNotExist(err) {
return false return false
} }
@@ -36,13 +39,21 @@ func (a *Archiver) Exists() bool {
} }
// Stat stats the archive file. // Stat stats the archive file.
func (a *Archiver) Stat() (*Stat, error) { func (a *Archiver) Stat() (*filesystem.Stat, error) {
return a.Server.Filesystem.unsafeStat(a.ArchivePath()) s, err := os.Stat(a.Path())
if err != nil {
return nil, err
}
return &filesystem.Stat{
Info: s,
Mimetype: "application/tar+gzip",
}, nil
} }
// Archive creates an archive of the server and deletes the previous one. // Archive creates an archive of the server and deletes the previous one.
func (a *Archiver) Archive() error { func (a *Archiver) Archive() error {
path := a.Server.Filesystem.Path() path := a.Server.Filesystem().Path()
// Get the list of root files and directories to archive. // Get the list of root files and directories to archive.
var files []string var files []string
@@ -52,49 +63,47 @@ func (a *Archiver) Archive() error {
} }
for _, file := range fileInfo { for _, file := range fileInfo {
f, err := a.Server.Filesystem.SafeJoin(path, file) f := filepath.Join(path, file.Name())
if err != nil { // If the file is a symlink we cannot safely assume that the result of a filepath.Join() will be
return err // a safe destination. We need to check if the file is a symlink, and if so pass off to the SafePath
// function to resolve it to the final destination.
//
// ioutil.ReadDir() calls Lstat, so this will work correctly. If it did not call Lstat, but rather
// just did a normal Stat call, this would fail since that would be looking at the symlink destination
// and not the actual file in this listing.
if file.Mode()&os.ModeSymlink != 0 {
f, err = a.Server.Filesystem().SafePath(filepath.Join(path, file.Name()))
if err != nil {
return err
}
} }
files = append(files, f) files = append(files, f)
} }
stat, err := a.Stat() if err := a.DeleteIfExists(); err != nil {
if err != nil && !os.IsNotExist(err) {
return err return err
} }
// Check if the file exists. return archiver.NewTarGz().Archive(files, a.Path())
if stat != nil {
if err := os.Remove(a.ArchivePath()); err != nil {
return err
}
}
return archiver.NewTarGz().Archive(files, a.ArchivePath())
} }
// DeleteIfExists deletes the archive if it exists. // DeleteIfExists deletes the archive if it exists.
func (a *Archiver) DeleteIfExists() error { func (a *Archiver) DeleteIfExists() error {
stat, err := a.Stat() if _, err := a.Stat(); err != nil {
if err != nil && !os.IsNotExist(err) { if errors.Is(err, os.ErrNotExist) {
return nil
}
return err return err
} }
// Check if the file exists. return errors.WithMessage(os.Remove(a.Path()), "archiver: failed to delete archive from system")
if stat != nil {
if err := os.Remove(a.ArchivePath()); err != nil {
return err
}
}
return nil
} }
// Checksum computes a SHA256 checksum of the server's archive. // Checksum computes a SHA256 checksum of the server's archive.
func (a *Archiver) Checksum() (string, error) { func (a *Archiver) Checksum() (string, error) {
file, err := os.Open(a.ArchivePath()) file, err := os.Open(a.Path())
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@@ -1,100 +1,80 @@
package server package server
import ( import (
"bufio" "io/ioutil"
"os"
"emperror.dev/errors"
"github.com/apex/log" "github.com/apex/log"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/server/backup" "github.com/pterodactyl/wings/server/backup"
"os"
"path"
) )
// Notifies the panel of a backup's state and returns an error if one is encountered // Notifies the panel of a backup's state and returns an error if one is encountered
// while performing this action. // while performing this action.
func (s *Server) notifyPanelOfBackup(uuid string, ad *backup.ArchiveDetails, successful bool) error { func (s *Server) notifyPanelOfBackup(uuid string, ad *backup.ArchiveDetails, successful bool) error {
r := api.NewRequester() if err := api.New().SendBackupStatus(uuid, ad.ToRequest(successful)); err != nil {
rerr, err := r.SendBackupStatus(uuid, ad.ToRequest(successful)) if !api.IsRequestError(err) {
if rerr != nil || err != nil {
if err != nil {
s.Log().WithFields(log.Fields{ s.Log().WithFields(log.Fields{
"backup": uuid, "backup": uuid,
"error": err, "error": err,
}).Error("failed to notify panel of backup status due to internal code error") }).Error("failed to notify panel of backup status due to wings error")
return err return err
} }
s.Log().WithField("backup", uuid).Warn(rerr.String()) return errors.New(err.Error())
return errors.New(rerr.String())
} }
return nil return nil
} }
// Get all of the ignored files for a server based on its .pteroignore file in the root. // Get all of the ignored files for a server based on its .pteroignore file in the root.
func (s *Server) getServerwideIgnoredFiles() ([]string, error) { func (s *Server) getServerwideIgnoredFiles() (string, error) {
var ignored []string f, st, err := s.Filesystem().File(".pteroignore")
f, err := os.Open(path.Join(s.Filesystem.Path(), ".pteroignore"))
if err != nil { if err != nil {
if !os.IsNotExist(err) { if errors.Is(err, os.ErrNotExist) {
return nil, err return "", nil
}
} else {
scanner := bufio.NewScanner(f)
for scanner.Scan() {
// Only include non-empty lines, for the sake of clarity...
if t := scanner.Text(); t != "" {
ignored = append(ignored, t)
}
}
if err := scanner.Err(); err != nil {
return nil, err
} }
return "", err
} }
defer f.Close()
return ignored, nil if st.Mode()&os.ModeSymlink != 0 || st.Size() > 32*1024 {
} // Don't read a symlinked ignore file, or a file larger than 32KiB in size.
return "", nil
// Get the backup files to include when generating it.
func (s *Server) GetIncludedBackupFiles(ignored []string) (*backup.IncludedFiles, error) {
// If no ignored files are present in the request, check for a .pteroignore file in the root
// of the server files directory, and use that to generate the backup.
if len(ignored) == 0 {
if i, err := s.getServerwideIgnoredFiles(); err != nil {
s.Log().WithField("error", err).Warn("failed to retrieve ignored files listing for server")
} else {
ignored = i
}
} }
b, err := ioutil.ReadAll(f)
// Get the included files based on the root path and the ignored files provided. if err != nil {
return s.Filesystem.GetIncludedFiles(s.Filesystem.Path(), ignored) return "", err
}
return string(b), nil
} }
// Performs a server backup and then emits the event over the server websocket. We // Performs a server backup and then emits the event over the server websocket. We
// let the actual backup system handle notifying the panel of the status, but that // let the actual backup system handle notifying the panel of the status, but that
// won't emit a websocket event. // won't emit a websocket event.
func (s *Server) Backup(b backup.BackupInterface) error { func (s *Server) Backup(b backup.BackupInterface) error {
// Get the included files based on the root path and the ignored files provided. ignored := b.Ignored()
inc, err := s.GetIncludedBackupFiles(b.Ignored()) if b.Ignored() == "" {
if err != nil { if i, err := s.getServerwideIgnoredFiles(); err != nil {
return errors.WithStack(err) log.WithField("server", s.Id()).WithField("error", err).Warn("failed to get server-wide ignored files")
} else {
ignored = i
}
} }
ad, err := b.Generate(inc, s.Filesystem.Path()) ad, err := b.Generate(s.Filesystem().Path(), ignored)
if err != nil { if err != nil {
if notifyError := s.notifyPanelOfBackup(b.Identifier(), &backup.ArchiveDetails{}, false); notifyError != nil { if err := s.notifyPanelOfBackup(b.Identifier(), &backup.ArchiveDetails{}, false); err != nil {
s.Log().WithFields(log.Fields{ s.Log().WithFields(log.Fields{
"backup": b.Identifier(), "backup": b.Identifier(),
"error": err, "error": err,
}).Warn("failed to notify panel of failed backup state") }).Warn("failed to notify panel of failed backup state")
} else {
s.Log().WithField("backup", b.Identifier()).Info("notified panel of failed backup state")
} }
s.Events().PublishJson(BackupCompletedEvent+":"+b.Identifier(), map[string]interface{}{ _ = s.Events().PublishJson(BackupCompletedEvent+":"+b.Identifier(), map[string]interface{}{
"uuid": b.Identifier(), "uuid": b.Identifier(),
"is_successful": false, "is_successful": false,
"checksum": "", "checksum": "",
@@ -102,20 +82,23 @@ func (s *Server) Backup(b backup.BackupInterface) error {
"file_size": 0, "file_size": 0,
}) })
return errors.WithStack(err) return errors.WrapIf(err, "backup: error while generating server backup")
} }
// Try to notify the panel about the status of this backup. If for some reason this request // Try to notify the panel about the status of this backup. If for some reason this request
// fails, delete the archive from the daemon and return that error up the chain to the caller. // fails, delete the archive from the daemon and return that error up the chain to the caller.
if notifyError := s.notifyPanelOfBackup(b.Identifier(), ad, true); notifyError != nil { if notifyError := s.notifyPanelOfBackup(b.Identifier(), ad, true); notifyError != nil {
b.Remove() _ = b.Remove()
return notifyError s.Log().WithField("error", notifyError).Info("failed to notify panel of successful backup state")
return err
} else {
s.Log().WithField("backup", b.Identifier()).Info("notified panel of successful backup state")
} }
// Emit an event over the socket so we can update the backup in realtime on // Emit an event over the socket so we can update the backup in realtime on
// the frontend for the server. // the frontend for the server.
s.Events().PublishJson(BackupCompletedEvent+":"+b.Identifier(), map[string]interface{}{ _ = s.Events().PublishJson(BackupCompletedEvent+":"+b.Identifier(), map[string]interface{}{
"uuid": b.Identifier(), "uuid": b.Identifier(),
"is_successful": true, "is_successful": true,
"checksum": ad.Checksum, "checksum": ad.Checksum,

View File

@@ -1,127 +0,0 @@
package backup
import (
"archive/tar"
"context"
"github.com/apex/log"
gzip "github.com/klauspost/pgzip"
"github.com/remeh/sizedwaitgroup"
"golang.org/x/sync/errgroup"
"io"
"os"
"runtime"
"strings"
"sync"
)
type Archive struct {
sync.Mutex
TrimPrefix string
Files *IncludedFiles
}
// Creates an archive at dst with all of the files defined in the included files struct.
func (a *Archive) Create(dst string, ctx context.Context) (os.FileInfo, error) {
f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return nil, err
}
defer f.Close()
maxCpu := runtime.NumCPU() / 2
if maxCpu > 4 {
maxCpu = 4
}
gzw, _ := gzip.NewWriterLevel(f, gzip.BestSpeed)
_ = gzw.SetConcurrency(1 << 20, maxCpu)
defer gzw.Flush()
defer gzw.Close()
tw := tar.NewWriter(gzw)
defer tw.Flush()
defer tw.Close()
wg := sizedwaitgroup.New(10)
g, ctx := errgroup.WithContext(ctx)
// Iterate over all of the files to be included and put them into the archive. This is
// done as a concurrent goroutine to speed things along. If an error is encountered at
// any step, the entire process is aborted.
for p, s := range a.Files.All() {
if (*s).IsDir() {
continue
}
pa := p
st := s
g.Go(func() error {
wg.Add()
defer wg.Done()
select {
case <-ctx.Done():
return ctx.Err()
default:
return a.addToArchive(pa, st, tw)
}
})
}
// Block until the entire routine is completed.
if err := g.Wait(); err != nil {
f.Close()
// Attempt to remove the archive if there is an error, report that error to
// the logger if it fails.
if rerr := os.Remove(dst); rerr != nil && !os.IsNotExist(rerr) {
log.WithField("location", dst).Warn("failed to delete corrupted backup archive")
}
return nil, err
}
st, err := f.Stat()
if err != nil {
return nil, err
}
return st, nil
}
// Adds a single file to the existing tar archive writer.
func (a *Archive) addToArchive(p string, s *os.FileInfo, w *tar.Writer) error {
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close()
st := *s
header := &tar.Header{
// Trim the long server path from the name of the file so that the resulting
// archive is exactly how the user would see it in the panel file manager.
Name: strings.TrimPrefix(p, a.TrimPrefix),
Size: st.Size(),
Mode: int64(st.Mode()),
ModTime: st.ModTime(),
}
// These actions must occur sequentially, even if this function is called multiple
// in parallel. You'll get some nasty panic's otherwise.
a.Lock()
defer a.Unlock()
if err := w.WriteHeader(header); err != nil {
return err
}
buf := make([]byte, 4*1024)
if _, err := io.CopyBuffer(w, f, buf); err != nil {
return err
}
return nil
}

View File

@@ -4,7 +4,6 @@ import (
"crypto/sha1" "crypto/sha1"
"encoding/hex" "encoding/hex"
"github.com/apex/log" "github.com/apex/log"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"io" "io"
@@ -13,9 +12,11 @@ import (
"sync" "sync"
) )
type AdapterType string
const ( const (
LocalBackupAdapter = "wings" LocalBackupAdapter AdapterType = "wings"
S3BackupAdapter = "s3" S3BackupAdapter AdapterType = "s3"
) )
type ArchiveDetails struct { type ArchiveDetails struct {
@@ -41,7 +42,10 @@ type Backup struct {
// An array of files to ignore when generating this backup. This should be // An array of files to ignore when generating this backup. This should be
// compatible with a standard .gitignore structure. // compatible with a standard .gitignore structure.
IgnoredFiles []string `json:"ignored_files"` Ignore string `json:"ignore"`
adapter AdapterType
logContext map[string]interface{}
} }
// noinspection GoNameStartsWithPackageName // noinspection GoNameStartsWithPackageName
@@ -49,14 +53,17 @@ type BackupInterface interface {
// Returns the UUID of this backup as tracked by the panel instance. // Returns the UUID of this backup as tracked by the panel instance.
Identifier() string Identifier() string
// Attaches additional context to the log output for this backup.
WithLogContext(map[string]interface{})
// Generates a backup in whatever the configured source for the specific // Generates a backup in whatever the configured source for the specific
// implementation is. // implementation is.
Generate(*IncludedFiles, string) (*ArchiveDetails, error) Generate(string, string) (*ArchiveDetails, error)
// Returns the ignored files for this backup instance. // Returns the ignored files for this backup instance.
Ignored() []string Ignored() string
// Returns a SHA256 checksum for the generated backup. // Returns a SHA1 checksum for the generated backup.
Checksum() ([]byte, error) Checksum() ([]byte, error)
// Returns the size of the generated backup. // Returns the size of the generated backup.
@@ -87,7 +94,7 @@ func (b *Backup) Path() string {
func (b *Backup) Size() (int64, error) { func (b *Backup) Size() (int64, error) {
st, err := os.Stat(b.Path()) st, err := os.Stat(b.Path())
if err != nil { if err != nil {
return 0, errors.WithStack(err) return 0, err
} }
return st.Size(), nil return st.Size(), nil
@@ -99,7 +106,7 @@ func (b *Backup) Checksum() ([]byte, error) {
f, err := os.Open(b.Path()) f, err := os.Open(b.Path())
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, err
} }
defer f.Close() defer f.Close()
@@ -117,20 +124,25 @@ func (b *Backup) Details() *ArchiveDetails {
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
wg.Add(2) wg.Add(2)
l := log.WithField("backup_id", b.Uuid)
var checksum string var checksum string
// Calculate the checksum for the file. // Calculate the checksum for the file.
go func() { go func() {
defer wg.Done() defer wg.Done()
l.Info("computing checksum for backup...")
resp, err := b.Checksum() resp, err := b.Checksum()
if err != nil { if err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"backup": b.Identifier(), "backup": b.Identifier(),
"error": err, "error": err,
}).Error("failed to calculate checksum for backup") }).Error("failed to calculate checksum for backup")
return
} }
checksum = hex.EncodeToString(resp) checksum = hex.EncodeToString(resp)
l.WithField("checksum", checksum).Info("computed checksum for backup")
}() }()
var sz int64 var sz int64
@@ -153,6 +165,16 @@ func (b *Backup) Details() *ArchiveDetails {
} }
} }
func (b *Backup) Ignored() []string { func (b *Backup) Ignored() string {
return b.IgnoredFiles return b.Ignore
}
// Returns a logger instance for this backup with the additional context fields
// assigned to the output.
func (b *Backup) log() *log.Entry {
l := log.WithField("backup", b.Identifier()).WithField("adapter", b.adapter)
for k, v := range b.logContext {
l = l.WithField(k, v)
}
return l
} }

View File

@@ -1,8 +1,8 @@
package backup package backup
import ( import (
"context" "errors"
"github.com/pkg/errors" "github.com/pterodactyl/wings/server/filesystem"
"os" "os"
) )
@@ -17,8 +17,8 @@ var _ BackupInterface = (*LocalBackup)(nil)
func LocateLocal(uuid string) (*LocalBackup, os.FileInfo, error) { func LocateLocal(uuid string) (*LocalBackup, os.FileInfo, error) {
b := &LocalBackup{ b := &LocalBackup{
Backup{ Backup{
Uuid: uuid, Uuid: uuid,
IgnoredFiles: nil, Ignore: "",
}, },
} }
@@ -28,7 +28,7 @@ func LocateLocal(uuid string) (*LocalBackup, os.FileInfo, error) {
} }
if st.IsDir() { if st.IsDir() {
return nil, nil, errors.New("invalid archive found; is directory") return nil, nil, errors.New("invalid archive, is directory")
} }
return b, st, nil return b, st, nil
@@ -39,17 +39,24 @@ func (b *LocalBackup) Remove() error {
return os.Remove(b.Path()) return os.Remove(b.Path())
} }
// Attaches additional context to the log output for this backup.
func (b *LocalBackup) WithLogContext(c map[string]interface{}) {
b.logContext = c
}
// Generates a backup of the selected files and pushes it to the defined location // Generates a backup of the selected files and pushes it to the defined location
// for this instance. // for this instance.
func (b *LocalBackup) Generate(included *IncludedFiles, prefix string) (*ArchiveDetails, error) { func (b *LocalBackup) Generate(basePath, ignore string) (*ArchiveDetails, error) {
a := &Archive{ a := &filesystem.Archive{
TrimPrefix: prefix, BasePath: basePath,
Files: included, Ignore: ignore,
} }
if _, err := a.Create(b.Path(), context.Background()); err != nil { b.log().Info("creating backup for server...")
if err := a.Create(b.Path()); err != nil {
return nil, err return nil, err
} }
b.log().Info("created backup successfully")
return b.Details(), nil return b.Details(), nil
} }

View File

@@ -1,15 +1,14 @@
package backup package backup
import ( import (
"errors"
"fmt" "fmt"
"github.com/pkg/errors"
) )
type Request struct { type Request struct {
Adapter string `json:"adapter"` Adapter AdapterType `json:"adapter"`
Uuid string `json:"uuid"` Uuid string `json:"uuid"`
IgnoredFiles []string `json:"ignored_files"` Ignore string `json:"ignore"`
PresignedUrl string `json:"presigned_url"`
} }
// Generates a new local backup struct. // Generates a new local backup struct.
@@ -20,8 +19,9 @@ func (r *Request) NewLocalBackup() (*LocalBackup, error) {
return &LocalBackup{ return &LocalBackup{
Backup{ Backup{
Uuid: r.Uuid, Uuid: r.Uuid,
IgnoredFiles: r.IgnoredFiles, Ignore: r.Ignore,
adapter: LocalBackupAdapter,
}, },
}, nil }, nil
} }
@@ -32,15 +32,11 @@ func (r *Request) NewS3Backup() (*S3Backup, error) {
return nil, errors.New(fmt.Sprintf("cannot create s3 backup using [%s] adapter", r.Adapter)) return nil, errors.New(fmt.Sprintf("cannot create s3 backup using [%s] adapter", r.Adapter))
} }
if len(r.PresignedUrl) == 0 {
return nil, errors.New("a valid presigned S3 upload URL must be provided to use the [s3] adapter")
}
return &S3Backup{ return &S3Backup{
Backup: Backup{ Backup: Backup{
Uuid: r.Uuid, Uuid: r.Uuid,
IgnoredFiles: r.IgnoredFiles, Ignore: r.Ignore,
adapter: S3BackupAdapter,
}, },
PresignedUrl: r.PresignedUrl,
}, nil }, nil
} }

View File

@@ -1,9 +1,9 @@
package backup package backup
import ( import (
"context"
"fmt" "fmt"
"github.com/apex/log" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/server/filesystem"
"io" "io"
"net/http" "net/http"
"os" "os"
@@ -12,29 +12,35 @@ import (
type S3Backup struct { type S3Backup struct {
Backup Backup
// The pre-signed upload endpoint for the generated backup. This must be
// provided otherwise this request will fail. This allows us to keep all
// of the keys off the daemon instances and the panel can handle generating
// the credentials for us.
PresignedUrl string
} }
var _ BackupInterface = (*S3Backup)(nil) var _ BackupInterface = (*S3Backup)(nil)
// Removes a backup from the system.
func (s *S3Backup) Remove() error {
return os.Remove(s.Path())
}
// Attaches additional context to the log output for this backup.
func (s *S3Backup) WithLogContext(c map[string]interface{}) {
s.logContext = c
}
// Generates a new backup on the disk, moves it into the S3 bucket via the provided // Generates a new backup on the disk, moves it into the S3 bucket via the provided
// presigned URL, and then deletes the backup from the disk. // presigned URL, and then deletes the backup from the disk.
func (s *S3Backup) Generate(included *IncludedFiles, prefix string) (*ArchiveDetails, error) { func (s *S3Backup) Generate(basePath, ignore string) (*ArchiveDetails, error) {
defer s.Remove() defer s.Remove()
a := &Archive{ a := &filesystem.Archive{
TrimPrefix: prefix, BasePath: basePath,
Files: included, Ignore: ignore,
} }
if _, err := a.Create(s.Path(), context.Background()); err != nil { s.log().Info("creating backup for server...")
if err := a.Create(s.Path()); err != nil {
return nil, err return nil, err
} }
s.log().Info("created backup successfully")
rc, err := os.Open(s.Path()) rc, err := os.Open(s.Path())
if err != nil { if err != nil {
@@ -42,45 +48,94 @@ func (s *S3Backup) Generate(included *IncludedFiles, prefix string) (*ArchiveDet
} }
defer rc.Close() defer rc.Close()
if resp, err := s.generateRemoteRequest(rc); err != nil { if err := s.generateRemoteRequest(rc); err != nil {
return nil, err return nil, err
} else {
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to put S3 object, %d:%s", resp.StatusCode, resp.Status)
}
} }
return s.Details(), err return s.Details(), nil
} }
// Removes a backup from the system. // Reader provides a wrapper around an existing io.Reader
func (s *S3Backup) Remove() error { // but implements io.Closer in order to satisfy an io.ReadCloser.
return os.Remove(s.Path()) type Reader struct {
io.Reader
}
func (Reader) Close() error {
return nil
} }
// Generates the remote S3 request and begins the upload. // Generates the remote S3 request and begins the upload.
func (s *S3Backup) generateRemoteRequest(rc io.ReadCloser) (*http.Response, error) { func (s *S3Backup) generateRemoteRequest(rc io.ReadCloser) error {
r, err := http.NewRequest(http.MethodPut, s.PresignedUrl, nil) defer rc.Close()
s.log().Debug("attempting to get size of backup...")
size, err := s.Backup.Size()
if err != nil { if err != nil {
return nil, err return err
} }
s.log().WithField("size", size).Debug("got size of backup")
if sz, err := s.Size(); err != nil { s.log().Debug("attempting to get S3 upload urls from Panel...")
return nil, err urls, err := api.New().GetBackupRemoteUploadURLs(s.Backup.Uuid, size)
} else { if err != nil {
r.ContentLength = sz return err
r.Header.Add("Content-Length", strconv.Itoa(int(sz))) }
s.log().Debug("got S3 upload urls from the Panel")
s.log().WithField("parts", len(urls.Parts)).Info("attempting to upload backup to s3 endpoint...")
handlePart := func(part string, size int64) (string, error) {
r, err := http.NewRequest(http.MethodPut, part, nil)
if err != nil {
return "", err
}
r.ContentLength = size
r.Header.Add("Content-Length", strconv.Itoa(int(size)))
r.Header.Add("Content-Type", "application/x-gzip") r.Header.Add("Content-Type", "application/x-gzip")
// Limit the reader to the size of the part.
r.Body = Reader{Reader: io.LimitReader(rc, size)}
// This http request can block forever due to it not having a timeout,
// but we are uploading up to 5GB of data, so there is not really
// a good way to handle a timeout on this.
res, err := http.DefaultClient.Do(r)
if err != nil {
return "", err
}
defer res.Body.Close()
// Handle non-200 status codes.
if res.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to put S3 object part, %d:%s", res.StatusCode, res.Status)
}
// Get the ETag from the uploaded part, this should be sent with the CompleteMultipartUpload request.
return res.Header.Get("ETag"), nil
} }
r.Body = rc for i, part := range urls.Parts {
// Get the size for the current part.
log.WithFields(log.Fields{ var partSize int64
"endpoint": s.PresignedUrl, if i+1 < len(urls.Parts) {
"headers": r.Header, partSize = urls.PartSize
}).Debug("uploading backup to remote S3 endpoint") } else {
// This is the remaining size for the last part,
// there is not a minimum size limit for the last part.
partSize = size - (int64(i) * urls.PartSize)
}
return http.DefaultClient.Do(r) // Attempt to upload the part.
if _, err := handlePart(part, partSize); err != nil {
s.log().WithField("part_id", i+1).WithError(err).Warn("failed to upload part")
return err
}
s.log().WithField("part_id", i+1).Info("successfully uploaded backup part")
}
s.log().WithField("parts", len(urls.Parts)).Info("backup has been successfully uploaded")
return nil
} }

View File

@@ -1,31 +0,0 @@
package backup
import (
"os"
"sync"
)
type IncludedFiles struct {
sync.RWMutex
files map[string]*os.FileInfo
}
// Pushes an additional file or folder onto the struct.
func (i *IncludedFiles) Push(info *os.FileInfo, p string) {
i.Lock()
defer i.Unlock()
if i.files == nil {
i.files = make(map[string]*os.FileInfo)
}
i.files[p] = info
}
// Returns all of the files that were marked as being included.
func (i *IncludedFiles) All() map[string]*os.FileInfo {
i.RLock()
defer i.RUnlock()
return i.files
}

View File

@@ -60,6 +60,8 @@ func (c *Collection) Find(filter func(*Server) bool) *Server {
} }
// Removes all items from the collection that match the filter function. // Removes all items from the collection that match the filter function.
//
// TODO: cancel the context?
func (c *Collection) Remove(filter func(*Server) bool) { func (c *Collection) Remove(filter func(*Server) bool) {
c.Lock() c.Lock()
defer c.Unlock() defer c.Unlock()

View File

@@ -1,8 +1,9 @@
package server package server
import ( import (
"github.com/gammazero/workerpool"
"runtime" "runtime"
"github.com/gammazero/workerpool"
) )
// Parent function that will update all of the defined configuration files for a server // Parent function that will update all of the defined configuration files for a server
@@ -15,7 +16,7 @@ func (s *Server) UpdateConfigurationFiles() {
f := cf f := cf
pool.Submit(func() { pool.Submit(func() {
p, err := s.Filesystem.SafePath(f.FileName) p, err := s.Filesystem().SafePath(f.FileName)
if err != nil { if err != nil {
s.Log().WithField("error", err).Error("failed to generate safe path for configuration file") s.Log().WithField("error", err).Error("failed to generate safe path for configuration file")
@@ -29,4 +30,4 @@ func (s *Server) UpdateConfigurationFiles() {
} }
pool.StopWait() pool.StopWait()
} }

View File

@@ -1,8 +1,9 @@
package server package server
import ( import (
"github.com/pterodactyl/wings/environment"
"sync" "sync"
"github.com/pterodactyl/wings/environment"
) )
type Configuration struct { type Configuration struct {
@@ -20,6 +21,10 @@ type Configuration struct {
// The command that should be used when booting up the server instance. // The command that should be used when booting up the server instance.
Invocation string `json:"invocation"` Invocation string `json:"invocation"`
// By default this is false, however if selected within the Panel while installing or re-installing a
// server, specific installation scripts will be skipped for the server process.
SkipEggScripts bool `default:"false" json:"skip_egg_scripts"`
// An array of environment variables that should be passed along to the running // An array of environment variables that should be passed along to the running
// server process. // server process.
EnvVars environment.Variables `json:"environment"` EnvVars environment.Variables `json:"environment"`
@@ -43,6 +48,21 @@ func (s *Server) Config() *Configuration {
return &s.cfg return &s.cfg
} }
// Returns the amount of disk space available to a server in bytes.
func (s *Server) DiskSpace() int64 {
s.cfg.mu.RLock()
defer s.cfg.mu.RUnlock()
return s.cfg.Build.DiskSpace * 1024.0 * 1024.0
}
func (s *Server) MemoryLimit() int64 {
s.cfg.mu.RLock()
defer s.cfg.mu.RUnlock()
return s.cfg.Build.MemoryLimit
}
func (c *Configuration) GetUuid() string { func (c *Configuration) GetUuid() string {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()

View File

@@ -1,60 +1,78 @@
package server package server
import ( import (
"context"
"fmt" "fmt"
"github.com/mitchellh/colorstring"
"github.com/pterodactyl/wings/config"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"emperror.dev/errors"
"github.com/mitchellh/colorstring"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/system"
) )
var ErrTooMuchConsoleData = errors.New("console is outputting too much data")
type ConsoleThrottler struct { type ConsoleThrottler struct {
sync.RWMutex mu sync.Mutex
config.ConsoleThrottles config.ConsoleThrottles
// The total number of activations that have occurred thus far. // The total number of activations that have occurred thus far.
activations uint64 activations uint64
// The total number of lines that have been sent since the last reset timer period.
count uint64
// Wether or not the console output is being throttled. It is up to calling code to
// determine what to do if it is.
isThrottled *system.AtomicBool
// The total number of lines processed so far during the given time period. // The total number of lines processed so far during the given time period.
lines uint64 timerCancel *context.CancelFunc
lastIntervalTime *time.Time
lastDecayTime *time.Time
} }
// Increments the number of activations for a server. // Resets the state of the throttler.
func (ct *ConsoleThrottler) AddActivation() uint64 { func (ct *ConsoleThrottler) Reset() {
ct.Lock() atomic.StoreUint64(&ct.count, 0)
defer ct.Unlock() atomic.StoreUint64(&ct.activations, 0)
ct.isThrottled.Store(false)
ct.activations += 1
return ct.activations
} }
// Decrements the number of activations for a server. // Triggers an activation for a server. You can also decrement the number of activations
func (ct *ConsoleThrottler) RemoveActivation() uint64 { // by passing a negative number.
ct.Lock() func (ct *ConsoleThrottler) markActivation(increment bool) uint64 {
defer ct.Unlock() if !increment {
if atomic.LoadUint64(&ct.activations) == 0 {
return 0
}
if ct.activations == 0 { // This weird dohicky subtracts 1 from the activation count.
return 0 return atomic.AddUint64(&ct.activations, ^uint64(0))
} }
ct.activations -= 1 return atomic.AddUint64(&ct.activations, 1)
return ct.activations
} }
// Increment the total count of lines that we have processed so far. // Determines if the console is currently being throttled. Calls to this function can be used to
func (ct *ConsoleThrottler) IncrementLineCount() uint64 { // determine if output should be funneled along to the websocket processes.
return atomic.AddUint64(&ct.lines, 1) func (ct *ConsoleThrottler) Throttled() bool {
return ct.isThrottled.Load()
} }
// Reset the line count to zero. // Starts a timer that runs in a seperate thread and will continually decrement the lines processed
func (ct *ConsoleThrottler) ResetLineCount() { // and number of activations, regardless of the current console message volume. All of the timers
atomic.SwapUint64(&ct.lines, 0) // are canceled if the context passed through is canceled.
func (ct *ConsoleThrottler) StartTimer(ctx context.Context) {
system.Every(ctx, time.Duration(int64(ct.LineResetInterval))*time.Millisecond, func(_ time.Time) {
ct.isThrottled.Store(false)
atomic.StoreUint64(&ct.count, 0)
})
system.Every(ctx, time.Duration(int64(ct.DecayInterval))*time.Millisecond, func(_ time.Time) {
ct.markActivation(false)
})
} }
// Handles output from a server's console. This code ensures that a server is not outputting // Handles output from a server's console. This code ensures that a server is not outputting
@@ -70,33 +88,40 @@ func (ct *ConsoleThrottler) ResetLineCount() {
// data all at once. These values are all configurable via the wings configuration file, however the // data all at once. These values are all configurable via the wings configuration file, however the
// defaults have been in the wild for almost two years at the time of this writing, so I feel quite // defaults have been in the wild for almost two years at the time of this writing, so I feel quite
// confident in them. // confident in them.
func (ct *ConsoleThrottler) Handle() { //
// This function returns an error if the server should be stopped due to violating throttle constraints
// and a boolean value indicating if a throttle is being violated when it is checked.
func (ct *ConsoleThrottler) Increment(onTrigger func()) error {
if !ct.Enabled {
return nil
}
// Increment the line count and if we have now output more lines than are allowed, trigger a throttle
// activation. Once the throttle is triggered and has passed the kill at value we will trigger a server
// stop automatically.
if atomic.AddUint64(&ct.count, 1) >= ct.Lines && !ct.Throttled() {
ct.isThrottled.Store(true)
if ct.markActivation(true) >= ct.MaximumTriggerCount {
return ErrTooMuchConsoleData
}
onTrigger()
}
return nil
} }
// Returns the throttler instance for the server or creates a new one. // Returns the throttler instance for the server or creates a new one.
func (s *Server) Throttler() *ConsoleThrottler { func (s *Server) Throttler() *ConsoleThrottler {
s.throttleLock.RLock() s.throttleOnce.Do(func() {
if s.throttler == nil {
// Release the read lock so that we can acquire a normal lock on the process and
// make modifications to the throttler.
s.throttleLock.RUnlock()
s.throttleLock.Lock()
s.throttler = &ConsoleThrottler{ s.throttler = &ConsoleThrottler{
isThrottled: system.NewAtomicBool(false),
ConsoleThrottles: config.Get().Throttles, ConsoleThrottles: config.Get().Throttles,
} }
s.throttleLock.Unlock() })
return s.throttler
return s.throttler
} else {
defer s.throttleLock.RUnlock()
return s.throttler
}
} }
// Sends output to the server console formatted to appear correctly as being sent // Sends output to the server console formatted to appear correctly as being sent
// from Wings. // from Wings.
func (s *Server) PublishConsoleOutputFromDaemon(data string) { func (s *Server) PublishConsoleOutputFromDaemon(data string) {

View File

@@ -2,11 +2,12 @@ package server
import ( import (
"fmt" "fmt"
"github.com/pkg/errors" "strconv"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment"
"sync" "sync"
"time" "time"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment"
) )
type CrashHandler struct { type CrashHandler struct {
@@ -35,7 +36,7 @@ func (cd *CrashHandler) SetLastCrash(t time.Time) {
// if it was the result of an event that we should try to recover from. // if it was the result of an event that we should try to recover from.
// //
// This function assumes it is called under circumstances where a crash is suspected // This function assumes it is called under circumstances where a crash is suspected
// of occuring. It will not do anything to determine if it was actually a crash, just // of occurring. It will not do anything to determine if it was actually a crash, just
// look at the exit state and check if it meets the criteria of being called a crash // look at the exit state and check if it meets the criteria of being called a crash
// by Wings. // by Wings.
// //
@@ -45,11 +46,10 @@ func (s *Server) handleServerCrash() error {
// No point in doing anything here if the server isn't currently offline, there // No point in doing anything here if the server isn't currently offline, there
// is no reason to do a crash detection event. If the server crash detection is // is no reason to do a crash detection event. If the server crash detection is
// disabled we want to skip anything after this as well. // disabled we want to skip anything after this as well.
if s.GetState() != environment.ProcessOfflineState || !s.Config().CrashDetectionEnabled { if s.Environment.State() != environment.ProcessOfflineState || !s.Config().CrashDetectionEnabled {
if !s.Config().CrashDetectionEnabled { if !s.Config().CrashDetectionEnabled {
s.Log().Debug("server triggered crash detection but handler is disabled for server process") s.Log().Debug("server triggered crash detection but handler is disabled for server process")
s.PublishConsoleOutputFromDaemon("Aborting automatic restart, crash detection is disabled for this instance.")
s.PublishConsoleOutputFromDaemon("Server detected as crashed; crash detection is disabled for this instance.")
} }
return nil return nil
@@ -57,14 +57,13 @@ func (s *Server) handleServerCrash() error {
exitCode, oomKilled, err := s.Environment.ExitState() exitCode, oomKilled, err := s.Environment.ExitState()
if err != nil { if err != nil {
return errors.WithStack(err) return err
} }
// If the system is not configured to detect a clean exit code as a crash, and the // If the system is not configured to detect a clean exit code as a crash, and the
// crash is not the result of the program running out of memory, do nothing. // crash is not the result of the program running out of memory, do nothing.
if exitCode == 0 && !oomKilled && !config.Get().System.DetectCleanExitAsCrash { if exitCode == 0 && !oomKilled && !config.Get().System.CrashDetection.DetectCleanExitAsCrash {
s.Log().Debug("server exited with successful exit code; system is configured to not detect this as a crash") s.Log().Debug("server exited with successful exit code; system is configured to not detect this as a crash")
return nil return nil
} }
@@ -73,15 +72,18 @@ func (s *Server) handleServerCrash() error {
s.PublishConsoleOutputFromDaemon(fmt.Sprintf("Out of memory: %t", oomKilled)) s.PublishConsoleOutputFromDaemon(fmt.Sprintf("Out of memory: %t", oomKilled))
c := s.crasher.LastCrashTime() c := s.crasher.LastCrashTime()
// If the last crash time was within the last 60 seconds we do not want to perform timeout := config.Get().System.CrashDetection.Timeout
// an automatic reboot of the process. Return an error that can be handled.
if !c.IsZero() && c.Add(time.Second * 60).After(time.Now()) {
s.PublishConsoleOutputFromDaemon("Aborting automatic reboot: last crash occurred less than 60 seconds ago.")
// If the last crash time was within the last `timeout` seconds we do not want to perform
// an automatic reboot of the process. Return an error that can be handled.
//
// If timeout is set to 0, always reboot the server (this is probably a terrible idea, but some people want it)
if timeout != 0 && !c.IsZero() && c.Add(time.Second*time.Duration(config.Get().System.CrashDetection.Timeout)).After(time.Now()) {
s.PublishConsoleOutputFromDaemon("Aborting automatic restart, last crash occurred less than " + strconv.Itoa(timeout) + " seconds ago.")
return &crashTooFrequent{} return &crashTooFrequent{}
} }
s.crasher.SetLastCrash(time.Now()) s.crasher.SetLastCrash(time.Now())
return s.HandlePowerAction(PowerActionStart) return s.HandlePowerAction(PowerActionStart)
} }

View File

@@ -1,17 +1,15 @@
package server package server
type suspendedError struct { import (
} "emperror.dev/errors"
)
func (e *suspendedError) Error() string { var (
return "server is currently in a suspended state" ErrIsRunning = errors.New("server is running")
} ErrSuspended = errors.New("server is currently in a suspended state")
ErrServerIsInstalling = errors.New("server is currently installing")
func IsSuspendedError(err error) bool { ErrServerIsTransferring = errors.New("server is currently being transferred")
_, ok := err.(*suspendedError) )
return ok
}
type crashTooFrequent struct { type crashTooFrequent struct {
} }
@@ -37,4 +35,4 @@ func IsServerDoesNotExistError(err error) bool {
_, ok := err.(*serverDoesNotExist) _, ok := err.(*serverDoesNotExist)
return ok return ok
} }

View File

@@ -15,6 +15,8 @@ const (
StatusEvent = "status" StatusEvent = "status"
StatsEvent = "stats" StatsEvent = "stats"
BackupCompletedEvent = "backup completed" BackupCompletedEvent = "backup completed"
TransferLogsEvent = "transfer logs"
TransferStatusEvent = "transfer status"
) )
// Returns the server's emitter instance. // Returns the server's emitter instance.

View File

@@ -1,834 +1,30 @@
package server package server
import ( import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/gabriel-vasile/mimetype"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/server/backup"
ignore "github.com/sabhiram/go-gitignore"
"golang.org/x/sync/errgroup"
"io"
"io/ioutil"
"os" "os"
"path"
"path/filepath" "github.com/pterodactyl/wings/server/filesystem"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
) )
// Error returned when there is a bad path provided to one of the FS calls. func (s *Server) Filesystem() *filesystem.Filesystem {
type PathResolutionError struct{} return s.fs
// Returns the error response in a string form that can be more easily consumed.
func (pre PathResolutionError) Error() string {
return "invalid path resolution"
}
func IsPathResolutionError(err error) bool {
_, ok := err.(PathResolutionError)
return ok
}
type Filesystem struct {
mu sync.RWMutex
lastLookupTime time.Time
diskUsage int64
Server *Server
}
// Returns the root path that contains all of a server's data.
func (fs *Filesystem) Path() string {
return filepath.Join(config.Get().System.Data, fs.Server.Id())
}
// Normalizes a directory being passed in to ensure the user is not able to escape
// from their data directory. After normalization if the directory is still within their home
// path it is returned. If they managed to "escape" an error will be returned.
//
// This logic is actually copied over from the SFTP server code. Ideally that eventually
// either gets ported into this application, or is able to make use of this package.
func (fs *Filesystem) SafePath(p string) (string, error) {
var nonExistentPathResolution string
// Start with a cleaned up path before checking the more complex bits.
r := fs.unsafeFilePath(p)
// At the same time, evaluate the symlink status and determine where this file or folder
// is truly pointing to.
p, err := filepath.EvalSymlinks(r)
if err != nil && !os.IsNotExist(err) {
return "", err
} else if os.IsNotExist(err) {
// The requested directory doesn't exist, so at this point we need to iterate up the
// path chain until we hit a directory that _does_ exist and can be validated.
parts := strings.Split(filepath.Dir(r), "/")
var try string
// Range over all of the path parts and form directory pathings from the end
// moving up until we have a valid resolution or we run out of paths to try.
for k := range parts {
try = strings.Join(parts[:(len(parts)-k)], "/")
if !fs.unsafeIsInDataDirectory(try) {
break
}
t, err := filepath.EvalSymlinks(try)
if err == nil {
nonExistentPathResolution = t
break
}
}
}
// If the new path doesn't start with their root directory there is clearly an escape
// attempt going on, and we should NOT resolve this path for them.
if nonExistentPathResolution != "" {
if !fs.unsafeIsInDataDirectory(nonExistentPathResolution) {
return "", PathResolutionError{}
}
// If the nonExistentPathResolution variable is not empty then the initial path requested
// did not exist and we looped through the pathway until we found a match. At this point
// we've confirmed the first matched pathway exists in the root server directory, so we
// can go ahead and just return the path that was requested initially.
return r, nil
}
// If the requested directory from EvalSymlinks begins with the server root directory go
// ahead and return it. If not we'll return an error which will block any further action
// on the file.
if fs.unsafeIsInDataDirectory(p) {
return p, nil
}
return "", PathResolutionError{}
}
// Generate a path to the file by cleaning it up and appending the root server path to it. This
// DOES NOT guarantee that the file resolves within the server data directory. You'll want to use
// the fs.unsafeIsInDataDirectory(p) function to confirm.
func (fs *Filesystem) unsafeFilePath(p string) string {
// Calling filepath.Clean on the joined directory will resolve it to the absolute path,
// removing any ../ type of resolution arguments, and leaving us with a direct path link.
//
// This will also trim the existing root path off the beginning of the path passed to
// the function since that can get a bit messy.
return filepath.Clean(filepath.Join(fs.Path(), strings.TrimPrefix(p, fs.Path())))
}
// Check that that path string starts with the server data directory path. This function DOES NOT
// validate that the rest of the path does not end up resolving out of this directory, or that the
// targeted file or folder is not a symlink doing the same thing.
func (fs *Filesystem) unsafeIsInDataDirectory(p string) bool {
return strings.HasPrefix(strings.TrimSuffix(p, "/")+"/", strings.TrimSuffix(fs.Path(), "/")+"/")
}
// Helper function to keep some of the codebase a little cleaner. Returns a "safe" version of the path
// joined with a file. This is important because you cannot just assume that appending a file to a cleaned
// path will result in a cleaned path to that file. For example, imagine you have the following scenario:
//
// my_bad_file -> symlink:/etc/passwd
//
// cleaned := SafePath("../../etc") -> "/"
// filepath.Join(cleaned, my_bad_file) -> "/my_bad_file"
//
// You might think that "/my_bad_file" is fine since it isn't pointing to the original "../../etc/my_bad_file".
// However, this doesn't account for symlinks where the file might be pointing outside of the directory, so
// calling a function such as Chown against it would chown the symlinked location, and not the file within the
// Wings daemon.
func (fs *Filesystem) SafeJoin(dir string, f os.FileInfo) (string, error) {
if f.Mode()&os.ModeSymlink != 0 {
return fs.SafePath(filepath.Join(dir, f.Name()))
}
return filepath.Join(dir, f.Name()), nil
}
// Executes the fs.SafePath function in parallel against an array of paths. If any of the calls
// fails an error will be returned.
func (fs *Filesystem) ParallelSafePath(paths []string) ([]string, error) {
var cleaned []string
// Simple locker function to avoid racy appends to the array of cleaned paths.
var m = new(sync.Mutex)
var push = func(c string) {
m.Lock()
cleaned = append(cleaned, c)
m.Unlock()
}
// Create an error group that we can use to run processes in parallel while retaining
// the ability to cancel the entire process immediately should any of it fail.
g, ctx := errgroup.WithContext(context.Background())
// Iterate over all of the paths and generate a cleaned path, if there is an error for any
// of the files, abort the process.
for _, p := range paths {
// Create copy so we can use it within the goroutine correctly.
pi := p
// Recursively call this function to continue digging through the directory tree within
// a seperate goroutine. If the context is canceled abort this process.
g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
default:
// If the callback returns true, go ahead and keep walking deeper. This allows
// us to programatically continue deeper into directories, or stop digging
// if that pathway knows it needs nothing else.
if c, err := fs.SafePath(pi); err != nil {
return err
} else {
push(c)
}
return nil
}
})
}
// Block until all of the routines finish and have returned a value.
return cleaned, g.Wait()
}
// Determines if the directory a file is trying to be added to has enough space available
// for the file to be written to.
//
// Because determining the amount of space being used by a server is a taxing operation we
// will load it all up into a cache and pull from that as long as the key is not expired.
func (fs *Filesystem) HasSpaceAvailable() bool {
space := fs.Server.Build().DiskSpace
size, err := fs.getCachedDiskUsage()
if err != nil {
fs.Server.Log().WithField("error", err).Warn("failed to determine root server directory size")
}
// Determine if their folder size, in bytes, is smaller than the amount of space they've
// been allocated.
fs.Server.Proc().SetDisk(size)
// If space is -1 or 0 just return true, means they're allowed unlimited.
//
// Technically we could skip disk space calculation because we don't need to check if the server exceeds it's limit
// but because this method caches the disk usage it would be best to calculate the disk usage and always
// return true.
if space <= 0 {
return true
}
return (size / 1000.0 / 1000.0) <= space
}
// Internal helper function to allow other parts of the codebase to check the total used disk space
// as needed without overly taxing the system. This will prioritize the value from the cache to avoid
// excessive IO usage. We will only walk the filesystem and determine the size of the directory if there
// is no longer a cached value.
func (fs *Filesystem) getCachedDiskUsage() (int64, error) {
// Obtain an exclusive lock on this process so that we don't unintentionally run it at the same
// time as another running process. Once the lock is available it'll read from the cache for the
// second call rather than hitting the disk in parallel.
//
// This effectively the same speed as running this call in parallel since this cache will return
// instantly on the second call.
fs.mu.Lock()
defer fs.mu.Unlock()
if fs.lastLookupTime.After(time.Now().Add(time.Second * -60)) {
return fs.diskUsage, nil
}
// If there is no size its either because there is no data (in which case running this function
// will have effectively no impact), or there is nothing in the cache, in which case we need to
// grab the size of their data directory. This is a taxing operation, so we want to store it in
// the cache once we've gotten it.
size, err := fs.DirectorySize("/")
// Always cache the size, even if there is an error. We want to always return that value
// so that we don't cause an endless loop of determining the disk size if there is a temporary
// error encountered.
fs.lastLookupTime = time.Now()
atomic.StoreInt64(&fs.diskUsage, size)
return size, err
}
// Determines the directory size of a given location by running parallel tasks to iterate
// through all of the folders. Returns the size in bytes. This can be a fairly taxing operation
// on locations with tons of files, so it is recommended that you cache the output.
func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
var size int64
err := fs.Walk(dir, func(_ string, f os.FileInfo, err error) error {
if err != nil {
return fs.handleWalkerError(err, f)
}
if !f.IsDir() {
atomic.AddInt64(&size, f.Size())
}
return nil
})
return size, err
}
// Reads a file on the system and returns it as a byte representation in a file
// reader. This is not the most memory efficient usage since it will be reading the
// entirety of the file into memory.
func (fs *Filesystem) Readfile(p string) (io.Reader, error) {
cleaned, err := fs.SafePath(p)
if err != nil {
return nil, err
}
b, err := ioutil.ReadFile(cleaned)
if err != nil {
return nil, err
}
return bytes.NewReader(b), nil
}
// Writes a file to the system. If the file does not already exist one will be created.
func (fs *Filesystem) Writefile(p string, r io.Reader) error {
cleaned, err := fs.SafePath(p)
if err != nil {
return errors.WithStack(err)
}
var currentSize int64
// If the file does not exist on the system already go ahead and create the pathway
// to it and an empty file. We'll then write to it later on after this completes.
if stat, err := os.Stat(cleaned); err != nil {
if !os.IsNotExist(err) {
return errors.WithStack(err)
}
if err := os.MkdirAll(filepath.Dir(cleaned), 0755); err != nil {
return errors.WithStack(err)
}
if err := fs.Chown(filepath.Dir(cleaned)); err != nil {
return errors.WithStack(err)
}
} else {
if stat.IsDir() {
return errors.New("cannot write file contents to a directory")
}
currentSize = stat.Size()
}
// This will either create the file if it does not already exist, or open and
// truncate the existing file.
file, err := os.OpenFile(cleaned, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return errors.WithStack(err)
}
defer file.Close()
buf := make([]byte, 1024*4)
sz, err := io.CopyBuffer(file, r, buf)
// Adjust the disk usage to account for the old size and the new size of the file.
atomic.AddInt64(&fs.diskUsage, sz-currentSize)
// Finally, chown the file to ensure the permissions don't end up out-of-whack
// if we had just created it.
return fs.Chown(cleaned)
}
// Defines the stat struct object.
type Stat struct {
Info os.FileInfo
Mimetype string
}
func (s *Stat) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Name string `json:"name"`
Created string `json:"created"`
Modified string `json:"modified"`
Mode string `json:"mode"`
Size int64 `json:"size"`
Directory bool `json:"directory"`
File bool `json:"file"`
Symlink bool `json:"symlink"`
Mime string `json:"mime"`
}{
Name: s.Info.Name(),
Created: s.CTime().Format(time.RFC3339),
Modified: s.Info.ModTime().Format(time.RFC3339),
Mode: s.Info.Mode().String(),
Size: s.Info.Size(),
Directory: s.Info.IsDir(),
File: !s.Info.IsDir(),
Symlink: s.Info.Mode().Perm()&os.ModeSymlink != 0,
Mime: s.Mimetype,
})
}
// Stats a file or folder and returns the base stat object from go along with the
// MIME data that can be used for editing files.
func (fs *Filesystem) Stat(p string) (*Stat, error) {
cleaned, err := fs.SafePath(p)
if err != nil {
return nil, err
}
return fs.unsafeStat(cleaned)
}
func (fs *Filesystem) unsafeStat(p string) (*Stat, error) {
s, err := os.Stat(p)
if err != nil {
return nil, err
}
var m *mimetype.MIME
if !s.IsDir() {
m, err = mimetype.DetectFile(p)
if err != nil {
return nil, err
}
}
st := &Stat{
Info: s,
Mimetype: "inode/directory",
}
if m != nil {
st.Mimetype = m.String()
}
return st, nil
}
// Creates a new directory (name) at a specified path (p) for the server.
func (fs *Filesystem) CreateDirectory(name string, p string) error {
cleaned, err := fs.SafePath(path.Join(p, name))
if err != nil {
return errors.WithStack(err)
}
return os.MkdirAll(cleaned, 0755)
}
// Moves (or renames) a file or directory.
func (fs *Filesystem) Rename(from string, to string) error {
cleanedFrom, err := fs.SafePath(from)
if err != nil {
return errors.WithStack(err)
}
cleanedTo, err := fs.SafePath(to)
if err != nil {
return errors.WithStack(err)
}
if f, err := os.Stat(cleanedFrom); err != nil {
return errors.WithStack(err)
} else {
d := cleanedTo
if !f.IsDir() {
d = strings.TrimSuffix(d, path.Base(cleanedTo))
}
// Ensure that the directory we're moving into exists correctly on the system.
if mkerr := os.MkdirAll(d, 0644); mkerr != nil {
return errors.WithStack(mkerr)
}
}
return os.Rename(cleanedFrom, cleanedTo)
}
// Recursively iterates over a file or directory and sets the permissions on all of the
// underlying files. Iterate over all of the files and directories. If it is a file just
// go ahead and perform the chown operation. Otherwise dig deeper into the directory until
// we've run out of directories to dig into.
func (fs *Filesystem) Chown(path string) error {
cleaned, err := fs.SafePath(path)
if err != nil {
return errors.WithStack(err)
}
uid := config.Get().System.User.Uid
gid := config.Get().System.User.Gid
// Start by just chowning the initial path that we received.
if err := os.Chown(cleaned, uid, gid); err != nil {
return errors.WithStack(err)
}
// If this is not a directory we can now return from the function, there is nothing
// left that we need to do.
if st, _ := os.Stat(cleaned); !st.IsDir() {
return nil
}
// If this was a directory, begin walking over its contents recursively and ensure that all
// of the subfiles and directories get their permissions updated as well.
return fs.Walk(cleaned, func(path string, f os.FileInfo, err error) error {
if err != nil {
return fs.handleWalkerError(err, f)
}
// Do not attempt to chmod a symlink. Go's os.Chown function will affect the symlink
// so if it points to a location outside the data directory the user would be able to
// (un)intentionally modify that files permissions.
if f.Mode()&os.ModeSymlink != 0 {
return nil
}
return os.Chown(path, uid, gid)
})
}
// Copies a given file to the same location and appends a suffix to the file to indicate that
// it has been copied.
//
// @todo need to get an exclusive lock on the file.
func (fs *Filesystem) Copy(p string) error {
cleaned, err := fs.SafePath(p)
if err != nil {
return errors.WithStack(err)
}
if s, err := os.Stat(cleaned); err != nil {
return errors.WithStack(err)
} else if s.IsDir() || !s.Mode().IsRegular() {
// If this is a directory or not a regular file, just throw a not-exist error
// since anything calling this function should understand what that means.
return os.ErrNotExist
}
base := filepath.Base(cleaned)
relative := strings.TrimSuffix(strings.TrimPrefix(cleaned, fs.Path()), base)
extension := filepath.Ext(base)
name := strings.TrimSuffix(base, filepath.Ext(base))
// Begin looping up to 50 times to try and create a unique copy file name. This will take
// an input of "file.txt" and generate "file copy.txt". If that name is already taken, it will
// then try to write "file copy 2.txt" and so on, until reaching 50 loops. At that point we
// won't waste anymore time, just use the current timestamp and make that copy.
//
// Could probably make this more efficient by checking if there are any files matching the copy
// pattern, and trying to find the highest number and then incrementing it by one rather than
// looping endlessly.
var i int
copySuffix := " copy"
for i = 0; i < 51; i++ {
if i > 0 {
copySuffix = " copy " + strconv.Itoa(i)
}
tryName := fmt.Sprintf("%s%s%s", name, copySuffix, extension)
tryLocation, err := fs.SafePath(path.Join(relative, tryName))
if err != nil {
return errors.WithStack(err)
}
// If the file exists, continue to the next loop, otherwise we're good to start a copy.
if _, err := os.Stat(tryLocation); err != nil && !os.IsNotExist(err) {
return errors.WithStack(err)
} else if os.IsNotExist(err) {
break
}
if i == 50 {
copySuffix = "." + time.Now().Format(time.RFC3339)
}
}
finalPath, err := fs.SafePath(path.Join(relative, fmt.Sprintf("%s%s%s", name, copySuffix, extension)))
if err != nil {
return errors.WithStack(err)
}
source, err := os.Open(cleaned)
if err != nil {
return errors.WithStack(err)
}
defer source.Close()
dest, err := os.Create(finalPath)
if err != nil {
return errors.WithStack(err)
}
defer dest.Close()
if _, err := io.Copy(dest, source); err != nil {
return errors.WithStack(err)
}
return nil
}
// Deletes a file or folder from the system. Prevents the user from accidentally
// (or maliciously) removing their root server data directory.
func (fs *Filesystem) Delete(p string) error {
// This is one of the few (only?) places in the codebase where we're explictly not using
// the SafePath functionality when working with user provided input. If we did, you would
// not be able to delete a file that is a symlink pointing to a location outside of the data
// directory.
//
// We also want to avoid resolving a symlink that points _within_ the data directory and thus
// deleting the actual source file for the symlink rather than the symlink itself. For these
// purposes just resolve the actual file path using filepath.Join() and confirm that the path
// exists within the data directory.
resolved := fs.unsafeFilePath(p)
if !fs.unsafeIsInDataDirectory(resolved) {
return PathResolutionError{}
}
// Block any whoopsies.
if resolved == fs.Path() {
return errors.New("cannot delete root server directory")
}
if st, err := os.Stat(resolved); err != nil {
if !os.IsNotExist(err) {
fs.Server.Log().WithField("error", err).WithField("path", resolved).Warn("error while attempting to stat file before deletion")
}
} else {
if !st.IsDir() {
atomic.SwapInt64(&fs.diskUsage, -st.Size())
} else {
go func(st os.FileInfo, resolved string) {
if s, err := fs.DirectorySize(resolved); err == nil {
atomic.AddInt64(&fs.diskUsage, -s)
}
}(st, resolved)
}
}
return os.RemoveAll(resolved)
}
// Lists the contents of a given directory and returns stat information about each
// file and folder within it.
func (fs *Filesystem) ListDirectory(p string) ([]*Stat, error) {
cleaned, err := fs.SafePath(p)
if err != nil {
return nil, err
}
files, err := ioutil.ReadDir(cleaned)
if err != nil {
return nil, err
}
var wg sync.WaitGroup
// You must initialize the output of this directory as a non-nil value otherwise
// when it is marshaled into a JSON object you'll just get 'null' back, which will
// break the panel badly.
out := make([]*Stat, len(files))
// Iterate over all of the files and directories returned and perform an async process
// to get the mime-type for them all.
for i, file := range files {
wg.Add(1)
go func(idx int, f os.FileInfo) {
defer wg.Done()
var m *mimetype.MIME
var d = "inode/directory"
if !f.IsDir() {
cleanedp, _ := fs.SafeJoin(cleaned, f)
if cleanedp != "" {
m, _ = mimetype.DetectFile(filepath.Join(cleaned, f.Name()))
} else {
// Just pass this for an unknown type because the file could not safely be resolved within
// the server data path.
d = "application/octet-stream"
}
}
st := &Stat{
Info: f,
Mimetype: d,
}
if m != nil {
st.Mimetype = m.String()
}
out[idx] = st
}(i, file)
}
wg.Wait()
// Sort the output alphabetically to begin with since we've run the output
// through an asynchronous process and the order is gonna be very random.
sort.SliceStable(out, func(i, j int) bool {
if out[i].Info.Name() == out[j].Info.Name() || out[i].Info.Name() > out[j].Info.Name() {
return true
}
return false
})
// Then, sort it so that directories are listed first in the output. Everything
// will continue to be alphabetized at this point.
sort.SliceStable(out, func(i, j int) bool {
return out[i].Info.IsDir()
})
return out, nil
} }
// Ensures that the data directory for the server instance exists. // Ensures that the data directory for the server instance exists.
func (fs *Filesystem) EnsureDataDirectory() error { func (s *Server) EnsureDataDirectoryExists() error {
if _, err := os.Stat(fs.Path()); err != nil && !os.IsNotExist(err) { if _, err := os.Stat(s.fs.Path()); err != nil && !os.IsNotExist(err) {
return errors.WithStack(err) return err
} else if err != nil { } else if err != nil {
// Create the server data directory because it does not currently exist // Create the server data directory because it does not currently exist
// on the system. // on the system.
if err := os.MkdirAll(fs.Path(), 0700); err != nil { if err := os.MkdirAll(s.fs.Path(), 0700); err != nil {
return errors.WithStack(err) return err
}
if err := s.fs.Chown("/"); err != nil {
s.Log().WithField("error", err).Warn("failed to chown server data directory")
} }
} }
return nil return nil
} }
// Given a directory, iterate through all of the files and folders within it and determine
// if they should be included in the output based on an array of ignored matches. This uses
// standard .gitignore formatting to make that determination.
//
// If no ignored files are passed through you'll get the entire directory listing.
func (fs *Filesystem) GetIncludedFiles(dir string, ignored []string) (*backup.IncludedFiles, error) {
cleaned, err := fs.SafePath(dir)
if err != nil {
return nil, err
}
i, err := ignore.CompileIgnoreLines(ignored...)
if err != nil {
return nil, err
}
// Walk through all of the files and directories on a server. This callback only returns
// files found, and will keep walking deeper and deeper into directories.
inc := new(backup.IncludedFiles)
if err := fs.Walk(cleaned, func(p string, f os.FileInfo, err error) error {
if err != nil {
return fs.handleWalkerError(err, f)
}
// Avoid unnecessary parsing if there are no ignored files, nothing will match anyways
// so no reason to call the function.
if len(ignored) == 0 || !i.MatchesPath(strings.TrimPrefix(p, fs.Path()+"/")) {
inc.Push(&f, p)
}
// We can't just abort if the path is technically ignored. It is possible there is a nested
// file or folder that should not be excluded, so in this case we need to just keep going
// until we get to a final state.
return nil
}); err != nil {
return nil, err
}
return inc, nil
}
// Compresses all of the files matching the given paths in the specified directory. This function
// also supports passing nested paths to only compress certain files and folders when working in
// a larger directory. This effectively creates a local backup, but rather than ignoring specific
// files and folders, it takes an allow-list of files and folders.
//
// All paths are relative to the dir that is passed in as the first argument, and the compressed
// file will be placed at that location named `archive-{date}.tar.gz`.
func (fs *Filesystem) CompressFiles(dir string, paths []string) (os.FileInfo, error) {
cleanedRootDir, err := fs.SafePath(dir)
if err != nil {
return nil, err
}
// Take all of the paths passed in and merge them together with the root directory we've gotten.
for i, p := range paths {
paths[i] = filepath.Join(cleanedRootDir, p)
}
cleaned, err := fs.ParallelSafePath(paths)
if err != nil {
return nil, err
}
inc := new(backup.IncludedFiles)
// Iterate over all of the cleaned paths and merge them into a large object of final file
// paths to pass into the archiver. As directories are encountered this will drop into them
// and look for all of the files.
for _, p := range cleaned {
f, err := os.Stat(p)
if err != nil {
fs.Server.Log().WithField("error", err).WithField("path", p).Debug("failed to stat file or directory for compression")
continue
}
if f.IsDir() {
err := fs.Walk(p, func(s string, info os.FileInfo, err error) error {
if err != nil {
return fs.handleWalkerError(err, info)
}
if !info.IsDir() {
inc.Push(&info, s)
}
return nil
})
if err != nil {
return nil, err
}
} else {
inc.Push(&f, p)
}
}
a := &backup.Archive{TrimPrefix: fs.Path(), Files: inc}
d := path.Join(cleanedRootDir, fmt.Sprintf("archive-%s.tar.gz", strings.ReplaceAll(time.Now().Format(time.RFC3339), ":", "")))
return a.Create(d, context.Background())
}
// Handle errors encountered when walking through directories.
//
// If there is a path resolution error just skip the item entirely. Only return this for a
// directory, otherwise return nil. Returning this error for a file will stop the walking
// for the remainder of the directory. This is assuming an os.FileInfo struct was even returned.
func (fs *Filesystem) handleWalkerError(err error, f os.FileInfo) error {
if !IsPathResolutionError(err) {
return err
}
if f != nil && f.IsDir() {
return filepath.SkipDir
}
return nil
}

View File

@@ -0,0 +1,224 @@
package filesystem
import (
"archive/tar"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/juju/ratelimit"
"github.com/karrick/godirwalk"
"github.com/klauspost/pgzip"
"github.com/pterodactyl/wings/config"
"github.com/sabhiram/go-gitignore"
"io"
"os"
"path/filepath"
"strings"
"sync"
)
const memory = 4 * 1024
var pool = sync.Pool{
New: func() interface{} {
b := make([]byte, memory)
return b
},
}
type Archive struct {
// BasePath is the absolute path to create the archive from where Files and Ignore are
// relative to.
BasePath string
// Ignore is a gitignore string (most likely read from a file) of files to ignore
// from the archive.
Ignore string
// Files specifies the files to archive, this takes priority over the Ignore option, if
// unspecified, all files in the BasePath will be archived unless Ignore is set.
Files []string
}
// Creates an archive at dst with all of the files defined in the included files struct.
func (a *Archive) Create(dst string) error {
f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer f.Close()
// Select a writer based off of the WriteLimit configuration option. If there is no
// write limit, use the file as the writer.
var writer io.Writer
if writeLimit := int64(config.Get().System.Backups.WriteLimit * 1024 * 1024); writeLimit > 0 {
// Token bucket with a capacity of "writeLimit" MiB, adding "writeLimit" MiB/s
// and then wrap the file writer with the token bucket limiter.
writer = ratelimit.Writer(f, ratelimit.NewBucketWithRate(float64(writeLimit), writeLimit))
} else {
writer = f
}
// Create a new gzip writer around the file.
gw, _ := pgzip.NewWriterLevel(writer, pgzip.BestSpeed)
_ = gw.SetConcurrency(1<<20, 1)
defer gw.Close()
// Create a new tar writer around the gzip writer.
tw := tar.NewWriter(gw)
defer tw.Close()
// Configure godirwalk.
options := &godirwalk.Options{
FollowSymbolicLinks: false,
Unsorted: true,
Callback: a.callback(tw),
}
// If we're specifically looking for only certain files, or have requested
// that certain files be ignored we'll update the callback function to reflect
// that request.
if len(a.Files) == 0 && len(a.Ignore) > 0 {
i := ignore.CompileIgnoreLines(strings.Split(a.Ignore, "\n")...)
options.Callback = a.callback(tw, func(_ string, rp string) error {
if i.MatchesPath(rp) {
return godirwalk.SkipThis
}
return nil
})
} else if len(a.Files) > 0 {
options.Callback = a.withFilesCallback(tw)
}
// Recursively walk the path we are archiving.
return godirwalk.Walk(a.BasePath, options)
}
// Callback function used to determine if a given file should be included in the archive
// being generated.
func (a *Archive) callback(tw *tar.Writer, opts ...func(path string, relative string) error) func(path string, de *godirwalk.Dirent) error {
return func(path string, de *godirwalk.Dirent) error {
// Skip directories because we walking them recursively.
if de.IsDir() {
return nil
}
relative := filepath.ToSlash(strings.TrimPrefix(path, a.BasePath+string(filepath.Separator)))
// Call the additional options passed to this callback function. If any of them return
// a non-nil error we will exit immediately.
for _, opt := range opts {
if err := opt(path, relative); err != nil {
return err
}
}
// Add the file to the archive, if it is nested in a directory,
// the directory will be automatically "created" in the archive.
return a.addToArchive(path, relative, tw)
}
}
// Pushes only files defined in the Files key to the final archive.
func (a *Archive) withFilesCallback(tw *tar.Writer) func(path string, de *godirwalk.Dirent) error {
return a.callback(tw, func(p string, rp string) error {
for _, f := range a.Files {
// If the given doesn't match, or doesn't have the same prefix continue
// to the next item in the loop.
if p != f && !strings.HasPrefix(p, f) {
continue
}
// Once we have a match return a nil value here so that the loop stops and the
// call to this function will correctly include the file in the archive. If there
// are no matches we'll never make it to this line, and the final error returned
// will be the godirwalk.SkipThis error.
return nil
}
return godirwalk.SkipThis
})
}
// Adds a given file path to the final archive being created.
func (a *Archive) addToArchive(p string, rp string, w *tar.Writer) error {
// Lstat the file, this will give us the same information as Stat except that it will not
// follow a symlink to it's target automatically. This is important to avoid including
// files that exist outside the server root unintentionally in the backup.
s, err := os.Lstat(p)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return errors.WrapIff(err, "failed executing os.Lstat on '%s'", rp)
}
// Resolve the symlink target if the file is a symlink.
var target string
if s.Mode()&os.ModeSymlink != 0 {
// Read the target of the symlink. If there are any errors we will dump them out to
// the logs, but we're not going to stop the backup. There are far too many cases of
// symlinks causing all sorts of unnecessary pain in this process. Sucks to suck if
// it doesn't work.
target, err = os.Readlink(s.Name())
if err != nil {
// Ignore the not exist errors specifically, since theres nothing important about that.
if !os.IsNotExist(err) {
log.WithField("path", rp).WithField("readlink_err", err.Error()).Warn("failed reading symlink for target path; skipping...")
}
return nil
}
}
// Get the tar FileInfoHeader in order to add the file to the archive.
header, err := tar.FileInfoHeader(s, filepath.ToSlash(target))
if err != nil {
return errors.WrapIff(err, "failed to get tar#FileInfoHeader for '%s'", rp)
}
// Fix the header name if the file is not a symlink.
if s.Mode()&os.ModeSymlink == 0 {
header.Name = rp
}
// Write the tar FileInfoHeader to the archive.
if err := w.WriteHeader(header); err != nil {
return errors.WrapIff(err, "failed to write tar#FileInfoHeader for '%s'", rp)
}
// If the size of the file is less than 1 (most likely for symlinks), skip writing the file.
if header.Size < 1 {
return nil
}
// If the buffer size is larger than the file size, create a smaller buffer to hold the file.
var buf []byte
if header.Size < memory {
buf = make([]byte, header.Size)
} else {
// Get a fixed-size buffer from the pool to save on allocations.
buf = pool.Get().([]byte)
defer func() {
buf = make([]byte, memory)
pool.Put(buf)
}()
}
// Open the file.
f, err := os.Open(p)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return errors.WrapIff(err, "failed to open '%s' for copying", header.Name)
}
defer f.Close()
// Copy the file's contents to the archive using our buffer.
if _, err := io.CopyBuffer(w, io.LimitReader(f, header.Size), buf); err != nil {
return errors.WrapIff(err, "failed to copy '%s' to archive", header.Name)
}
return nil
}

View File

@@ -0,0 +1,59 @@
package filesystem
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"time"
)
// Compresses all of the files matching the given paths in the specified directory. This function
// also supports passing nested paths to only compress certain files and folders when working in
// a larger directory. This effectively creates a local backup, but rather than ignoring specific
// files and folders, it takes an allow-list of files and folders.
//
// All paths are relative to the dir that is passed in as the first argument, and the compressed
// file will be placed at that location named `archive-{date}.tar.gz`.
func (fs *Filesystem) CompressFiles(dir string, paths []string) (os.FileInfo, error) {
cleanedRootDir, err := fs.SafePath(dir)
if err != nil {
return nil, err
}
// Take all of the paths passed in and merge them together with the root directory we've gotten.
for i, p := range paths {
paths[i] = filepath.Join(cleanedRootDir, p)
}
cleaned, err := fs.ParallelSafePath(paths)
if err != nil {
return nil, err
}
a := &Archive{BasePath: cleanedRootDir, Files: cleaned}
d := path.Join(
cleanedRootDir,
fmt.Sprintf("archive-%s.tar.gz", strings.ReplaceAll(time.Now().Format(time.RFC3339), ":", "")),
)
if err := a.Create(d); err != nil {
return nil, err
}
f, err := os.Stat(d)
if err != nil {
_ = os.Remove(d)
return nil, err
}
if err := fs.HasSpaceFor(f.Size()); err != nil {
_ = os.Remove(d)
return nil, err
}
fs.addDisk(f.Size())
return f, nil
}

View File

@@ -1,16 +1,16 @@
package server package filesystem
import ( import (
"archive/tar" "archive/tar"
"archive/zip" "archive/zip"
"compress/gzip" "compress/gzip"
"emperror.dev/errors"
"fmt" "fmt"
"github.com/mholt/archiver/v3" "github.com/mholt/archiver/v3"
"github.com/pkg/errors"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"sync" "strings"
"sync/atomic" "sync/atomic"
) )
@@ -19,7 +19,7 @@ import (
func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) (bool, error) { func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) (bool, error) {
// Don't waste time trying to determine this if we know the server will have the space for // Don't waste time trying to determine this if we know the server will have the space for
// it since there is no limit. // it since there is no limit.
if fs.Server.Build().DiskSpace <= 0 { if fs.MaxDisk() <= 0 {
return true, nil return true, nil
} }
@@ -28,37 +28,29 @@ func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) (b
return false, err return false, err
} }
wg := new(sync.WaitGroup)
var dirSize int64
var cErr error
// Get the cached size in a parallel process so that if it is not cached we are not // Get the cached size in a parallel process so that if it is not cached we are not
// waiting an unnecessary amount of time on this call. // waiting an unnecessary amount of time on this call.
go func() { dirSize, err := fs.DiskUsage(false)
wg.Add(1)
defer wg.Done()
dirSize, cErr = fs.getCachedDiskUsage()
}()
var size int64 var size int64
// In a seperate thread, walk over the archive and figure out just how large the final // Walk over the archive and figure out just how large the final output would be from unarchiving it.
// output would be from dearchiving it. err = archiver.Walk(source, func(f archiver.File) error {
go func() { if atomic.AddInt64(&size, f.Size())+dirSize > fs.MaxDisk() {
wg.Add(1) return &Error{code: ErrCodeDiskSpace}
defer wg.Done() }
// Walk all of the files and calculate the total decompressed size of this archive. return nil
archiver.Walk(source, func(f archiver.File) error { })
atomic.AddInt64(&size, f.Size())
return nil if err != nil {
}) if strings.HasPrefix(err.Error(), "format ") {
}() return false, &Error{code: ErrCodeUnknownArchive}
}
wg.Wait() return false, err
}
return ((dirSize + size) / 1000.0 / 1000.0) <= fs.Server.Build().DiskSpace, cErr return true, err
} }
// Decompress a file in a given directory by using the archiver tool to infer the file // Decompress a file in a given directory by using the archiver tool to infer the file
@@ -68,18 +60,18 @@ func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) (b
func (fs *Filesystem) DecompressFile(dir string, file string) error { func (fs *Filesystem) DecompressFile(dir string, file string) error {
source, err := fs.SafePath(filepath.Join(dir, file)) source, err := fs.SafePath(filepath.Join(dir, file))
if err != nil { if err != nil {
return errors.WithStack(err) return err
} }
// Make sure the file exists basically. // Make sure the file exists basically.
if _, err := os.Stat(source); err != nil { if _, err := os.Stat(source); err != nil {
return errors.WithStack(err) return err
} }
// Walk over all of the files spinning up an additional go-routine for each file we've encountered // Walk over all of the files spinning up an additional go-routine for each file we've encountered
// and then extract that file from the archive and write it to the disk. If any part of this process // and then extract that file from the archive and write it to the disk. If any part of this process
// encounters an error the entire process will be stopped. // encounters an error the entire process will be stopped.
return archiver.Walk(source, func(f archiver.File) error { err = archiver.Walk(source, func(f archiver.File) error {
// Don't waste time with directories, we don't need to create them if they have no contents, and // Don't waste time with directories, we don't need to create them if they have no contents, and
// we will ensure the directory exists when opening the file for writing anyways. // we will ensure the directory exists when opening the file for writing anyways.
if f.IsDir() { if f.IsDir() {
@@ -99,6 +91,20 @@ func (fs *Filesystem) DecompressFile(dir string, file string) error {
return errors.New(fmt.Sprintf("could not parse underlying data source with type %s", reflect.TypeOf(s).String())) return errors.New(fmt.Sprintf("could not parse underlying data source with type %s", reflect.TypeOf(s).String()))
} }
return errors.Wrap(fs.Writefile(name, f), "could not extract file from archive") p, err := fs.SafePath(filepath.Join(dir, name))
if err != nil {
return errors.WithMessage(err, "failed to generate a safe path to server file")
}
return errors.WithMessage(fs.Writefile(p, f), "could not extract file from archive")
}) })
if err != nil {
if strings.HasPrefix(err.Error(), "format ") {
return &Error{code: ErrCodeUnknownArchive}
}
return err
}
return nil
} }

View File

@@ -0,0 +1,237 @@
package filesystem
import (
"github.com/apex/log"
"github.com/karrick/godirwalk"
"sync"
"sync/atomic"
"syscall"
"time"
)
type SpaceCheckingOpts struct {
AllowStaleResponse bool
}
type usageLookupTime struct {
sync.RWMutex
value time.Time
}
// Update the last time that a disk space lookup was performed.
func (ult *usageLookupTime) Set(t time.Time) {
ult.Lock()
ult.value = t
ult.Unlock()
}
// Get the last time that we performed a disk space usage lookup.
func (ult *usageLookupTime) Get() time.Time {
ult.RLock()
defer ult.RUnlock()
return ult.value
}
// Returns the maximum amount of disk space that this Filesystem instance is allowed to use.
func (fs *Filesystem) MaxDisk() int64 {
return atomic.LoadInt64(&fs.diskLimit)
}
// Sets the disk space limit for this Filesystem instance.
func (fs *Filesystem) SetDiskLimit(i int64) {
atomic.SwapInt64(&fs.diskLimit, i)
}
// The same concept as HasSpaceAvailable however this will return an error if there is
// no space, rather than a boolean value.
func (fs *Filesystem) HasSpaceErr(allowStaleValue bool) error {
if !fs.HasSpaceAvailable(allowStaleValue) {
return &Error{code: ErrCodeDiskSpace}
}
return nil
}
// Determines if the directory a file is trying to be added to has enough space available
// for the file to be written to.
//
// Because determining the amount of space being used by a server is a taxing operation we
// will load it all up into a cache and pull from that as long as the key is not expired.
//
// This operation will potentially block unless allowStaleValue is set to true. See the
// documentation on DiskUsage for how this affects the call.
func (fs *Filesystem) HasSpaceAvailable(allowStaleValue bool) bool {
size, err := fs.DiskUsage(allowStaleValue)
if err != nil {
log.WithField("root", fs.root).WithField("error", err).Warn("failed to determine root fs directory size")
}
// If space is -1 or 0 just return true, means they're allowed unlimited.
//
// Technically we could skip disk space calculation because we don't need to check if the
// server exceeds it's limit but because this method caches the disk usage it would be best
// to calculate the disk usage and always return true.
if fs.MaxDisk() == 0 {
return true
}
return size <= fs.MaxDisk()
}
// Returns the cached value for the amount of disk space used by the filesystem. Do not rely on this
// function for critical logical checks. It should only be used in areas where the actual disk usage
// does not need to be perfect, e.g. API responses for server resource usage.
func (fs *Filesystem) CachedUsage() int64 {
return atomic.LoadInt64(&fs.diskUsed)
}
// Internal helper function to allow other parts of the codebase to check the total used disk space
// as needed without overly taxing the system. This will prioritize the value from the cache to avoid
// excessive IO usage. We will only walk the filesystem and determine the size of the directory if there
// is no longer a cached value.
//
// If "allowStaleValue" is set to true, a stale value MAY be returned to the caller if there is an
// expired cache value AND there is currently another lookup in progress. If there is no cached value but
// no other lookup is in progress, a fresh disk space response will be returned to the caller.
//
// This is primarily to avoid a bunch of I/O operations from piling up on the server, especially on servers
// with a large amount of files.
func (fs *Filesystem) DiskUsage(allowStaleValue bool) (int64, error) {
// A disk check interval of 0 means this functionality is completely disabled.
if fs.diskCheckInterval == 0 {
return 0, nil
}
if !fs.lastLookupTime.Get().After(time.Now().Add(time.Second * fs.diskCheckInterval * -1)) {
// If we are now allowing a stale response go ahead and perform the lookup and return the fresh
// value. This is a blocking operation to the calling process.
if !allowStaleValue {
return fs.updateCachedDiskUsage()
} else if !fs.lookupInProgress.Load() {
// Otherwise, if we allow a stale value and there isn't a valid item in the cache and we aren't
// currently performing a lookup, just do the disk usage calculation in the background.
go func(fs *Filesystem) {
if _, err := fs.updateCachedDiskUsage(); err != nil {
log.WithField("root", fs.root).WithField("error", err).Warn("failed to update fs disk usage from within routine")
}
}(fs)
}
}
// Return the currently cached value back to the calling function.
return atomic.LoadInt64(&fs.diskUsed), nil
}
// Updates the currently used disk space for a server.
func (fs *Filesystem) updateCachedDiskUsage() (int64, error) {
// Obtain an exclusive lock on this process so that we don't unintentionally run it at the same
// time as another running process. Once the lock is available it'll read from the cache for the
// second call rather than hitting the disk in parallel.
fs.mu.Lock()
defer fs.mu.Unlock()
// Signal that we're currently updating the disk size so that other calls to the disk checking
// functions can determine if they should queue up additional calls to this function. Ensure that
// we always set this back to "false" when this process is done executing.
fs.lookupInProgress.Store(true)
defer fs.lookupInProgress.Store(false)
// If there is no size its either because there is no data (in which case running this function
// will have effectively no impact), or there is nothing in the cache, in which case we need to
// grab the size of their data directory. This is a taxing operation, so we want to store it in
// the cache once we've gotten it.
size, err := fs.DirectorySize("/")
// Always cache the size, even if there is an error. We want to always return that value
// so that we don't cause an endless loop of determining the disk size if there is a temporary
// error encountered.
fs.lastLookupTime.Set(time.Now())
atomic.StoreInt64(&fs.diskUsed, size)
return size, err
}
// Determines the directory size of a given location by running parallel tasks to iterate
// through all of the folders. Returns the size in bytes. This can be a fairly taxing operation
// on locations with tons of files, so it is recommended that you cache the output.
func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
d, err := fs.SafePath(dir)
if err != nil {
return 0, err
}
var size int64
var st syscall.Stat_t
err = godirwalk.Walk(d, &godirwalk.Options{
Unsorted: true,
Callback: func(p string, e *godirwalk.Dirent) error {
// If this is a symlink then resolve the final destination of it before trying to continue walking
// over its contents. If it resolves outside the server data directory just skip everything else for
// it. Otherwise, allow it to continue.
if e.IsSymlink() {
if _, err := fs.SafePath(p); err != nil {
if IsErrorCode(err, ErrCodePathResolution) {
return godirwalk.SkipThis
}
return err
}
}
if !e.IsDir() {
syscall.Lstat(p, &st)
atomic.AddInt64(&size, st.Size)
}
return nil
},
})
return size, err
}
// Helper function to determine if a server has space available for a file of a given size.
// If space is available, no error will be returned, otherwise an ErrNotEnoughSpace error
// will be raised.
func (fs *Filesystem) HasSpaceFor(size int64) error {
if fs.MaxDisk() == 0 {
return nil
}
s, err := fs.DiskUsage(true)
if err != nil {
return err
}
if (s + size) > fs.MaxDisk() {
return &Error{code: ErrCodeDiskSpace}
}
return nil
}
// Updates the disk usage for the Filesystem instance.
func (fs *Filesystem) addDisk(i int64) int64 {
size := atomic.LoadInt64(&fs.diskUsed)
// Sorry go gods. This is ugly but the best approach I can come up with for right
// now without completely re-evaluating the logic we use for determining disk space.
//
// Normally I would just be using the atomic load right below, but I'm not sure about
// the scenarios where it is 0 because nothing has run that would trigger a disk size
// calculation?
//
// Perhaps that isn't even a concern for the sake of this?
if !fs.isTest {
size, _ = fs.DiskUsage(true)
}
// If we're dropping below 0 somehow just cap it to 0.
if (size + i) < 0 {
return atomic.SwapInt64(&fs.diskUsed, 0)
}
return atomic.AddInt64(&fs.diskUsed, i)
}

View File

@@ -0,0 +1,95 @@
package filesystem
import (
"emperror.dev/errors"
"fmt"
"github.com/apex/log"
"os"
"path/filepath"
)
type ErrorCode string
const (
ErrCodeIsDirectory ErrorCode = "E_ISDIR"
ErrCodeDiskSpace ErrorCode = "E_NODISK"
ErrCodeUnknownArchive ErrorCode = "E_UNKNFMT"
ErrCodePathResolution ErrorCode = "E_BADPATH"
)
type Error struct {
code ErrorCode
path string
resolved string
}
// Returns a human-readable error string to identify the Error by.
func (e *Error) Error() string {
switch e.code {
case ErrCodeIsDirectory:
return "filesystem: is a directory"
case ErrCodeDiskSpace:
return "filesystem: not enough disk space"
case ErrCodeUnknownArchive:
return "filesystem: unknown archive format"
case ErrCodePathResolution:
r := e.resolved
if r == "" {
r = "<empty>"
}
return fmt.Sprintf("filesystem: server path [%s] resolves to a location outside the server root: %s", e.path, r)
}
return "filesystem: unhandled error type"
}
// Returns the ErrorCode for this specific error instance.
func (e *Error) Code() ErrorCode {
return e.code
}
// Checks if the given error is one of the Filesystem errors.
func IsFilesystemError(err error) (*Error, bool) {
if e := errors.Unwrap(err); e != nil {
err = e
}
if fserr, ok := err.(*Error); ok {
return fserr, true
}
return nil, false
}
// Checks if "err" is a filesystem Error type. If so, it will then drop in and check
// that the error code is the same as the provided ErrorCode passed in "code".
func IsErrorCode(err error, code ErrorCode) bool {
if e, ok := IsFilesystemError(err); ok {
return e.code == code
}
return false
}
// Returns a new BadPathResolution error.
func NewBadPathResolution(path string, resolved string) *Error {
return &Error{code: ErrCodePathResolution, path: path, resolved: resolved}
}
// Generates an error logger instance with some basic information.
func (fs *Filesystem) error(err error) *log.Entry {
return log.WithField("subsystem", "filesystem").WithField("root", fs.root).WithField("error", err)
}
// Handle errors encountered when walking through directories.
//
// If there is a path resolution error just skip the item entirely. Only return this for a
// directory, otherwise return nil. Returning this error for a file will stop the walking
// for the remainder of the directory. This is assuming an os.FileInfo struct was even returned.
func (fs *Filesystem) handleWalkerError(err error, f os.FileInfo) error {
if !IsErrorCode(err, ErrCodePathResolution) {
return err
}
if f != nil && f.IsDir() {
return filepath.SkipDir
}
return nil
}

View File

@@ -0,0 +1,24 @@
package filesystem
import (
. "github.com/franela/goblin"
"testing"
)
func TestFilesystem_PathResolutionError(t *testing.T) {
g := Goblin(t)
g.Describe("NewBadPathResolutionError", func() {
g.It("is can detect itself as an error correctly", func() {
err := NewBadPathResolution("foo", "bar")
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
g.Assert(err.Error()).Equal("filesystem: server path [foo] resolves to a location outside the server root: bar")
g.Assert(IsErrorCode(&Error{code: ErrCodeIsDirectory}, ErrCodePathResolution)).IsFalse()
})
g.It("returns <empty> if no destination path is provided", func() {
err := NewBadPathResolution("foo", "")
g.Assert(err.Error()).Equal("filesystem: server path [foo] resolves to a location outside the server root: <empty>")
})
})
}

View File

@@ -0,0 +1,491 @@
package filesystem
import (
"bufio"
"emperror.dev/errors"
"github.com/gabriel-vasile/mimetype"
"github.com/karrick/godirwalk"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/system"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
)
type Filesystem struct {
mu sync.RWMutex
lastLookupTime *usageLookupTime
lookupInProgress *system.AtomicBool
diskUsed int64
diskCheckInterval time.Duration
// The maximum amount of disk space (in bytes) that this Filesystem instance can use.
diskLimit int64
// The root data directory path for this Filesystem instance.
root string
isTest bool
}
// Creates a new Filesystem instance for a given server.
func New(root string, size int64) *Filesystem {
return &Filesystem{
root: root,
diskLimit: size,
diskCheckInterval: time.Duration(config.Get().System.DiskCheckInterval),
lastLookupTime: &usageLookupTime{},
lookupInProgress: system.NewAtomicBool(false),
}
}
// Returns the root path for the Filesystem instance.
func (fs *Filesystem) Path() string {
return fs.root
}
// Returns a reader for a file instance.
func (fs *Filesystem) File(p string) (*os.File, os.FileInfo, error) {
cleaned, err := fs.SafePath(p)
if err != nil {
return nil, nil, err
}
st, err := os.Stat(cleaned)
if err != nil {
return nil, nil, err
}
if st.IsDir() {
return nil, nil, &Error{code: ErrCodeIsDirectory}
}
f, err := os.Open(cleaned)
if err != nil {
return nil, nil, err
}
return f, st, nil
}
// Reads a file on the system and returns it as a byte representation in a file
// reader. This is not the most memory efficient usage since it will be reading the
// entirety of the file into memory.
func (fs *Filesystem) Readfile(p string, w io.Writer) error {
file, _, err := fs.File(p)
if err != nil {
return err
}
defer file.Close()
_, err = bufio.NewReader(file).WriteTo(w)
return err
}
// Writes a file to the system. If the file does not already exist one will be created.
func (fs *Filesystem) Writefile(p string, r io.Reader) error {
cleaned, err := fs.SafePath(p)
if err != nil {
return err
}
var currentSize int64
// If the file does not exist on the system already go ahead and create the pathway
// to it and an empty file. We'll then write to it later on after this completes.
stat, err := os.Stat(cleaned)
if err != nil && !os.IsNotExist(err) {
return err
} else if err == nil {
if stat.IsDir() {
return &Error{code: ErrCodeIsDirectory}
}
currentSize = stat.Size()
}
br := bufio.NewReader(r)
// Check that the new size we're writing to the disk can fit. If there is currently
// a file we'll subtract that current file size from the size of the buffer to determine
// the amount of new data we're writing (or amount we're removing if smaller).
if err := fs.HasSpaceFor(int64(br.Size()) - currentSize); err != nil {
return err
}
// If we were unable to stat the location because it did not exist, go ahead and create
// it now. We do this after checking the disk space so that we do not just create empty
// directories at random.
if err != nil {
if err := os.MkdirAll(filepath.Dir(cleaned), 0755); err != nil {
return err
}
if err := fs.Chown(filepath.Dir(cleaned)); err != nil {
return err
}
}
o := &fileOpener{}
// This will either create the file if it does not already exist, or open and
// truncate the existing file.
file, err := o.open(cleaned, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer file.Close()
buf := make([]byte, 1024*4)
sz, err := io.CopyBuffer(file, r, buf)
// Adjust the disk usage to account for the old size and the new size of the file.
fs.addDisk(sz - currentSize)
// Finally, chown the file to ensure the permissions don't end up out-of-whack
// if we had just created it.
return fs.Chown(cleaned)
}
// Creates a new directory (name) at a specified path (p) for the server.
func (fs *Filesystem) CreateDirectory(name string, p string) error {
cleaned, err := fs.SafePath(path.Join(p, name))
if err != nil {
return err
}
return os.MkdirAll(cleaned, 0755)
}
// Moves (or renames) a file or directory.
func (fs *Filesystem) Rename(from string, to string) error {
cleanedFrom, err := fs.SafePath(from)
if err != nil {
return err
}
cleanedTo, err := fs.SafePath(to)
if err != nil {
return err
}
// If the target file or directory already exists the rename function will fail, so just
// bail out now.
if _, err := os.Stat(cleanedTo); err == nil {
return os.ErrExist
}
if cleanedTo == fs.Path() {
return errors.New("attempting to rename into an invalid directory space")
}
d := strings.TrimSuffix(cleanedTo, path.Base(cleanedTo))
// Ensure that the directory we're moving into exists correctly on the system. Only do this if
// we're not at the root directory level.
if d != fs.Path() {
if mkerr := os.MkdirAll(d, 0755); mkerr != nil {
return errors.WithMessage(mkerr, "failed to create directory structure for file rename")
}
}
return os.Rename(cleanedFrom, cleanedTo)
}
// Recursively iterates over a file or directory and sets the permissions on all of the
// underlying files. Iterate over all of the files and directories. If it is a file just
// go ahead and perform the chown operation. Otherwise dig deeper into the directory until
// we've run out of directories to dig into.
func (fs *Filesystem) Chown(path string) error {
cleaned, err := fs.SafePath(path)
if err != nil {
return err
}
if fs.isTest {
return nil
}
uid := config.Get().System.User.Uid
gid := config.Get().System.User.Gid
// Start by just chowning the initial path that we received.
if err := os.Chown(cleaned, uid, gid); err != nil {
return err
}
// If this is not a directory we can now return from the function, there is nothing
// left that we need to do.
if st, _ := os.Stat(cleaned); !st.IsDir() {
return nil
}
// If this was a directory, begin walking over its contents recursively and ensure that all
// of the subfiles and directories get their permissions updated as well.
return godirwalk.Walk(cleaned, &godirwalk.Options{
Unsorted: true,
Callback: func(p string, e *godirwalk.Dirent) error {
// Do not attempt to chmod a symlink. Go's os.Chown function will affect the symlink
// so if it points to a location outside the data directory the user would be able to
// (un)intentionally modify that files permissions.
if e.IsSymlink() {
if e.IsDir() {
return godirwalk.SkipThis
}
return nil
}
return os.Chown(p, uid, gid)
},
})
}
func (fs *Filesystem) Chmod(path string, mode os.FileMode) error {
cleaned, err := fs.SafePath(path)
if err != nil {
return err
}
if fs.isTest {
return nil
}
if err := os.Chmod(cleaned, mode); err != nil {
return err
}
return nil
}
// Begin looping up to 50 times to try and create a unique copy file name. This will take
// an input of "file.txt" and generate "file copy.txt". If that name is already taken, it will
// then try to write "file copy 2.txt" and so on, until reaching 50 loops. At that point we
// won't waste anymore time, just use the current timestamp and make that copy.
//
// Could probably make this more efficient by checking if there are any files matching the copy
// pattern, and trying to find the highest number and then incrementing it by one rather than
// looping endlessly.
func (fs *Filesystem) findCopySuffix(dir string, name string, extension string) (string, error) {
var i int
suffix := " copy"
for i = 0; i < 51; i++ {
if i > 0 {
suffix = " copy " + strconv.Itoa(i)
}
n := name + suffix + extension
// If we stat the file and it does not exist that means we're good to create the copy. If it
// does exist, we'll just continue to the next loop and try again.
if _, err := fs.Stat(path.Join(dir, n)); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return "", err
}
break
}
if i == 50 {
suffix = "copy." + time.Now().Format(time.RFC3339)
}
}
return name + suffix + extension, nil
}
// Copies a given file to the same location and appends a suffix to the file to indicate that
// it has been copied.
func (fs *Filesystem) Copy(p string) error {
cleaned, err := fs.SafePath(p)
if err != nil {
return err
}
s, err := os.Stat(cleaned)
if err != nil {
return err
} else if s.IsDir() || !s.Mode().IsRegular() {
// If this is a directory or not a regular file, just throw a not-exist error
// since anything calling this function should understand what that means.
return os.ErrNotExist
}
// Check that copying this file wouldn't put the server over its limit.
if err := fs.HasSpaceFor(s.Size()); err != nil {
return err
}
base := filepath.Base(cleaned)
relative := strings.TrimSuffix(strings.TrimPrefix(cleaned, fs.Path()), base)
extension := filepath.Ext(base)
name := strings.TrimSuffix(base, extension)
// Ensure that ".tar" is also counted as apart of the file extension.
// There might be a better way to handle this for other double file extensions,
// but this is a good workaround for now.
if strings.HasSuffix(name, ".tar") {
extension = ".tar" + extension
name = strings.TrimSuffix(name, ".tar")
}
source, err := os.Open(cleaned)
if err != nil {
return err
}
defer source.Close()
n, err := fs.findCopySuffix(relative, name, extension)
if err != nil {
return err
}
return fs.Writefile(path.Join(relative, n), source)
}
// Deletes a file or folder from the system. Prevents the user from accidentally
// (or maliciously) removing their root server data directory.
func (fs *Filesystem) Delete(p string) error {
wg := sync.WaitGroup{}
// This is one of the few (only?) places in the codebase where we're explicitly not using
// the SafePath functionality when working with user provided input. If we did, you would
// not be able to delete a file that is a symlink pointing to a location outside of the data
// directory.
//
// We also want to avoid resolving a symlink that points _within_ the data directory and thus
// deleting the actual source file for the symlink rather than the symlink itself. For these
// purposes just resolve the actual file path using filepath.Join() and confirm that the path
// exists within the data directory.
resolved := fs.unsafeFilePath(p)
if !fs.unsafeIsInDataDirectory(resolved) {
return NewBadPathResolution(p, resolved)
}
// Block any whoopsies.
if resolved == fs.Path() {
return errors.New("cannot delete root server directory")
}
if st, err := os.Lstat(resolved); err != nil {
if !os.IsNotExist(err) {
fs.error(err).Warn("error while attempting to stat file before deletion")
}
} else {
if !st.IsDir() {
fs.addDisk(-st.Size())
} else {
wg.Add(1)
go func(wg *sync.WaitGroup, st os.FileInfo, resolved string) {
defer wg.Done()
if s, err := fs.DirectorySize(resolved); err == nil {
fs.addDisk(-s)
}
}(&wg, st, resolved)
}
}
wg.Wait()
return os.RemoveAll(resolved)
}
type fileOpener struct {
busy uint
}
// Attempts to open a given file up to "attempts" number of times, using a backoff. If the file
// cannot be opened because of a "text file busy" error, we will attempt until the number of attempts
// has been exhaused, at which point we will abort with an error.
func (fo *fileOpener) open(path string, flags int, perm os.FileMode) (*os.File, error) {
for {
f, err := os.OpenFile(path, flags, perm)
// If there is an error because the text file is busy, go ahead and sleep for a few
// hundred milliseconds and then try again up to three times before just returning the
// error back to the caller.
//
// Based on code from: https://github.com/golang/go/issues/22220#issuecomment-336458122
if err != nil && fo.busy < 3 && strings.Contains(err.Error(), "text file busy") {
time.Sleep(100 * time.Millisecond << fo.busy)
fo.busy++
continue
}
return f, err
}
}
// Lists the contents of a given directory and returns stat information about each
// file and folder within it.
func (fs *Filesystem) ListDirectory(p string) ([]*Stat, error) {
cleaned, err := fs.SafePath(p)
if err != nil {
return nil, err
}
files, err := ioutil.ReadDir(cleaned)
if err != nil {
return nil, err
}
var wg sync.WaitGroup
// You must initialize the output of this directory as a non-nil value otherwise
// when it is marshaled into a JSON object you'll just get 'null' back, which will
// break the panel badly.
out := make([]*Stat, len(files))
// Iterate over all of the files and directories returned and perform an async process
// to get the mime-type for them all.
for i, file := range files {
wg.Add(1)
go func(idx int, f os.FileInfo) {
defer wg.Done()
var m *mimetype.MIME
var d = "inode/directory"
if !f.IsDir() {
cleanedp := filepath.Join(cleaned, f.Name())
if f.Mode()&os.ModeSymlink != 0 {
cleanedp, _ = fs.SafePath(filepath.Join(cleaned, f.Name()))
}
if cleanedp != "" {
m, _ = mimetype.DetectFile(filepath.Join(cleaned, f.Name()))
} else {
// Just pass this for an unknown type because the file could not safely be resolved within
// the server data path.
d = "application/octet-stream"
}
}
st := &Stat{
Info: f,
Mimetype: d,
}
if m != nil {
st.Mimetype = m.String()
}
out[idx] = st
}(i, file)
}
wg.Wait()
// Sort the output alphabetically to begin with since we've run the output
// through an asynchronous process and the order is gonna be very random.
sort.SliceStable(out, func(i, j int) bool {
if out[i].Info.Name() == out[j].Info.Name() || out[i].Info.Name() > out[j].Info.Name() {
return true
}
return false
})
// Then, sort it so that directories are listed first in the output. Everything
// will continue to be alphabetized at this point.
sort.SliceStable(out, func(i, j int) bool {
return out[i].Info.IsDir()
})
return out, nil
}

View File

@@ -0,0 +1,570 @@
package filesystem
import (
"bytes"
"errors"
. "github.com/franela/goblin"
"github.com/pterodactyl/wings/config"
"io/ioutil"
"math/rand"
"os"
"path/filepath"
"sync/atomic"
"testing"
"unicode/utf8"
)
func NewFs() (*Filesystem, *rootFs) {
config.Set(&config.Configuration{
AuthenticationToken: "abc",
System: config.SystemConfiguration{
RootDirectory: "/server",
DiskCheckInterval: 150,
},
})
tmpDir, err := ioutil.TempDir(os.TempDir(), "pterodactyl")
if err != nil {
panic(err)
}
// defer os.RemoveAll(tmpDir)
rfs := rootFs{root: tmpDir}
rfs.reset()
fs := New(filepath.Join(tmpDir, "/server"), 0)
fs.isTest = true
return fs, &rfs
}
type rootFs struct {
root string
}
func (rfs *rootFs) CreateServerFile(p string, c string) error {
f, err := os.Create(filepath.Join(rfs.root, "/server", p))
if err == nil {
f.Write([]byte(c))
f.Close()
}
return err
}
func (rfs *rootFs) StatServerFile(p string) (os.FileInfo, error) {
return os.Stat(filepath.Join(rfs.root, "/server", p))
}
func (rfs *rootFs) reset() {
if err := os.RemoveAll(filepath.Join(rfs.root, "/server")); err != nil {
if !os.IsNotExist(err) {
panic(err)
}
}
if err := os.Mkdir(filepath.Join(rfs.root, "/server"), 0755); err != nil {
panic(err)
}
}
func TestFilesystem_Readfile(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
g.Describe("Readfile", func() {
buf := &bytes.Buffer{}
g.It("opens a file if it exists on the system", func() {
err := rfs.CreateServerFile("test.txt", "testing")
g.Assert(err).IsNil()
err = fs.Readfile("test.txt", buf)
g.Assert(err).IsNil()
g.Assert(buf.String()).Equal("testing")
})
g.It("returns an error if the file does not exist", func() {
err := fs.Readfile("test.txt", buf)
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
})
g.It("returns an error if the \"file\" is a directory", func() {
err := os.Mkdir(filepath.Join(rfs.root, "/server/test.txt"), 0755)
g.Assert(err).IsNil()
err = fs.Readfile("test.txt", buf)
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodeIsDirectory)).IsTrue()
})
g.It("cannot open a file outside the root directory", func() {
err := rfs.CreateServerFile("/../test.txt", "testing")
g.Assert(err).IsNil()
err = fs.Readfile("/../test.txt", buf)
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.AfterEach(func() {
buf.Truncate(0)
atomic.StoreInt64(&fs.diskUsed, 0)
rfs.reset()
})
})
}
func TestFilesystem_Writefile(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
g.Describe("Open and WriteFile", func() {
buf := &bytes.Buffer{}
// Test that a file can be written to the disk and that the disk space used as a result
// is updated correctly in the end.
g.It("can create a new file", func() {
r := bytes.NewReader([]byte("test file content"))
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
err := fs.Writefile("test.txt", r)
g.Assert(err).IsNil()
err = fs.Readfile("test.txt", buf)
g.Assert(err).IsNil()
g.Assert(buf.String()).Equal("test file content")
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(r.Size())
})
g.It("can create a new file inside a nested directory with leading slash", func() {
r := bytes.NewReader([]byte("test file content"))
err := fs.Writefile("/some/nested/test.txt", r)
g.Assert(err).IsNil()
err = fs.Readfile("/some/nested/test.txt", buf)
g.Assert(err).IsNil()
g.Assert(buf.String()).Equal("test file content")
})
g.It("can create a new file inside a nested directory without a trailing slash", func() {
r := bytes.NewReader([]byte("test file content"))
err := fs.Writefile("some/../foo/bar/test.txt", r)
g.Assert(err).IsNil()
err = fs.Readfile("foo/bar/test.txt", buf)
g.Assert(err).IsNil()
g.Assert(buf.String()).Equal("test file content")
})
g.It("cannot create a file outside the root directory", func() {
r := bytes.NewReader([]byte("test file content"))
err := fs.Writefile("/some/../foo/../../test.txt", r)
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.It("cannot write a file that exceeds the disk limits", func() {
atomic.StoreInt64(&fs.diskLimit, 1024)
b := make([]byte, 1025)
_, err := rand.Read(b)
g.Assert(err).IsNil()
g.Assert(len(b)).Equal(1025)
r := bytes.NewReader(b)
err = fs.Writefile("test.txt", r)
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodeDiskSpace)).IsTrue()
})
/*g.It("updates the total space used when a file is appended to", func() {
atomic.StoreInt64(&fs.diskUsed, 100)
b := make([]byte, 100)
_, _ = rand.Read(b)
r := bytes.NewReader(b)
err := fs.Writefile("test.txt", r)
g.Assert(err).IsNil()
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(200))
// If we write less data than already exists, we should expect the total
// disk used to be decremented.
b = make([]byte, 50)
_, _ = rand.Read(b)
r = bytes.NewReader(b)
err = fs.Writefile("test.txt", r)
g.Assert(err).IsNil()
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(150))
})*/
g.It("truncates the file when writing new contents", func() {
r := bytes.NewReader([]byte("original data"))
err := fs.Writefile("test.txt", r)
g.Assert(err).IsNil()
r = bytes.NewReader([]byte("new data"))
err = fs.Writefile("test.txt", r)
g.Assert(err).IsNil()
err = fs.Readfile("test.txt", buf)
g.Assert(err).IsNil()
g.Assert(buf.String()).Equal("new data")
})
g.AfterEach(func() {
buf.Truncate(0)
rfs.reset()
atomic.StoreInt64(&fs.diskUsed, 0)
atomic.StoreInt64(&fs.diskLimit, 0)
})
})
}
func TestFilesystem_CreateDirectory(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
g.Describe("CreateDirectory", func() {
g.It("should create missing directories automatically", func() {
err := fs.CreateDirectory("test", "foo/bar/baz")
g.Assert(err).IsNil()
st, err := rfs.StatServerFile("foo/bar/baz/test")
g.Assert(err).IsNil()
g.Assert(st.IsDir()).IsTrue()
g.Assert(st.Name()).Equal("test")
})
g.It("should work with leading and trailing slashes", func() {
err := fs.CreateDirectory("test", "/foozie/barzie/bazzy/")
g.Assert(err).IsNil()
st, err := rfs.StatServerFile("foozie/barzie/bazzy/test")
g.Assert(err).IsNil()
g.Assert(st.IsDir()).IsTrue()
g.Assert(st.Name()).Equal("test")
})
g.It("should not allow the creation of directories outside the root", func() {
err := fs.CreateDirectory("test", "e/../../something")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.It("should not increment the disk usage", func() {
err := fs.CreateDirectory("test", "/")
g.Assert(err).IsNil()
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
})
g.AfterEach(func() {
rfs.reset()
})
})
}
func TestFilesystem_Rename(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
g.Describe("Rename", func() {
g.BeforeEach(func() {
if err := rfs.CreateServerFile("source.txt", "text content"); err != nil {
panic(err)
}
})
g.It("returns an error if the target already exists", func() {
err := rfs.CreateServerFile("target.txt", "taget content")
g.Assert(err).IsNil()
err = fs.Rename("source.txt", "target.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrExist)).IsTrue()
})
g.It("returns an error if the final destination is the root directory", func() {
err := fs.Rename("source.txt", "/")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrExist)).IsTrue()
})
g.It("returns an error if the source destination is the root directory", func() {
err := fs.Rename("source.txt", "/")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrExist)).IsTrue()
})
g.It("does not allow renaming to a location outside the root", func() {
err := fs.Rename("source.txt", "../target.txt")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.It("does not allow renaming from a location outside the root", func() {
err := rfs.CreateServerFile("/../ext-source.txt", "taget content")
err = fs.Rename("/../ext-source.txt", "target.txt")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.It("allows a file to be renamed", func() {
err := fs.Rename("source.txt", "target.txt")
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("source.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
st, err := rfs.StatServerFile("target.txt")
g.Assert(err).IsNil()
g.Assert(st.Name()).Equal("target.txt")
g.Assert(st.Size()).IsNotZero()
})
g.It("allows a folder to be renamed", func() {
err := os.Mkdir(filepath.Join(rfs.root, "/server/source_dir"), 0755)
g.Assert(err).IsNil()
err = fs.Rename("source_dir", "target_dir")
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("source_dir")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
st, err := rfs.StatServerFile("target_dir")
g.Assert(err).IsNil()
g.Assert(st.IsDir()).IsTrue()
})
g.It("returns an error if the source does not exist", func() {
err := fs.Rename("missing.txt", "target.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
})
g.It("creates directories if they are missing", func() {
err := fs.Rename("source.txt", "nested/folder/target.txt")
g.Assert(err).IsNil()
st, err := rfs.StatServerFile("nested/folder/target.txt")
g.Assert(err).IsNil()
g.Assert(st.Name()).Equal("target.txt")
})
g.AfterEach(func() {
rfs.reset()
})
})
}
func TestFilesystem_Copy(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
g.Describe("Copy", func() {
g.BeforeEach(func() {
if err := rfs.CreateServerFile("source.txt", "text content"); err != nil {
panic(err)
}
atomic.StoreInt64(&fs.diskUsed, int64(utf8.RuneCountInString("test content")))
})
g.It("should return an error if the source does not exist", func() {
err := fs.Copy("foo.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
})
g.It("should return an error if the source is outside the root", func() {
err := rfs.CreateServerFile("/../ext-source.txt", "text content")
err = fs.Copy("../ext-source.txt")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.It("should return an error if the source directory is outside the root", func() {
err := os.MkdirAll(filepath.Join(rfs.root, "/nested/in/dir"), 0755)
g.Assert(err).IsNil()
err = rfs.CreateServerFile("/../nested/in/dir/ext-source.txt", "external content")
g.Assert(err).IsNil()
err = fs.Copy("../nested/in/dir/ext-source.txt")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
err = fs.Copy("nested/in/../../../nested/in/dir/ext-source.txt")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.It("should return an error if the source is a directory", func() {
err := os.Mkdir(filepath.Join(rfs.root, "/server/dir"), 0755)
g.Assert(err).IsNil()
err = fs.Copy("dir")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
})
g.It("should return an error if there is not space to copy the file", func() {
atomic.StoreInt64(&fs.diskLimit, 2)
err := fs.Copy("source.txt")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodeDiskSpace)).IsTrue()
})
g.It("should create a copy of the file and increment the disk used", func() {
err := fs.Copy("source.txt")
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("source.txt")
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("source copy.txt")
g.Assert(err).IsNil()
})
g.It("should create a copy of the file with a suffix if a copy already exists", func() {
err := fs.Copy("source.txt")
g.Assert(err).IsNil()
err = fs.Copy("source.txt")
g.Assert(err).IsNil()
r := []string{"source.txt", "source copy.txt", "source copy 1.txt"}
for _, name := range r {
_, err = rfs.StatServerFile(name)
g.Assert(err).IsNil()
}
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(utf8.RuneCountInString("test content")) * 3)
})
g.It("should create a copy inside of a directory", func() {
err := os.MkdirAll(filepath.Join(rfs.root, "/server/nested/in/dir"), 0755)
g.Assert(err).IsNil()
err = rfs.CreateServerFile("nested/in/dir/source.txt", "test content")
g.Assert(err).IsNil()
err = fs.Copy("nested/in/dir/source.txt")
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("nested/in/dir/source.txt")
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("nested/in/dir/source copy.txt")
g.Assert(err).IsNil()
})
g.AfterEach(func() {
rfs.reset()
atomic.StoreInt64(&fs.diskUsed, 0)
atomic.StoreInt64(&fs.diskLimit, 0)
})
})
}
func TestFilesystem_Delete(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
g.Describe("Delete", func() {
g.BeforeEach(func() {
if err := rfs.CreateServerFile("source.txt", "test content"); err != nil {
panic(err)
}
atomic.StoreInt64(&fs.diskUsed, int64(utf8.RuneCountInString("test content")))
})
g.It("does not delete files outside the root directory", func() {
err := rfs.CreateServerFile("/../ext-source.txt", "external content")
err = fs.Delete("../ext-source.txt")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.It("does not allow the deletion of the root directory", func() {
err := fs.Delete("/")
g.Assert(err).IsNotNil()
g.Assert(err.Error()).Equal("cannot delete root server directory")
})
g.It("does not return an error if the target does not exist", func() {
err := fs.Delete("missing.txt")
g.Assert(err).IsNil()
st, err := rfs.StatServerFile("source.txt")
g.Assert(err).IsNil()
g.Assert(st.Name()).Equal("source.txt")
})
g.It("deletes files and subtracts their size from the disk usage", func() {
err := fs.Delete("source.txt")
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("source.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
})
g.It("deletes all items inside a directory if the directory is deleted", func() {
sources := []string{
"foo/source.txt",
"foo/bar/source.txt",
"foo/bar/baz/source.txt",
}
err := os.MkdirAll(filepath.Join(rfs.root, "/server/foo/bar/baz"), 0755)
g.Assert(err).IsNil()
for _, s := range sources {
err = rfs.CreateServerFile(s, "test content")
g.Assert(err).IsNil()
}
atomic.StoreInt64(&fs.diskUsed, int64(utf8.RuneCountInString("test content")*3))
err = fs.Delete("foo")
g.Assert(err).IsNil()
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
for _, s := range sources {
_, err = rfs.StatServerFile(s)
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
}
})
g.AfterEach(func() {
rfs.reset()
atomic.StoreInt64(&fs.diskUsed, 0)
atomic.StoreInt64(&fs.diskLimit, 0)
})
})
}

141
server/filesystem/path.go Normal file
View File

@@ -0,0 +1,141 @@
package filesystem
import (
"context"
"golang.org/x/sync/errgroup"
"os"
"path/filepath"
"strings"
"sync"
)
// Normalizes a directory being passed in to ensure the user is not able to escape
// from their data directory. After normalization if the directory is still within their home
// path it is returned. If they managed to "escape" an error will be returned.
//
// This logic is actually copied over from the SFTP server code. Ideally that eventually
// either gets ported into this application, or is able to make use of this package.
func (fs *Filesystem) SafePath(p string) (string, error) {
var nonExistentPathResolution string
// Start with a cleaned up path before checking the more complex bits.
r := fs.unsafeFilePath(p)
// At the same time, evaluate the symlink status and determine where this file or folder
// is truly pointing to.
ep, err := filepath.EvalSymlinks(r)
if err != nil && !os.IsNotExist(err) {
return "", err
} else if os.IsNotExist(err) {
// The requested directory doesn't exist, so at this point we need to iterate up the
// path chain until we hit a directory that _does_ exist and can be validated.
parts := strings.Split(filepath.Dir(r), "/")
var try string
// Range over all of the path parts and form directory pathings from the end
// moving up until we have a valid resolution or we run out of paths to try.
for k := range parts {
try = strings.Join(parts[:(len(parts)-k)], "/")
if !fs.unsafeIsInDataDirectory(try) {
break
}
t, err := filepath.EvalSymlinks(try)
if err == nil {
nonExistentPathResolution = t
break
}
}
}
// If the new path doesn't start with their root directory there is clearly an escape
// attempt going on, and we should NOT resolve this path for them.
if nonExistentPathResolution != "" {
if !fs.unsafeIsInDataDirectory(nonExistentPathResolution) {
return "", NewBadPathResolution(p, nonExistentPathResolution)
}
// If the nonExistentPathResolution variable is not empty then the initial path requested
// did not exist and we looped through the pathway until we found a match. At this point
// we've confirmed the first matched pathway exists in the root server directory, so we
// can go ahead and just return the path that was requested initially.
return r, nil
}
// If the requested directory from EvalSymlinks begins with the server root directory go
// ahead and return it. If not we'll return an error which will block any further action
// on the file.
if fs.unsafeIsInDataDirectory(ep) {
return ep, nil
}
return "", NewBadPathResolution(p, r)
}
// Generate a path to the file by cleaning it up and appending the root server path to it. This
// DOES NOT guarantee that the file resolves within the server data directory. You'll want to use
// the fs.unsafeIsInDataDirectory(p) function to confirm.
func (fs *Filesystem) unsafeFilePath(p string) string {
// Calling filepath.Clean on the joined directory will resolve it to the absolute path,
// removing any ../ type of resolution arguments, and leaving us with a direct path link.
//
// This will also trim the existing root path off the beginning of the path passed to
// the function since that can get a bit messy.
return filepath.Clean(filepath.Join(fs.Path(), strings.TrimPrefix(p, fs.Path())))
}
// Check that that path string starts with the server data directory path. This function DOES NOT
// validate that the rest of the path does not end up resolving out of this directory, or that the
// targeted file or folder is not a symlink doing the same thing.
func (fs *Filesystem) unsafeIsInDataDirectory(p string) bool {
return strings.HasPrefix(strings.TrimSuffix(p, "/")+"/", strings.TrimSuffix(fs.Path(), "/")+"/")
}
// Executes the fs.SafePath function in parallel against an array of paths. If any of the calls
// fails an error will be returned.
func (fs *Filesystem) ParallelSafePath(paths []string) ([]string, error) {
var cleaned []string
// Simple locker function to avoid racy appends to the array of cleaned paths.
var m = new(sync.Mutex)
var push = func(c string) {
m.Lock()
cleaned = append(cleaned, c)
m.Unlock()
}
// Create an error group that we can use to run processes in parallel while retaining
// the ability to cancel the entire process immediately should any of it fail.
g, ctx := errgroup.WithContext(context.Background())
// Iterate over all of the paths and generate a cleaned path, if there is an error for any
// of the files, abort the process.
for _, p := range paths {
// Create copy so we can use it within the goroutine correctly.
pi := p
// Recursively call this function to continue digging through the directory tree within
// a separate goroutine. If the context is canceled abort this process.
g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
default:
// If the callback returns true, go ahead and keep walking deeper. This allows
// us to programmatically continue deeper into directories, or stop digging
// if that pathway knows it needs nothing else.
if c, err := fs.SafePath(pi); err != nil {
return err
} else {
push(c)
}
return nil
}
})
}
// Block until all of the routines finish and have returned a value.
return cleaned, g.Wait()
}

View File

@@ -0,0 +1,228 @@
package filesystem
import (
"bytes"
"emperror.dev/errors"
. "github.com/franela/goblin"
"os"
"path/filepath"
"testing"
)
func TestFilesystem_Path(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
g.Describe("Path", func() {
g.It("returns the root path for the instance", func() {
g.Assert(fs.Path()).Equal(filepath.Join(rfs.root, "/server"))
})
})
}
func TestFilesystem_SafePath(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
prefix := filepath.Join(rfs.root, "/server")
g.Describe("SafePath", func() {
g.It("returns a cleaned path to a given file", func() {
p, err := fs.SafePath("test.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/test.txt")
p, err = fs.SafePath("/test.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/test.txt")
p, err = fs.SafePath("./test.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/test.txt")
p, err = fs.SafePath("/foo/../test.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/test.txt")
p, err = fs.SafePath("/foo/bar")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/foo/bar")
})
g.It("handles root directory access", func() {
p, err := fs.SafePath("/")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix)
p, err = fs.SafePath("")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix)
})
g.It("removes trailing slashes from paths", func() {
p, err := fs.SafePath("/foo/bar/")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/foo/bar")
})
g.It("handles deeply nested directories that do not exist", func() {
p, err := fs.SafePath("/foo/bar/baz/quaz/../../ducks/testing.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/foo/bar/ducks/testing.txt")
})
g.It("blocks access to files outside the root directory", func() {
p, err := fs.SafePath("../test.txt")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
g.Assert(p).Equal("")
p, err = fs.SafePath("/../test.txt")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
g.Assert(p).Equal("")
p, err = fs.SafePath("./foo/../../test.txt")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
g.Assert(p).Equal("")
p, err = fs.SafePath("..")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
g.Assert(p).Equal("")
})
})
}
// We test against accessing files outside the root directory in the tests, however it
// is still possible for someone to mess up and not properly use this safe path call. In
// order to truly confirm this, we'll try to pass in a symlinked malicious file to all of
// the calls and ensure they all fail with the same reason.
func TestFilesystem_Blocks_Symlinks(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
if err := rfs.CreateServerFile("/../malicious.txt", "external content"); err != nil {
panic(err)
}
if err := os.Mkdir(filepath.Join(rfs.root, "/malicious_dir"), 0777); err != nil {
panic(err)
}
if err := os.Symlink(filepath.Join(rfs.root, "malicious.txt"), filepath.Join(rfs.root, "/server/symlinked.txt")); err != nil {
panic(err)
}
if err := os.Symlink(filepath.Join(rfs.root, "/malicious_dir"), filepath.Join(rfs.root, "/server/external_dir")); err != nil {
panic(err)
}
g.Describe("Readfile", func() {
g.It("cannot read a file symlinked outside the root", func() {
b := bytes.Buffer{}
err := fs.Readfile("symlinked.txt", &b)
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
})
g.Describe("Writefile", func() {
g.It("cannot write to a file symlinked outside the root", func() {
r := bytes.NewReader([]byte("testing"))
err := fs.Writefile("symlinked.txt", r)
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.It("cannot write a file to a directory symlinked outside the root", func() {
r := bytes.NewReader([]byte("testing"))
err := fs.Writefile("external_dir/foo.txt", r)
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
})
g.Describe("CreateDirectory", func() {
g.It("cannot create a directory outside the root", func() {
err := fs.CreateDirectory("my_dir", "external_dir")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.It("cannot create a nested directory outside the root", func() {
err := fs.CreateDirectory("my/nested/dir", "external_dir/foo/bar")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.It("cannot create a nested directory outside the root", func() {
err := fs.CreateDirectory("my/nested/dir", "external_dir/server")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
})
g.Describe("Rename", func() {
g.It("cannot rename a file symlinked outside the directory root", func() {
err := fs.Rename("symlinked.txt", "foo.txt")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.It("cannot rename a symlinked directory outside the root", func() {
err := fs.Rename("external_dir", "foo")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.It("cannot rename a file to a location outside the directory root", func() {
rfs.CreateServerFile("my_file.txt", "internal content")
err := fs.Rename("my_file.txt", "external_dir/my_file.txt")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
})
g.Describe("Chown", func() {
g.It("cannot chown a file symlinked outside the directory root", func() {
err := fs.Chown("symlinked.txt")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.It("cannot chown a directory symlinked outside the directory root", func() {
err := fs.Chown("external_dir")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
})
g.Describe("Copy", func() {
g.It("cannot copy a file symlinked outside the directory root", func() {
err := fs.Copy("symlinked.txt")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
})
g.Describe("Delete", func() {
g.It("deletes the symlinked file but leaves the source", func() {
err := fs.Delete("symlinked.txt")
g.Assert(err).IsNil()
_, err = os.Stat(filepath.Join(rfs.root, "malicious.txt"))
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("symlinked.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
})
})
rfs.reset()
}

78
server/filesystem/stat.go Normal file
View File

@@ -0,0 +1,78 @@
package filesystem
import (
"encoding/json"
"github.com/gabriel-vasile/mimetype"
"os"
"strconv"
"time"
)
type Stat struct {
Info os.FileInfo
Mimetype string
}
func (s *Stat) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Name string `json:"name"`
Created string `json:"created"`
Modified string `json:"modified"`
Mode string `json:"mode"`
ModeBits string `json:"mode_bits"`
Size int64 `json:"size"`
Directory bool `json:"directory"`
File bool `json:"file"`
Symlink bool `json:"symlink"`
Mime string `json:"mime"`
}{
Name: s.Info.Name(),
Created: s.CTime().Format(time.RFC3339),
Modified: s.Info.ModTime().Format(time.RFC3339),
Mode: s.Info.Mode().String(),
// Using `&os.ModePerm` on the file's mode will cause the mode to only have the permission values, and nothing else.
ModeBits: strconv.FormatUint(uint64(s.Info.Mode()&os.ModePerm), 8),
Size: s.Info.Size(),
Directory: s.Info.IsDir(),
File: !s.Info.IsDir(),
Symlink: s.Info.Mode().Perm()&os.ModeSymlink != 0,
Mime: s.Mimetype,
})
}
// Stats a file or folder and returns the base stat object from go along with the
// MIME data that can be used for editing files.
func (fs *Filesystem) Stat(p string) (*Stat, error) {
cleaned, err := fs.SafePath(p)
if err != nil {
return nil, err
}
return fs.unsafeStat(cleaned)
}
func (fs *Filesystem) unsafeStat(p string) (*Stat, error) {
s, err := os.Stat(p)
if err != nil {
return nil, err
}
var m *mimetype.MIME
if !s.IsDir() {
m, err = mimetype.DetectFile(p)
if err != nil {
return nil, err
}
}
st := &Stat{
Info: s,
Mimetype: "inode/directory",
}
if m != nil {
st.Mimetype = m.String()
}
return st, nil
}

View File

@@ -1,4 +1,4 @@
package server package filesystem
import ( import (
"syscall" "syscall"
@@ -9,5 +9,5 @@ import (
func (s *Stat) CTime() time.Time { func (s *Stat) CTime() time.Time {
st := s.Info.Sys().(*syscall.Stat_t) st := s.Info.Sys().(*syscall.Stat_t)
return time.Unix(int64(st.Ctim.Sec), int64(st.Ctim.Nsec)) return time.Unix(st.Ctimespec.Sec, st.Ctimespec.Nsec)
} }

View File

@@ -0,0 +1,14 @@
package filesystem
import (
"syscall"
"time"
)
// Returns the time that the file/folder was created.
func (s *Stat) CTime() time.Time {
st := s.Info.Sys().(*syscall.Stat_t)
// Do not remove these "redundant" type-casts, they are required for 32-bit builds to work.
return time.Unix(int64(st.Ctim.Sec), int64(st.Ctim.Nsec))
}

View File

@@ -1,4 +1,4 @@
package server package filesystem
import ( import (
"time" "time"
@@ -9,4 +9,4 @@ import (
// for right now. // for right now.
func (s *Stat) CTime() time.Time { func (s *Stat) CTime() time.Time {
return s.Info.ModTime() return s.Info.ModTime()
} }

View File

@@ -1,13 +0,0 @@
package server
import (
"syscall"
"time"
)
// Returns the time that the file/folder was created.
func (s *Stat) CTime() time.Time {
st := s.Info.Sys().(*syscall.Stat_t)
return time.Unix(int64(st.Ctimespec.Sec), int64(st.Ctimespec.Nsec))
}

View File

@@ -1,141 +0,0 @@
package server
import (
"context"
"github.com/gammazero/workerpool"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"sync"
)
type FileWalker struct {
*Filesystem
}
type PooledFileWalker struct {
wg sync.WaitGroup
pool *workerpool.WorkerPool
callback filepath.WalkFunc
cancel context.CancelFunc
err error
errOnce sync.Once
Filesystem *Filesystem
}
// Returns a new walker instance.
func (fs *Filesystem) NewWalker() *FileWalker {
return &FileWalker{fs}
}
// Creates a new pooled file walker that will concurrently walk over a given directory but limit itself
// to a worker pool as to not completely flood out the system or cause a process crash.
func newPooledWalker(fs *Filesystem) *PooledFileWalker {
return &PooledFileWalker{
Filesystem: fs,
// Create a worker pool that is the same size as the number of processors available on the
// system. Going much higher doesn't provide much of a performance boost, and is only more
// likely to lead to resource overloading anyways.
pool: workerpool.New(runtime.NumCPU()),
}
}
// Process a given path by calling the callback function for all of the files and directories within
// the path, and then dropping into any directories that we come across.
func (w *PooledFileWalker) process(path string) error {
p, err := w.Filesystem.SafePath(path)
if err != nil {
return err
}
files, err := ioutil.ReadDir(p)
if err != nil {
return err
}
// Loop over all of the files and directories in the given directory and call the provided
// callback function. If we encounter a directory, push that directory onto the worker queue
// to be processed.
for _, f := range files {
sp, err := w.Filesystem.SafeJoin(p, f)
if err != nil {
// Let the callback function handle what to do if there is a path resolution error because a
// dangerous path was resolved. If there is an error returned, return from this entire process
// otherwise just skip over this specific file. We don't care if its a file or a directory at
// this point since either way we're skipping it, however, still check for the SkipDir since that
// would be thrown otherwise.
if err = w.callback(sp, f, err); err != nil && err != filepath.SkipDir {
return err
}
continue
}
i, err := os.Stat(sp)
// You might end up getting an error about a file or folder not existing if the given path
// if it is an invalid symlink. We can safely just skip over these files I believe.
if os.IsNotExist(err) {
continue
}
// Call the user-provided callback for this file or directory. If an error is returned that is
// not a SkipDir call, abort the entire process and bubble that error up.
if err = w.callback(sp, i, err); err != nil && err != filepath.SkipDir {
return err
}
// If this is a directory, and we didn't get a SkipDir error, continue through by pushing another
// job to the pool to handle it. If we requested a skip, don't do anything just continue on to the
// next item.
if i.IsDir() && err != filepath.SkipDir {
w.push(sp)
} else if !i.IsDir() && err == filepath.SkipDir {
// Per the spec for the callback, if we get a SkipDir error but it is returned for an item
// that is _not_ a directory, abort the remaining operations on the directory.
return nil
}
}
return nil
}
// Push a new path into the worker pool and increment the waitgroup so that we do not return too
// early and cause panic's as internal directories attempt to submit to the pool.
func (w *PooledFileWalker) push(path string) {
w.wg.Add(1)
w.pool.Submit(func() {
defer w.wg.Done()
if err := w.process(path); err != nil {
w.errOnce.Do(func() {
w.err = err
if w.cancel != nil {
w.cancel()
}
})
}
})
}
// Walks the given directory and executes the callback function for all of the files and directories
// that are encountered.
func (fs *Filesystem) Walk(dir string, callback filepath.WalkFunc) error {
w := newPooledWalker(fs)
w.callback = callback
_, cancel := context.WithCancel(context.Background())
w.cancel = cancel
w.push(dir)
w.wg.Wait()
w.pool.StopWait()
if w.err != nil {
return w.err
}
return nil
}

View File

@@ -4,28 +4,29 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"context" "context"
"github.com/apex/log"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment"
"golang.org/x/sync/semaphore"
"html/template" "html/template"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"time" "strings"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/system"
) )
// Executes the installation stack for a server process. Bubbles any errors up to the calling // Executes the installation stack for a server process. Bubbles any errors up to the calling
// function which should handle contacting the panel to notify it of the server state. // function which should handle contacting the panel to notify it of the server state.
// //
// Pass true as the first arugment in order to execute a server sync before the process to // Pass true as the first argument in order to execute a server sync before the process to
// ensure the latest information is used. // ensure the latest information is used.
func (s *Server) Install(sync bool) error { func (s *Server) Install(sync bool) error {
if sync { if sync {
@@ -35,10 +36,17 @@ func (s *Server) Install(sync bool) error {
} }
} }
// Send the start event so the Panel can automatically update. var err error
s.Events().Publish(InstallStartedEvent, "") if !s.Config().SkipEggScripts {
// Send the start event so the Panel can automatically update. We don't send this unless the process
// is actually going to run, otherwise all sorts of weird rapid UI behavior happens since there isn't
// an actual install process being executed.
s.Events().Publish(InstallStartedEvent, "")
err := s.internalInstall() err = s.internalInstall()
} else {
s.Log().Info("server configured to skip running installation scripts for this egg, not executing process")
}
s.Log().Debug("notifying panel of server install state") s.Log().Debug("notifying panel of server install state")
if serr := s.SyncInstallState(err == nil); serr != nil { if serr := s.SyncInstallState(err == nil); serr != nil {
@@ -56,7 +64,7 @@ func (s *Server) Install(sync bool) error {
// Ensure that the server is marked as offline at this point, otherwise you end up // Ensure that the server is marked as offline at this point, otherwise you end up
// with a blank value which is a bit confusing. // with a blank value which is a bit confusing.
s.SetState(environment.ProcessOfflineState) s.Environment.SetState(environment.ProcessOfflineState)
// Push an event to the websocket so we can auto-refresh the information in the panel once // Push an event to the websocket so we can auto-refresh the information in the panel once
// the install is completed. // the install is completed.
@@ -68,7 +76,7 @@ func (s *Server) Install(sync bool) error {
// Reinstalls a server's software by utilizing the install script for the server egg. This // Reinstalls a server's software by utilizing the install script for the server egg. This
// does not touch any existing files for the server, other than what the script modifies. // does not touch any existing files for the server, other than what the script modifies.
func (s *Server) Reinstall() error { func (s *Server) Reinstall() error {
if s.GetState() != environment.ProcessOfflineState { if s.Environment.State() != environment.ProcessOfflineState {
s.Log().Debug("waiting for server instance to enter a stopped state") s.Log().Debug("waiting for server instance to enter a stopped state")
if err := s.Environment.WaitForStop(10, true); err != nil { if err := s.Environment.WaitForStop(10, true); err != nil {
return err return err
@@ -80,18 +88,18 @@ func (s *Server) Reinstall() error {
// Internal installation function used to simplify reporting back to the Panel. // Internal installation function used to simplify reporting back to the Panel.
func (s *Server) internalInstall() error { func (s *Server) internalInstall() error {
script, rerr, err := api.NewRequester().GetInstallationScript(s.Id()) script, err := api.New().GetInstallationScript(s.Id())
if err != nil || rerr != nil { if err != nil {
if err != nil { if !api.IsRequestError(err) {
return err return err
} }
return errors.New(rerr.String()) return errors.New(err.Error())
} }
p, err := NewInstallationProcess(s, &script) p, err := NewInstallationProcess(s, &script)
if err != nil { if err != nil {
return errors.WithStack(err) return err
} }
s.Log().Info("beginning installation process for server") s.Log().Info("beginning installation process for server")
@@ -119,106 +127,69 @@ func NewInstallationProcess(s *Server, script *api.InstallationScript) (*Install
Server: s, Server: s,
} }
ctx, cancel := context.WithCancel(context.Background()) if c, err := environment.DockerClient(); err != nil {
s.installer.cancel = &cancel return nil, err
if c, err := client.NewClientWithOpts(client.FromEnv); err != nil {
return nil, errors.WithStack(err)
} else { } else {
proc.client = c proc.client = c
proc.context = ctx proc.context = s.Context()
} }
return proc, nil return proc, nil
} }
// Try to obtain an exclusive lock on the installation process for the server. Waits up to 10
// seconds before aborting with a context timeout.
func (s *Server) acquireInstallationLock() error {
if s.installer.sem == nil {
s.installer.sem = semaphore.NewWeighted(1)
}
ctx, _ := context.WithTimeout(context.Background(), time.Second*10)
return s.installer.sem.Acquire(ctx, 1)
}
// Determines if the server is actively running the installation process by checking the status // Determines if the server is actively running the installation process by checking the status
// of the semaphore lock. // of the installer lock.
func (s *Server) IsInstalling() bool { func (s *Server) IsInstalling() bool {
if s.installer.sem == nil { return s.installing.Load()
return false
}
if s.installer.sem.TryAcquire(1) {
// If we made it into this block it means we were able to obtain an exclusive lock
// on the semaphore. In that case, go ahead and release that lock immediately, and
// return false.
s.installer.sem.Release(1)
return false
}
return true
} }
// Aborts the server installation process by calling the cancel function on the installer func (s *Server) IsTransferring() bool {
// context. return s.transferring.Load()
func (s *Server) AbortInstallation() { }
if !s.IsInstalling() {
return
}
if s.installer.cancel != nil { func (s *Server) SetTransferring(state bool) {
cancel := *s.installer.cancel s.transferring.Store(state)
s.Log().Warn("aborting running installation process")
cancel()
}
} }
// Removes the installer container for the server. // Removes the installer container for the server.
func (ip *InstallationProcess) RemoveContainer() { func (ip *InstallationProcess) RemoveContainer() error {
err := ip.client.ContainerRemove(ip.context, ip.Server.Id()+"_installer", types.ContainerRemoveOptions{ err := ip.client.ContainerRemove(ip.context, ip.Server.Id()+"_installer", types.ContainerRemoveOptions{
RemoveVolumes: true, RemoveVolumes: true,
Force: true, Force: true,
}) })
if err != nil && !client.IsErrNotFound(err) { if err != nil && !client.IsErrNotFound(err) {
ip.Server.Log().WithField("error", errors.WithStack(err)).Warn("failed to delete server install container") return err
} }
return nil
} }
// Runs the installation process, this is done as a backgrounded thread. This will configure // Runs the installation process, this is done as in a background thread. This will configure
// the required environment, and then spin up the installation container. // the required environment, and then spin up the installation container.
// //
// Once the container finishes installing the results will be stored in an installation // Once the container finishes installing the results will be stored in an installation
// log in the server's configuration directory. // log in the server's configuration directory.
func (ip *InstallationProcess) Run() error { func (ip *InstallationProcess) Run() error {
ip.Server.Log().Debug("acquiring installation process lock") ip.Server.Log().Debug("acquiring installation process lock")
if err := ip.Server.acquireInstallationLock(); err != nil { if !ip.Server.installing.SwapIf(true) {
return err return errors.New("install: cannot obtain installation lock")
} }
// We now have an exclusive lock on this installation process. Ensure that whenever this // We now have an exclusive lock on this installation process. Ensure that whenever this
// process is finished that the semaphore is released so that other processes and be executed // process is finished that the semaphore is released so that other processes and be executed
// without encounting a wait timeout. // without encountering a wait timeout.
defer func() { defer func() {
ip.Server.Log().Debug("releasing installation process lock") ip.Server.Log().Debug("releasing installation process lock")
ip.Server.installer.sem.Release(1) ip.Server.installing.Store(false)
ip.Server.installer.cancel = nil
}() }()
if err := ip.BeforeExecute(); err != nil { if err := ip.BeforeExecute(); err != nil {
return errors.WithStack(err) return err
} }
cid, err := ip.Execute() cid, err := ip.Execute()
if err != nil { if err != nil {
ip.RemoveContainer() ip.RemoveContainer()
return err
return errors.WithStack(err)
} }
// If this step fails, log a warning but don't exit out of the process. This is completely // If this step fails, log a warning but don't exit out of the process. This is completely
@@ -241,12 +212,12 @@ func (ip *InstallationProcess) writeScriptToDisk() error {
// Make sure the temp directory root exists before trying to make a directory within it. The // Make sure the temp directory root exists before trying to make a directory within it. The
// ioutil.TempDir call expects this base to exist, it won't create it for you. // ioutil.TempDir call expects this base to exist, it won't create it for you.
if err := os.MkdirAll(ip.tempDir(), 0700); err != nil { if err := os.MkdirAll(ip.tempDir(), 0700); err != nil {
return errors.Wrap(err, "could not create temporary directory for install process") return errors.WithMessage(err, "could not create temporary directory for install process")
} }
f, err := os.OpenFile(filepath.Join(ip.tempDir(), "install.sh"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) f, err := os.OpenFile(filepath.Join(ip.tempDir(), "install.sh"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to write server installation script to disk before mount") return errors.WithMessage(err, "failed to write server installation script to disk before mount")
} }
defer f.Close() defer f.Close()
@@ -258,7 +229,7 @@ func (ip *InstallationProcess) writeScriptToDisk() error {
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return errors.WithStack(err) return err
} }
w.Flush() w.Flush()
@@ -268,11 +239,62 @@ func (ip *InstallationProcess) writeScriptToDisk() error {
// Pulls the docker image to be used for the installation container. // Pulls the docker image to be used for the installation container.
func (ip *InstallationProcess) pullInstallationImage() error { func (ip *InstallationProcess) pullInstallationImage() error {
r, err := ip.client.ImagePull(ip.context, ip.Script.ContainerImage, types.ImagePullOptions{}) // Get a registry auth configuration from the config.
if err != nil { var registryAuth *config.RegistryConfiguration
return errors.WithStack(err) for registry, c := range config.Get().Docker.Registries {
if !strings.HasPrefix(ip.Script.ContainerImage, registry) {
continue
}
log.WithField("registry", registry).Debug("using authentication for registry")
registryAuth = &c
break
} }
// Get the ImagePullOptions.
imagePullOptions := types.ImagePullOptions{All: false}
if registryAuth != nil {
b64, err := registryAuth.Base64()
if err != nil {
log.WithError(err).Error("failed to get registry auth credentials")
}
// b64 is a string so if there is an error it will just be empty, not nil.
imagePullOptions.RegistryAuth = b64
}
r, err := ip.client.ImagePull(context.Background(), ip.Script.ContainerImage, imagePullOptions)
if err != nil {
images, ierr := ip.client.ImageList(context.Background(), types.ImageListOptions{})
if ierr != nil {
// Well damn, something has gone really wrong here, just go ahead and abort there
// isn't much anything we can do to try and self-recover from this.
return ierr
}
for _, img := range images {
for _, t := range img.RepoTags {
if t != ip.Script.ContainerImage {
continue
}
log.WithFields(log.Fields{
"image": ip.Script.ContainerImage,
"err": err.Error(),
}).Warn("unable to pull requested image from remote source, however the image exists locally")
// Okay, we found a matching container image, in that case just go ahead and return
// from this function, since there is nothing else we need to do here.
return nil
}
}
return err
}
defer r.Close()
log.WithField("image", ip.Script.ContainerImage).Debug("pulling docker image... this could take a bit of time")
// Block continuation until the image has been pulled successfully. // Block continuation until the image has been pulled successfully.
scanner := bufio.NewScanner(r) scanner := bufio.NewScanner(r)
for scanner.Scan() { for scanner.Scan() {
@@ -280,7 +302,7 @@ func (ip *InstallationProcess) pullInstallationImage() error {
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return errors.WithStack(err) return err
} }
return nil return nil
@@ -291,24 +313,14 @@ func (ip *InstallationProcess) pullInstallationImage() error {
// manner, if either one fails the error is returned. // manner, if either one fails the error is returned.
func (ip *InstallationProcess) BeforeExecute() error { func (ip *InstallationProcess) BeforeExecute() error {
if err := ip.writeScriptToDisk(); err != nil { if err := ip.writeScriptToDisk(); err != nil {
return errors.Wrap(err, "failed to write installation script to disk") return errors.WithMessage(err, "failed to write installation script to disk")
} }
if err := ip.pullInstallationImage(); err != nil { if err := ip.pullInstallationImage(); err != nil {
return errors.Wrap(err, "failed to pull updated installation container image for server") return errors.WithMessage(err, "failed to pull updated installation container image for server")
} }
if err := ip.RemoveContainer(); err != nil {
opts := types.ContainerRemoveOptions{ return errors.WithMessage(err, "failed to remove existing install container for server")
RemoveVolumes: true,
Force: true,
} }
if err := ip.client.ContainerRemove(ip.context, ip.Server.Id()+"_installer", opts); err != nil {
if !client.IsErrNotFound(err) {
return errors.Wrap(err, "failed to remove existing install container for server")
}
}
return nil return nil
} }
@@ -331,12 +343,12 @@ func (ip *InstallationProcess) AfterExecute(containerId string) error {
}) })
if err != nil && !client.IsErrNotFound(err) { if err != nil && !client.IsErrNotFound(err) {
return errors.WithStack(err) return err
} }
f, err := os.OpenFile(ip.GetLogPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) f, err := os.OpenFile(ip.GetLogPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil { if err != nil {
return errors.WithStack(err) return err
} }
defer f.Close() defer f.Close()
@@ -365,15 +377,15 @@ func (ip *InstallationProcess) AfterExecute(containerId string) error {
| ------------------------------ | ------------------------------
`) `)
if err != nil { if err != nil {
return errors.WithStack(err) return err
} }
if err := tmpl.Execute(f, ip); err != nil { if err := tmpl.Execute(f, ip); err != nil {
return errors.WithStack(err) return err
} }
if _, err := io.Copy(f, reader); err != nil { if _, err := io.Copy(f, reader); err != nil {
return errors.WithStack(err) return err
} }
return nil return nil
@@ -381,6 +393,12 @@ func (ip *InstallationProcess) AfterExecute(containerId string) error {
// Executes the installation process inside a specially created docker container. // Executes the installation process inside a specially created docker container.
func (ip *InstallationProcess) Execute() (string, error) { func (ip *InstallationProcess) Execute() (string, error) {
// Create a child context that is canceled once this function is done running. This
// will also be canceled if the parent context (from the Server struct) is canceled
// which occurs if the server is deleted.
ctx, cancel := context.WithCancel(ip.context)
defer cancel()
conf := &container.Config{ conf := &container.Config{
Hostname: "installer", Hostname: "installer",
AttachStdout: true, AttachStdout: true,
@@ -388,7 +406,7 @@ func (ip *InstallationProcess) Execute() (string, error) {
AttachStdin: true, AttachStdin: true,
OpenStdin: true, OpenStdin: true,
Tty: true, Tty: true,
Cmd: []string{ip.Script.Entrypoint, "./mnt/install/install.sh"}, Cmd: []string{ip.Script.Entrypoint, "/mnt/install/install.sh"},
Image: ip.Script.ContainerImage, Image: ip.Script.ContainerImage,
Env: ip.Server.GetEnvironmentVariables(), Env: ip.Server.GetEnvironmentVariables(),
Labels: map[string]string{ Labels: map[string]string{
@@ -402,7 +420,7 @@ func (ip *InstallationProcess) Execute() (string, error) {
Mounts: []mount.Mount{ Mounts: []mount.Mount{
{ {
Target: "/mnt/server", Target: "/mnt/server",
Source: ip.Server.Filesystem.Path(), Source: ip.Server.Filesystem().Path(),
Type: mount.TypeBind, Type: mount.TypeBind,
ReadOnly: false, ReadOnly: false,
}, },
@@ -439,31 +457,38 @@ func (ip *InstallationProcess) Execute() (string, error) {
} }
}() }()
r, err := ip.client.ContainerCreate(ip.context, conf, hostConf, nil, ip.Server.Id()+"_installer") r, err := ip.client.ContainerCreate(ctx, conf, hostConf, nil, nil, ip.Server.Id()+"_installer")
if err != nil { if err != nil {
return "", errors.WithStack(err)
}
ip.Server.Log().WithField("container_id", r.ID).Info("running installation script for server in container")
if err := ip.client.ContainerStart(ip.context, r.ID, types.ContainerStartOptions{}); err != nil {
return "", err return "", err
} }
ip.Server.Log().WithField("container_id", r.ID).Info("running installation script for server in container")
if err := ip.client.ContainerStart(ctx, r.ID, types.ContainerStartOptions{}); err != nil {
return "", err
}
// Process the install event in the background by listening to the stream output until the
// container has stopped, at which point we'll disconnect from it.
//
// If there is an error during the streaming output just report it and do nothing else, the
// install can still run, the console just won't have any output.
go func(id string) { go func(id string) {
ip.Server.Events().Publish(DaemonMessageEvent, "Starting installation process, this could take a few minutes...") ip.Server.Events().Publish(DaemonMessageEvent, "Starting installation process, this could take a few minutes...")
if err := ip.StreamOutput(id); err != nil { if err := ip.StreamOutput(ctx, id); err != nil {
ip.Server.Log().WithField("error", err).Error("error while handling output stream for server install process") ip.Server.Log().WithField("error", err).Warn("error connecting to server install stream output")
} }
ip.Server.Events().Publish(DaemonMessageEvent, "Installation process completed.")
}(r.ID) }(r.ID)
sChann, eChann := ip.client.ContainerWait(ip.context, r.ID, container.WaitConditionNotRunning) sChan, eChan := ip.client.ContainerWait(ctx, r.ID, container.WaitConditionNotRunning)
select { select {
case err := <-eChann: case err := <-eChan:
if err != nil { // Once the container has stopped running we can mark the install process as being completed.
return "", errors.WithStack(err) if err == nil {
ip.Server.Events().Publish(DaemonMessageEvent, "Installation process completed.")
} else {
return "", err
} }
case <-sChann: case <-sChan:
} }
return r.ID, nil return r.ID, nil
@@ -472,31 +497,25 @@ func (ip *InstallationProcess) Execute() (string, error) {
// Streams the output of the installation process to a log file in the server configuration // Streams the output of the installation process to a log file in the server configuration
// directory, as well as to a websocket listener so that the process can be viewed in // directory, as well as to a websocket listener so that the process can be viewed in
// the panel by administrators. // the panel by administrators.
func (ip *InstallationProcess) StreamOutput(id string) error { func (ip *InstallationProcess) StreamOutput(ctx context.Context, id string) error {
reader, err := ip.client.ContainerLogs(ip.context, id, types.ContainerLogsOptions{ reader, err := ip.client.ContainerLogs(ctx, id, types.ContainerLogsOptions{
ShowStdout: true, ShowStdout: true,
ShowStderr: true, ShowStderr: true,
Follow: true, Follow: true,
}) })
if err != nil { if err != nil {
return errors.WithStack(err) return err
} }
defer reader.Close() defer reader.Close()
s := bufio.NewScanner(reader) evts := ip.Server.Events()
for s.Scan() { err = system.ScanReader(reader, func(line string) {
ip.Server.Events().Publish(InstallOutputEvent, s.Text()) evts.Publish(InstallOutputEvent, line)
})
if err != nil {
ip.Server.Log().WithFields(log.Fields{"container_id": id, "error": err}).Warn("error processing install output lines")
} }
if err := s.Err(); err != nil {
ip.Server.Log().WithFields(log.Fields{
"container_id": id,
"error": errors.WithStack(err),
}).Warn("error processing scanner line in installation output for server")
}
return nil return nil
} }
@@ -505,15 +524,13 @@ func (ip *InstallationProcess) StreamOutput(id string) error {
// value of "true" means everything was successful, "false" means something went // value of "true" means everything was successful, "false" means something went
// wrong and the server must be deleted and re-created. // wrong and the server must be deleted and re-created.
func (s *Server) SyncInstallState(successful bool) error { func (s *Server) SyncInstallState(successful bool) error {
r := api.NewRequester() err := api.New().SendInstallationStatus(s.Id(), successful)
if err != nil {
rerr, err := r.SendInstallationStatus(s.Id(), successful) if !api.IsRequestError(err) {
if rerr != nil || err != nil { return err
if err != nil {
return errors.WithStack(err)
} }
return errors.New(rerr.String()) return errors.New(err.Error())
} }
return nil return nil

View File

@@ -2,59 +2,147 @@ package server
import ( import (
"encoding/json" "encoding/json"
"regexp"
"strconv"
"sync"
"github.com/apex/log" "github.com/apex/log"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/events" "github.com/pterodactyl/wings/events"
"regexp"
) )
// Adds all of the internal event listeners we want to use for a server. var dockerEvents = []string{
environment.DockerImagePullStatus,
environment.DockerImagePullStarted,
environment.DockerImagePullCompleted,
}
type diskSpaceLimiter struct {
o sync.Once
mu sync.Mutex
server *Server
}
func newDiskLimiter(s *Server) *diskSpaceLimiter {
return &diskSpaceLimiter{server: s}
}
// Reset the disk space limiter status.
func (dsl *diskSpaceLimiter) Reset() {
dsl.mu.Lock()
dsl.o = sync.Once{}
dsl.mu.Unlock()
}
// Trigger the disk space limiter which will attempt to stop a running server instance within
// 15 seconds, and terminate it forcefully if it does not stop.
//
// This function is only executed one time, so whenever a server is marked as booting the limiter
// should be reset so it can properly be triggered as needed.
func (dsl *diskSpaceLimiter) Trigger() {
dsl.o.Do(func() {
dsl.server.PublishConsoleOutputFromDaemon("Server is exceeding the assigned disk space limit, stopping process now.")
if err := dsl.server.Environment.WaitForStop(60, true); err != nil {
dsl.server.Log().WithField("error", err).Error("failed to stop server after exceeding space limit!")
}
})
}
// Adds all of the internal event listeners we want to use for a server. These listeners can only be
// removed by deleting the server as they should last for the duration of the process' lifetime.
func (s *Server) StartEventListeners() { func (s *Server) StartEventListeners() {
console := make(chan events.Event) console := func(e events.Event) {
state := make(chan events.Event) t := s.Throttler()
stats := make(chan events.Event) err := t.Increment(func() {
s.PublishConsoleOutputFromDaemon("Your server is outputting too much data and is being throttled.")
})
s.Environment.Events().Subscribe(environment.ConsoleOutputEvent, console) // An error is only returned if the server has breached the thresholds set.
s.Environment.Events().Subscribe(environment.StateChangeEvent, state) if err != nil {
s.Environment.Events().Subscribe(environment.ResourceEvent, stats) // If the process is already stopping, just let it continue with that action rather than attempting
// to terminate again.
if s.Environment.State() != environment.ProcessStoppingState {
s.Environment.SetState(environment.ProcessStoppingState)
// TODO: this is leaky I imagine since the routines aren't destroyed when the server is? go func() {
go func() { s.Log().Warn("stopping server instance, violating throttle limits")
for { s.PublishConsoleOutputFromDaemon("Your server is being stopped for outputting too much data in a short period of time.")
select {
case data := <-console:
// Immediately emit this event back over the server event stream since it is
// being called from the environment event stream and things probably aren't
// listening to that event.
s.Events().Publish(ConsoleOutputEvent, data.Data)
// Also pass the data along to the console output channel. // Completely skip over server power actions and terminate the running instance. This gives the
s.onConsoleOutput(data.Data) // server 15 seconds to finish stopping gracefully before it is forcefully terminated.
case data := <-state: if err := s.Environment.WaitForStop(config.Get().Throttles.StopGracePeriod, true); err != nil {
s.SetState(data.Data) // If there is an error set the process back to running so that this throttler is called
case data := <-stats: // again and hopefully kills the server.
st := new(environment.Stats) if s.Environment.State() != environment.ProcessOfflineState {
if err := json.Unmarshal([]byte(data.Data), st); err != nil { s.Environment.SetState(environment.ProcessRunningState)
s.Log().WithField("error", errors.WithStack(err)).Warn("failed to unmarshal server environment stats") }
continue
}
// Update the server resource tracking object with the resources we got here. s.Log().WithField("error", err).Error("failed to terminate environment after triggering throttle")
s.resources.mu.Lock() }
s.resources.Stats = *st }()
s.resources.mu.Unlock()
// TODO: we'll need to handle this better since calling it in rapid succession will
// cause it to block until the first call is done calculating disk usage, which will
// case stat events to pile up for the server.
s.Filesystem.HasSpaceAvailable()
s.emitProcUsage()
} }
} }
}()
// If we are not throttled, go ahead and output the data.
if !t.Throttled() {
s.Events().Publish(ConsoleOutputEvent, e.Data)
}
// Also pass the data along to the console output channel.
s.onConsoleOutput(e.Data)
}
l := newDiskLimiter(s)
state := func(e events.Event) {
// Reset the throttler when the process is started.
if e.Data == environment.ProcessStartingState {
l.Reset()
s.Throttler().Reset()
}
s.OnStateChange()
}
stats := func(e events.Event) {
st := new(environment.Stats)
if err := json.Unmarshal([]byte(e.Data), st); err != nil {
s.Log().WithField("error", err).Warn("failed to unmarshal server environment stats")
return
}
// Update the server resource tracking object with the resources we got here.
s.resources.mu.Lock()
s.resources.Stats = *st
s.resources.mu.Unlock()
// If there is no disk space available at this point, trigger the server disk limiter logic
// which will start to stop the running instance.
if !s.Filesystem().HasSpaceAvailable(true) {
l.Trigger()
}
s.emitProcUsage()
}
docker := func(e events.Event) {
if e.Topic == environment.DockerImagePullStatus {
s.Events().Publish(InstallOutputEvent, e.Data)
} else if e.Topic == environment.DockerImagePullStarted {
s.PublishConsoleOutputFromDaemon("Pulling Docker container image, this could take a few minutes to complete...")
} else {
s.PublishConsoleOutputFromDaemon("Finished pulling Docker container image")
}
}
s.Log().Info("registering event listeners: console, state, resources...")
s.Environment.Events().On(environment.ConsoleOutputEvent, &console)
s.Environment.Events().On(environment.StateChangeEvent, &state)
s.Environment.Events().On(environment.ResourceEvent, &stats)
for _, evt := range dockerEvents {
s.Environment.Events().On(evt, &docker)
}
} }
var stripAnsiRegex = regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))") var stripAnsiRegex = regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))")
@@ -66,7 +154,7 @@ func (s *Server) onConsoleOutput(data string) {
processConfiguration := s.ProcessConfiguration() processConfiguration := s.ProcessConfiguration()
// Check if the server is currently starting. // Check if the server is currently starting.
if s.GetState() == environment.ProcessStartingState { if s.Environment.State() == environment.ProcessStartingState {
// Check if we should strip ansi color codes. // Check if we should strip ansi color codes.
if processConfiguration.Startup.StripAnsi { if processConfiguration.Startup.StripAnsi {
// Strip ansi color codes from the data string. // Strip ansi color codes from the data string.
@@ -81,13 +169,13 @@ func (s *Server) onConsoleOutput(data string) {
s.Log().WithFields(log.Fields{ s.Log().WithFields(log.Fields{
"match": l.String(), "match": l.String(),
"against": data, "against": strconv.QuoteToASCII(data),
}).Debug("detected server in running state based on console line output") }).Debug("detected server in running state based on console line output")
// If the specific line of output is one that would mark the server as started, // If the specific line of output is one that would mark the server as started,
// set the server to that state. Only do this if the server is not currently stopped // set the server to that state. Only do this if the server is not currently stopped
// or stopping. // or stopping.
_ = s.SetState(environment.ProcessRunningState) s.Environment.SetState(environment.ProcessRunningState)
break break
} }
} }
@@ -99,7 +187,7 @@ func (s *Server) onConsoleOutput(data string) {
stop := processConfiguration.Stop stop := processConfiguration.Stop
if stop.Type == api.ProcessStopCommand && data == stop.Value { if stop.Type == api.ProcessStopCommand && data == stop.Value {
_ = s.SetState(environment.ProcessOfflineState) s.Environment.SetState(environment.ProcessOfflineState)
} }
} }
} }

View File

@@ -1,17 +1,21 @@
package server package server
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/apex/log"
"github.com/creasty/defaults"
"github.com/gammazero/workerpool"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/environment/docker"
"os" "os"
"path/filepath"
"runtime" "runtime"
"time" "time"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/gammazero/workerpool"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/environment/docker"
"github.com/pterodactyl/wings/server/filesystem"
) )
var servers = NewCollection(nil) var servers = NewCollection(nil)
@@ -28,40 +32,41 @@ func LoadDirectory() error {
} }
log.Info("fetching list of servers from API") log.Info("fetching list of servers from API")
configs, rerr, err := api.NewRequester().GetAllServerConfigurations() configs, err := api.New().GetServers()
if err != nil || rerr != nil { if err != nil {
if err != nil { if !api.IsRequestError(err) {
return errors.WithStack(err) return err
} }
return errors.New(rerr.String()) return errors.New(err.Error())
}
log.Debug("retrieving cached server states from disk")
states, err := getServerStates()
if err != nil {
log.WithField("error", errors.WithStack(err)).Error("failed to retrieve locally cached server states from disk, assuming all servers in offline state")
} }
start := time.Now() start := time.Now()
log.WithField("total_configs", len(configs)).Info("processing servers returned by the API") log.WithField("total_configs", len(configs)).Info("processing servers returned by the API")
pool := workerpool.New(runtime.NumCPU()) pool := workerpool.New(runtime.NumCPU())
for uuid, data := range configs { log.Debugf("using %d workerpools to instantiate server instances", runtime.NumCPU())
uuid := uuid for _, data := range configs {
data := data data := data
pool.Submit(func() { pool.Submit(func() {
log.WithField("server", uuid).Info("creating new server object from API response") // Parse the json.RawMessage into an expected struct value. We do this here so that a single broken
s, err := FromConfiguration(data) // server does not cause the entire boot process to hang, and allows us to show more useful error
if err != nil { // messaging in the output.
log.WithField("server", uuid).WithField("error", err).Error("failed to load server, skipping...") d := api.ServerConfigurationResponse{
Settings: data.Settings,
}
log.WithField("server", data.Uuid).Info("creating new server object from API response")
if err := json.Unmarshal(data.ProcessConfiguration, &d.ProcessConfiguration); err != nil {
log.WithField("server", data.Uuid).WithField("error", err).Error("failed to parse server configuration from API response, skipping...")
return return
} }
if state, exists := states[s.Id()]; exists { s, err := FromConfiguration(d)
s.Log().WithField("state", state).Debug("found existing server state in cache file; re-instantiating server state") if err != nil {
s.SetState(state) log.WithField("server", data.Uuid).WithField("error", err).Error("failed to load server, skipping...")
return
} }
servers.Add(s) servers.Add(s)
@@ -81,32 +86,30 @@ func LoadDirectory() error {
// Initializes a server using a data byte array. This will be marshaled into the // Initializes a server using a data byte array. This will be marshaled into the
// given struct using a YAML marshaler. This will also configure the given environment // given struct using a YAML marshaler. This will also configure the given environment
// for a server. // for a server.
func FromConfiguration(data *api.ServerConfigurationResponse) (*Server, error) { func FromConfiguration(data api.ServerConfigurationResponse) (*Server, error) {
cfg := Configuration{} s, err := New()
if err := defaults.Set(&cfg); err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to set struct defaults for server configuration") return nil, errors.WithMessage(err, "loader: failed to instantiate empty server struct")
} }
if err := s.UpdateDataStructure(data.Settings); err != nil {
s := new(Server)
if err := defaults.Set(s); err != nil {
return nil, errors.Wrap(err, "failed to set struct defaults for server")
}
s.cfg = cfg
if err := s.UpdateDataStructure(data.Settings, false); err != nil {
return nil, err return nil, err
} }
s.Archiver = Archiver{Server: s} s.Archiver = Archiver{Server: s}
s.Filesystem = Filesystem{Server: s} s.fs = filesystem.New(filepath.Join(config.Get().System.Data, s.Id()), s.DiskSpace())
// Right now we only support a Docker based environment, so I'm going to hard code // Right now we only support a Docker based environment, so I'm going to hard code
// this logic in. When we're ready to support other environment we'll need to make // this logic in. When we're ready to support other environment we'll need to make
// some modifications here obviously. // some modifications here obviously.
envCfg := environment.NewConfiguration(s.Mounts(), s.cfg.Allocations, s.cfg.Build, s.cfg.EnvVars) settings := environment.Settings{
Mounts: s.Mounts(),
Allocations: s.cfg.Allocations,
Limits: s.cfg.Build,
}
envCfg := environment.NewConfiguration(settings, s.GetEnvironmentVariables())
meta := docker.Metadata{ meta := docker.Metadata{
Invocation: s.Config().Invocation, Image: s.Config().Container.Image,
Image: s.Config().Container.Image,
} }
if env, err := docker.New(s.Id(), &meta, envCfg); err != nil { if env, err := docker.New(s.Id(), &meta, envCfg); err != nil {
@@ -114,6 +117,7 @@ func FromConfiguration(data *api.ServerConfigurationResponse) (*Server, error) {
} else { } else {
s.Environment = env s.Environment = env
s.StartEventListeners() s.StartEventListeners()
s.Throttler().StartTimer(s.Context())
} }
// Forces the configuration to be synced with the panel. // Forces the configuration to be synced with the panel.
@@ -122,8 +126,8 @@ func FromConfiguration(data *api.ServerConfigurationResponse) (*Server, error) {
} }
// If the server's data directory exists, force disk usage calculation. // If the server's data directory exists, force disk usage calculation.
if _, err := os.Stat(s.Filesystem.Path()); err == nil { if _, err := os.Stat(s.Filesystem().Path()); err == nil {
go s.Filesystem.HasSpaceAvailable() s.Filesystem().HasSpaceAvailable(true)
} }
return s, nil return s, nil

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