Compare commits

...

22 Commits

Author SHA1 Message Date
Matthew Penner
774c0af0b0 Update CHANGELOG.md 2023-01-30 18:33:30 -07:00
Alexander Trost
71fbd9271e activity: fix ip validity check (#159) 2023-01-30 09:09:36 -07:00
Matthew Penner
2d640209e5 backup: fix restore erroring due to closed reader 2023-01-29 17:06:49 -07:00
Matthew Penner
304fd91283 Update CHANGELOG.md 2023-01-27 12:14:22 -07:00
Matthew Penner
18de96d7b8 activity: fix IP parsing, drop all columns with malformed ips 2023-01-24 14:36:18 -07:00
Matthew Penner
a36cab1783 router(transfer): throw error if server fails to stop 2023-01-24 12:36:02 -07:00
Matthew Penner
6e0c095bb8 router(transfer): decrease WaitForStop timeout 2023-01-24 12:34:05 -07:00
Matthew Penner
14eea3b1e4 router: close body once finished 2023-01-24 12:33:42 -07:00
Matthew Penner
1bc77dc969 system: close Docker client once finished 2023-01-24 12:33:20 -07:00
Matthew Penner
b8715d1d4f ci: update push-artifact action 2023-01-24 12:32:55 -07:00
Matthew Penner
13d3490bcf server(filesystem): fix Writefile being broken 2023-01-17 18:44:56 -07:00
Matthew Penner
e9b8b11fec Ensure files are closed after they are done being used 2023-01-17 18:34:08 -07:00
Matthew Penner
43b7aa2536 sftp: disable insecure protocols 2023-01-17 11:50:06 -07:00
Matthew Penner
9b8b3c90fb environment(docker): improve logging and stacks 2023-01-17 11:47:27 -07:00
Matthew Penner
e74d8e3501 ci: update go versions 2023-01-13 11:07:09 -07:00
Matthew Penner
4b3bd2ff47 ci(docker): fix latest tag 2022-12-04 18:37:53 -07:00
Matthew Penner
e652d2df84 ci: cleanup 2022-12-04 17:39:55 -07:00
Matthew Penner
e4d790ea40 ci: remove use of deprecated ::set-output 2022-12-04 17:29:36 -07:00
Matthew Penner
5641e45059 ci: overhaul workflows 2022-12-04 17:24:33 -07:00
Matthew Penner
9a718b699f Update CHANGELOG.md 2022-12-04 15:23:29 -07:00
Matthew Penner
92efdb1981 remote: use POST for server tranfer status 2022-12-01 11:50:56 -07:00
Matthew Penner
43227bf24d server(transfer): fix dead-lock while uploading archive
Previously we waited for both the request and multipart writer
to "complete", before handing any errors.  This lead to a problem
where if the request returns before all the data has been read,
the upload would become stuck and keep the server in a transferring
state when the transfer should've been aborted.

Closes https://github.com/pterodactyl/panel/issues/4578
2022-11-29 14:04:27 -07:00
23 changed files with 152 additions and 80 deletions

View File

@@ -4,8 +4,9 @@ on:
push:
branches:
- develop
tags:
- "v*"
release:
types:
- published
jobs:
build-and-push:
@@ -13,21 +14,26 @@ jobs:
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
- name: Code checkout
uses: actions/checkout@v3
- name: Docker Meta
- name: Docker metadata
id: docker_meta
uses: crazy-max/ghaction-docker-meta@v1
uses: docker/metadata-action@v4
with:
images: ghcr.io/pterodactyl/wings
flavor: |
latest=false
tags: |
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.action == 'published' && github.event.release.prerelease == false }}
type=ref,event=tag
type=ref,event=branch
- name: Set up QEMU
- name: Setup QEMU
uses: docker/setup-qemu-action@v2
- name: Install buildx
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry
@@ -40,12 +46,12 @@ jobs:
- name: Get Build Information
id: build_info
run: |
echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\/v/}"
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
echo "version_tag=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Build and push (latest)
uses: docker/build-push-action@v2
if: "!contains(github.ref, 'develop')"
- name: Build and Push (tag)
uses: docker/build-push-action@v3
if: "github.event_name == 'release' && github.event.action == 'published'"
with:
context: .
file: ./Dockerfile
@@ -56,9 +62,9 @@ jobs:
labels: ${{ steps.docker_meta.outputs.labels }}
tags: ${{ steps.docker_meta.outputs.tags }}
- name: Build and push (develop)
uses: docker/build-push-action@v2
if: "contains(github.ref, 'develop')"
- name: Build and Push (develop)
uses: docker/build-push-action@v3
if: "github.event_name == 'push' && contains(github.ref, 'develop')"
with:
context: .
file: ./Dockerfile

View File

@@ -16,7 +16,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-20.04]
go: ["1.18.8", "1.19.3"]
go: ["1.18.10", "1.19.5"]
goos: [linux]
goarch: [amd64, arm64]
@@ -86,14 +86,14 @@ jobs:
go test -race $(go list ./...)
- name: Upload Release Artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'pull_request' }}
with:
name: wings_linux_${{ matrix.goarch }}
path: dist/wings
- name: Upload Debug Artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'pull_request' }}
with:
name: wings_linux_${{ matrix.goarch }}_debug

View File

@@ -17,7 +17,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: "1.18.8"
go-version: "1.18.10"
- name: Build release binaries
env:
@@ -34,7 +34,6 @@ jobs:
REF: ${{ github.ref }}
run: |
sed -n "/^## ${REF:10}/,/^## /{/^## /b;p}" CHANGELOG.md > ./RELEASE_CHANGELOG
echo "version_name=`sed -nr "s/^## (${REF:10} .*)$/\1/p" CHANGELOG.md`" > $GITHUB_OUTPUT
- name: Create checksum and add to changelog
run: |
@@ -59,15 +58,13 @@ jobs:
- name: Create release
id: create_release
uses: actions/create-release@v1
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ steps.extract_changelog.outputs.version_name }}
body_path: ./RELEASE_CHANGELOG
draft: true
prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
body_path: ./RELEASE_CHANGELOG
- name: Upload amd64 binary
uses: actions/upload-release-asset@v1

View File

@@ -1,5 +1,42 @@
# Changelog
## v1.11.2
### Fixed
* Backups being restored from remote storage (s3) erroring out due to a closed stream.
* Fix IP validation logic for activity logs filtering out valid IPs instead of invalid IPs
## v1.11.1
### Changed
* Release binaries are now built with Go 1.18.10
* Timeout when stopping a server before a transfer begins has been reduced to 15 seconds from 1 minute
* Removed insecure SSH protocols for use with the SFTP server
### Fixed
* Unnecessary Docker client connections being left open, causing a slow leak of file descriptors
* Files being left open in parts of the server's filesystem, causing a leak of file descriptors
* IPv6 addresses being corrupted by flawed port stripping logic for activity logs, old entries with malformed IPs will be deleted from the local SQLite database automatically
* A server that times out while being stopped at the beginning of a transfer no longer causes the server to become stuck in a transferring state
## v1.11.0
### Added (since 1.7.2)
* More detailed information returned by the `/api/system` endpoint when using the `?v=2` query parameter.
### Changed (since 1.7.2)
* Send re-installation status separately from installation status.
* Wings release versions will now follow the major and minor version of the Panel.
* Transfers no longer buffer to disk, instead they are fully streamed with only a small amount of memory used for buffering.
* Release binaries are no longer compressed with UPX.
* Use `POST` instead of `GET` for sending the status of a transfer to the Panel.
### Fixed (since 1.7.2)
* Fixed servers outgoing IP not being updated whenever a server's primary allocation is changed when using the Force Outgoing IP option.
* Fixed servers being terminated rather than gracefully stopped when a signal is used to stop the container rather than a command.
* Fixed file not found errors being treated as an internal error, they are now treated as a 404.
* Wings can be run with Podman instead of Docker, this is still experimental and not recommended for production use.
* Archive progress is now reported correctly.
* Labels for containers can now be set by the Panel.
* Fixed servers becoming deadlocked when the target node of a transfer goes offline.
## v1.11.0-rc.2
### Added
* More detailed information returned by the `/api/system` endpoint when using the `?v=2` query parameter.

View File

@@ -58,7 +58,7 @@ func (e *Environment) Attach(ctx context.Context) error {
// Set the stream again with the container.
if st, err := e.client.ContainerAttach(ctx, e.Id, opts); err != nil {
return err
return errors.WrapIf(err, "environment/docker: error while attaching to container")
} else {
e.SetStream(&st)
}
@@ -143,7 +143,7 @@ func (e *Environment) Create() error {
if _, err := e.ContainerInspect(ctx); err == nil {
return nil
} else if !client.IsErrNotFound(err) {
return errors.Wrap(err, "environment/docker: failed to inspect container")
return errors.WrapIf(err, "environment/docker: failed to inspect container")
}
// Try to pull the requested image before creating the container.

View File

@@ -161,7 +161,7 @@ func (e *Environment) ExitState() (uint32, bool, error) {
if client.IsErrNotFound(err) {
return 1, false, nil
}
return 0, false, err
return 0, false, errors.WrapIf(err, "environment/docker: failed to inspect container")
}
return uint32(c.State.ExitCode), c.State.OOMKilled, nil
}

View File

@@ -103,7 +103,7 @@ func (e *Environment) Start(ctx context.Context) error {
// exists on the system, and rebuild the container if that is required for server booting to
// occur.
if err := e.OnBeforeStart(ctx); err != nil {
return errors.WithStackIf(err)
return errors.WrapIf(err, "environment/docker: failed to run pre-boot process")
}
// If we cannot start & attach to the container in 30 seconds something has gone
@@ -119,7 +119,7 @@ func (e *Environment) Start(ctx context.Context) error {
// By explicitly attaching to the instance before we start it, we can immediately
// react to errors/output stopping/etc. when starting.
if err := e.Attach(actx); err != nil {
return err
return errors.WrapIf(err, "environment/docker: failed to attach to container")
}
if err := e.client.ContainerStart(actx, e.Id, types.ContainerStartOptions{}); err != nil {

View File

@@ -2,6 +2,7 @@ package cron
import (
"context"
"net"
"emperror.dev/errors"
@@ -17,9 +18,9 @@ type activityCron struct {
max int
}
// Run executes the cronjob and ensures we fetch and send all of the stored activity to the
// Run executes the cronjob and ensures we fetch and send all 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
// SFTP specific events are not handled in this cron, they're handled separately 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
@@ -34,7 +35,6 @@ func (ac *activityCron) Run(ctx context.Context) error {
Where("event NOT LIKE ?", "server:sftp.%").
Limit(ac.max).
Find(&activity)
if tx.Error != nil {
return errors.WithStack(tx.Error)
}
@@ -42,15 +42,42 @@ func (ac *activityCron) Run(ctx context.Context) error {
return nil
}
if err := ac.manager.Client().SendActivityLogs(ctx, activity); err != nil {
// ids to delete from the database.
ids := make([]int, 0, len(activity))
// activities to send to the panel.
activities := make([]models.Activity, 0, len(activity))
for _, v := range activity {
// Delete any activity that has an invalid IP address. This is a fix for
// a bug that truncated the last octet of an IPv6 address in the database.
if ip := net.ParseIP(v.IP); ip == nil {
ids = append(ids, v.ID)
continue
}
activities = append(activities, v)
}
if len(ids) > 0 {
tx = database.Instance().WithContext(ctx).Where("id IN ?", ids).Delete(&models.Activity{})
if tx.Error != nil {
return errors.WithStack(tx.Error)
}
}
if len(activities) == 0 {
return nil
}
if err := ac.manager.Client().SendActivityLogs(ctx, activities); 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)
// Add all the successful activities to the list of IDs to delete.
ids = make([]int, len(activities))
for i, v := range activities {
ids[i] = v.ID
}
// Delete all the activities that were sent to the Panel (or that were invalid).
tx = database.Instance().WithContext(ctx).Where("id IN ?", ids).Delete(&models.Activity{})
if tx.Error != nil {
return errors.WithStack(tx.Error)

View File

@@ -1,11 +1,11 @@
package models
import (
"net"
"strings"
"time"
"gorm.io/gorm"
"github.com/pterodactyl/wings/system"
)
type Event string
@@ -57,7 +57,9 @@ func (a Activity) SetUser(u string) *Activity {
// 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 ip, _, err := net.SplitHostPort(strings.TrimSpace(a.IP)); err == nil {
a.IP = ip
}
if a.Timestamp.IsZero() {
a.Timestamp = time.Now()
}

View File

@@ -115,7 +115,7 @@ func (c *client) SetTransferStatus(ctx context.Context, uuid string, successful
if successful {
state = "success"
}
resp, err := c.Get(ctx, fmt.Sprintf("/servers/%s/transfer/%s", uuid, state), nil)
resp, err := c.Post(ctx, fmt.Sprintf("/servers/%s/transfer/%s", uuid, state), nil)
if err != nil {
return err
}

View File

@@ -95,6 +95,7 @@ func getDownloadFile(c *gin.Context) {
middleware.CaptureAndAbort(c, err)
return
}
defer f.Close()
c.Header("Content-Length", strconv.Itoa(int(st.Size())))
c.Header("Content-Disposition", "attachment; filename="+strconv.Quote(st.Name()))

View File

@@ -6,6 +6,7 @@ import (
"strings"
"time"
"emperror.dev/errors"
"github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/environment"
@@ -63,11 +64,11 @@ func postServerTransfer(c *gin.Context) {
if s.Environment.State() != environment.ProcessOfflineState {
if err := s.Environment.WaitForStop(
s.Context(),
time.Minute,
time.Second*15,
false,
); err != nil && !strings.Contains(strings.ToLower(err.Error()), "no such container") {
notifyPanelOfFailure()
s.Log().WithError(err).Error("failed to stop server for transfer")
s.SetTransferring(false)
middleware.CaptureAndAbort(c, errors.Wrap(err, "failed to stop server for transfer"))
return
}
}

View File

@@ -58,7 +58,7 @@ func getServerWebsocket(c *gin.Context) {
case <-ctx.Done():
break
case <-s.Context().Done():
handler.Connection.WriteControl(ws.CloseMessage, ws.FormatCloseMessage(ws.CloseGoingAway, "server deleted"), time.Now().Add(time.Second*5))
_ = handler.Connection.WriteControl(ws.CloseMessage, ws.FormatCloseMessage(ws.CloseGoingAway, "server deleted"), time.Now().Add(time.Second*5))
break
}
}()
@@ -83,7 +83,7 @@ func getServerWebsocket(c *gin.Context) {
go func(msg websocket.Message) {
if err := handler.HandleInbound(ctx, msg); err != nil {
handler.SendErrorJson(msg, err)
_ = handler.SendErrorJson(msg, err)
}
}(j)
}

View File

@@ -85,6 +85,7 @@ func (b *LocalBackup) Restore(ctx context.Context, _ io.Reader, callback Restore
if err != nil {
return err
}
defer f.Close()
var reader io.Reader = f
// Steal the logic we use for making backups which will be applied when restoring

View File

@@ -6,6 +6,8 @@ import (
"sync"
"time"
"emperror.dev/errors"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment"
)
@@ -57,7 +59,7 @@ func (s *Server) handleServerCrash() error {
exitCode, oomKilled, err := s.Environment.ExitState()
if err != nil {
return err
return errors.Wrap(err, "failed to get exit state for server process")
}
// If the system is not configured to detect a clean exit code as a crash, and the
@@ -85,5 +87,5 @@ func (s *Server) handleServerCrash() error {
s.crasher.SetLastCrash(time.Now())
return s.HandlePowerAction(PowerActionStart)
return errors.Wrap(s.HandlePowerAction(PowerActionStart), "failed to start server after crash detection")
}

View File

@@ -148,7 +148,7 @@ func (fs *Filesystem) DecompressFileUnsafe(ctx context.Context, dir string, file
if err != nil {
return err
}
// TODO: defer file close?
defer f.Close()
// Identify the type of archive we are dealing with.
format, input, err := archiver.Identify(filepath.Base(file), f)

View File

@@ -183,7 +183,7 @@ func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
}
if !e.IsDir() {
syscall.Lstat(p, &st)
_ = syscall.Lstat(p, &st)
atomic.AddInt64(&size, st.Size)
}

View File

@@ -92,6 +92,9 @@ func (fs *Filesystem) Touch(p string, flag int) (*os.File, error) {
if err == nil {
return f, nil
}
if f != nil {
_ = f.Close()
}
// If the error is not because it doesn't exist then we just need to bail at this point.
if !errors.Is(err, os.ErrNotExist) {
return nil, errors.Wrap(err, "server/filesystem: touch: failed to open file handle")

View File

@@ -147,6 +147,7 @@ func (s *Server) Context() context.Context {
// server instance.
func (s *Server) GetEnvironmentVariables() []string {
out := []string{
// TODO: allow this to be overridden by the user.
fmt.Sprintf("TZ=%s", config.Get().System.Timezone),
fmt.Sprintf("STARTUP=%s", s.Config().Invocation),
fmt.Sprintf("SERVER_MEMORY=%d", s.MemoryLimit()),

View File

@@ -128,6 +128,13 @@ func (t *Transfer) PushArchiveToTarget(url, token string) ([]byte, error) {
t.Log().Debug("sending archive to destination")
client := http.Client{Timeout: 0}
res, err := client.Do(req)
if err != nil {
t.Log().Debug("error while sending archive to destination")
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code from destination: %d", res.StatusCode)
}
t.Log().Debug("waiting for stream to complete")
select {
case <-ctx.Done():

View File

@@ -68,6 +68,21 @@ func (c *SFTPServer) Run() error {
}
conf := &ssh.ServerConfig{
Config: ssh.Config{
KeyExchanges: []string{
"curve25519-sha256", "curve25519-sha256@libssh.org",
"ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521",
"diffie-hellman-group14-sha256",
},
Ciphers: []string{
"aes128-gcm@openssh.com",
"chacha20-poly1305@openssh.com",
"aes128-ctr", "aes192-ctr", "aes256-ctr",
},
MACs: []string{
"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256",
},
},
NoClientAuth: false,
MaxAuthTries: 6,
PasswordCallback: func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {

View File

@@ -1,29 +0,0 @@
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, "")
}

View File

@@ -127,6 +127,7 @@ func GetDockerInfo(ctx context.Context) (types.Version, types.Info, error) {
if err != nil {
return types.Version{}, types.Info{}, err
}
defer c.Close()
dockerVersion, err := c.ServerVersion(ctx)
if err != nil {