Compare commits
14 Commits
v1.6.4
...
dane/sqlit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4634c93182 | ||
|
|
8a867ccc44 | ||
|
|
61baccb1a3 | ||
|
|
7bd11c1c28 | ||
|
|
e1e7916790 | ||
|
|
f28e06267c | ||
|
|
59fbd2bcea | ||
|
|
204a4375fc | ||
|
|
dda7d10d37 | ||
|
|
ed330fa6be | ||
|
|
9864a0fe34 | ||
|
|
214baf83fb | ||
|
|
41fc1973d1 | ||
|
|
a51ce6f4ac |
@@ -1,19 +1,18 @@
|
|||||||
# Stage 1 (Build)
|
# Stage 1 (Build)
|
||||||
FROM --platform=$BUILDPLATFORM golang:1.17-alpine AS builder
|
FROM golang:1.17-alpine AS builder
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
RUN apk add --update --no-cache git make upx
|
RUN apk add --update --no-cache git make
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
COPY go.mod go.sum /app/
|
COPY go.mod go.sum /app/
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . /app/
|
COPY . /app/
|
||||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
RUN CGO_ENABLED=0 go build \
|
||||||
-ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=$VERSION" \
|
-ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=$VERSION" \
|
||||||
-v \
|
-v \
|
||||||
-trimpath \
|
-trimpath \
|
||||||
-o wings \
|
-o wings \
|
||||||
wings.go
|
wings.go
|
||||||
RUN upx wings
|
|
||||||
RUN echo "ID=\"distroless\"" > /etc/os-release
|
RUN echo "ID=\"distroless\"" > /etc/os-release
|
||||||
|
|
||||||
# Stage 2 (Final)
|
# Stage 2 (Final)
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -19,22 +19,17 @@ I would like to extend my sincere thanks to the following sponsors for helping f
|
|||||||
| Company | About |
|
| Company | About |
|
||||||
| ------- | ----- |
|
| ------- | ----- |
|
||||||
| [**WISP**](https://wisp.gg) | Extra features. |
|
| [**WISP**](https://wisp.gg) | Extra features. |
|
||||||
| [**MixmlHosting**](https://mixmlhosting.com) | MixmlHosting provides high quality Virtual Private Servers along with game servers, all at a affordable price. |
|
|
||||||
| [**BisectHosting**](https://www.bisecthosting.com/) | BisectHosting provides Minecraft, Valheim and other server hosting services with the highest reliability and lightning fast support since 2012. |
|
| [**BisectHosting**](https://www.bisecthosting.com/) | BisectHosting provides Minecraft, Valheim and other server hosting services with the highest reliability and lightning fast support since 2012. |
|
||||||
|
| [**Fragnet**](https://fragnet.net) | Providing low latency, high-end game hosting solutions to gamers, game studios and eSports platforms. |
|
||||||
|
| [**Tempest**](https://tempest.net/) | Tempest Hosting is a subsidiary of Path Network, Inc. offering unmetered DDoS protected 10Gbps dedicated servers, starting at just $80/month. Full anycast, tons of filters. |
|
||||||
| [**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. |
|
| [**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. |
|
| [**MineStrator**](https://minestrator.com/) | Looking for the most highend French hosting company for your minecraft server? More than 24,000 members on our discord trust us. Give us a try! |
|
||||||
| [**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! |
|
| [**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 RoyaleHosting’s 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 for inexpensive 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. |
|
| [**DeinServerHost**](https://deinserverhost.de/) | DeinServerHost offers Dedicated, vps and Gameservers for many popular Games like Minecraft and Rust in Germany since 2013. |
|
||||||
| [**HostBend**](https://hostbend.com/) | HostBend offers a variety of solutions for developers, students, and others who have a tight budget but don't want to compromise quality and support. |
|
|
||||||
| [**Capitol Hosting Solutions**](https://chs.gg/) | CHS is *the* budget friendly hosting company for Australian and American gamers, offering a variety of plans from Web Hosting to Game Servers; Custom Solutions too! |
|
|
||||||
| [**ByteAnia**](https://byteania.com/?utm_source=pterodactyl) | ByteAnia offers the best performing and most affordable **Ryzen 5000 Series hosting** on the market for *unbeatable prices*! |
|
|
||||||
| [**Aussie Server Hosts**](https://aussieserverhosts.com/) | No frills Australian Owned and operated High Performance Server hosting for some of the most demanding games serving Australia and New Zealand. |
|
| [**Aussie Server Hosts**](https://aussieserverhosts.com/) | No frills Australian Owned and operated High Performance Server hosting for some of the most demanding games serving Australia and New Zealand. |
|
||||||
|
| [**HostEZ**](https://hostez.io) | Providing North America Valheim, Minecraft and other popular games with low latency, high uptime and maximum availability. EZ! |
|
||||||
| [**VibeGAMES**](https://vibegames.net/) | VibeGAMES is a game server provider that specializes in DDOS protection for the games we offer. We have multiple locations in the US, Brazil, France, Germany, Singapore, Australia and South Africa.|
|
| [**VibeGAMES**](https://vibegames.net/) | VibeGAMES is a game server provider that specializes in DDOS protection for the games we offer. We have multiple locations in the US, Brazil, France, Germany, Singapore, Australia and South Africa.|
|
||||||
| [**RocketNode**](https://rocketnode.net) | RocketNode is a VPS and Game Server provider that offers the best performing VPS and Game hosting Solutions at affordable prices! |
|
| [**Gamenodes**](https://gamenodes.nl) | Gamenodes love quality. For Minecraft, Discord Bots and other services, among others. With our own programmers, we provide just that little bit of extra service! |
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
|
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
|
||||||
|
|||||||
13
cmd/root.go
13
cmd/root.go
@@ -5,6 +5,8 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/pterodactyl/wings/internal/cron"
|
||||||
|
"github.com/pterodactyl/wings/internal/database"
|
||||||
log2 "log"
|
log2 "log"
|
||||||
"net/http"
|
"net/http"
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
@@ -130,6 +132,10 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if err := database.Initialize(); err != nil {
|
||||||
|
log.WithField("error", err).Fatal("failed to initialize database")
|
||||||
|
}
|
||||||
|
|
||||||
manager, err := server.NewManager(cmd.Context(), pclient)
|
manager, err := server.NewManager(cmd.Context(), pclient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithField("error", err).Fatal("failed to load server configurations")
|
log.WithField("error", err).Fatal("failed to load server configurations")
|
||||||
@@ -259,6 +265,13 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if s, err := cron.Scheduler(cmd.Context(), manager); err != nil {
|
||||||
|
log.WithField("error", err).Fatal("failed to initialize cron system")
|
||||||
|
} else {
|
||||||
|
log.WithField("subsystem", "cron").Info("starting cron processes")
|
||||||
|
s.StartAsync()
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
// Run the SFTP server.
|
// Run the SFTP server.
|
||||||
if err := sftp.New(manager).Run(); err != nil {
|
if err := sftp.New(manager).Run(); err != nil {
|
||||||
|
|||||||
@@ -163,6 +163,15 @@ type SystemConfiguration struct {
|
|||||||
// disk usage is not a concern.
|
// disk usage is not a concern.
|
||||||
DiskCheckInterval int64 `default:"150" yaml:"disk_check_interval"`
|
DiskCheckInterval int64 `default:"150" yaml:"disk_check_interval"`
|
||||||
|
|
||||||
|
// ActivitySendInterval is the amount of time that should ellapse between aggregated server activity
|
||||||
|
// being sent to the Panel. By default this will send activity collected over the last minute. Keep
|
||||||
|
// in mind that only a fixed number of activity log entries, defined by ActivitySendCount, will be sent
|
||||||
|
// in each run.
|
||||||
|
ActivitySendInterval int64 `default:"60" yaml:"activity_send_interval"`
|
||||||
|
|
||||||
|
// ActivitySendCount is the number of activity events to send per batch.
|
||||||
|
ActivitySendCount int64 `default:"100" yaml:"activity_send_count"`
|
||||||
|
|
||||||
// If set to true, file permissions for a server will be checked when the process is
|
// 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
|
// 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
|
// cases disabling this should not have any major impact unless external processes are
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type DockerNetworkConfiguration struct {
|
|||||||
Mode string `default:"pterodactyl_nw" yaml:"network_mode"`
|
Mode string `default:"pterodactyl_nw" yaml:"network_mode"`
|
||||||
IsInternal bool `default:"false" yaml:"is_internal"`
|
IsInternal bool `default:"false" yaml:"is_internal"`
|
||||||
EnableICC bool `default:"true" yaml:"enable_icc"`
|
EnableICC bool `default:"true" yaml:"enable_icc"`
|
||||||
|
NetworkMTU int64 `default:"1500" yaml:"network_mtu"`
|
||||||
Interfaces dockerNetworkInterfaces `yaml:"interfaces"`
|
Interfaces dockerNetworkInterfaces `yaml:"interfaces"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func createDockerNetwork(ctx context.Context, cli *client.Client) error {
|
|||||||
"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",
|
||||||
"com.docker.network.driver.mtu": "1500",
|
"com.docker.network.driver.mtu": strconv.FormatInt(nw.NetworkMTU, 10),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
24
go.mod
24
go.mod
@@ -37,16 +37,22 @@ require (
|
|||||||
github.com/pkg/sftp v1.13.4
|
github.com/pkg/sftp v1.13.4
|
||||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
|
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
|
||||||
github.com/spf13/cobra v1.4.0
|
github.com/spf13/cobra v1.4.0
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.5
|
||||||
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
|
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||||
gopkg.in/ini.v1 v1.66.4
|
gopkg.in/ini.v1 v1.66.4
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/goccy/go-json v0.9.6
|
require (
|
||||||
|
github.com/glebarez/sqlite v1.4.6
|
||||||
|
github.com/go-co-op/gocron v1.15.0
|
||||||
|
github.com/goccy/go-json v0.9.6
|
||||||
|
github.com/klauspost/compress v1.15.1
|
||||||
|
gorm.io/gorm v1.23.8
|
||||||
|
)
|
||||||
|
|
||||||
require golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect
|
require golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||||
@@ -65,6 +71,7 @@ require (
|
|||||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||||
github.com/gammazero/deque v0.1.1 // indirect
|
github.com/gammazero/deque v0.1.1 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/glebarez/go-sqlite v1.17.3 // indirect
|
||||||
github.com/go-playground/locales v0.14.0 // indirect
|
github.com/go-playground/locales v0.14.0 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.10.1 // indirect
|
github.com/go-playground/validator/v10 v10.10.1 // indirect
|
||||||
@@ -73,9 +80,10 @@ require (
|
|||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/gorilla/mux v1.7.4 // indirect
|
github.com/gorilla/mux v1.7.4 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/klauspost/compress v1.15.1 // indirect
|
|
||||||
github.com/kr/fs v0.1.0 // indirect
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
github.com/magefile/mage v1.13.0 // indirect
|
github.com/magefile/mage v1.13.0 // indirect
|
||||||
@@ -96,6 +104,8 @@ require (
|
|||||||
github.com/prometheus/client_model v0.2.0 // indirect
|
github.com/prometheus/client_model v0.2.0 // indirect
|
||||||
github.com/prometheus/common v0.32.1 // indirect
|
github.com/prometheus/common v0.32.1 // indirect
|
||||||
github.com/prometheus/procfs v0.7.3 // indirect
|
github.com/prometheus/procfs v0.7.3 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||||
@@ -111,5 +121,9 @@ require (
|
|||||||
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb // indirect
|
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb // indirect
|
||||||
google.golang.org/grpc v1.45.0 // indirect
|
google.golang.org/grpc v1.45.0 // indirect
|
||||||
google.golang.org/protobuf v1.28.0 // indirect
|
google.golang.org/protobuf v1.28.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
modernc.org/libc v1.16.17 // indirect
|
||||||
|
modernc.org/mathutil v1.4.1 // indirect
|
||||||
|
modernc.org/memory v1.1.1 // indirect
|
||||||
|
modernc.org/sqlite v1.17.3 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
60
go.sum
60
go.sum
@@ -400,6 +400,12 @@ 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.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
|
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
|
||||||
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
|
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
|
||||||
|
github.com/glebarez/go-sqlite v1.17.3 h1:Rji9ROVSTTfjuWD6j5B+8DtkNvPILoUC3xRhkQzGxvk=
|
||||||
|
github.com/glebarez/go-sqlite v1.17.3/go.mod h1:Hg+PQuhUy98XCxWEJEaWob8x7lhJzhNYF1nZbUiRGIY=
|
||||||
|
github.com/glebarez/sqlite v1.4.6 h1:D5uxD2f6UJ82cHnVtO2TZ9pqsLyto3fpDKHIk2OsR8A=
|
||||||
|
github.com/glebarez/sqlite v1.4.6/go.mod h1:WYEtEFjhADPaPJqL/PGlbQQGINBA3eUAfDNbKFJf/zA=
|
||||||
|
github.com/go-co-op/gocron v1.15.0 h1:XmiPazahD9aq0/QdK5toCVHfgTXfrZ/s83RpAgzr6SM=
|
||||||
|
github.com/go-co-op/gocron v1.15.0/go.mod h1:On9zUZTv7FBeuj9D/cdYyAWcPUiLqqAx7nsPHd0EmKM=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
@@ -607,6 +613,11 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
|
|||||||
github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ=
|
github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ=
|
||||||
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
|
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
|
||||||
github.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw=
|
github.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
@@ -697,6 +708,7 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp
|
|||||||
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||||
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
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/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
|
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
@@ -868,6 +880,10 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
|
|||||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||||
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/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
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/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
@@ -932,14 +948,17 @@ github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
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/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
|
||||||
|
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||||
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||||
@@ -1290,12 +1309,14 @@ golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs=
|
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
@@ -1379,6 +1400,7 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc
|
|||||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||||
golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
@@ -1555,8 +1577,11 @@ 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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/gorm v1.23.8 h1:h8sGJ+biDgBA1AD1Ha9gFCx7h8npU7AsLdlkX0n2TpE=
|
||||||
|
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||||
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/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||||
@@ -1612,6 +1637,33 @@ k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
|
|||||||
k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
|
k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
|
||||||
k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
|
k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
|
||||||
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
|
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
|
||||||
|
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||||
|
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
|
||||||
|
modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
|
||||||
|
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
|
||||||
|
modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
||||||
|
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
||||||
|
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||||
|
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||||
|
modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
|
||||||
|
modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
|
||||||
|
modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU=
|
||||||
|
modernc.org/libc v1.16.7/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
|
||||||
|
modernc.org/libc v1.16.8/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
|
||||||
|
modernc.org/libc v1.16.17 h1:rXo8IZJvP+QSN1KrlV23dtkM3XfGYXjx3RbLLzBtndM=
|
||||||
|
modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
|
||||||
|
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||||
|
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
|
||||||
|
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||||
|
modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU=
|
||||||
|
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
|
||||||
|
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
|
modernc.org/sqlite v1.17.3 h1:iE+coC5g17LtByDYDWKpR6m2Z9022YrSh3bumwOnIrI=
|
||||||
|
modernc.org/sqlite v1.17.3/go.mod h1:10hPVYar9C0kfXuTWGz8s0XtB8uAGymUy51ZzStYe3k=
|
||||||
|
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||||
|
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
|
||||||
|
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
|
|||||||
57
internal/cron/activity_cron.go
Normal file
57
internal/cron/activity_cron.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package cron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"emperror.dev/errors"
|
||||||
|
"github.com/pterodactyl/wings/internal/database"
|
||||||
|
"github.com/pterodactyl/wings/internal/models"
|
||||||
|
"github.com/pterodactyl/wings/server"
|
||||||
|
"github.com/pterodactyl/wings/system"
|
||||||
|
)
|
||||||
|
|
||||||
|
type activityCron struct {
|
||||||
|
mu *system.AtomicBool
|
||||||
|
manager *server.Manager
|
||||||
|
max int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes the cronjob and ensures we fetch and send all of the stored activity to the
|
||||||
|
// Panel instance. Once activity is sent it is deleted from the local database instance. Any
|
||||||
|
// SFTP specific events are not handled in this cron, they're handled seperately to account
|
||||||
|
// for de-duplication and event merging.
|
||||||
|
func (ac *activityCron) Run(ctx context.Context) error {
|
||||||
|
// Don't execute this cron if there is currently one running. Once this task is completed
|
||||||
|
// go ahead and mark it as no longer running.
|
||||||
|
if !ac.mu.SwapIf(true) {
|
||||||
|
return errors.WithStack(ErrCronRunning)
|
||||||
|
}
|
||||||
|
defer ac.mu.Store(false)
|
||||||
|
|
||||||
|
var activity []models.Activity
|
||||||
|
tx := database.Instance().WithContext(ctx).
|
||||||
|
Where("event NOT LIKE ?", "server:sftp.%").
|
||||||
|
Limit(int(ac.max)).
|
||||||
|
Find(&activity)
|
||||||
|
|
||||||
|
if tx.Error != nil {
|
||||||
|
return errors.WithStack(tx.Error)
|
||||||
|
}
|
||||||
|
if len(activity) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ac.manager.Client().SendActivityLogs(ctx, activity); err != nil {
|
||||||
|
return errors.WrapIf(err, "cron: failed to send activity events to Panel")
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids []int
|
||||||
|
for _, v := range activity {
|
||||||
|
ids = append(ids, v.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx = database.Instance().WithContext(ctx).Where("id IN ?", ids).Delete(&models.Activity{})
|
||||||
|
if tx.Error != nil {
|
||||||
|
return errors.WithStack(tx.Error)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
64
internal/cron/cron.go
Normal file
64
internal/cron/cron.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package cron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"emperror.dev/errors"
|
||||||
|
"github.com/apex/log"
|
||||||
|
"github.com/go-co-op/gocron"
|
||||||
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/pterodactyl/wings/server"
|
||||||
|
"github.com/pterodactyl/wings/system"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ErrCronRunning = errors.Sentinel("cron: job already running")
|
||||||
|
|
||||||
|
var o system.AtomicBool
|
||||||
|
|
||||||
|
// Scheduler configures the internal cronjob system for Wings and returns the scheduler
|
||||||
|
// instance to the caller. This should only be called once per application lifecycle, additional
|
||||||
|
// calls will result in an error being returned.
|
||||||
|
func Scheduler(ctx context.Context, m *server.Manager) (*gocron.Scheduler, error) {
|
||||||
|
if !o.SwapIf(true) {
|
||||||
|
return nil, errors.New("cron: cannot call scheduler more than once in application lifecycle")
|
||||||
|
}
|
||||||
|
l, err := time.LoadLocation(config.Get().System.Timezone)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cron: failed to parse configured system timezone")
|
||||||
|
}
|
||||||
|
|
||||||
|
activity := activityCron{
|
||||||
|
mu: system.NewAtomicBool(false),
|
||||||
|
manager: m,
|
||||||
|
max: config.Get().System.ActivitySendCount,
|
||||||
|
}
|
||||||
|
|
||||||
|
sftp := sftpCron{
|
||||||
|
mu: system.NewAtomicBool(false),
|
||||||
|
manager: m,
|
||||||
|
max: config.Get().System.ActivitySendCount,
|
||||||
|
}
|
||||||
|
|
||||||
|
s := gocron.NewScheduler(l)
|
||||||
|
_, _ = s.Tag("activity").Every(config.Get().System.ActivitySendInterval).Seconds().Do(func() {
|
||||||
|
if err := activity.Run(ctx); err != nil {
|
||||||
|
if errors.Is(err, ErrCronRunning) {
|
||||||
|
log.WithField("cron", "activity").Warn("activity process is already running, skipping...")
|
||||||
|
} else {
|
||||||
|
log.WithField("cron", "activity").WithField("error", err).Error("activity process failed to execute")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
_, _ = s.Tag("sftp").Every(config.Get().System.ActivitySendInterval).Seconds().Do(func() {
|
||||||
|
if err := sftp.Run(ctx); err != nil {
|
||||||
|
if errors.Is(err, ErrCronRunning) {
|
||||||
|
log.WithField("cron", "sftp").Warn("sftp events process already running, skipping...")
|
||||||
|
} else {
|
||||||
|
log.WithField("cron", "sftp").WithField("error", err).Error("sftp events process failed to execute")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
180
internal/cron/sftp_cron.go
Normal file
180
internal/cron/sftp_cron.go
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
package cron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"emperror.dev/errors"
|
||||||
|
"github.com/pterodactyl/wings/internal/database"
|
||||||
|
"github.com/pterodactyl/wings/internal/models"
|
||||||
|
"github.com/pterodactyl/wings/server"
|
||||||
|
"github.com/pterodactyl/wings/system"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sftpCron struct {
|
||||||
|
mu *system.AtomicBool
|
||||||
|
manager *server.Manager
|
||||||
|
max int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type mapKey struct {
|
||||||
|
User string
|
||||||
|
Server string
|
||||||
|
IP string
|
||||||
|
Event models.Event
|
||||||
|
Timestamp string
|
||||||
|
}
|
||||||
|
|
||||||
|
type eventMap struct {
|
||||||
|
max int
|
||||||
|
ids []int
|
||||||
|
m map[mapKey]*models.Activity
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes the SFTP reconciliation cron. This job will pull all of the SFTP specific events
|
||||||
|
// and merge them together across user, server, ip, and event. This allows a SFTP event that deletes
|
||||||
|
// tens or hundreds of files to be tracked as a single "deletion" event so long as they all occur
|
||||||
|
// within the same one minute period of time (starting at the first timestamp for the group). Without
|
||||||
|
// this we'd end up flooding the Panel event log with excessive data that is of no use to end users.
|
||||||
|
func (sc *sftpCron) Run(ctx context.Context) error {
|
||||||
|
if !sc.mu.SwapIf(true) {
|
||||||
|
return errors.WithStack(ErrCronRunning)
|
||||||
|
}
|
||||||
|
defer sc.mu.Store(false)
|
||||||
|
|
||||||
|
var o int
|
||||||
|
activity, err := sc.fetchRecords(ctx, o)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o += len(activity)
|
||||||
|
|
||||||
|
events := &eventMap{
|
||||||
|
m: map[mapKey]*models.Activity{},
|
||||||
|
ids: []int{},
|
||||||
|
max: int(sc.max),
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if len(activity) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
slen := len(events.ids)
|
||||||
|
for _, a := range activity {
|
||||||
|
events.Push(a)
|
||||||
|
}
|
||||||
|
if len(events.ids) > slen {
|
||||||
|
// Execute the query again, we found some events so we want to continue
|
||||||
|
// with this. Start at the next offset.
|
||||||
|
activity, err = sc.fetchRecords(ctx, o)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
o += len(activity)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(events.m) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = database.Instance().Transaction(func(tx *gorm.DB) error {
|
||||||
|
tx.Where("id IN ?", events.ids).Delete(&models.Activity{})
|
||||||
|
if tx.Error != nil {
|
||||||
|
return tx.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return sc.manager.Client().SendActivityLogs(ctx, events.Elements())
|
||||||
|
})
|
||||||
|
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchRecords returns a group of activity events starting at the given offset. This is used
|
||||||
|
// since we might need to make multiple database queries to select enough events to properly
|
||||||
|
// fill up our request to the given maximum. This is due to the fact that this cron merges any
|
||||||
|
// activity that line up across user, server, ip, and event into a single activity record when
|
||||||
|
// sending the data to the Panel.
|
||||||
|
func (sc *sftpCron) fetchRecords(ctx context.Context, offset int) (activity []models.Activity, err error) {
|
||||||
|
tx := database.Instance().WithContext(ctx).
|
||||||
|
Where("event LIKE ?", "server:sftp.%").
|
||||||
|
Order("event DESC").
|
||||||
|
Offset(offset).
|
||||||
|
Limit(int(sc.max)).
|
||||||
|
Find(&activity)
|
||||||
|
if tx.Error != nil {
|
||||||
|
err = errors.WithStack(tx.Error)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push adds an activity to the event mapping, or de-duplicates it and merges the files metadata
|
||||||
|
// into the existing entity that exists.
|
||||||
|
func (em *eventMap) Push(a models.Activity) {
|
||||||
|
m := em.forActivity(a)
|
||||||
|
// If no activity entity is returned we've hit the cap for the number of events to
|
||||||
|
// send along to the Panel. Just skip over this record and we'll account for it in
|
||||||
|
// the next iteration.
|
||||||
|
if m == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
em.ids = append(em.ids, a.ID)
|
||||||
|
// Always reduce this to the first timestamp that was recorded for the set
|
||||||
|
// of events, and not
|
||||||
|
if a.Timestamp.Before(m.Timestamp) {
|
||||||
|
m.Timestamp = a.Timestamp
|
||||||
|
}
|
||||||
|
list := m.Metadata["files"].([]interface{})
|
||||||
|
if s, ok := a.Metadata["files"]; ok {
|
||||||
|
v := reflect.ValueOf(s)
|
||||||
|
if v.Kind() != reflect.Slice || v.IsNil() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
list = append(list, v.Index(i).Interface())
|
||||||
|
}
|
||||||
|
// You must set it again at the end of the process, otherwise you've only updated the file
|
||||||
|
// slice in this one loop since it isn't passed by reference. This is just shorter than having
|
||||||
|
// to explicitly keep casting it to the slice.
|
||||||
|
m.Metadata["files"] = list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Elements returns the finalized activity models.
|
||||||
|
func (em *eventMap) Elements() (out []models.Activity) {
|
||||||
|
for _, v := range em.m {
|
||||||
|
out = append(out, *v)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// forActivity returns an event entity from our map which allows existing matches to be
|
||||||
|
// updated with additional files.
|
||||||
|
func (em *eventMap) forActivity(a models.Activity) *models.Activity {
|
||||||
|
key := mapKey{
|
||||||
|
User: a.User.String,
|
||||||
|
Server: a.Server,
|
||||||
|
IP: a.IP,
|
||||||
|
Event: a.Event,
|
||||||
|
// We group by the minute, don't care about the seconds for this logic.
|
||||||
|
Timestamp: a.Timestamp.Format("2006-01-02_15:04"),
|
||||||
|
}
|
||||||
|
if v, ok := em.m[key]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
// Cap the size of the events map at the defined maximum events to send to the Panel. Just
|
||||||
|
// return nil and let the caller handle it.
|
||||||
|
if len(em.m) >= em.max {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Doesn't exist in our map yet, create a copy of the activity passed into this
|
||||||
|
// function and then assign it into the map with an empty metadata value.
|
||||||
|
v := a
|
||||||
|
v.Metadata = models.ActivityMeta{
|
||||||
|
"files": make([]interface{}, 0),
|
||||||
|
}
|
||||||
|
em.m[key] = &v
|
||||||
|
return &v
|
||||||
|
}
|
||||||
41
internal/database/database.go
Normal file
41
internal/database/database.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"emperror.dev/errors"
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/pterodactyl/wings/internal/models"
|
||||||
|
"github.com/pterodactyl/wings/system"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
var o system.AtomicBool
|
||||||
|
var db *gorm.DB
|
||||||
|
|
||||||
|
// Initialize configures the local SQLite database for Wings and ensures that the models have
|
||||||
|
// been fully migrated.
|
||||||
|
func Initialize() error {
|
||||||
|
if !o.SwapIf(true) {
|
||||||
|
panic("database: attempt to initialize more than once during application lifecycle")
|
||||||
|
}
|
||||||
|
p := filepath.Join(config.Get().System.RootDirectory, "wings.db")
|
||||||
|
instance, err := gorm.Open(sqlite.Open(p), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "database: could not open database file")
|
||||||
|
}
|
||||||
|
db = instance
|
||||||
|
if err := db.AutoMigrate(&models.Activity{}); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance returns the gorm database instance that was configured when the application was
|
||||||
|
// booted.
|
||||||
|
func Instance() *gorm.DB {
|
||||||
|
if db == nil {
|
||||||
|
panic("database: attempt to access instance before initialized")
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
64
internal/models/activity.go
Normal file
64
internal/models/activity.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pterodactyl/wings/system"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event string
|
||||||
|
|
||||||
|
type ActivityMeta map[string]interface{}
|
||||||
|
|
||||||
|
// Activity defines an activity log event for a server entity performed by a user. This is
|
||||||
|
// used for tracking commands, power actions, and SFTP events so that they can be reconciled
|
||||||
|
// and sent back to the Panel instance to be displayed to the user.
|
||||||
|
type Activity struct {
|
||||||
|
ID int `gorm:"primaryKey;not null" json:"-"`
|
||||||
|
// User is UUID of the user that triggered this event, or an empty string if the event
|
||||||
|
// cannot be tied to a specific user, in which case we will assume it was the system
|
||||||
|
// user.
|
||||||
|
User JsonNullString `gorm:"type:uuid" json:"user"`
|
||||||
|
// Server is the UUID of the server this event is associated with.
|
||||||
|
Server string `gorm:"type:uuid;not null" json:"server"`
|
||||||
|
// Event is a string that describes what occurred, and is used by the Panel instance to
|
||||||
|
// properly associate this event in the activity logs.
|
||||||
|
Event Event `gorm:"index;not null" json:"event"`
|
||||||
|
// Metadata is either a null value, string, or a JSON blob with additional event specific
|
||||||
|
// metadata that can be provided.
|
||||||
|
Metadata ActivityMeta `gorm:"serializer:json" json:"metadata"`
|
||||||
|
// IP is the IP address that triggered this event, or an empty string if it cannot be
|
||||||
|
// determined properly. This should be the connecting user's IP address, and not the
|
||||||
|
// internal system IP.
|
||||||
|
IP string `gorm:"not null" json:"ip"`
|
||||||
|
Timestamp time.Time `gorm:"not null" json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUser sets the current user that performed the action. If an empty string is provided
|
||||||
|
// it is cast into a null value when stored.
|
||||||
|
func (a Activity) SetUser(u string) *Activity {
|
||||||
|
var ns JsonNullString
|
||||||
|
if u == "" {
|
||||||
|
if err := ns.Scan(nil); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := ns.Scan(u); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.User = ns
|
||||||
|
return &a
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate executes before we create any activity entry to ensure the IP address
|
||||||
|
// is trimmed down to remove any extraneous data, and the timestamp is set to the current
|
||||||
|
// system time and then stored as UTC.
|
||||||
|
func (a *Activity) BeforeCreate(_ *gorm.DB) error {
|
||||||
|
a.IP = system.TrimIPSuffix(a.IP)
|
||||||
|
if a.Timestamp.IsZero() {
|
||||||
|
a.Timestamp = time.Now()
|
||||||
|
}
|
||||||
|
a.Timestamp = a.Timestamp.UTC()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
31
internal/models/models.go
Normal file
31
internal/models/models.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"emperror.dev/errors"
|
||||||
|
"github.com/goccy/go-json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JsonNullString struct {
|
||||||
|
sql.NullString
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v JsonNullString) MarshalJSON() ([]byte, error) {
|
||||||
|
if v.Valid {
|
||||||
|
return json.Marshal(v.String)
|
||||||
|
} else {
|
||||||
|
return json.Marshal(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *JsonNullString) UnmarshalJSON(data []byte) error {
|
||||||
|
var s *string
|
||||||
|
if err := json.Unmarshal(data, &s); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
if s != nil {
|
||||||
|
v.String = *s
|
||||||
|
}
|
||||||
|
v.Valid = s != nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/pterodactyl/wings/internal/models"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -30,6 +31,7 @@ type Client interface {
|
|||||||
SetInstallationStatus(ctx context.Context, uuid string, successful bool) error
|
SetInstallationStatus(ctx context.Context, uuid string, successful bool) error
|
||||||
SetTransferStatus(ctx context.Context, uuid string, successful bool) error
|
SetTransferStatus(ctx context.Context, uuid string, successful bool) error
|
||||||
ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error)
|
ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error)
|
||||||
|
SendActivityLogs(ctx context.Context, activity []models.Activity) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type client struct {
|
type client struct {
|
||||||
@@ -128,10 +130,19 @@ func (c *client) requestOnce(ctx context.Context, method, path string, body io.R
|
|||||||
// and adds the required authentication headers to the request that is being
|
// and adds the required authentication headers to the request that is being
|
||||||
// created. Errors returned will be of the RequestError type if there was some
|
// created. Errors returned will be of the RequestError type if there was some
|
||||||
// type of response from the API that can be parsed.
|
// type of response from the API that can be parsed.
|
||||||
func (c *client) request(ctx context.Context, method, path string, body io.Reader, opts ...func(r *http.Request)) (*Response, error) {
|
func (c *client) request(ctx context.Context, method, path string, body *bytes.Buffer, opts ...func(r *http.Request)) (*Response, error) {
|
||||||
var res *Response
|
var res *Response
|
||||||
err := backoff.Retry(func() error {
|
err := backoff.Retry(func() error {
|
||||||
r, err := c.requestOnce(ctx, method, path, body, opts...)
|
var b bytes.Buffer
|
||||||
|
if body != nil {
|
||||||
|
// We have to create a copy of the body, otherwise attempting this request again will
|
||||||
|
// send no data if there was initially a body since the "requestOnce" method will read
|
||||||
|
// the whole buffer, thus leaving it empty at the end.
|
||||||
|
if _, err := b.Write(body.Bytes()); err != nil {
|
||||||
|
return backoff.Permanent(errors.Wrap(err, "http: failed to copy body buffer"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r, err := c.requestOnce(ctx, method, path, &b, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
return backoff.Permanent(err)
|
return backoff.Permanent(err)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package remote
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/pterodactyl/wings/internal/models"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -178,6 +179,16 @@ func (c *client) SendRestorationStatus(ctx context.Context, backup string, succe
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendActivityLogs sends activity logs back to the Panel for processing.
|
||||||
|
func (c *client) SendActivityLogs(ctx context.Context, activity []models.Activity) error {
|
||||||
|
resp, err := c.Post(ctx, "/activity", d{"data": activity})
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStackIf(err)
|
||||||
|
}
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// getServersPaged returns a subset of servers from the Panel API using the
|
// getServersPaged returns a subset of servers from the Panel API using the
|
||||||
// pagination query parameters.
|
// pagination query parameters.
|
||||||
func (c *client) getServersPaged(ctx context.Context, page, limit int) ([]RawServerData, Pagination, error) {
|
func (c *client) getServersPaged(ctx context.Context, page, limit int) ([]RawServerData, Pagination, error) {
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ package remote
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/pterodactyl/wings/parser"
|
"github.com/pterodactyl/wings/parser"
|
||||||
)
|
)
|
||||||
@@ -87,6 +86,7 @@ type SftpAuthRequest struct {
|
|||||||
// user for the SFTP subsystem.
|
// user for the SFTP subsystem.
|
||||||
type SftpAuthResponse struct {
|
type SftpAuthResponse struct {
|
||||||
Server string `json:"server"`
|
Server string `json:"server"`
|
||||||
|
User string `json:"user"`
|
||||||
Permissions []string `json:"permissions"`
|
Permissions []string `json:"permissions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
|
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/gbrlsnchs/jwt/v3"
|
"github.com/gbrlsnchs/jwt/v3"
|
||||||
"github.com/goccy/go-json"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// The time at which Wings was booted. No JWT's created before this time are allowed to
|
// The time at which Wings was booted. No JWT's created before this time are allowed to
|
||||||
@@ -35,13 +34,13 @@ func DenyJTI(jti string) {
|
|||||||
denylist.Store(jti, time.Now())
|
denylist.Store(jti, time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
// A JWT payload for Websocket connections. This JWT is passed along to the Websocket after
|
// WebsocketPayload defines the JWT payload for a websocket connection. This JWT is passed along to
|
||||||
// it has been connected to by sending an "auth" event.
|
// 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
|
||||||
|
|
||||||
UserID json.Number `json:"user_id"`
|
UserUUID string `json:"user_uuid"`
|
||||||
ServerUUID string `json:"server_uuid"`
|
ServerUUID string `json:"server_uuid"`
|
||||||
Permissions []string `json:"permissions"`
|
Permissions []string `json:"permissions"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package websocket
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/pterodactyl/wings/internal/models"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -40,6 +41,7 @@ type Handler struct {
|
|||||||
Connection *websocket.Conn `json:"-"`
|
Connection *websocket.Conn `json:"-"`
|
||||||
jwt *tokens.WebsocketPayload
|
jwt *tokens.WebsocketPayload
|
||||||
server *server.Server
|
server *server.Server
|
||||||
|
ra server.RequestActivity
|
||||||
uuid uuid.UUID
|
uuid uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +111,7 @@ func GetHandler(s *server.Server, w http.ResponseWriter, r *http.Request) (*Hand
|
|||||||
Connection: conn,
|
Connection: conn,
|
||||||
jwt: nil,
|
jwt: nil,
|
||||||
server: s,
|
server: s,
|
||||||
|
ra: s.NewRequestActivity("", r.RemoteAddr),
|
||||||
uuid: u,
|
uuid: u,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -264,6 +267,7 @@ func (h *Handler) GetJwt() *tokens.WebsocketPayload {
|
|||||||
// setJwt sets the JWT for the websocket in a race-safe manner.
|
// setJwt sets the JWT for the websocket in a race-safe manner.
|
||||||
func (h *Handler) setJwt(token *tokens.WebsocketPayload) {
|
func (h *Handler) setJwt(token *tokens.WebsocketPayload) {
|
||||||
h.Lock()
|
h.Lock()
|
||||||
|
h.ra = h.ra.SetUser(token.UserUUID)
|
||||||
h.jwt = token
|
h.jwt = token
|
||||||
h.Unlock()
|
h.Unlock()
|
||||||
}
|
}
|
||||||
@@ -365,6 +369,10 @@ func (h *Handler) HandleInbound(ctx context.Context, m Message) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
_ = h.ra.Save(h.server, models.Event(server.ActivityPowerPrefix+action), nil)
|
||||||
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case SendServerLogsEvent:
|
case SendServerLogsEvent:
|
||||||
@@ -421,6 +429,10 @@ func (h *Handler) HandleInbound(ctx context.Context, m Message) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = h.ra.Save(h.server, server.ActivityConsoleCommand, models.ActivityMeta{
|
||||||
|
"command": strings.Join(m.Args, ""),
|
||||||
|
})
|
||||||
|
|
||||||
return h.server.Environment.SendCommand(strings.Join(m.Args, ""))
|
return h.server.Environment.SendCommand(strings.Join(m.Args, ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
70
server/activity.go
Normal file
70
server/activity.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"emperror.dev/errors"
|
||||||
|
"github.com/pterodactyl/wings/internal/database"
|
||||||
|
"github.com/pterodactyl/wings/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ActivityPowerPrefix = "server:power."
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActivityConsoleCommand = models.Event("server:console.command")
|
||||||
|
ActivitySftpWrite = models.Event("server:sftp.write")
|
||||||
|
ActivitySftpCreate = models.Event("server:sftp.create")
|
||||||
|
ActivitySftpCreateDirectory = models.Event("server:sftp.create-directory")
|
||||||
|
ActivitySftpRename = models.Event("server:sftp.rename")
|
||||||
|
ActivitySftpDelete = models.Event("server:sftp.delete")
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequestActivity is a wrapper around a LoggedEvent that is able to track additional request
|
||||||
|
// specific metadata including the specific user and IP address associated with all subsequent
|
||||||
|
// events. The internal logged event structure can be extracted by calling RequestEvent.Event().
|
||||||
|
type RequestActivity struct {
|
||||||
|
server string
|
||||||
|
user string
|
||||||
|
ip string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event returns the underlying logged event from the RequestEvent instance and sets the
|
||||||
|
// specific event and metadata on it.
|
||||||
|
func (ra RequestActivity) Event(event models.Event, metadata models.ActivityMeta) *models.Activity {
|
||||||
|
a := models.Activity{Server: ra.server, IP: ra.ip, Event: event, Metadata: metadata}
|
||||||
|
|
||||||
|
return a.SetUser(ra.user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save creates a new event instance and saves it. If an error is encountered it is automatically
|
||||||
|
// logged to the provided server's error logging output. The error is also returned to the caller
|
||||||
|
// but can be ignored.
|
||||||
|
func (ra RequestActivity) Save(s *Server, event models.Event, metadata models.ActivityMeta) error {
|
||||||
|
if tx := database.Instance().Create(ra.Event(event, metadata)); tx.Error != nil {
|
||||||
|
err := errors.WithStackIf(tx.Error)
|
||||||
|
|
||||||
|
s.Log().WithField("error", err).WithField("event", event).Error("activity: failed to save event")
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP returns the IP address associated with this entry.
|
||||||
|
func (ra RequestActivity) IP() string {
|
||||||
|
return ra.ip
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ra *RequestActivity) User() string {
|
||||||
|
return ra.user
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUser clones the RequestActivity struct and sets a new user value on the copy
|
||||||
|
// before returning it.
|
||||||
|
func (ra RequestActivity) SetUser(u string) RequestActivity {
|
||||||
|
c := ra
|
||||||
|
c.user = u
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) NewRequestActivity(user string, ip string) RequestActivity {
|
||||||
|
return RequestActivity{server: s.ID(), user: user, ip: ip}
|
||||||
|
}
|
||||||
@@ -52,6 +52,24 @@ func (m *Manager) Client() remote.Client {
|
|||||||
return m.client
|
return m.client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Len returns the count of servers stored in the manager instance.
|
||||||
|
func (m *Manager) Len() int {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
return len(m.servers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys returns all of the server UUIDs stored in the manager set.
|
||||||
|
func (m *Manager) Keys() []string {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
keys := make([]string, len(m.servers))
|
||||||
|
for i, s := range m.servers {
|
||||||
|
keys[i] = s.ID()
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
// Put replaces all the current values in the collection with the value that
|
// Put replaces all the current values in the collection with the value that
|
||||||
// is passed through.
|
// is passed through.
|
||||||
func (m *Manager) Put(s []*Server) {
|
func (m *Manager) Put(s []*Server) {
|
||||||
|
|||||||
58
sftp/event.go
Normal file
58
sftp/event.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"emperror.dev/errors"
|
||||||
|
"github.com/apex/log"
|
||||||
|
"github.com/pterodactyl/wings/internal/database"
|
||||||
|
"github.com/pterodactyl/wings/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type eventHandler struct {
|
||||||
|
ip string
|
||||||
|
user string
|
||||||
|
server string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileAction struct {
|
||||||
|
// Entity is the targeted file or directory (depending on the event) that the action
|
||||||
|
// is being performed _against_, such as "/foo/test.txt". This will always be the full
|
||||||
|
// path to the element.
|
||||||
|
Entity string
|
||||||
|
// Target is an optional (often blank) field that only has a value in it when the event
|
||||||
|
// is specifically modifying the entity, such as a rename or move event. In that case
|
||||||
|
// the Target field will be the final value, such as "/bar/new.txt"
|
||||||
|
Target string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log parses a SFTP specific file activity event and then passes it off to be stored
|
||||||
|
// in the normal activity database.
|
||||||
|
func (eh *eventHandler) Log(e models.Event, fa FileAction) error {
|
||||||
|
metadata := map[string]interface{}{
|
||||||
|
"files": []string{fa.Entity},
|
||||||
|
}
|
||||||
|
if fa.Target != "" {
|
||||||
|
metadata["files"] = []map[string]string{
|
||||||
|
{"from": fa.Entity, "to": fa.Target},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a := models.Activity{
|
||||||
|
Server: eh.server,
|
||||||
|
Event: e,
|
||||||
|
Metadata: metadata,
|
||||||
|
IP: eh.ip,
|
||||||
|
}
|
||||||
|
|
||||||
|
if tx := database.Instance().Create(a.SetUser(eh.user)); tx.Error != nil {
|
||||||
|
return errors.Wrap(tx.Error, "sftp: failed to save event to database")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustLog is a wrapper around log that will trigger a fatal error and exit the application
|
||||||
|
// if an error is encountered during the logging of the event.
|
||||||
|
func (eh *eventHandler) MustLog(e models.Event, fa FileAction) {
|
||||||
|
if err := eh.Log(e, fa); err != nil {
|
||||||
|
log.WithField("error", err).Fatal("sftp: failed to log event")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,31 +28,39 @@ const (
|
|||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
permissions []string
|
|
||||||
server *server.Server
|
server *server.Server
|
||||||
fs *filesystem.Filesystem
|
fs *filesystem.Filesystem
|
||||||
|
events *eventHandler
|
||||||
|
permissions []string
|
||||||
logger *log.Entry
|
logger *log.Entry
|
||||||
ro bool
|
ro bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a new connection handler for the SFTP server. This allows a given user
|
// NewHandler returns a new connection handler for the SFTP server. This allows a given user
|
||||||
// to access the underlying filesystem.
|
// to access the underlying filesystem.
|
||||||
func NewHandler(sc *ssh.ServerConn, srv *server.Server) *Handler {
|
func NewHandler(sc *ssh.ServerConn, srv *server.Server) (*Handler, error) {
|
||||||
|
uuid, ok := sc.Permissions.Extensions["user"]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("sftp: mismatched Wings and Panel versions — Panel 1.10 is required for this version of Wings.")
|
||||||
|
}
|
||||||
|
|
||||||
|
events := eventHandler{
|
||||||
|
ip: sc.RemoteAddr().String(),
|
||||||
|
user: uuid,
|
||||||
|
server: srv.ID(),
|
||||||
|
}
|
||||||
|
|
||||||
return &Handler{
|
return &Handler{
|
||||||
permissions: strings.Split(sc.Permissions.Extensions["permissions"], ","),
|
permissions: strings.Split(sc.Permissions.Extensions["permissions"], ","),
|
||||||
server: srv,
|
server: srv,
|
||||||
fs: srv.Filesystem(),
|
fs: srv.Filesystem(),
|
||||||
|
events: &events,
|
||||||
ro: config.Get().System.Sftp.ReadOnly,
|
ro: config.Get().System.Sftp.ReadOnly,
|
||||||
logger: log.WithFields(log.Fields{
|
logger: log.WithFields(log.Fields{"subsystem": "sftp", "user": uuid, "ip": sc.RemoteAddr()}),
|
||||||
"subsystem": "sftp",
|
}, nil
|
||||||
"username": sc.User(),
|
|
||||||
"ip": sc.RemoteAddr(),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the sftp.Handlers for this struct.
|
// Handlers returns the sftp.Handlers for this struct.
|
||||||
func (h *Handler) Handlers() sftp.Handlers {
|
func (h *Handler) Handlers() sftp.Handlers {
|
||||||
return sftp.Handlers{
|
return sftp.Handlers{
|
||||||
FileGet: h,
|
FileGet: h,
|
||||||
@@ -121,7 +129,12 @@ func (h *Handler) Filewrite(request *sftp.Request) (io.WriterAt, error) {
|
|||||||
}
|
}
|
||||||
// Chown may or may not have been called in the touch function, so always do
|
// Chown may or may not have been called in the touch function, so always do
|
||||||
// it at this point to avoid the file being improperly owned.
|
// it at this point to avoid the file being improperly owned.
|
||||||
_ = h.server.Filesystem().Chown(request.Filepath)
|
_ = h.fs.Chown(request.Filepath)
|
||||||
|
event := server.ActivitySftpWrite
|
||||||
|
if permission == PermissionFileCreate {
|
||||||
|
event = server.ActivitySftpCreate
|
||||||
|
}
|
||||||
|
h.events.MustLog(event, FileAction{Entity: request.Filepath})
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +185,7 @@ func (h *Handler) Filecmd(request *sftp.Request) error {
|
|||||||
l.WithField("error", err).Error("failed to rename file")
|
l.WithField("error", err).Error("failed to rename file")
|
||||||
return sftp.ErrSSHFxFailure
|
return sftp.ErrSSHFxFailure
|
||||||
}
|
}
|
||||||
|
h.events.MustLog(server.ActivitySftpRename, FileAction{Entity: request.Filepath, Target: request.Target})
|
||||||
break
|
break
|
||||||
// Handle deletion of a directory. This will properly delete all of the files and
|
// Handle deletion of a directory. This will properly delete all of the files and
|
||||||
// folders within that directory if it is not already empty (unlike a lot of SFTP
|
// folders within that directory if it is not already empty (unlike a lot of SFTP
|
||||||
@@ -180,10 +194,12 @@ func (h *Handler) Filecmd(request *sftp.Request) error {
|
|||||||
if !h.can(PermissionFileDelete) {
|
if !h.can(PermissionFileDelete) {
|
||||||
return sftp.ErrSSHFxPermissionDenied
|
return sftp.ErrSSHFxPermissionDenied
|
||||||
}
|
}
|
||||||
if err := h.fs.Delete(request.Filepath); err != nil {
|
p := filepath.Clean(request.Filepath)
|
||||||
|
if err := h.fs.Delete(p); err != nil {
|
||||||
l.WithField("error", err).Error("failed to remove directory")
|
l.WithField("error", err).Error("failed to remove directory")
|
||||||
return sftp.ErrSSHFxFailure
|
return sftp.ErrSSHFxFailure
|
||||||
}
|
}
|
||||||
|
h.events.MustLog(server.ActivitySftpDelete, FileAction{Entity: request.Filepath})
|
||||||
return sftp.ErrSSHFxOk
|
return sftp.ErrSSHFxOk
|
||||||
// Handle requests to create a new Directory.
|
// Handle requests to create a new Directory.
|
||||||
case "Mkdir":
|
case "Mkdir":
|
||||||
@@ -191,11 +207,12 @@ func (h *Handler) Filecmd(request *sftp.Request) error {
|
|||||||
return sftp.ErrSSHFxPermissionDenied
|
return sftp.ErrSSHFxPermissionDenied
|
||||||
}
|
}
|
||||||
name := strings.Split(filepath.Clean(request.Filepath), "/")
|
name := strings.Split(filepath.Clean(request.Filepath), "/")
|
||||||
err := h.fs.CreateDirectory(name[len(name)-1], strings.Join(name[0:len(name)-1], "/"))
|
p := strings.Join(name[0:len(name)-1], "/")
|
||||||
if err != nil {
|
if err := h.fs.CreateDirectory(name[len(name)-1], p); err != nil {
|
||||||
l.WithField("error", err).Error("failed to create directory")
|
l.WithField("error", err).Error("failed to create directory")
|
||||||
return sftp.ErrSSHFxFailure
|
return sftp.ErrSSHFxFailure
|
||||||
}
|
}
|
||||||
|
h.events.MustLog(server.ActivitySftpCreateDirectory, FileAction{Entity: request.Filepath})
|
||||||
break
|
break
|
||||||
// Support creating symlinks between files. The source and target must resolve within
|
// Support creating symlinks between files. The source and target must resolve within
|
||||||
// the server home directory.
|
// the server home directory.
|
||||||
@@ -228,6 +245,7 @@ func (h *Handler) Filecmd(request *sftp.Request) error {
|
|||||||
l.WithField("error", err).Error("failed to remove a file")
|
l.WithField("error", err).Error("failed to remove a file")
|
||||||
return sftp.ErrSSHFxFailure
|
return sftp.ErrSSHFxFailure
|
||||||
}
|
}
|
||||||
|
h.events.MustLog(server.ActivitySftpDelete, FileAction{Entity: request.Filepath})
|
||||||
return sftp.ErrSSHFxOk
|
return sftp.ErrSSHFxOk
|
||||||
default:
|
default:
|
||||||
return sftp.ErrSSHFxOpUnsupported
|
return sftp.ErrSSHFxOpUnsupported
|
||||||
@@ -287,7 +305,6 @@ func (h *Handler) can(permission string) bool {
|
|||||||
if h.server.IsSuspended() {
|
if h.server.IsSuspended() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range h.permissions {
|
for _, p := range h.permissions {
|
||||||
// If we match the permission specifically, or the user has been granted the "*"
|
// If we match the permission specifically, or the user has been granted the "*"
|
||||||
// permission because they're an admin, let them through.
|
// permission because they're an admin, let them through.
|
||||||
|
|||||||
@@ -91,19 +91,21 @@ func (c *SFTPServer) Run() error {
|
|||||||
if conn, _ := listener.Accept(); conn != nil {
|
if conn, _ := listener.Accept(); conn != nil {
|
||||||
go func(conn net.Conn) {
|
go func(conn net.Conn) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
c.AcceptInbound(conn, conf)
|
if err := c.AcceptInbound(conn, conf); err != nil {
|
||||||
|
log.WithField("error", err).Error("sftp: failed to accept inbound connection")
|
||||||
|
}
|
||||||
}(conn)
|
}(conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles an inbound connection to the instance and determines if we should serve the
|
// AcceptInbound handles an inbound connection to the instance and determines if we should
|
||||||
// request or not.
|
// serve the request or not.
|
||||||
func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) {
|
func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) error {
|
||||||
// Before beginning a handshake must be performed on the incoming net.Conn
|
// Before beginning a handshake must be performed on the incoming net.Conn
|
||||||
sconn, chans, reqs, err := ssh.NewServerConn(conn, config)
|
sconn, chans, reqs, err := ssh.NewServerConn(conn, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
defer sconn.Close()
|
defer sconn.Close()
|
||||||
go ssh.DiscardRequests(reqs)
|
go ssh.DiscardRequests(reqs)
|
||||||
@@ -149,11 +151,17 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) {
|
|||||||
|
|
||||||
// Spin up a SFTP server instance for the authenticated user's server allowing
|
// Spin up a SFTP server instance for the authenticated user's server allowing
|
||||||
// them access to the underlying filesystem.
|
// them access to the underlying filesystem.
|
||||||
handler := sftp.NewRequestServer(channel, NewHandler(sconn, srv).Handlers())
|
handler, err := NewHandler(sconn, srv)
|
||||||
if err := handler.Serve(); err == io.EOF {
|
if err != nil {
|
||||||
handler.Close()
|
return errors.WithStackIf(err)
|
||||||
|
}
|
||||||
|
rs := sftp.NewRequestServer(channel, handler.Handlers())
|
||||||
|
if err := rs.Serve(); err == io.EOF {
|
||||||
|
_ = rs.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates a new ED25519 private key that is used for host authentication when
|
// Generates a new ED25519 private key that is used for host authentication when
|
||||||
@@ -213,8 +221,9 @@ func (c *SFTPServer) makeCredentialsRequest(conn ssh.ConnMetadata, t remote.Sftp
|
|||||||
logger.WithField("server", resp.Server).Debug("credentials validated and matched to server instance")
|
logger.WithField("server", resp.Server).Debug("credentials validated and matched to server instance")
|
||||||
permissions := ssh.Permissions{
|
permissions := ssh.Permissions{
|
||||||
Extensions: map[string]string{
|
Extensions: map[string]string{
|
||||||
|
"ip": conn.RemoteAddr().String(),
|
||||||
"uuid": resp.Server,
|
"uuid": resp.Server,
|
||||||
"user": conn.User(),
|
"user": resp.User,
|
||||||
"permissions": strings.Join(resp.Permissions, ","),
|
"permissions": strings.Join(resp.Permissions, ","),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
29
system/strings.go
Normal file
29
system/strings.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ipTrimRegex = regexp.MustCompile(`(:\d*)?$`)
|
||||||
|
|
||||||
|
const characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||||
|
|
||||||
|
// RandomString generates a random string of alpha-numeric characters using a
|
||||||
|
// pseudo-random number generator. The output of this function IS NOT cryptographically
|
||||||
|
// secure, it is used solely for generating random strings outside a security context.
|
||||||
|
func RandomString(n int) string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.Grow(n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
b.WriteByte(characters[rand.Intn(len(characters))])
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimIPSuffix removes the internal port value from an IP address to ensure we're only
|
||||||
|
// ever working directly with the IP address.
|
||||||
|
func TrimIPSuffix(s string) string {
|
||||||
|
return ipTrimRegex.ReplaceAllString(s, "")
|
||||||
|
}
|
||||||
8
wings.go
8
wings.go
@@ -2,8 +2,16 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pterodactyl/wings/cmd"
|
"github.com/pterodactyl/wings/cmd"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Since we make use of the math/rand package in the code, especially for generating
|
||||||
|
// non-cryptographically secure random strings we need to seed the RNG. Just make use
|
||||||
|
// of the current time for this.
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
|
||||||
|
// Execute the main binary code.
|
||||||
cmd.Execute()
|
cmd.Execute()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user