Compare commits
9 Commits
v2
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c0c0239ff | ||
|
|
f390784973 | ||
|
|
5df1acd10e | ||
|
|
1927a59cd0 | ||
|
|
5bcf4164fb | ||
|
|
37e4d57cdf | ||
|
|
7ededdb9a2 | ||
|
|
1d197714df | ||
|
|
6c98a955e3 |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,5 +1,19 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.6.2
|
||||||
|
### Fixed
|
||||||
|
* Fixes file upload size not being properly enforced.
|
||||||
|
* Fixes a bug that prevented listing a directory when it contained a named pipe. Also added a check to prevent attempting to read a named pipe directly.
|
||||||
|
* Fixes a bug with the archiver logic that would include folders that had the same name prefix. (for example, requesting only `map` would also include `map2` and `map3`)
|
||||||
|
* Requests to the Panel that return a client error (4xx response code) no longer trigger an exponential backoff, they immediately stop the request.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* CPU limit fields are only set on the Docker container if they have been specified for the server — otherwise they are left empty.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* Added the ability to define the location of the temporary folder used by Wings — defaults to `/tmp/pterodactyl`.
|
||||||
|
* Adds the ability to authenticate for SFTP using public keys (requires `Panel@1.8.0`).
|
||||||
|
|
||||||
## v1.6.1
|
## v1.6.1
|
||||||
### Fixed
|
### Fixed
|
||||||
* Fixes error that would sometimes occur when starting a server that would cause the temporary power action lock to never be released due to a blocked channel.
|
* Fixes error that would sometimes occur when starting a server that would cause the temporary power action lock to never be released due to a blocked channel.
|
||||||
|
|||||||
17
cmd/root.go
17
cmd/root.go
@@ -9,13 +9,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/NYTimes/logrotate"
|
"github.com/NYTimes/logrotate"
|
||||||
@@ -30,7 +28,6 @@ import (
|
|||||||
|
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
"github.com/pterodactyl/wings/internal/notify"
|
|
||||||
"github.com/pterodactyl/wings/loggers/cli"
|
"github.com/pterodactyl/wings/loggers/cli"
|
||||||
"github.com/pterodactyl/wings/remote"
|
"github.com/pterodactyl/wings/remote"
|
||||||
"github.com/pterodactyl/wings/router"
|
"github.com/pterodactyl/wings/router"
|
||||||
@@ -327,7 +324,6 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if the server should run with TLS but using autocert.
|
// Check if the server should run with TLS but using autocert.
|
||||||
go func(s *http.Server, api config.ApiConfiguration, sys config.SystemConfiguration, autotls bool, tlshostname string) {
|
|
||||||
if autotls {
|
if autotls {
|
||||||
m := autocert.Manager{
|
m := autocert.Manager{
|
||||||
Prompt: autocert.AcceptTOS,
|
Prompt: autocert.AcceptTOS,
|
||||||
@@ -366,19 +362,6 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
|
|||||||
if err := s.ListenAndServe(); err != nil {
|
if err := s.ListenAndServe(); err != nil {
|
||||||
log.WithField("error", err).Fatal("failed to configure HTTP server")
|
log.WithField("error", err).Fatal("failed to configure HTTP server")
|
||||||
}
|
}
|
||||||
}(s, api, sys, autotls, tlshostname)
|
|
||||||
|
|
||||||
if err := notify.Readiness(); err != nil {
|
|
||||||
log.WithField("error", err).Error("failed to notify systemd of readiness state")
|
|
||||||
}
|
|
||||||
|
|
||||||
c := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
<-c
|
|
||||||
|
|
||||||
if err := notify.Stopping(); err != nil {
|
|
||||||
log.WithField("error", err).Error("failed to notify systemd of stopping state")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads the configuration from the disk and then sets up the global singleton
|
// Reads the configuration from the disk and then sets up the global singleton
|
||||||
|
|||||||
@@ -480,21 +480,3 @@ func (e *Environment) convertMounts() []mount.Mount {
|
|||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Environment) resources() container.Resources {
|
|
||||||
l := e.Configuration.Limits()
|
|
||||||
pids := l.ProcessLimit()
|
|
||||||
|
|
||||||
return container.Resources{
|
|
||||||
Memory: l.BoundedMemoryLimit(),
|
|
||||||
MemoryReservation: l.MemoryLimit * 1_000_000,
|
|
||||||
MemorySwap: l.ConvertedSwap(),
|
|
||||||
CPUQuota: l.ConvertedCpuLimit(),
|
|
||||||
CPUPeriod: 100_000,
|
|
||||||
CPUShares: 1024,
|
|
||||||
BlkioWeight: l.IoWeight,
|
|
||||||
OomKillDisable: &l.OOMDisabled,
|
|
||||||
CpusetCpus: l.Threads,
|
|
||||||
PidsLimit: &pids,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -99,21 +99,36 @@ func (l Limits) ProcessLimit() int64 {
|
|||||||
return config.Get().Docker.ContainerPidLimit
|
return config.Get().Docker.ContainerPidLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AsContainerResources returns the available resources for a container in a format
|
||||||
|
// that Docker understands.
|
||||||
func (l Limits) AsContainerResources() container.Resources {
|
func (l Limits) AsContainerResources() container.Resources {
|
||||||
pids := l.ProcessLimit()
|
pids := l.ProcessLimit()
|
||||||
|
resources := container.Resources{
|
||||||
return container.Resources{
|
|
||||||
Memory: l.BoundedMemoryLimit(),
|
Memory: l.BoundedMemoryLimit(),
|
||||||
MemoryReservation: l.MemoryLimit * 1_000_000,
|
MemoryReservation: l.MemoryLimit * 1_000_000,
|
||||||
MemorySwap: l.ConvertedSwap(),
|
MemorySwap: l.ConvertedSwap(),
|
||||||
CPUQuota: l.ConvertedCpuLimit(),
|
|
||||||
CPUPeriod: 100_000,
|
|
||||||
CPUShares: 1024,
|
|
||||||
BlkioWeight: l.IoWeight,
|
BlkioWeight: l.IoWeight,
|
||||||
OomKillDisable: &l.OOMDisabled,
|
OomKillDisable: &l.OOMDisabled,
|
||||||
CpusetCpus: l.Threads,
|
|
||||||
PidsLimit: &pids,
|
PidsLimit: &pids,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the CPU Limit is not set, don't send any of these fields through. Providing
|
||||||
|
// them seems to break some Java services that try to read the available processors.
|
||||||
|
//
|
||||||
|
// @see https://github.com/pterodactyl/panel/issues/3988
|
||||||
|
if l.CpuLimit > 0 {
|
||||||
|
resources.CPUQuota = l.CpuLimit * 1_000
|
||||||
|
resources.CPUPeriod = 100_00
|
||||||
|
resources.CPUShares = 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar to above, don't set the specific assigned CPUs if we didn't actually limit
|
||||||
|
// the server to any of them.
|
||||||
|
if l.Threads != "" {
|
||||||
|
resources.CpusetCpus = l.Threads
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources
|
||||||
}
|
}
|
||||||
|
|
||||||
type Variables map[string]interface{}
|
type Variables map[string]interface{}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
// Package notify handles notifying the operating system of the program's state.
|
|
||||||
//
|
|
||||||
// For linux based operating systems, this is done through the systemd socket
|
|
||||||
// set by "NOTIFY_SOCKET" environment variable.
|
|
||||||
//
|
|
||||||
// Currently, no other operating systems are supported.
|
|
||||||
package notify
|
|
||||||
|
|
||||||
func Readiness() error {
|
|
||||||
return readiness()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Reloading() error {
|
|
||||||
return reloading()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Stopping() error {
|
|
||||||
return stopping()
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package notify
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func notify(path string, r io.Reader) error {
|
|
||||||
s := &net.UnixAddr{
|
|
||||||
Name: path,
|
|
||||||
Net: "unixgram",
|
|
||||||
}
|
|
||||||
c, err := net.DialUnix(s.Net, nil, s)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(c, r); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func socketNotify(payload string) error {
|
|
||||||
v, ok := os.LookupEnv("NOTIFY_SOCKET")
|
|
||||||
if !ok || v == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := notify(v, strings.NewReader(payload)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readiness() error {
|
|
||||||
return socketNotify("READY=1")
|
|
||||||
}
|
|
||||||
|
|
||||||
func reloading() error {
|
|
||||||
return socketNotify("RELOADING=1")
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopping() error {
|
|
||||||
return socketNotify("STOPPING=1")
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
//go:build !linux
|
|
||||||
// +build !linux
|
|
||||||
|
|
||||||
package notify
|
|
||||||
|
|
||||||
func readiness() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func reloading() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopping() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -142,12 +142,10 @@ func (c *client) request(ctx context.Context, method, path string, body io.Reade
|
|||||||
if r.HasError() {
|
if r.HasError() {
|
||||||
// Close the request body after returning the error to free up resources.
|
// Close the request body after returning the error to free up resources.
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
// Don't keep spamming the endpoint if we've already made too many requests or
|
// Don't keep attempting to access this endpoint if the response is a 4XX
|
||||||
// if we're not even authenticated correctly. Retrying generally won't fix either
|
// level error which indicates a client mistake. Only retry when the error
|
||||||
// of these issues.
|
// is due to a server issue (5XX error).
|
||||||
if r.StatusCode == http.StatusForbidden ||
|
if r.StatusCode >= 400 && r.StatusCode < 500 {
|
||||||
r.StatusCode == http.StatusTooManyRequests ||
|
|
||||||
r.StatusCode == http.StatusUnauthorized {
|
|
||||||
return backoff.Permanent(r.Error())
|
return backoff.Permanent(r.Error())
|
||||||
}
|
}
|
||||||
return r.Error()
|
return r.Error()
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ import (
|
|||||||
"github.com/pterodactyl/wings/parser"
|
"github.com/pterodactyl/wings/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SftpAuthPassword = SftpAuthRequestType("password")
|
||||||
|
SftpAuthPublicKey = SftpAuthRequestType("public_key")
|
||||||
|
)
|
||||||
|
|
||||||
// A generic type allowing for easy binding use when making requests to API
|
// A generic type allowing for easy binding use when making requests to API
|
||||||
// endpoints that only expect a singular argument or something that would not
|
// endpoints that only expect a singular argument or something that would not
|
||||||
// benefit from being a typed struct.
|
// benefit from being a typed struct.
|
||||||
@@ -63,15 +68,17 @@ type RawServerData struct {
|
|||||||
ProcessConfiguration json.RawMessage `json:"process_configuration"`
|
ProcessConfiguration json.RawMessage `json:"process_configuration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SftpAuthRequestType string
|
||||||
|
|
||||||
// SftpAuthRequest defines the request details that are passed along to the Panel
|
// SftpAuthRequest defines the request details that are passed along to the Panel
|
||||||
// when determining if the credentials provided to Wings are valid.
|
// when determining if the credentials provided to Wings are valid.
|
||||||
type SftpAuthRequest struct {
|
type SftpAuthRequest struct {
|
||||||
|
Type SftpAuthRequestType `json:"type"`
|
||||||
User string `json:"username"`
|
User string `json:"username"`
|
||||||
Pass string `json:"password"`
|
Pass string `json:"password"`
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
SessionID []byte `json:"session_id"`
|
SessionID []byte `json:"session_id"`
|
||||||
ClientVersion []byte `json:"client_version"`
|
ClientVersion []byte `json:"client_version"`
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SftpAuthResponse is returned by the Panel when a pair of SFTP credentials
|
// SftpAuthResponse is returned by the Panel when a pair of SFTP credentials
|
||||||
@@ -79,7 +86,6 @@ type SftpAuthRequest struct {
|
|||||||
// matched as well as the permissions that are assigned to the authenticated
|
// matched as well as the permissions that are assigned to the authenticated
|
||||||
// user for the SFTP subsystem.
|
// user for the SFTP subsystem.
|
||||||
type SftpAuthResponse struct {
|
type SftpAuthResponse struct {
|
||||||
SSHKeys []string `json:"ssh_keys"`
|
|
||||||
Server string `json:"server"`
|
Server string `json:"server"`
|
||||||
Permissions []string `json:"permissions"`
|
Permissions []string `json:"permissions"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,15 @@ func getServerFileContents(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
// Don't allow a named pipe to be opened.
|
||||||
|
//
|
||||||
|
// @see https://github.com/pterodactyl/panel/issues/4059
|
||||||
|
if st.Mode()&os.ModeNamedPipe != 0 {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "Cannot open files of this type.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.Header("X-Mime-Type", st.Mimetype)
|
c.Header("X-Mime-Type", st.Mimetype)
|
||||||
c.Header("Content-Length", strconv.Itoa(int(st.Size())))
|
c.Header("Content-Length", strconv.Itoa(int(st.Size())))
|
||||||
@@ -122,6 +131,10 @@ func putServerRenameFiles(c *gin.Context) {
|
|||||||
// Return nil if the error is an is not exists.
|
// Return nil if the error is an is not exists.
|
||||||
// NOTE: os.IsNotExist() does not work if the error is wrapped.
|
// NOTE: os.IsNotExist() does not work if the error is wrapped.
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
s.Log().WithField("error", err).
|
||||||
|
WithField("from_path", pf).
|
||||||
|
WithField("to_path", pt).
|
||||||
|
Warn("failed to rename: source or target does not exist")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -164,5 +164,6 @@ func (h *Handler) listenForServerEvents(ctx context.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ func (a *Archive) withFilesCallback(tw *tar.Writer) func(path string, de *godirw
|
|||||||
for _, f := range a.Files {
|
for _, f := range a.Files {
|
||||||
// If the given doesn't match, or doesn't have the same prefix continue
|
// If the given doesn't match, or doesn't have the same prefix continue
|
||||||
// to the next item in the loop.
|
// to the next item in the loop.
|
||||||
if p != f && !strings.HasPrefix(p, f) {
|
if p != f && !strings.HasPrefix(strings.TrimSuffix(p, "/")+"/", f) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,19 +115,6 @@ func (fs *Filesystem) Touch(p string, flag int) (*os.File, error) {
|
|||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads a file on the system and returns it as a byte representation in a file
|
|
||||||
// reader. This is not the most memory efficient usage since it will be reading the
|
|
||||||
// entirety of the file into memory.
|
|
||||||
func (fs *Filesystem) Readfile(p string, w io.Writer) error {
|
|
||||||
file, _, err := fs.File(p)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
_, err = bufio.NewReader(file).WriteTo(w)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writefile writes a file to the system. If the file does not already exist one
|
// Writefile writes a file to the system. If the file does not already exist one
|
||||||
// will be created. This will also properly recalculate the disk space used by
|
// will be created. This will also properly recalculate the disk space used by
|
||||||
// the server when writing new files or modifying existing ones.
|
// the server when writing new files or modifying existing ones.
|
||||||
@@ -184,16 +171,16 @@ func (fs *Filesystem) CreateDirectory(name string, p string) error {
|
|||||||
return os.MkdirAll(cleaned, 0o755)
|
return os.MkdirAll(cleaned, 0o755)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Moves (or renames) a file or directory.
|
// Rename moves (or renames) a file or directory.
|
||||||
func (fs *Filesystem) Rename(from string, to string) error {
|
func (fs *Filesystem) Rename(from string, to string) error {
|
||||||
cleanedFrom, err := fs.SafePath(from)
|
cleanedFrom, err := fs.SafePath(from)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanedTo, err := fs.SafePath(to)
|
cleanedTo, err := fs.SafePath(to)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the target file or directory already exists the rename function will fail, so just
|
// If the target file or directory already exists the rename function will fail, so just
|
||||||
@@ -215,7 +202,10 @@ func (fs *Filesystem) Rename(from string, to string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.Rename(cleanedFrom, cleanedTo)
|
if err := os.Rename(cleanedFrom, cleanedTo); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively iterates over a file or directory and sets the permissions on all of the
|
// Recursively iterates over a file or directory and sets the permissions on all of the
|
||||||
@@ -492,7 +482,11 @@ func (fs *Filesystem) ListDirectory(p string) ([]Stat, error) {
|
|||||||
cleanedp, _ = fs.SafePath(filepath.Join(cleaned, f.Name()))
|
cleanedp, _ = fs.SafePath(filepath.Join(cleaned, f.Name()))
|
||||||
}
|
}
|
||||||
|
|
||||||
if cleanedp != "" {
|
// Don't try to detect the type on a pipe — this will just hang the application and
|
||||||
|
// you'll never get a response back.
|
||||||
|
//
|
||||||
|
// @see https://github.com/pterodactyl/panel/issues/4059
|
||||||
|
if cleanedp != "" && f.Mode()&os.ModeNamedPipe == 0 {
|
||||||
m, _ = mimetype.DetectFile(filepath.Join(cleaned, f.Name()))
|
m, _ = mimetype.DetectFile(filepath.Join(cleaned, f.Name()))
|
||||||
} else {
|
} else {
|
||||||
// Just pass this for an unknown type because the file could not safely be resolved within
|
// Just pass this for an unknown type because the file could not safely be resolved within
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package filesystem
|
package filesystem
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
@@ -44,6 +45,14 @@ type rootFs struct {
|
|||||||
root string
|
root string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getFileContent(file *os.File) string {
|
||||||
|
var w bytes.Buffer
|
||||||
|
if _, err := bufio.NewReader(file).WriteTo(&w); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return w.String()
|
||||||
|
}
|
||||||
|
|
||||||
func (rfs *rootFs) CreateServerFile(p string, c []byte) error {
|
func (rfs *rootFs) CreateServerFile(p string, c []byte) error {
|
||||||
f, err := os.Create(filepath.Join(rfs.root, "/server", p))
|
f, err := os.Create(filepath.Join(rfs.root, "/server", p))
|
||||||
|
|
||||||
@@ -75,54 +84,6 @@ func (rfs *rootFs) reset() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilesystem_Readfile(t *testing.T) {
|
|
||||||
g := Goblin(t)
|
|
||||||
fs, rfs := NewFs()
|
|
||||||
|
|
||||||
g.Describe("Readfile", func() {
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
|
|
||||||
g.It("opens a file if it exists on the system", func() {
|
|
||||||
err := rfs.CreateServerFileFromString("test.txt", "testing")
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
|
|
||||||
err = fs.Readfile("test.txt", buf)
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(buf.String()).Equal("testing")
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("returns an error if the file does not exist", func() {
|
|
||||||
err := fs.Readfile("test.txt", buf)
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("returns an error if the \"file\" is a directory", func() {
|
|
||||||
err := os.Mkdir(filepath.Join(rfs.root, "/server/test.txt"), 0o755)
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
|
|
||||||
err = fs.Readfile("test.txt", buf)
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(IsErrorCode(err, ErrCodeIsDirectory)).IsTrue()
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("cannot open a file outside the root directory", func() {
|
|
||||||
err := rfs.CreateServerFileFromString("/../test.txt", "testing")
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
|
|
||||||
err = fs.Readfile("/../test.txt", buf)
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
|
|
||||||
})
|
|
||||||
|
|
||||||
g.AfterEach(func() {
|
|
||||||
buf.Truncate(0)
|
|
||||||
atomic.StoreInt64(&fs.diskUsed, 0)
|
|
||||||
rfs.reset()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFilesystem_Writefile(t *testing.T) {
|
func TestFilesystem_Writefile(t *testing.T) {
|
||||||
g := Goblin(t)
|
g := Goblin(t)
|
||||||
fs, rfs := NewFs()
|
fs, rfs := NewFs()
|
||||||
@@ -140,9 +101,10 @@ func TestFilesystem_Writefile(t *testing.T) {
|
|||||||
err := fs.Writefile("test.txt", r)
|
err := fs.Writefile("test.txt", r)
|
||||||
g.Assert(err).IsNil()
|
g.Assert(err).IsNil()
|
||||||
|
|
||||||
err = fs.Readfile("test.txt", buf)
|
f, _, err := fs.File("test.txt")
|
||||||
g.Assert(err).IsNil()
|
g.Assert(err).IsNil()
|
||||||
g.Assert(buf.String()).Equal("test file content")
|
defer f.Close()
|
||||||
|
g.Assert(getFileContent(f)).Equal("test file content")
|
||||||
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(r.Size())
|
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(r.Size())
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -152,9 +114,10 @@ func TestFilesystem_Writefile(t *testing.T) {
|
|||||||
err := fs.Writefile("/some/nested/test.txt", r)
|
err := fs.Writefile("/some/nested/test.txt", r)
|
||||||
g.Assert(err).IsNil()
|
g.Assert(err).IsNil()
|
||||||
|
|
||||||
err = fs.Readfile("/some/nested/test.txt", buf)
|
f, _, err := fs.File("/some/nested/test.txt")
|
||||||
g.Assert(err).IsNil()
|
g.Assert(err).IsNil()
|
||||||
g.Assert(buf.String()).Equal("test file content")
|
defer f.Close()
|
||||||
|
g.Assert(getFileContent(f)).Equal("test file content")
|
||||||
})
|
})
|
||||||
|
|
||||||
g.It("can create a new file inside a nested directory without a trailing slash", func() {
|
g.It("can create a new file inside a nested directory without a trailing slash", func() {
|
||||||
@@ -163,9 +126,10 @@ func TestFilesystem_Writefile(t *testing.T) {
|
|||||||
err := fs.Writefile("some/../foo/bar/test.txt", r)
|
err := fs.Writefile("some/../foo/bar/test.txt", r)
|
||||||
g.Assert(err).IsNil()
|
g.Assert(err).IsNil()
|
||||||
|
|
||||||
err = fs.Readfile("foo/bar/test.txt", buf)
|
f, _, err := fs.File("foo/bar/test.txt")
|
||||||
g.Assert(err).IsNil()
|
g.Assert(err).IsNil()
|
||||||
g.Assert(buf.String()).Equal("test file content")
|
defer f.Close()
|
||||||
|
g.Assert(getFileContent(f)).Equal("test file content")
|
||||||
})
|
})
|
||||||
|
|
||||||
g.It("cannot create a file outside the root directory", func() {
|
g.It("cannot create a file outside the root directory", func() {
|
||||||
@@ -190,28 +154,6 @@ func TestFilesystem_Writefile(t *testing.T) {
|
|||||||
g.Assert(IsErrorCode(err, ErrCodeDiskSpace)).IsTrue()
|
g.Assert(IsErrorCode(err, ErrCodeDiskSpace)).IsTrue()
|
||||||
})
|
})
|
||||||
|
|
||||||
/*g.It("updates the total space used when a file is appended to", func() {
|
|
||||||
atomic.StoreInt64(&fs.diskUsed, 100)
|
|
||||||
|
|
||||||
b := make([]byte, 100)
|
|
||||||
_, _ = rand.Read(b)
|
|
||||||
|
|
||||||
r := bytes.NewReader(b)
|
|
||||||
err := fs.Writefile("test.txt", r)
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(200))
|
|
||||||
|
|
||||||
// If we write less data than already exists, we should expect the total
|
|
||||||
// disk used to be decremented.
|
|
||||||
b = make([]byte, 50)
|
|
||||||
_, _ = rand.Read(b)
|
|
||||||
|
|
||||||
r = bytes.NewReader(b)
|
|
||||||
err = fs.Writefile("test.txt", r)
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(150))
|
|
||||||
})*/
|
|
||||||
|
|
||||||
g.It("truncates the file when writing new contents", func() {
|
g.It("truncates the file when writing new contents", func() {
|
||||||
r := bytes.NewReader([]byte("original data"))
|
r := bytes.NewReader([]byte("original data"))
|
||||||
err := fs.Writefile("test.txt", r)
|
err := fs.Writefile("test.txt", r)
|
||||||
@@ -221,9 +163,10 @@ func TestFilesystem_Writefile(t *testing.T) {
|
|||||||
err = fs.Writefile("test.txt", r)
|
err = fs.Writefile("test.txt", r)
|
||||||
g.Assert(err).IsNil()
|
g.Assert(err).IsNil()
|
||||||
|
|
||||||
err = fs.Readfile("test.txt", buf)
|
f, _, err := fs.File("test.txt")
|
||||||
g.Assert(err).IsNil()
|
g.Assert(err).IsNil()
|
||||||
g.Assert(buf.String()).Equal("new data")
|
defer f.Close()
|
||||||
|
g.Assert(getFileContent(f)).Equal("new data")
|
||||||
})
|
})
|
||||||
|
|
||||||
g.AfterEach(func() {
|
g.AfterEach(func() {
|
||||||
|
|||||||
@@ -119,16 +119,6 @@ func TestFilesystem_Blocks_Symlinks(t *testing.T) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
g.Describe("Readfile", func() {
|
|
||||||
g.It("cannot read a file symlinked outside the root", func() {
|
|
||||||
b := bytes.Buffer{}
|
|
||||||
|
|
||||||
err := fs.Readfile("symlinked.txt", &b)
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("Writefile", func() {
|
g.Describe("Writefile", func() {
|
||||||
g.It("cannot write to a file symlinked outside the root", func() {
|
g.It("cannot write to a file symlinked outside the root", func() {
|
||||||
r := bytes.NewReader([]byte("testing"))
|
r := bytes.NewReader([]byte("testing"))
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ func (h *Handler) Filewrite(request *sftp.Request) (io.WriterAt, error) {
|
|||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filecmd handler for basic SFTP system calls related to files, but not anything to do with reading
|
// Filecmd hander for basic SFTP system calls related to files, but not anything to do with reading
|
||||||
// or writing to those files.
|
// or writing to those files.
|
||||||
func (h *Handler) Filecmd(request *sftp.Request) error {
|
func (h *Handler) Filecmd(request *sftp.Request) error {
|
||||||
if h.ro {
|
if h.ro {
|
||||||
|
|||||||
271
sftp/server.go
271
sftp/server.go
@@ -1,17 +1,11 @@
|
|||||||
package sftp
|
package sftp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/ed25519"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -22,6 +16,7 @@ import (
|
|||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
@@ -56,7 +51,18 @@ func New(m *server.Manager) *SFTPServer {
|
|||||||
// SFTP connections. This will automatically generate an ED25519 key if one does
|
// SFTP connections. This will automatically generate an ED25519 key if one does
|
||||||
// not already exist on the system for host key verification purposes.
|
// not already exist on the system for host key verification purposes.
|
||||||
func (c *SFTPServer) Run() error {
|
func (c *SFTPServer) Run() error {
|
||||||
keys, err := c.loadPrivateKeys()
|
if _, err := os.Stat(c.PrivateKeyPath()); os.IsNotExist(err) {
|
||||||
|
if err := c.generateED25519PrivateKey(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return errors.Wrap(err, "sftp: could not stat private key file")
|
||||||
|
}
|
||||||
|
pb, err := os.ReadFile(c.PrivateKeyPath())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "sftp: could not read private key file")
|
||||||
|
}
|
||||||
|
private, err := ssh.ParsePrivateKey(pb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -64,20 +70,23 @@ func (c *SFTPServer) Run() error {
|
|||||||
conf := &ssh.ServerConfig{
|
conf := &ssh.ServerConfig{
|
||||||
NoClientAuth: false,
|
NoClientAuth: false,
|
||||||
MaxAuthTries: 6,
|
MaxAuthTries: 6,
|
||||||
PasswordCallback: c.passwordCallback,
|
PasswordCallback: func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||||
PublicKeyCallback: c.publicKeyCallback,
|
return c.makeCredentialsRequest(conn, remote.SftpAuthPassword, string(password))
|
||||||
}
|
},
|
||||||
|
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||||
for _, k := range keys {
|
return c.makeCredentialsRequest(conn, remote.SftpAuthPublicKey, string(ssh.MarshalAuthorizedKey(key)))
|
||||||
conf.AddHostKey(k)
|
},
|
||||||
}
|
}
|
||||||
|
conf.AddHostKey(private)
|
||||||
|
|
||||||
listener, err := net.Listen("tcp", c.Listen)
|
listener, err := net.Listen("tcp", c.Listen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.WithField("listen", c.Listen).Info("sftp server listening for connections")
|
public := string(ssh.MarshalAuthorizedKey(private.PublicKey()))
|
||||||
|
log.WithField("listen", c.Listen).WithField("public_key", strings.Trim(public, "\n")).Info("sftp server listening for connections")
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if conn, _ := listener.Accept(); conn != nil {
|
if conn, _ := listener.Accept(); conn != nil {
|
||||||
go func(conn net.Conn) {
|
go func(conn net.Conn) {
|
||||||
@@ -103,7 +112,7 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) {
|
|||||||
// If its not a session channel we just move on because its not something we
|
// If its not a session channel we just move on because its not something we
|
||||||
// know how to handle at this point.
|
// know how to handle at this point.
|
||||||
if ch.ChannelType() != "session" {
|
if ch.ChannelType() != "session" {
|
||||||
_ = ch.Reject(ssh.UnknownChannelType, "unknown channel type")
|
ch.Reject(ssh.UnknownChannelType, "unknown channel type")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +126,7 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) {
|
|||||||
// Channels have a type that is dependent on the protocol. For SFTP
|
// Channels have a type that is dependent on the protocol. For SFTP
|
||||||
// this is "subsystem" with a payload that (should) be "sftp". Discard
|
// this is "subsystem" with a payload that (should) be "sftp". Discard
|
||||||
// anything else we receive ("pty", "shell", etc)
|
// anything else we receive ("pty", "shell", etc)
|
||||||
_ = req.Reply(req.Type == "subsystem" && string(req.Payload[4:]) == "sftp", nil)
|
req.Reply(req.Type == "subsystem" && string(req.Payload[4:]) == "sftp", nil)
|
||||||
}
|
}
|
||||||
}(requests)
|
}(requests)
|
||||||
|
|
||||||
@@ -135,7 +144,6 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) {
|
|||||||
return s.ID() == uuid
|
return s.ID() == uuid
|
||||||
})
|
})
|
||||||
if srv == nil {
|
if srv == nil {
|
||||||
_ = conn.Close()
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,172 +151,48 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) {
|
|||||||
// them access to the underlying filesystem.
|
// them access to the underlying filesystem.
|
||||||
handler := sftp.NewRequestServer(channel, NewHandler(sconn, srv).Handlers())
|
handler := sftp.NewRequestServer(channel, NewHandler(sconn, srv).Handlers())
|
||||||
if err := handler.Serve(); err == io.EOF {
|
if err := handler.Serve(); err == io.EOF {
|
||||||
_ = handler.Close()
|
handler.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SFTPServer) loadPrivateKeys() ([]ssh.Signer, error) {
|
// Generates a new ED25519 private key that is used for host authentication when
|
||||||
if _, err := os.Stat(path.Join(c.BasePath, ".sftp/id_rsa")); err != nil {
|
// a user connects to the SFTP server.
|
||||||
if !os.IsNotExist(err) {
|
func (c *SFTPServer) generateED25519PrivateKey() error {
|
||||||
return nil, err
|
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.generateRSAPrivateKey(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rsaBytes, err := ioutil.ReadFile(path.Join(c.BasePath, ".sftp/id_rsa"))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "sftp/server: could not read private key file")
|
return errors.Wrap(err, "sftp: failed to generate ED25519 private key")
|
||||||
}
|
}
|
||||||
rsaPrivateKey, err := ssh.ParsePrivateKey(rsaBytes)
|
if err := os.MkdirAll(path.Dir(c.PrivateKeyPath()), 0o755); err != nil {
|
||||||
|
return errors.Wrap(err, "sftp: could not create internal sftp data directory")
|
||||||
|
}
|
||||||
|
o, err := os.OpenFile(c.PrivateKeyPath(), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return errors.WithStack(err)
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat(path.Join(c.BasePath, ".sftp/id_ecdsa")); err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.generateECDSAPrivateKey(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ecdsaBytes, err := ioutil.ReadFile(path.Join(c.BasePath, ".sftp/id_ecdsa"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "sftp/server: could not read private key file")
|
|
||||||
}
|
|
||||||
ecdsaPrivateKey, err := ssh.ParsePrivateKey(ecdsaBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat(path.Join(c.BasePath, ".sftp/id_ed25519")); err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.generateEd25519PrivateKey(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ed25519Bytes, err := ioutil.ReadFile(path.Join(c.BasePath, ".sftp/id_ed25519"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "sftp/server: could not read private key file")
|
|
||||||
}
|
|
||||||
ed25519PrivateKey, err := ssh.ParsePrivateKey(ed25519Bytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return []ssh.Signer{
|
|
||||||
rsaPrivateKey,
|
|
||||||
ecdsaPrivateKey,
|
|
||||||
ed25519PrivateKey,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateRSAPrivateKey generates a RSA-4096 private key that will be used by the SFTP server.
|
|
||||||
func (c *SFTPServer) generateRSAPrivateKey() error {
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 4096)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(path.Dir(c.PrivateKeyPath("rsa")), 0o755); err != nil {
|
|
||||||
return errors.Wrap(err, "sftp/server: could not create .sftp directory")
|
|
||||||
}
|
|
||||||
o, err := os.OpenFile(c.PrivateKeyPath("rsa"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
defer o.Close()
|
defer o.Close()
|
||||||
|
|
||||||
if err := pem.Encode(o, &pem.Block{
|
b, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||||
Type: "RSA PRIVATE KEY",
|
if err != nil {
|
||||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
return errors.Wrap(err, "sftp: failed to marshal private key into bytes")
|
||||||
}); err != nil {
|
}
|
||||||
return err
|
if err := pem.Encode(o, &pem.Block{Type: "PRIVATE KEY", Bytes: b}); err != nil {
|
||||||
|
return errors.Wrap(err, "sftp: failed to write ED25519 private key to disk")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateECDSAPrivateKey generates a ECDSA-P256 private key that will be used by the SFTP server.
|
func (c *SFTPServer) makeCredentialsRequest(conn ssh.ConnMetadata, t remote.SftpAuthRequestType, p string) (*ssh.Permissions, error) {
|
||||||
func (c *SFTPServer) generateECDSAPrivateKey() error {
|
|
||||||
key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(path.Dir(c.PrivateKeyPath("ecdsa")), 0o755); err != nil {
|
|
||||||
return errors.Wrap(err, "sftp/server: could not create .sftp directory")
|
|
||||||
}
|
|
||||||
o, err := os.OpenFile(c.PrivateKeyPath("ecdsa"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer o.Close()
|
|
||||||
|
|
||||||
privBytes, err := x509.MarshalPKCS8PrivateKey(key)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pem.Encode(o, &pem.Block{
|
|
||||||
Type: "PRIVATE KEY",
|
|
||||||
Bytes: privBytes,
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateEd25519PrivateKey generates an ed25519 private key that will be used by the SFTP server.
|
|
||||||
func (c *SFTPServer) generateEd25519PrivateKey() error {
|
|
||||||
_, key, err := ed25519.GenerateKey(rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(path.Dir(c.PrivateKeyPath("ed25519")), 0o755); err != nil {
|
|
||||||
return errors.Wrap(err, "sftp/server: could not create .sftp directory")
|
|
||||||
}
|
|
||||||
o, err := os.OpenFile(c.PrivateKeyPath("ed25519"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer o.Close()
|
|
||||||
|
|
||||||
privBytes, err := x509.MarshalPKCS8PrivateKey(key)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pem.Encode(o, &pem.Block{
|
|
||||||
Type: "PRIVATE KEY",
|
|
||||||
Bytes: privBytes,
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrivateKeyPath returns the path the host private key for this server instance.
|
|
||||||
func (c *SFTPServer) PrivateKeyPath(name string) string {
|
|
||||||
return path.Join(c.BasePath, ".sftp", "id_"+name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A function capable of validating user credentials with the Panel API.
|
|
||||||
func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
|
||||||
request := remote.SftpAuthRequest{
|
request := remote.SftpAuthRequest{
|
||||||
|
Type: t,
|
||||||
User: conn.User(),
|
User: conn.User(),
|
||||||
Pass: string(pass),
|
Pass: p,
|
||||||
IP: conn.RemoteAddr().String(),
|
IP: conn.RemoteAddr().String(),
|
||||||
SessionID: conn.SessionID(),
|
SessionID: conn.SessionID(),
|
||||||
ClientVersion: conn.ClientVersion(),
|
ClientVersion: conn.ClientVersion(),
|
||||||
Type: "password",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger := log.WithFields(log.Fields{"subsystem": "sftp", "username": conn.User(), "ip": conn.RemoteAddr().String()})
|
logger := log.WithFields(log.Fields{"subsystem": "sftp", "method": request.Type, "username": request.User, "ip": request.IP})
|
||||||
logger.Debug("validating credentials for SFTP connection")
|
logger.Debug("validating credentials for SFTP connection")
|
||||||
|
|
||||||
if !validUsernameRegexp.MatchString(request.User) {
|
if !validUsernameRegexp.MatchString(request.User) {
|
||||||
@@ -316,11 +200,6 @@ func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.
|
|||||||
return nil, &remote.SftpInvalidCredentialsError{}
|
return nil, &remote.SftpInvalidCredentialsError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(pass) < 1 {
|
|
||||||
logger.Warn("failed to validate user credentials (invalid format)")
|
|
||||||
return nil, &remote.SftpInvalidCredentialsError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.manager.Client().ValidateSftpCredentials(context.Background(), request)
|
resp, err := c.manager.Client().ValidateSftpCredentials(context.Background(), request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*remote.SftpInvalidCredentialsError); ok {
|
if _, ok := err.(*remote.SftpInvalidCredentialsError); ok {
|
||||||
@@ -332,7 +211,7 @@ func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.
|
|||||||
}
|
}
|
||||||
|
|
||||||
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")
|
||||||
sshPerm := &ssh.Permissions{
|
permissions := ssh.Permissions{
|
||||||
Extensions: map[string]string{
|
Extensions: map[string]string{
|
||||||
"uuid": resp.Server,
|
"uuid": resp.Server,
|
||||||
"user": conn.User(),
|
"user": conn.User(),
|
||||||
@@ -340,58 +219,10 @@ func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return sshPerm, nil
|
return &permissions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SFTPServer) publicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
// PrivateKeyPath returns the path the host private key for this server instance.
|
||||||
request := remote.SftpAuthRequest{
|
func (c *SFTPServer) PrivateKeyPath() string {
|
||||||
User: conn.User(),
|
return path.Join(c.BasePath, ".sftp/id_ed25519")
|
||||||
Pass: "KEKW",
|
|
||||||
IP: conn.RemoteAddr().String(),
|
|
||||||
SessionID: conn.SessionID(),
|
|
||||||
ClientVersion: conn.ClientVersion(),
|
|
||||||
Type: "publicKey",
|
|
||||||
}
|
|
||||||
|
|
||||||
logger := log.WithFields(log.Fields{"subsystem": "sftp", "username": conn.User(), "ip": conn.RemoteAddr().String()})
|
|
||||||
logger.Debug("validating public key for SFTP connection")
|
|
||||||
|
|
||||||
if !validUsernameRegexp.MatchString(request.User) {
|
|
||||||
logger.Warn("failed to validate user credentials (invalid format)")
|
|
||||||
return nil, &remote.SftpInvalidCredentialsError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.manager.Client().ValidateSftpCredentials(context.Background(), request)
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(*remote.SftpInvalidCredentialsError); ok {
|
|
||||||
logger.Warn("failed to validate user credentials (invalid username or password)")
|
|
||||||
} else {
|
|
||||||
logger.WithField("error", err).Error("encountered an error while trying to validate user credentials")
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(resp.SSHKeys) < 1 {
|
|
||||||
return nil, &remote.SftpInvalidCredentialsError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, k := range resp.SSHKeys {
|
|
||||||
storedPublicKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(key.Marshal(), storedPublicKey.Marshal()) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ssh.Permissions{
|
|
||||||
Extensions: map[string]string{
|
|
||||||
"uuid": resp.Server,
|
|
||||||
"user": conn.User(),
|
|
||||||
"permissions": strings.Join(resp.Permissions, ","),
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return nil, &remote.SftpInvalidCredentialsError{}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
var Version = "develop"
|
var Version = "1.6.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user