Compare commits

..

1 Commits

Author SHA1 Message Date
Pterodactyl CI
1040b7d8ef bump version for release 2022-11-22 20:54:03 +00:00
27 changed files with 125 additions and 208 deletions

View File

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

View File

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

View File

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

View File

@@ -1,46 +1,5 @@
# Changelog # Changelog
## v1.11.3
### Fixed
* CVE-2023-25152
## 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 ## v1.11.0-rc.2
### Added ### Added
* More detailed information returned by the `/api/system` endpoint when using the `?v=2` query parameter. * More detailed information returned by the `/api/system` endpoint when using the `?v=2` query parameter.
@@ -64,10 +23,6 @@
* Archive progress is now reported correctly. * Archive progress is now reported correctly.
* Labels for containers can now be set by the Panel. * Labels for containers can now be set by the Panel.
## v1.7.3
### Fixed
* CVE-2023-25152
## v1.7.2 ## v1.7.2
### Fixed ### Fixed
* The S3 backup driver now supports Cloudflare R2 * The S3 backup driver now supports Cloudflare R2

View File

@@ -15,12 +15,14 @@ dependencies, and allowing users to authenticate with the same credentials they
## Sponsors ## Sponsors
I would like to extend my sincere thanks to the following sponsors for helping find Pterodactyl's development. I would like to extend my sincere thanks to the following sponsors for helping find Pterodactyl's developement.
[Interested in becoming a sponsor?](https://github.com/sponsors/matthewpi) [Interested in becoming a sponsor?](https://github.com/sponsors/matthewpi)
| Company | About | | Company | About |
|-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [**WISP**](https://wisp.gg) | Extra features. | | [**WISP**](https://wisp.gg) | Extra features. |
| [**Fragnet**](https://fragnet.net) | Providing low latency, high-end game hosting solutions to gamers, game studios and eSports platforms. |
| [**RocketNode**](https://rocketnode.com/) | Innovative game server hosting combined with a straightforward control panel, affordable prices, and Rocket-Fast support. |
| [**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. |
| [**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. |
| [**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! | | [**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! |
@@ -28,7 +30,6 @@ I would like to extend my sincere thanks to the following sponsors for helping f
| [**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. |
| [**Pterodactyl Market**](https://pterodactylmarket.com/) | Pterodactyl Market is a one-and-stop shop for Pterodactyl. In our market, you can find Add-ons, Themes, Eggs, and more for Pterodactyl. | | [**Pterodactyl Market**](https://pterodactylmarket.com/) | Pterodactyl Market is a one-and-stop shop for Pterodactyl. In our market, you can find Add-ons, Themes, Eggs, and more for Pterodactyl. |
| [**UltraServers**](https://ultraservers.com/) | Deploy premium games hosting with the click of a button. Manage and swap games with ease and let us take care of the rest. We currently support Minecraft, Rust, ARK, 7 Days to Die, Garys MOD, CS:GO, Satisfactory and others. | | [**UltraServers**](https://ultraservers.com/) | Deploy premium games hosting with the click of a button. Manage and swap games with ease and let us take care of the rest. We currently support Minecraft, Rust, ARK, 7 Days to Die, Garys MOD, CS:GO, Satisfactory and others. |
| [**Realms Hosting**](https://realmshosting.com/) | Want to build your Gaming Empire? Use Realms Hosting today to kick start your game server hosting with outstanding DDOS Protection, 24/7 Support, Cheap Prices and a Custom Control Panel. | |
## Documentation ## Documentation

View File

@@ -58,7 +58,7 @@ func (e *Environment) Attach(ctx context.Context) error {
// Set the stream again with the container. // Set the stream again with the container.
if st, err := e.client.ContainerAttach(ctx, e.Id, opts); err != nil { if st, err := e.client.ContainerAttach(ctx, e.Id, opts); err != nil {
return errors.WrapIf(err, "environment/docker: error while attaching to container") return err
} else { } else {
e.SetStream(&st) e.SetStream(&st)
} }
@@ -143,7 +143,7 @@ func (e *Environment) Create() error {
if _, err := e.ContainerInspect(ctx); err == nil { if _, err := e.ContainerInspect(ctx); err == nil {
return nil return nil
} else if !client.IsErrNotFound(err) { } else if !client.IsErrNotFound(err) {
return errors.WrapIf(err, "environment/docker: failed to inspect container") return errors.Wrap(err, "environment/docker: failed to inspect container")
} }
// Try to pull the requested image before creating the 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) { if client.IsErrNotFound(err) {
return 1, false, nil return 1, false, nil
} }
return 0, false, errors.WrapIf(err, "environment/docker: failed to inspect container") return 0, false, err
} }
return uint32(c.State.ExitCode), c.State.OOMKilled, nil 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 // exists on the system, and rebuild the container if that is required for server booting to
// occur. // occur.
if err := e.OnBeforeStart(ctx); err != nil { if err := e.OnBeforeStart(ctx); err != nil {
return errors.WrapIf(err, "environment/docker: failed to run pre-boot process") return errors.WithStackIf(err)
} }
// If we cannot start & attach to the container in 30 seconds something has gone // 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 // By explicitly attaching to the instance before we start it, we can immediately
// react to errors/output stopping/etc. when starting. // react to errors/output stopping/etc. when starting.
if err := e.Attach(actx); err != nil { if err := e.Attach(actx); err != nil {
return errors.WrapIf(err, "environment/docker: failed to attach to container") return err
} }
if err := e.client.ContainerStart(actx, e.Id, types.ContainerStartOptions{}); err != nil { if err := e.client.ContainerStart(actx, e.Id, types.ContainerStartOptions{}); err != nil {

View File

@@ -2,7 +2,6 @@ package cron
import ( import (
"context" "context"
"net"
"emperror.dev/errors" "emperror.dev/errors"
@@ -18,9 +17,9 @@ type activityCron struct {
max int max int
} }
// Run executes the cronjob and ensures we fetch and send all the stored activity to the // 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 // 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 separately to account // SFTP specific events are not handled in this cron, they're handled seperately to account
// for de-duplication and event merging. // for de-duplication and event merging.
func (ac *activityCron) Run(ctx context.Context) error { func (ac *activityCron) Run(ctx context.Context) error {
// Don't execute this cron if there is currently one running. Once this task is completed // Don't execute this cron if there is currently one running. Once this task is completed
@@ -35,6 +34,7 @@ func (ac *activityCron) Run(ctx context.Context) error {
Where("event NOT LIKE ?", "server:sftp.%"). Where("event NOT LIKE ?", "server:sftp.%").
Limit(ac.max). Limit(ac.max).
Find(&activity) Find(&activity)
if tx.Error != nil { if tx.Error != nil {
return errors.WithStack(tx.Error) return errors.WithStack(tx.Error)
} }
@@ -42,42 +42,15 @@ func (ac *activityCron) Run(ctx context.Context) error {
return nil return nil
} }
// ids to delete from the database. if err := ac.manager.Client().SendActivityLogs(ctx, activity); err != nil {
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") return errors.WrapIf(err, "cron: failed to send activity events to Panel")
} }
// Add all the successful activities to the list of IDs to delete. var ids []int
ids = make([]int, len(activities)) for _, v := range activity {
for i, v := range activities { ids = append(ids, v.ID)
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{}) tx = database.Instance().WithContext(ctx).Where("id IN ?", ids).Delete(&models.Activity{})
if tx.Error != nil { if tx.Error != nil {
return errors.WithStack(tx.Error) return errors.WithStack(tx.Error)

View File

@@ -1,11 +1,11 @@
package models package models
import ( import (
"net"
"strings"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
"github.com/pterodactyl/wings/system"
) )
type Event string type Event string
@@ -57,9 +57,7 @@ func (a Activity) SetUser(u string) *Activity {
// is trimmed down to remove any extraneous data, and the timestamp is set to the current // is trimmed down to remove any extraneous data, and the timestamp is set to the current
// system time and then stored as UTC. // system time and then stored as UTC.
func (a *Activity) BeforeCreate(_ *gorm.DB) error { func (a *Activity) BeforeCreate(_ *gorm.DB) error {
if ip, _, err := net.SplitHostPort(strings.TrimSpace(a.IP)); err == nil { a.IP = system.TrimIPSuffix(a.IP)
a.IP = ip
}
if a.Timestamp.IsZero() { if a.Timestamp.IsZero() {
a.Timestamp = time.Now() a.Timestamp = time.Now()
} }

View File

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

View File

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

View File

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

View File

@@ -58,7 +58,7 @@ func getServerWebsocket(c *gin.Context) {
case <-ctx.Done(): case <-ctx.Done():
break break
case <-s.Context().Done(): 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 break
} }
}() }()
@@ -83,7 +83,7 @@ func getServerWebsocket(c *gin.Context) {
go func(msg websocket.Message) { go func(msg websocket.Message) {
if err := handler.HandleInbound(ctx, msg); err != nil { if err := handler.HandleInbound(ctx, msg); err != nil {
_ = handler.SendErrorJson(msg, err) handler.SendErrorJson(msg, err)
} }
}(j) }(j)
} }

View File

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

View File

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

View File

@@ -148,7 +148,7 @@ func (fs *Filesystem) DecompressFileUnsafe(ctx context.Context, dir string, file
if err != nil { if err != nil {
return err return err
} }
defer f.Close() // TODO: defer file close?
// Identify the type of archive we are dealing with. // Identify the type of archive we are dealing with.
format, input, err := archiver.Identify(filepath.Base(file), f) 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() { if !e.IsDir() {
_ = syscall.Lstat(p, &st) syscall.Lstat(p, &st)
atomic.AddInt64(&size, st.Size) atomic.AddInt64(&size, st.Size)
} }

View File

@@ -92,9 +92,6 @@ func (fs *Filesystem) Touch(p string, flag int) (*os.File, error) {
if err == nil { if err == nil {
return f, 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 the error is not because it doesn't exist then we just need to bail at this point.
if !errors.Is(err, os.ErrNotExist) { if !errors.Is(err, os.ErrNotExist) {
return nil, errors.Wrap(err, "server/filesystem: touch: failed to open file handle") return nil, errors.Wrap(err, "server/filesystem: touch: failed to open file handle")
@@ -165,7 +162,7 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error {
// Adjust the disk usage to account for the old size and the new size of the file. // Adjust the disk usage to account for the old size and the new size of the file.
fs.addDisk(sz - currentSize) fs.addDisk(sz - currentSize)
return fs.unsafeChown(cleaned) return fs.Chown(cleaned)
} }
// Creates a new directory (name) at a specified path (p) for the server. // Creates a new directory (name) at a specified path (p) for the server.
@@ -223,12 +220,7 @@ func (fs *Filesystem) Chown(path string) error {
if err != nil { if err != nil {
return err return err
} }
return fs.unsafeChown(cleaned)
}
// unsafeChown chowns the given path, without checking if the path is safe. This should only be used
// when the path has already been checked.
func (fs *Filesystem) unsafeChown(path string) error {
if fs.isTest { if fs.isTest {
return nil return nil
} }
@@ -237,19 +229,19 @@ func (fs *Filesystem) unsafeChown(path string) error {
gid := config.Get().System.User.Gid gid := config.Get().System.User.Gid
// Start by just chowning the initial path that we received. // Start by just chowning the initial path that we received.
if err := os.Chown(path, uid, gid); err != nil { if err := os.Chown(cleaned, uid, gid); err != nil {
return errors.Wrap(err, "server/filesystem: chown: failed to chown path") return errors.Wrap(err, "server/filesystem: chown: failed to chown path")
} }
// If this is not a directory we can now return from the function, there is nothing // If this is not a directory we can now return from the function, there is nothing
// left that we need to do. // left that we need to do.
if st, err := os.Stat(path); err != nil || !st.IsDir() { if st, err := os.Stat(cleaned); err != nil || !st.IsDir() {
return nil return nil
} }
// If this was a directory, begin walking over its contents recursively and ensure that all // If this was a directory, begin walking over its contents recursively and ensure that all
// of the subfiles and directories get their permissions updated as well. // of the subfiles and directories get their permissions updated as well.
err := godirwalk.Walk(path, &godirwalk.Options{ err = godirwalk.Walk(cleaned, &godirwalk.Options{
Unsorted: true, Unsorted: true,
Callback: func(p string, e *godirwalk.Dirent) error { Callback: func(p string, e *godirwalk.Dirent) error {
// Do not attempt to chown a symlink. Go's os.Chown function will affect the symlink // Do not attempt to chown a symlink. Go's os.Chown function will affect the symlink
@@ -266,6 +258,7 @@ func (fs *Filesystem) unsafeChown(path string) error {
return os.Chown(p, uid, gid) return os.Chown(p, uid, gid)
}, },
}) })
return errors.Wrap(err, "server/filesystem: chown: failed to chown during walk function") return errors.Wrap(err, "server/filesystem: chown: failed to chown during walk function")
} }

View File

@@ -2,7 +2,6 @@ package filesystem
import ( import (
"context" "context"
iofs "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -34,6 +33,8 @@ func (fs *Filesystem) IsIgnored(paths ...string) error {
// This logic is actually copied over from the SFTP server code. Ideally that eventually // This logic is actually copied over from the SFTP server code. Ideally that eventually
// either gets ported into this application, or is able to make use of this package. // either gets ported into this application, or is able to make use of this package.
func (fs *Filesystem) SafePath(p string) (string, error) { func (fs *Filesystem) SafePath(p string) (string, error) {
var nonExistentPathResolution string
// Start with a cleaned up path before checking the more complex bits. // Start with a cleaned up path before checking the more complex bits.
r := fs.unsafeFilePath(p) r := fs.unsafeFilePath(p)
@@ -43,24 +44,47 @@ func (fs *Filesystem) SafePath(p string) (string, error) {
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
return "", errors.Wrap(err, "server/filesystem: failed to evaluate symlink") return "", errors.Wrap(err, "server/filesystem: failed to evaluate symlink")
} else if os.IsNotExist(err) { } else if os.IsNotExist(err) {
// The target of one of the symlinks (EvalSymlinks is recursive) does not exist. // The requested directory doesn't exist, so at this point we need to iterate up the
// So we get what target path does not exist and check if it's within the data // path chain until we hit a directory that _does_ exist and can be validated.
// directory. If it is, we return the original path, otherwise we return an error. parts := strings.Split(filepath.Dir(r), "/")
pErr, ok := err.(*iofs.PathError)
if !ok { var try string
return "", errors.Wrap(err, "server/filesystem: failed to evaluate symlink") // Range over all of the path parts and form directory pathings from the end
// moving up until we have a valid resolution or we run out of paths to try.
for k := range parts {
try = strings.Join(parts[:(len(parts)-k)], "/")
if !fs.unsafeIsInDataDirectory(try) {
break
}
t, err := filepath.EvalSymlinks(try)
if err == nil {
nonExistentPathResolution = t
break
}
} }
ep = pErr.Path }
// If the new path doesn't start with their root directory there is clearly an escape
// attempt going on, and we should NOT resolve this path for them.
if nonExistentPathResolution != "" {
if !fs.unsafeIsInDataDirectory(nonExistentPathResolution) {
return "", NewBadPathResolution(p, nonExistentPathResolution)
}
// If the nonExistentPathResolution variable is not empty then the initial path requested
// did not exist and we looped through the pathway until we found a match. At this point
// we've confirmed the first matched pathway exists in the root server directory, so we
// can go ahead and just return the path that was requested initially.
return r, nil
} }
// If the requested directory from EvalSymlinks begins with the server root directory go // If the requested directory from EvalSymlinks begins with the server root directory go
// ahead and return it. If not we'll return an error which will block any further action // ahead and return it. If not we'll return an error which will block any further action
// on the file. // on the file.
if fs.unsafeIsInDataDirectory(ep) { if fs.unsafeIsInDataDirectory(ep) {
// Returning the original path here instead of the resolved path ensures that return ep, nil
// whatever the user is trying to do will work as expected. If we returned the
// resolved path, the user would be unable to know that it is in fact a symlink.
return r, nil
} }
return "", NewBadPathResolution(p, r) return "", NewBadPathResolution(p, r)

View File

@@ -115,14 +115,6 @@ func TestFilesystem_Blocks_Symlinks(t *testing.T) {
panic(err) panic(err)
} }
if err := os.Symlink(filepath.Join(rfs.root, "malicious_does_not_exist.txt"), filepath.Join(rfs.root, "/server/symlinked_does_not_exist.txt")); err != nil {
panic(err)
}
if err := os.Symlink(filepath.Join(rfs.root, "/server/symlinked_does_not_exist.txt"), filepath.Join(rfs.root, "/server/symlinked_does_not_exist2.txt")); err != nil {
panic(err)
}
if err := os.Symlink(filepath.Join(rfs.root, "/malicious_dir"), filepath.Join(rfs.root, "/server/external_dir")); err != nil { if err := os.Symlink(filepath.Join(rfs.root, "/malicious_dir"), filepath.Join(rfs.root, "/server/external_dir")); err != nil {
panic(err) panic(err)
} }
@@ -136,22 +128,6 @@ func TestFilesystem_Blocks_Symlinks(t *testing.T) {
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue() g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
}) })
g.It("cannot write to a non-existent file symlinked outside the root", func() {
r := bytes.NewReader([]byte("testing what the fuck"))
err := fs.Writefile("symlinked_does_not_exist.txt", r)
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.It("cannot write to chained symlinks with target that does not exist outside the root", func() {
r := bytes.NewReader([]byte("testing what the fuck"))
err := fs.Writefile("symlinked_does_not_exist2.txt", r)
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.It("cannot write a file to a directory symlinked outside the root", func() { g.It("cannot write a file to a directory symlinked outside the root", func() {
r := bytes.NewReader([]byte("testing")) r := bytes.NewReader([]byte("testing"))

View File

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

View File

@@ -128,13 +128,6 @@ func (t *Transfer) PushArchiveToTarget(url, token string) ([]byte, error) {
t.Log().Debug("sending archive to destination") t.Log().Debug("sending archive to destination")
client := http.Client{Timeout: 0} client := http.Client{Timeout: 0}
res, err := client.Do(req) 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") t.Log().Debug("waiting for stream to complete")
select { select {
case <-ctx.Done(): case <-ctx.Done():

View File

@@ -68,21 +68,6 @@ func (c *SFTPServer) Run() error {
} }
conf := &ssh.ServerConfig{ 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, NoClientAuth: false,
MaxAuthTries: 6, MaxAuthTries: 6,
PasswordCallback: func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) { PasswordCallback: func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {

View File

@@ -1,3 +1,3 @@
package system package system
var Version = "develop" var Version = "1.11.0-rc.2"

29
system/strings.go Normal file
View 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, "")
}

View File

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