2019-04-06 05:20:26 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2019-04-07 00:32:35 +00:00
|
|
|
"bufio"
|
2019-05-04 23:04:41 +00:00
|
|
|
"bytes"
|
2020-04-04 20:08:10 +00:00
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/hex"
|
2019-05-04 23:04:41 +00:00
|
|
|
"github.com/buger/jsonparser"
|
2019-04-20 06:29:52 +00:00
|
|
|
"github.com/gorilla/websocket"
|
2019-04-06 05:20:26 +00:00
|
|
|
"github.com/julienschmidt/httprouter"
|
2020-04-04 22:15:49 +00:00
|
|
|
"github.com/mholt/archiver/v3"
|
2019-12-10 05:05:55 +00:00
|
|
|
"github.com/pkg/errors"
|
2020-04-04 05:17:26 +00:00
|
|
|
"github.com/pterodactyl/wings/api"
|
2019-12-17 05:43:07 +00:00
|
|
|
"github.com/pterodactyl/wings/config"
|
2019-11-16 23:10:53 +00:00
|
|
|
"github.com/pterodactyl/wings/installer"
|
2019-04-06 05:20:26 +00:00
|
|
|
"github.com/pterodactyl/wings/server"
|
2019-04-06 05:55:48 +00:00
|
|
|
"go.uber.org/zap"
|
|
|
|
"io"
|
2020-04-04 20:08:10 +00:00
|
|
|
"io/ioutil"
|
2019-04-06 05:20:26 +00:00
|
|
|
"net/http"
|
2019-04-06 05:55:48 +00:00
|
|
|
"os"
|
2020-04-04 20:08:10 +00:00
|
|
|
"path/filepath"
|
2019-04-06 19:27:44 +00:00
|
|
|
"strconv"
|
2019-04-06 17:49:31 +00:00
|
|
|
"strings"
|
2019-04-06 05:20:26 +00:00
|
|
|
)
|
|
|
|
|
2019-04-06 05:34:53 +00:00
|
|
|
// Retrieves a server out of the collection by UUID.
|
2019-12-08 00:43:00 +00:00
|
|
|
func (rt *Router) GetServer(uuid string) *server.Server {
|
|
|
|
return server.GetServers().Find(func(i *server.Server) bool {
|
|
|
|
return i.Uuid == uuid
|
|
|
|
})
|
2019-04-06 05:34:53 +00:00
|
|
|
}
|
|
|
|
|
2019-04-06 05:20:26 +00:00
|
|
|
type Router struct {
|
2019-04-20 06:29:52 +00:00
|
|
|
upgrader websocket.Upgrader
|
2019-04-07 23:28:01 +00:00
|
|
|
|
2019-04-06 17:49:31 +00:00
|
|
|
// The authentication token defined in the config.yml file that allows
|
2020-04-04 06:51:35 +00:00
|
|
|
// a request to perform any action against the daemon.
|
2019-04-06 17:49:31 +00:00
|
|
|
token string
|
2019-04-06 05:20:26 +00:00
|
|
|
}
|
|
|
|
|
2019-09-06 04:08:03 +00:00
|
|
|
func (rt *Router) AuthenticateRequest(h httprouter.Handle) httprouter.Handle {
|
|
|
|
return rt.AuthenticateToken(rt.AuthenticateServer(h))
|
|
|
|
}
|
|
|
|
|
2019-04-06 05:34:53 +00:00
|
|
|
// Middleware to protect server specific routes. This will ensure that the server exists and
|
|
|
|
// is in a state that allows it to be exposed to the API.
|
|
|
|
func (rt *Router) AuthenticateServer(h httprouter.Handle) httprouter.Handle {
|
|
|
|
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
2019-12-08 00:43:00 +00:00
|
|
|
if rt.GetServer(ps.ByName("server")) != nil {
|
2019-04-06 05:34:53 +00:00
|
|
|
h(w, r, ps)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-17 05:43:07 +00:00
|
|
|
// Attaches required access control headers to all of the requests.
|
|
|
|
func (rt *Router) AttachAccessControlHeaders(w http.ResponseWriter, r *http.Request, ps httprouter.Params) (http.ResponseWriter, *http.Request, httprouter.Params) {
|
|
|
|
w.Header().Set("Access-Control-Allow-Origin", config.Get().PanelLocation)
|
|
|
|
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
|
|
|
|
|
|
|
return w, r, ps
|
|
|
|
}
|
|
|
|
|
2020-04-04 05:17:26 +00:00
|
|
|
// Authenticates the request token against the given permission string, ensuring that
|
2019-04-06 17:49:31 +00:00
|
|
|
// if it is a server permission, the token has control over that server. If it is a global
|
|
|
|
// token, this will ensure that the request is using a properly signed global token.
|
2019-09-06 04:08:03 +00:00
|
|
|
func (rt *Router) AuthenticateToken(h httprouter.Handle) httprouter.Handle {
|
2019-04-06 17:49:31 +00:00
|
|
|
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
2019-04-20 06:29:52 +00:00
|
|
|
// Adds support for using this middleware on the websocket routes for servers. Those
|
|
|
|
// routes don't support Authorization headers, per the spec, so we abuse the socket
|
|
|
|
// protocol header and use that to pass the authorization token along to Wings without
|
|
|
|
// exposing the token in the URL directly. Neat. 📸
|
2019-04-06 17:49:31 +00:00
|
|
|
auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
|
2019-04-20 06:29:52 +00:00
|
|
|
|
2019-04-06 17:49:31 +00:00
|
|
|
if len(auth) != 2 || auth[0] != "Bearer" {
|
|
|
|
w.Header().Set("WWW-Authenticate", "Bearer")
|
|
|
|
http.Error(w, "authorization failed", http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-04-04 06:51:35 +00:00
|
|
|
// Try to match the request against the global token for the Daemon, regardless
|
2019-04-06 17:49:31 +00:00
|
|
|
// of the permission type. If nothing is matched we will fall through to the Panel
|
|
|
|
// API to try and validate permissions for a server.
|
2019-09-06 04:08:03 +00:00
|
|
|
if auth[1] == rt.token {
|
2019-12-17 05:43:07 +00:00
|
|
|
h(rt.AttachAccessControlHeaders(w, r, ps))
|
2019-09-06 04:08:03 +00:00
|
|
|
return
|
2019-04-06 17:49:31 +00:00
|
|
|
}
|
|
|
|
|
2019-04-20 06:29:52 +00:00
|
|
|
// Happens because we don't have any of the server handling code here.
|
2019-04-06 17:49:31 +00:00
|
|
|
http.Error(w, "not implemented", http.StatusNotImplemented)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-04 05:17:26 +00:00
|
|
|
func (rt *Router) routeGetServerArchive(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
2020-04-04 06:51:35 +00:00
|
|
|
auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
|
|
|
|
|
|
|
|
if len(auth) != 2 || auth[0] != "Bearer" {
|
|
|
|
w.Header().Set("WWW-Authenticate", "Bearer")
|
|
|
|
http.Error(w, "authorization failed", http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
token, err := ParseArchiveJWT([]byte(auth[1]))
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, "authorization failed", http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if token.Subject != ps.ByName("server") {
|
|
|
|
http.Error(w, "forbidden", http.StatusForbidden)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-04-04 05:17:26 +00:00
|
|
|
s := rt.GetServer(ps.ByName("server"))
|
|
|
|
|
|
|
|
st, err := s.Archiver.Stat()
|
|
|
|
if err != nil {
|
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
zap.S().Errorw("failed to stat archive for reading", zap.String("server", s.Uuid), zap.Error(err))
|
|
|
|
http.Error(w, "failed to stat archive", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
checksum, err := s.Archiver.Checksum()
|
|
|
|
if err != nil {
|
|
|
|
zap.S().Errorw("failed to calculate checksum", zap.String("server", s.Uuid), zap.Error(err))
|
|
|
|
http.Error(w, "failed to calculate checksum", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
file, err := os.Open(s.Archiver.ArchivePath())
|
|
|
|
if err != nil {
|
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
zap.S().Errorw("failed to open archive for reading", zap.String("server", s.Uuid), zap.Error(err))
|
|
|
|
}
|
|
|
|
|
|
|
|
http.Error(w, "failed to open archive", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
w.Header().Set("X-Checksum", checksum)
|
|
|
|
w.Header().Set("X-Mime-Type", st.Mimetype)
|
|
|
|
w.Header().Set("Content-Length", strconv.Itoa(int(st.Info.Size())))
|
|
|
|
w.Header().Set("Content-Disposition", "attachment; filename="+s.Archiver.ArchiveName())
|
|
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
|
|
|
|
|
|
bufio.NewReader(file).WriteTo(w)
|
|
|
|
}
|
|
|
|
|
2020-04-04 06:51:35 +00:00
|
|
|
func (rt *Router) routeIncomingTransfer(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
2020-04-04 20:08:10 +00:00
|
|
|
zap.S().Debug("incoming transfer from panel!")
|
|
|
|
defer r.Body.Close()
|
|
|
|
|
|
|
|
go func(data []byte) {
|
|
|
|
serverID, _ := jsonparser.GetString(data, "server_id")
|
|
|
|
url, _ := jsonparser.GetString(data, "url")
|
|
|
|
token, _ := jsonparser.GetString(data, "token")
|
|
|
|
|
|
|
|
// Create an http client with no timeout.
|
|
|
|
client := &http.Client{Timeout: 0}
|
|
|
|
|
2020-04-05 00:27:31 +00:00
|
|
|
hasError := true
|
|
|
|
defer func() {
|
|
|
|
if !hasError {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
zap.S().Errorw("server transfer has failed", zap.String("server", serverID))
|
|
|
|
rerr, err := api.NewRequester().SendTransferFailure(serverID)
|
|
|
|
if rerr != nil || err != nil {
|
|
|
|
if err != nil {
|
|
|
|
zap.S().Errorw("failed to notify panel with transfer failure", zap.String("server", serverID), zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
zap.S().Errorw("panel returned an error when notifying of a transfer failure", zap.String("server", serverID), zap.Error(errors.New(rerr.String())))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
zap.S().Debugw("successfully notified panel about transfer failure", zap.String("server", serverID))
|
|
|
|
}()
|
|
|
|
|
2020-04-04 20:08:10 +00:00
|
|
|
// Make a new GET request to the URL the panel gave us.
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
|
|
if err != nil {
|
|
|
|
zap.S().Errorw("failed to create http request", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add the authorization header.
|
|
|
|
req.Header.Set("Authorization", token)
|
|
|
|
|
|
|
|
// Execute the http request.
|
|
|
|
res, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
zap.S().Errorw("failed to send http request", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
2020-04-04 22:15:49 +00:00
|
|
|
// Handle non-200 status codes.
|
2020-04-04 20:08:10 +00:00
|
|
|
if res.StatusCode != 200 {
|
|
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
|
|
if err != nil {
|
|
|
|
zap.S().Errorw("failed to read response body", zap.Int("status", res.StatusCode), zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
zap.S().Errorw("failed to request server archive", zap.Int("status", res.StatusCode), zap.String("body", string(body)))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-04-04 22:15:49 +00:00
|
|
|
// Get the path to the archive.
|
2020-04-04 20:08:10 +00:00
|
|
|
archivePath := filepath.Join(config.Get().System.ArchiveDirectory, serverID + ".tar.gz")
|
|
|
|
|
2020-04-05 00:27:31 +00:00
|
|
|
// Check if the archive already exists and delete it if it does.
|
|
|
|
_, err = os.Stat(archivePath)
|
|
|
|
if err != nil {
|
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
zap.S().Errorw("failed to stat file", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if err := os.Remove(archivePath); err != nil {
|
|
|
|
zap.S().Errorw("failed to delete old file", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-04 22:15:49 +00:00
|
|
|
// Create the file.
|
2020-04-04 20:08:10 +00:00
|
|
|
file, err := os.Create(archivePath)
|
|
|
|
if err != nil {
|
|
|
|
zap.S().Errorw("failed to open file on disk", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-04-04 22:15:49 +00:00
|
|
|
// Copy the file.
|
2020-04-04 20:08:10 +00:00
|
|
|
_, err = io.Copy(file, res.Body)
|
|
|
|
if err != nil {
|
|
|
|
zap.S().Errorw("failed to copy file to disk", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-04-04 22:15:49 +00:00
|
|
|
// Close the file so it can be opened to verify the checksum.
|
2020-04-04 20:08:10 +00:00
|
|
|
if err := file.Close(); err != nil {
|
|
|
|
zap.S().Errorw("failed to close archive file", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
2020-04-04 22:15:49 +00:00
|
|
|
zap.S().Debug("server archive has been downloaded, computing checksum..", zap.String("server", serverID))
|
2020-04-04 20:08:10 +00:00
|
|
|
|
2020-04-04 22:15:49 +00:00
|
|
|
// Open the archive file for computing a checksum.
|
2020-04-04 20:08:10 +00:00
|
|
|
file, err = os.Open(archivePath)
|
|
|
|
if err != nil {
|
|
|
|
zap.S().Errorw("failed to open file on disk", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-04-04 22:15:49 +00:00
|
|
|
// Compute the sha256 checksum of the file.
|
2020-04-04 20:08:10 +00:00
|
|
|
hash := sha256.New()
|
|
|
|
if _, err := io.Copy(hash, file); err != nil {
|
|
|
|
zap.S().Errorw("failed to copy file for checksum verification", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-04-04 22:15:49 +00:00
|
|
|
// Verify the two checksums.
|
2020-04-04 20:08:10 +00:00
|
|
|
if hex.EncodeToString(hash.Sum(nil)) != res.Header.Get("X-Checksum") {
|
|
|
|
zap.S().Errorw("checksum failed verification")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-04-04 22:15:49 +00:00
|
|
|
// Close the file.
|
|
|
|
if err := file.Close(); err != nil {
|
|
|
|
zap.S().Errorw("failed to close archive file", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-04-04 20:08:10 +00:00
|
|
|
zap.S().Infow("server archive transfer was successful", zap.String("server", serverID))
|
2020-04-04 22:15:49 +00:00
|
|
|
|
|
|
|
// Get the server data from the request.
|
|
|
|
serverData, t, _, _ := jsonparser.Get(data, "server")
|
|
|
|
if t != jsonparser.Object {
|
|
|
|
zap.S().Errorw("invalid server data passed in request")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
zap.S().Debug(string(serverData))
|
|
|
|
|
|
|
|
// Create a new server installer (note this does not execute the install script)
|
|
|
|
i, err := installer.New(serverData)
|
|
|
|
if err != nil {
|
|
|
|
zap.S().Warnw("failed to validate the received server data", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add the server to the collection.
|
|
|
|
server.GetServers().Add(i.Server())
|
|
|
|
|
|
|
|
// Create the server's environment (note this does not execute the install script)
|
|
|
|
i.Execute()
|
|
|
|
|
|
|
|
// Un-archive the archive. That sounds weird..
|
|
|
|
archiver.NewTarGz().Unarchive(archivePath, i.Server().Filesystem.Path())
|
|
|
|
|
|
|
|
rerr, err := api.NewRequester().SendTransferSuccess(serverID)
|
|
|
|
if rerr != nil || err != nil {
|
|
|
|
if err != nil {
|
2020-04-05 00:27:31 +00:00
|
|
|
zap.S().Errorw("failed to notify panel with transfer success", zap.String("server", serverID), zap.Error(err))
|
2020-04-04 22:15:49 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-04-05 00:27:31 +00:00
|
|
|
zap.S().Errorw("panel returned an error when notifying of a transfer success", zap.String("server", serverID), zap.Error(errors.New(rerr.String())))
|
2020-04-04 22:15:49 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
zap.S().Debugw("successfully notified panel about transfer success", zap.String("server", serverID))
|
2020-04-05 00:27:31 +00:00
|
|
|
hasError = false
|
2020-04-04 20:08:10 +00:00
|
|
|
}(rt.ReaderToBytes(r.Body))
|
|
|
|
|
|
|
|
w.WriteHeader(202)
|
2020-04-04 06:51:35 +00:00
|
|
|
}
|
|
|
|
|
2019-05-04 23:04:41 +00:00
|
|
|
func (rt *Router) ReaderToBytes(r io.Reader) []byte {
|
|
|
|
buf := bytes.Buffer{}
|
|
|
|
buf.ReadFrom(r)
|
|
|
|
|
|
|
|
return buf.Bytes()
|
|
|
|
}
|
|
|
|
|
2019-04-06 17:49:31 +00:00
|
|
|
// Configures the router and all of the associated routes.
|
2019-04-06 05:34:53 +00:00
|
|
|
func (rt *Router) ConfigureRouter() *httprouter.Router {
|
|
|
|
router := httprouter.New()
|
2019-04-06 05:20:26 +00:00
|
|
|
|
2020-04-04 06:51:35 +00:00
|
|
|
router.GET("/api/servers/:server/archive", rt.AuthenticateServer(rt.routeGetServerArchive))
|
|
|
|
|
|
|
|
router.POST("/api/transfer", rt.AuthenticateToken(rt.routeIncomingTransfer))
|
2020-04-04 05:17:26 +00:00
|
|
|
|
2019-04-06 05:20:26 +00:00
|
|
|
return router
|
2019-04-06 05:55:48 +00:00
|
|
|
}
|