From cf2ef1a173028f61d5d2efca5627c8cdef7480c4 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 5 Apr 2020 18:00:33 -0700 Subject: [PATCH] Port most of the HTTP code over to gin --- archive_auth.go | 2 + go.mod | 21 +- go.sum | 52 ++++ http.go | 514 ---------------------------------- router/error.go | 93 ++++++ router/middleware.go | 67 +++++ router/router.go | 50 ++++ router/router_download.go | 1 + router/router_server.go | 216 ++++++++++++++ router/router_server_files.go | 155 ++++++++++ router/router_server_ws.go | 60 ++++ router/router_system.go | 64 +++++ router/websocket/listeners.go | 76 +++++ router/websocket/message.go | 26 ++ router/websocket/payload.go | 24 ++ router/websocket/websocket.go | 277 ++++++++++++++++++ server/power.go | 12 + server/server.go | 21 ++ const.go => system/const.go | 2 +- system.go => system/system.go | 8 +- websocket.go | 426 ---------------------------- wings.go | 34 +-- 22 files changed, 1233 insertions(+), 968 deletions(-) create mode 100644 router/error.go create mode 100644 router/middleware.go create mode 100644 router/router.go create mode 100644 router/router_download.go create mode 100644 router/router_server.go create mode 100644 router/router_server_files.go create mode 100644 router/router_server_ws.go create mode 100644 router/router_system.go create mode 100644 router/websocket/listeners.go create mode 100644 router/websocket/message.go create mode 100644 router/websocket/payload.go create mode 100644 router/websocket/websocket.go create mode 100644 server/power.go rename const.go => system/const.go (82%) rename system.go => system/system.go (80%) delete mode 100644 websocket.go diff --git a/archive_auth.go b/archive_auth.go index c486cb6..e6d22d3 100644 --- a/archive_auth.go +++ b/archive_auth.go @@ -6,6 +6,8 @@ import ( "time" ) +var alg *jwt.HMACSHA + // ArchiveTokenPayload represents an Archive Token Payload. type ArchiveTokenPayload struct { jwt.Payload diff --git a/go.mod b/go.mod index e53810d..f7d9924 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,10 @@ require ( github.com/gabriel-vasile/mimetype v0.1.4 github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0 github.com/ghodss/yaml v1.0.0 + github.com/gin-gonic/gin v1.6.2 github.com/gogo/protobuf v1.2.1 // indirect + github.com/golang/protobuf v1.3.5 // indirect + github.com/google/gofuzz v1.1.0 // indirect github.com/google/uuid v1.1.1 github.com/gorilla/websocket v1.4.0 github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect @@ -38,6 +41,8 @@ require ( github.com/mattn/go-shellwords v1.0.10 // indirect github.com/mholt/archiver/v3 v3.3.0 github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect github.com/onsi/ginkgo v1.8.0 // indirect github.com/onsi/gomega v1.5.0 // indirect github.com/opencontainers/go-digest v1.0.0-rc1 // indirect @@ -49,20 +54,22 @@ require ( github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce github.com/sirupsen/logrus v1.0.5 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect + github.com/stretchr/objx v0.2.0 // indirect + github.com/stretchr/testify v1.5.1 // indirect go.uber.org/atomic v1.5.1 // indirect go.uber.org/multierr v1.4.0 // indirect go.uber.org/zap v1.13.0 - golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect + golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8 // indirect golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect - golang.org/x/net v0.0.0-20190923162816-aa69164e4478 // indirect - golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect - golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab // indirect - golang.org/x/text v0.3.2 // indirect + golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect + golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a // indirect + golang.org/x/sys v0.0.0-20200331124033-c3d80250170d // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect - golang.org/x/tools v0.0.0-20191206204035-259af5ff87bd // indirect + golang.org/x/tools v0.0.0-20200403190813-44a64ad78b9b // indirect gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect gopkg.in/ini.v1 v1.51.0 - gopkg.in/yaml.v2 v2.2.2 + gopkg.in/yaml.v2 v2.2.8 gotest.tools v2.2.0+incompatible // indirect ) diff --git a/go.sum b/go.sum index 028166f..eeb48c8 100644 --- a/go.sum +++ b/go.sum @@ -42,17 +42,33 @@ github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0 h1:7KeiSrO5puFH1+vdAdbpiie2TrNnkvFc/eOQz github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0/go.mod h1:D1+3UtCYAJ1os1PI+zhTVEj6Tb+IHJvXjXKz83OstmM= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.2 h1:88crIK23zO6TqlQBt+f9FrPJNKm9ZEr7qjp9vl/d5TM= +github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721 h1:KRMr9A3qfbVM7iV/WcLY/rL5LICqwMHLhwRXKu99fXw= github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -68,6 +84,7 @@ github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFE github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g= @@ -87,16 +104,24 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE= github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw= github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mholt/archiver/v3 v3.3.0 h1:vWjhY8SQp5yzM9P6OJ/eZEkmi3UAbRrxCq48MxjAzig= github.com/mholt/archiver/v3 v3.3.0/go.mod h1:YnQtqsp+94Rwd0D/rk5cnLrxusUBUXg+08Ebtr1Mqao= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs= github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -134,14 +159,21 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1 github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/uber-go/zap v1.9.1/go.mod h1:GY+83l3yxBcBw2kmHu/sAWwItnTn+ynxHCRo+WiIQOY= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -163,14 +195,18 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8 h1:fpnn/HnJONpIu6hkXi1u/7rR0NzilgWr4T0JmWkEitk= +golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -178,17 +214,25 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM= golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -204,17 +248,23 @@ golang.org/x/tools v0.0.0-20190710153321-831012c29e42/go.mod h1:jcCCGcm9btYwXyDq golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191206204035-259af5ff87bd h1:Zc7EU2PqpsNeIfOoVA7hvQX4cS3YDJEs5KlfatT3hLo= golang.org/x/tools v0.0.0-20191206204035-259af5ff87bd/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200403190813-44a64ad78b9b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools/gopls v0.1.3/go.mod h1:vrCQzOKxvuiZLjCKSmbbov04oeBQQOb4VQqwYK2PWIY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= @@ -227,6 +277,8 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= diff --git a/http.go b/http.go index 6a6cddb..16b5fff 100644 --- a/http.go +++ b/http.go @@ -5,8 +5,6 @@ import ( "bytes" "crypto/sha256" "encoding/hex" - "encoding/json" - "fmt" "github.com/buger/jsonparser" "github.com/gorilla/websocket" "github.com/julienschmidt/httprouter" @@ -98,426 +96,6 @@ func (rt *Router) AuthenticateToken(h httprouter.Handle) httprouter.Handle { } } -// Returns the basic Wings index page without anything else. -func (rt *Router) routeIndex(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { - fmt.Fprint(w, "Welcome!\n") -} - -// Returns all of the servers that exist on the Daemon. This route is only accessible to -// requests that include an administrative control key, otherwise a 404 is returned. This -// authentication is handled by a middleware. -func (rt *Router) routeAllServers(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { - json.NewEncoder(w).Encode(server.GetServers().All()) -} - -// Returns basic information about a single server found on the Daemon. -func (rt *Router) routeServer(w http.ResponseWriter, _ *http.Request, ps httprouter.Params) { - s := rt.GetServer(ps.ByName("server")) - - json.NewEncoder(w).Encode(s) -} - -type PowerActionRequest struct { - Action string `json:"action"` -} - -type CreateDirectoryRequest struct { - Name string `json:"name"` - Path string `json:"path"` -} - -func (pr *PowerActionRequest) IsValid() bool { - return pr.Action == "start" || pr.Action == "stop" || pr.Action == "kill" || pr.Action == "restart" -} - -// Handles a request to control the power state of a server. If the action being passed -// through is invalid a 404 is returned. Otherwise, a HTTP/202 Accepted response is returned -// and the actual power action is run asynchronously so that we don't have to block the -// request until a potentially slow operation completes. -// -// This is done because for the most part the Panel is using websockets to determine when -// things are happening, so theres no reason to sit and wait for a request to finish. We'll -// just see over the socket if something isn't working correctly. -func (rt *Router) routeServerPower(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - s := rt.GetServer(ps.ByName("server")) - defer r.Body.Close() - - dec := json.NewDecoder(r.Body) - var action PowerActionRequest - - if err := dec.Decode(&action); err != nil { - // Don't flood the logs with error messages if someone sends through bad - // JSON data. We don't really care. - if err != io.EOF && err != io.ErrUnexpectedEOF { - zap.S().Errorw("failed to decode power action", zap.Error(err)) - } - - http.Error(w, "could not parse power action from request", http.StatusInternalServerError) - return - } - - if !action.IsValid() { - http.NotFound(w, r) - return - } - - // Because we route all of the actual bootup process to a separate thread we need to - // check the suspension status here, otherwise the user will hit the endpoint and then - // just sit there wondering why it returns a success but nothing actually happens. - // - // We don't really care about any of the other actions at this point, they'll all result - // in the process being stopped, which should have happened anyways if the server is suspended. - if action.Action == "start" && s.Suspended { - http.Error(w, "server is suspended", http.StatusBadRequest) - return - } - - // Pass the actual heavy processing off to a seperate thread to handle so that - // we can immediately return a response from the server. - go func(a string, s *server.Server) { - switch a { - case "start": - if err := s.Environment.Start(); err != nil { - zap.S().Errorw( - "encountered unexpected error starting server process", - zap.Error(err), - zap.String("server", s.Uuid), - zap.String("action", "start"), - ) - } - break - case "stop": - if err := s.Environment.Stop(); err != nil { - zap.S().Errorw( - "encountered unexpected error stopping server process", - zap.Error(err), - zap.String("server", s.Uuid), - zap.String("action", "stop"), - ) - } - break - case "restart": - if err := s.Environment.WaitForStop(60, false); err != nil { - zap.S().Errorw( - "encountered unexpected error waiting for server process to stop", - zap.Error(err), - zap.String("server", s.Uuid), - zap.String("action", "restart"), - ) - } - - if err := s.Environment.Start(); err != nil { - zap.S().Errorw( - "encountered unexpected error starting server process", - zap.Error(err), - zap.String("server", s.Uuid), - zap.String("action", "restart"), - ) - } - break - case "kill": - if err := s.Environment.Terminate(os.Kill); err != nil { - zap.S().Errorw( - "encountered unexpected error killing server process", - zap.Error(err), - zap.String("server", s.Uuid), - zap.String("action", "kill"), - ) - } - } - }(action.Action, s) - - w.WriteHeader(http.StatusAccepted) -} - -// Return the last 1Kb of the server log file. -func (rt *Router) routeServerLogs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - s := rt.GetServer(ps.ByName("server")) - - l, _ := strconv.ParseInt(r.URL.Query().Get("size"), 10, 64) - if l <= 0 { - l = 2048 - } - - out, err := s.ReadLogfile(l) - if err != nil { - zap.S().Errorw("failed to read server log file", zap.Error(err)) - http.Error(w, "failed to read log", http.StatusInternalServerError) - return - } - - json.NewEncoder(w).Encode(struct{ Data []string `json:"data"` }{Data: out}) -} - -// Handle a request to get the contents of a file on the server. -func (rt *Router) routeServerFileRead(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - s := rt.GetServer(ps.ByName("server")) - - cleaned, err := s.Filesystem.SafePath(r.URL.Query().Get("file")) - if err != nil { - http.NotFound(w, r) - return - } - - st, err := s.Filesystem.Stat(cleaned) - if err != nil { - if !os.IsNotExist(err) { - zap.S().Errorw("failed to stat file for reading", zap.String("path", ps.ByName("path")), zap.String("server", s.Uuid), zap.Error(err)) - - http.Error(w, "failed to stat file", http.StatusInternalServerError) - return - } - - http.NotFound(w, r) - return - } - - f, err := os.Open(cleaned) - if err != nil { - if !os.IsNotExist(err) { - zap.S().Errorw("failed to open file for reading", zap.String("path", ps.ByName("path")), zap.String("server", s.Uuid), zap.Error(err)) - } - - http.Error(w, "failed to open file", http.StatusInternalServerError) - return - } - defer f.Close() - - w.Header().Set("X-Mime-Type", st.Mimetype) - w.Header().Set("Content-Length", strconv.Itoa(int(st.Info.Size()))) - - // If a download parameter is included in the URL go ahead and attach the necessary headers - // so that the file can be downloaded. - if r.URL.Query().Get("download") != "" { - w.Header().Set("Content-Disposition", "attachment; filename="+st.Info.Name()) - w.Header().Set("Content-Type", "application/octet-stream") - } - - bufio.NewReader(f).WriteTo(w) -} - -// Lists the contents of a directory. -func (rt *Router) routeServerListDirectory(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - s := rt.GetServer(ps.ByName("server")) - - stats, err := s.Filesystem.ListDirectory(r.URL.Query().Get("directory")) - if os.IsNotExist(err) { - http.NotFound(w, r) - return - } else if err != nil { - zap.S().Errorw("failed to list contents of directory", zap.String("server", s.Uuid), zap.String("path", ps.ByName("path")), zap.Error(err)) - - http.Error(w, "failed to list directory", http.StatusInternalServerError) - return - } - - json.NewEncoder(w).Encode(stats) -} - -// Writes a file to the system for the server. -func (rt *Router) routeServerWriteFile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - s := rt.GetServer(ps.ByName("server")) - - p := r.URL.Query().Get("file") - defer r.Body.Close() - err := s.Filesystem.Writefile(p, r.Body) - - if err != nil { - zap.S().Errorw("failed to write file to directory", zap.String("server", s.Uuid), zap.String("path", p), zap.Error(err)) - - http.Error(w, "failed to write file to directory", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -// Creates a new directory for the server. -func (rt *Router) routeServerCreateDirectory(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - s := rt.GetServer(ps.ByName("server")) - defer r.Body.Close() - - dec := json.NewDecoder(r.Body) - var data CreateDirectoryRequest - - if err := dec.Decode(&data); err != nil { - // Don't flood the logs with error messages if someone sends through bad - // JSON data. We don't really care. - if err != io.EOF && err != io.ErrUnexpectedEOF { - zap.S().Errorw("failed to decode directory creation data", zap.Error(err)) - } - - http.Error(w, "could not parse data in request", http.StatusUnprocessableEntity) - return - } - - if err := s.Filesystem.CreateDirectory(data.Name, data.Path); err != nil { - zap.S().Errorw("failed to create directory for server", zap.String("server", s.Uuid), zap.Error(err)) - - http.Error(w, "an error was encountered while creating the directory", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -func (rt *Router) routeServerRenameFile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - s := rt.GetServer(ps.ByName("server")) - defer r.Body.Close() - - data := rt.ReaderToBytes(r.Body) - oldPath, _ := jsonparser.GetString(data, "rename_from") - newPath, _ := jsonparser.GetString(data, "rename_to") - - if oldPath == "" || newPath == "" { - http.Error(w, "invalid paths provided; did you forget to provide an old path and new path?", http.StatusUnprocessableEntity) - return - } - - if err := s.Filesystem.Rename(oldPath, newPath); err != nil { - zap.S().Errorw("failed to rename file on server", zap.String("server", s.Uuid), zap.Error(err)) - - http.Error(w, "an error occurred while renaming the file", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -func (rt *Router) routeServerCopyFile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - s := rt.GetServer(ps.ByName("server")) - defer r.Body.Close() - - data := rt.ReaderToBytes(r.Body) - loc, _ := jsonparser.GetString(data, "location") - - if err := s.Filesystem.Copy(loc); err != nil { - zap.S().Errorw("error copying file for server", zap.String("server", s.Uuid), zap.Error(err)) - - http.Error(w, "an error occurred while copying the file", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -func (rt *Router) routeServerDeleteFile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - s := rt.GetServer(ps.ByName("server")) - defer r.Body.Close() - - data := rt.ReaderToBytes(r.Body) - loc, _ := jsonparser.GetString(data, "location") - - if err := s.Filesystem.Delete(loc); err != nil { - zap.S().Errorw("failed to delete a file or directory for server", zap.String("server", s.Uuid), zap.Error(err)) - - http.Error(w, "an error occurred while trying to delete a file or directory", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -func (rt *Router) routeServerSendCommand(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - s := rt.GetServer(ps.ByName("server")) - defer r.Body.Close() - - if running, err := s.Environment.IsRunning(); !running || err != nil { - http.Error(w, "cannot send commands to a stopped instance", http.StatusBadGateway) - return - } - - data := rt.ReaderToBytes(r.Body) - commands, dt, _, _ := jsonparser.Get(data, "commands") - if dt != jsonparser.Array { - http.Error(w, "commands must be an array of strings", http.StatusUnprocessableEntity) - return - } - - for _, command := range commands { - if err := s.Environment.SendCommand(string(command)); err != nil { - zap.S().Warnw("failed to send command to server", zap.Any("command", command), zap.Error(err)) - return - } - } - - w.WriteHeader(http.StatusNoContent) -} - -func (rt *Router) routeServerInstall(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - s := rt.GetServer(ps.ByName("server")) - defer r.Body.Close() - - go func(serv *server.Server) { - if err := serv.Install(); err != nil { - zap.S().Errorw("failed to execute server installation process", zap.String("server", s.Uuid), zap.Error(err)) - } - }(s) - - w.WriteHeader(http.StatusAccepted) -} - -func (rt *Router) routeServerUpdate(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - s := rt.GetServer(ps.ByName("server")) - defer r.Body.Close() - - data := rt.ReaderToBytes(r.Body) - if err := s.UpdateDataStructure(data, true); err != nil { - zap.S().Errorw("failed to update a server's data structure", zap.String("server", s.Uuid), zap.Error(err)) - - http.Error(w, "failed to update data structure", http.StatusInternalServerError) - return - } - - zap.S().Debugw("updated server's data structure", zap.String("server", s.Uuid)) - w.WriteHeader(http.StatusNoContent) -} - -func (rt *Router) routeCreateServer(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - defer r.Body.Close() - - inst, err := installer.New(rt.ReaderToBytes(r.Body)) - - if err != nil { - zap.S().Warnw("failed to validate the received data", zap.Error(err)) - - http.Error(w, "failed to validate data", http.StatusUnprocessableEntity) - return - } - - // Plop that server instance onto the request so that it can be referenced in - // requests from here-on out. - server.GetServers().Add(inst.Server()) - - zap.S().Infow("beginning installation process for server", zap.String("server", inst.Uuid())) - // Begin the installation process in the background to not block the request - // cycle. If there are any errors they will be logged and communicated back - // to the Panel where a reinstall may take place. - go func(i *installer.Installer) { - i.Execute() - - if err := i.Server().Install(); err != nil { - zap.S().Errorw("failed to run install process for server", zap.String("server", i.Uuid()), zap.Error(err)) - } - }(inst) - - w.WriteHeader(http.StatusAccepted) -} - -func (rt *Router) routeServerReinstall(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - s := rt.GetServer(ps.ByName("server")) - defer r.Body.Close() - - zap.S().Infow("beginning reinstall process for server", zap.String("server", s.Uuid)) - go func(server *server.Server) { - if err := server.Reinstall(); err != nil { - zap.S().Errorw("failed to complete server reinstall process", zap.String("server", server.Uuid), zap.Error(err)) - } - }(s) - - w.WriteHeader(http.StatusAccepted) -} - func (rt *Router) routeServerBackup(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { s := rt.GetServer(ps.ByName("server")) defer r.Body.Close() @@ -543,74 +121,6 @@ func (rt *Router) routeServerBackup(w http.ResponseWriter, r *http.Request, ps h w.WriteHeader(http.StatusAccepted) } -func (rt *Router) routeSystemInformation(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - defer r.Body.Close() - - s, err := GetSystemInformation() - if err != nil { - zap.S().Errorw("failed to retrieve system information", zap.Error(errors.WithStack(err))) - - http.Error(w, "failed to retrieve information", http.StatusInternalServerError) - return - } - - json.NewEncoder(w).Encode(s) -} - -func (rt *Router) routeServerDelete(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - s := rt.GetServer(ps.ByName("server")) - defer r.Body.Close() - - // Immediately suspend the server to prevent a user from attempting - // to start it while this process is running. - s.Suspended = true - - // Delete the server's archive if it exists. - if err := s.Archiver.DeleteIfExists(); err != nil { - zap.S().Errorw("failed to delete server's archive", zap.String("server", s.Uuid), zap.Error(err)) - // We intentionally don't return here, if the archive fails to delete, the server can still be removed. - } - - zap.S().Infow("processing server deletion request", zap.String("server", s.Uuid)) - // Destroy the environment; in Docker this will handle a running container and - // forcibly terminate it before removing the container, so we do not need to handle - // that here. - if err := s.Environment.Destroy(); err != nil { - zap.S().Errorw("failed to destroy server environment", zap.Error(errors.WithStack(err))) - - http.Error(w, "failed to destroy server environment", http.StatusInternalServerError) - return - } - - // Once the environment is terminated, remove the server files from the system. This is - // done in a separate process since failure is not the end of the world and can be - // manually cleaned up after the fact. - // - // In addition, servers with large amounts of files can take some time to finish deleting - // so we don't want to block the HTTP call while waiting on this. - go func(p string) { - if err := os.RemoveAll(p); err != nil { - zap.S().Warnw("failed to remove server files on deletion", zap.String("path", p), zap.Error(errors.WithStack(err))) - } - }(s.Filesystem.Path()) - - var uuid = s.Uuid - server.GetServers().Remove(func(s2 *server.Server) bool { - return s2.Uuid == uuid - }) - - s = nil - - // Remove the configuration file stored on the Daemon for this server. - go func(u string) { - if err := os.Remove("data/servers/" + u + ".yml"); err != nil { - zap.S().Warnw("failed to delete server configuration file on deletion", zap.String("server", u), zap.Error(errors.WithStack(err))) - } - }(uuid) - - w.WriteHeader(http.StatusAccepted) -} - func (rt *Router) routeRequestServerArchive(w http.ResponseWriter, _ *http.Request, ps httprouter.Params) { s := rt.GetServer(ps.ByName("server")) @@ -886,32 +396,8 @@ func (rt *Router) ReaderToBytes(r io.Reader) []byte { func (rt *Router) ConfigureRouter() *httprouter.Router { router := httprouter.New() - router.OPTIONS("/api/system", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - rt.AttachAccessControlHeaders(w, r, ps) - }) - - router.GET("/", rt.routeIndex) router.GET("/download/backup", rt.routeDownloadBackup) - router.GET("/api/system", rt.AuthenticateToken(rt.routeSystemInformation)) - router.GET("/api/servers", rt.AuthenticateToken(rt.routeAllServers)) - router.GET("/api/servers/:server", rt.AuthenticateRequest(rt.routeServer)) - router.GET("/api/servers/:server/ws", rt.AuthenticateServer(rt.routeWebsocket)) - router.GET("/api/servers/:server/logs", rt.AuthenticateRequest(rt.routeServerLogs)) - router.GET("/api/servers/:server/files/contents", rt.AuthenticateRequest(rt.routeServerFileRead)) - router.GET("/api/servers/:server/files/list-directory", rt.AuthenticateRequest(rt.routeServerListDirectory)) - router.PUT("/api/servers/:server/files/rename", rt.AuthenticateRequest(rt.routeServerRenameFile)) - router.POST("/api/servers", rt.AuthenticateToken(rt.routeCreateServer)) - router.POST("/api/servers/:server/install", rt.AuthenticateRequest(rt.routeServerInstall)) - router.POST("/api/servers/:server/files/copy", rt.AuthenticateRequest(rt.routeServerCopyFile)) - router.POST("/api/servers/:server/files/write", rt.AuthenticateRequest(rt.routeServerWriteFile)) - router.POST("/api/servers/:server/files/create-directory", rt.AuthenticateRequest(rt.routeServerCreateDirectory)) - router.POST("/api/servers/:server/files/delete", rt.AuthenticateRequest(rt.routeServerDeleteFile)) - router.POST("/api/servers/:server/power", rt.AuthenticateRequest(rt.routeServerPower)) - router.POST("/api/servers/:server/commands", rt.AuthenticateRequest(rt.routeServerSendCommand)) - router.POST("/api/servers/:server/reinstall", rt.AuthenticateRequest(rt.routeServerReinstall)) router.POST("/api/servers/:server/backup", rt.AuthenticateRequest(rt.routeServerBackup)) - router.PATCH("/api/servers/:server", rt.AuthenticateRequest(rt.routeServerUpdate)) - router.DELETE("/api/servers/:server", rt.AuthenticateRequest(rt.routeServerDelete)) router.POST("/api/servers/:server/archive", rt.AuthenticateRequest(rt.routeRequestServerArchive)) router.GET("/api/servers/:server/archive", rt.AuthenticateServer(rt.routeGetServerArchive)) diff --git a/router/error.go b/router/error.go new file mode 100644 index 0000000..ed84fe5 --- /dev/null +++ b/router/error.go @@ -0,0 +1,93 @@ +package router + +import ( + "fmt" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/pterodactyl/wings/server" + "go.uber.org/zap" + "net/http" + "os" +) + +type RequestError struct { + Err error + Uuid string + Message string + server *server.Server +} + +// Generates a new tracked error, which simply tracks the specific error that +// is being passed in, and also assigned a UUID to the error so that it can be +// cross referenced in the logs. +func TrackedError(err error) *RequestError { + return &RequestError{ + Err: err, + Uuid: uuid.Must(uuid.NewRandom()).String(), + Message: "", + } +} + +// Same as TrackedError, except this will also attach the server instance that +// generated this server for the purposes of logging. +func TrackedServerError(err error, s *server.Server) *RequestError { + return &RequestError{ + Err: err, + Uuid: uuid.Must(uuid.NewRandom()).String(), + Message: "", + server: s, + } +} + +// Sets the output message to display to the user in the error. +func (e *RequestError) SetMessage(msg string) *RequestError { + e.Message = msg + return e +} + +// Aborts the request with the given status code, and responds with the error. This +// will also include the error UUID in the output so that the user can report that +// and link the response to a specific error in the logs. +func (e *RequestError) AbortWithStatus(status int, c *gin.Context) { + // If this error is because the resource does not exist, we likely do not need to log + // the error anywhere, just return a 404 and move on with our lives. + if os.IsNotExist(e.Err) { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ + "error": "The requested resource was not found on the system.", + }) + return + } + + // Otherwise, log the error to zap, and then report the error back to the user. + if status >= 500 { + if e.server != nil { + zap.S().Errorw("encountered error while handling HTTP request", zap.String("server", e.server.Uuid), zap.String("error_id", e.Uuid), zap.Error(e.Err)) + } else { + zap.S().Errorw("encountered error while handling HTTP request", zap.String("error_id", e.Uuid), zap.Error(e.Err)) + } + + c.Error(errors.WithStack(e)) + } + + msg := "An unexpected error was encountered while processing this request." + if e.Message != "" { + msg = e.Message + } + + c.AbortWithStatusJSON(status, gin.H{ + "error": msg, + "error_id": e.Uuid, + }) +} + +// Helper function to just abort with an internal server error. This is generally the response +// from most errors encountered by the API. +func (e *RequestError) AbortWithServerError(c *gin.Context) { + e.AbortWithStatus(http.StatusInternalServerError, c) +} + +// Format the error to a string and include the UUID. +func (e *RequestError) Error() string { + return fmt.Sprintf("%v (uuid: %s)", e.Err, e.Uuid) +} diff --git a/router/middleware.go b/router/middleware.go new file mode 100644 index 0000000..281cda2 --- /dev/null +++ b/router/middleware.go @@ -0,0 +1,67 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/pterodactyl/wings/config" + "github.com/pterodactyl/wings/server" + "net/http" + "strings" +) + +// Set the access request control headers on all of the requests. +func SetAccessControlHeaders(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", config.Get().PanelLocation) + c.Header("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") + c.Next() +} + +// Authenticates the request token against the given permission string, ensuring that +// 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. +func AuthorizationMiddleware(c *gin.Context) { + auth := strings.SplitN(c.GetHeader("Authorization"), " ", 2) + + if len(auth) != 2 || auth[0] != "Bearer" { + c.Header("WWW-Authenticate", "Bearer") + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ + "error": "The required authorization heads were not present in the request.", + }) + + return + } + + // Try to match the request against the global token for the Daemon, regardless + // of the permission type. If nothing is matched we will fall through to the Panel + // API to try and validate permissions for a server. + if auth[1] == config.Get().AuthenticationToken { + c.Next() + + return + } + + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ + "error": "You are not authorized to access this endpoint.", + }) +} + +// Helper function to fetch a server out of the servers collection stored in memory. +func GetServer (uuid string) *server.Server { + return server.GetServers().Find(func(s *server.Server) bool { + return uuid == s.Uuid + }) +} + +// Ensure that the requested server exists in this setup. Returns a 404 if we cannot +// locate it. +func ServerExists(c *gin.Context) { + u, err := uuid.Parse(c.Param("server")) + if err != nil || GetServer(u.String()) == nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ + "error": "The requested server does not exist.", + }) + return + } + + c.Next() +} \ No newline at end of file diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000..dfb78f1 --- /dev/null +++ b/router/router.go @@ -0,0 +1,50 @@ +package router + +import "github.com/gin-gonic/gin" + +// Configures the routing infrastructure for this daemon instance. +func Configure() *gin.Engine { + router := gin.Default() + router.Use(SetAccessControlHeaders) + + // This route is special is sits above all of the other requests because we are + // using a JWT to authorize access to it, therefore it needs to be publically + // accessible. + router.GET("/api/servers/:server/ws", getServerWebsocket) + + // All of the routes beyond this mount will use an authorization middleware + // and will not be accessible without the correct Authorization header provided. + protected := router.Use(AuthorizationMiddleware) + protected.GET("/api/system", getSystemInformation) + protected.GET("/api/servers", getAllServers) + protected.POST("/api/servers", postCreateServer) + + // These are server specific routes, and require that the request be authorized, and + // that the server exist on the Daemon. + server := router.Group("/api/servers/:server") + server.Use(AuthorizationMiddleware, ServerExists) + { + server.GET("", getServer) + server.PATCH("", patchServer) + server.DELETE("", deleteServer) + + server.GET("/logs", getServerLogs) + server.POST("/power", postServerPower) + server.POST("/commands", postServerCommands) + server.POST("/install", postServerInstall) + server.POST("/reinstall", postServerReinstall) + + files := server.Group("/files") + { + files.GET("/contents", getServerFileContents) + files.GET("/list-directory", getServerListDirectory) + files.PUT("/rename", putServerRenameFile) + files.POST("/copy", postServerCopyFile) + files.POST("/write", postServerWriteFile) + files.POST("/create-directory", postServerCreateDirectory) + files.POST("/delete", postServerDeleteFile) + } + } + + return router +} \ No newline at end of file diff --git a/router/router_download.go b/router/router_download.go new file mode 100644 index 0000000..7ef135b --- /dev/null +++ b/router/router_download.go @@ -0,0 +1 @@ +package router diff --git a/router/router_server.go b/router/router_server.go new file mode 100644 index 0000000..2ee5161 --- /dev/null +++ b/router/router_server.go @@ -0,0 +1,216 @@ +package router + +import ( + "bytes" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" + "github.com/pterodactyl/wings/server" + "go.uber.org/zap" + "net/http" + "os" + "strconv" +) + +// Returns a single server from the collection of servers. +func getServer(c *gin.Context) { + c.JSON(http.StatusOK, GetServer(c.Param("server"))) +} + +// Returns the logs for a given server instance. +func getServerLogs(c *gin.Context) { + s := GetServer(c.Param("server")) + + l, _ := strconv.ParseInt(c.DefaultQuery("size", "8192"), 10, 64) + if l <= 0 { + l = 2048 + } + + out, err := s.ReadLogfile(l) + if err != nil { + TrackedServerError(err, s).AbortWithServerError(c) + return + } + + c.JSON(http.StatusOK, gin.H{"data": out}) +} + +// Handles a request to control the power state of a server. If the action being passed +// through is invalid a 404 is returned. Otherwise, a HTTP/202 Accepted response is returned +// and the actual power action is run asynchronously so that we don't have to block the +// request until a potentially slow operation completes. +// +// This is done because for the most part the Panel is using websockets to determine when +// things are happening, so theres no reason to sit and wait for a request to finish. We'll +// just see over the socket if something isn't working correctly. +func postServerPower(c *gin.Context) { + s := GetServer(c.Param("server")) + + var data server.PowerAction + c.BindJSON(&data) + + if !data.IsValid() { + c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{ + "error": "The power action provided was not valid, should be one of \"stop\", \"start\", \"restart\", \"kill\"", + }) + return + } + + // Because we route all of the actual bootup process to a separate thread we need to + // check the suspension status here, otherwise the user will hit the endpoint and then + // just sit there wondering why it returns a success but nothing actually happens. + // + // We don't really care about any of the other actions at this point, they'll all result + // in the process being stopped, which should have happened anyways if the server is suspended. + if (data.Action == "start" || data.Action == "restart") && s.Suspended { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ + "error": "Cannot start or restart a server that is suspended.", + }) + return + } + + // Pass the actual heavy processing off to a seperate thread to handle so that + // we can immediately return a response from the server. Some of these actions + // can take quite some time, especially stopping or restarting. + go func() { + if err := s.HandlePowerAction(data); err != nil { + zap.S().Errorw( + "encountered an error processing a server power action", + zap.String("server", s.Uuid), + zap.Error(err), + ) + } + }() + + c.Status(http.StatusAccepted) +} + +// Sends an array of commands to a running server instance. +func postServerCommands(c *gin.Context) { + s := GetServer(c.Param("server")) + + if running, err := s.Environment.IsRunning(); err != nil { + TrackedServerError(err, s).AbortWithServerError(c) + return + } else if !running { + c.AbortWithStatusJSON(http.StatusBadGateway, gin.H{ + "error": "Cannot send commands to a stopped server instance.", + }) + return + } + + var data struct{ Commands []string `json:"commands"` } + c.BindJSON(&data) + + for _, command := range data.Commands { + if err := s.Environment.SendCommand(command); err != nil { + zap.S().Warnw( + "failed to send command to server", + zap.String("server", s.Uuid), + zap.String("command", command), + zap.Error(err), + ) + } + } + + c.Status(http.StatusNoContent) +} + +// Updates information about a server internally. +func patchServer(c *gin.Context) { + s := GetServer(c.Param("server")) + + buf := bytes.Buffer{} + buf.ReadFrom(c.Request.Body) + + if err := s.UpdateDataStructure(buf.Bytes(), true); err != nil { + TrackedServerError(err, s).AbortWithServerError(c) + return + } + + c.Status(http.StatusNoContent) +} + +// Performs a server installation in a backgrounded thread. +func postServerInstall(c *gin.Context) { + s := GetServer(c.Param("server")) + + go func(serv *server.Server) { + if err := serv.Install(); err != nil { + zap.S().Errorw( + "failed to execute server installation process", + zap.String("server", serv.Uuid), + zap.Error(err), + ) + } + }(s) + + c.Status(http.StatusAccepted) +} + +// Reinstalls a server. +func postServerReinstall(c *gin.Context) { + s := GetServer(c.Param("server")) + + go func(serv *server.Server) { + if err := serv.Reinstall(); err != nil { + zap.S().Errorw( + "failed to complete server reinstall process", + zap.String("server", serv.Uuid), + zap.Error(err), + ) + } + }(s) + + c.Status(http.StatusAccepted) +} + +// Deletes a server from the wings daemon and deassociates its objects. +func deleteServer(c *gin.Context) { + s := GetServer(c.Param("server")) + + // Immediately suspend the server to prevent a user from attempting + // to start it while this process is running. + s.Suspended = true + + // Delete the server's archive if it exists. We intentionally don't return + // here, if the archive fails to delete, the server can still be removed. + if err := s.Archiver.DeleteIfExists(); err != nil { + zap.S().Warnw("failed to delete server archive during deletion process", zap.String("server", s.Uuid), zap.Error(err)) + } + + // Destroy the environment; in Docker this will handle a running container and + // forcibly terminate it before removing the container, so we do not need to handle + // that here. + if err := s.Environment.Destroy(); err != nil { + TrackedServerError(err, s).AbortWithServerError(c) + } + + // Once the environment is terminated, remove the server files from the system. This is + // done in a separate process since failure is not the end of the world and can be + // manually cleaned up after the fact. + // + // In addition, servers with large amounts of files can take some time to finish deleting + // so we don't want to block the HTTP call while waiting on this. + go func(p string) { + if err := os.RemoveAll(p); err != nil { + zap.S().Warnw("failed to remove server files during deletion process", zap.String("path", p), zap.Error(errors.WithStack(err))) + } + }(s.Filesystem.Path()) + + var uuid = s.Uuid + server.GetServers().Remove(func(s2 *server.Server) bool { + return s2.Uuid == uuid + }) + + // Deallocate the reference to this server. + s = nil + + // Remove the configuration file stored on the Daemon for this server. + go func(u string) { + if err := os.Remove("data/servers/" + u + ".yml"); err != nil { + zap.S().Warnw("failed to delete server configuration file while processing deletion request", zap.String("server", u), zap.Error(errors.WithStack(err))) + } + }(uuid) + + c.Status(http.StatusNoContent) +} \ No newline at end of file diff --git a/router/router_server_files.go b/router/router_server_files.go new file mode 100644 index 0000000..7678e89 --- /dev/null +++ b/router/router_server_files.go @@ -0,0 +1,155 @@ +package router + +import ( + "bufio" + "github.com/gin-gonic/gin" + "net/http" + "os" + "strconv" +) + +// Returns the contents of a file on the server. +func getServerFileContents(c *gin.Context) { + s := GetServer(c.Param("server")) + cleaned, err := s.Filesystem.SafePath(c.Query("file")) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ + "error": "The file requested could not be found.", + }) + return + } + + st, err := s.Filesystem.Stat(cleaned) + if err != nil { + TrackedServerError(err, s).AbortWithServerError(c) + return + } + + if st.Info.IsDir() { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ + "error": "The requested resource was not found on the system.", + }) + return + } + + f, err := os.Open(cleaned) + if err != nil { + TrackedServerError(err, s).AbortWithServerError(c) + return + } + defer f.Close() + + c.Header("X-Mime-Type", st.Mimetype) + c.Header("Content-Length", strconv.Itoa(int(st.Info.Size()))) + + // If a download parameter is included in the URL go ahead and attach the necessary headers + // so that the file can be downloaded. + if c.Query("download") != "" { + c.Header("Content-Disposition", "attachment; filename="+st.Info.Name()) + c.Header("Content-Type", "application/octet-stream") + } + + bufio.NewReader(f).WriteTo(c.Writer) +} + +// Returns the contents of a directory for a server. +func getServerListDirectory(c *gin.Context) { + s := GetServer(c.Param("server")) + + stats, err := s.Filesystem.ListDirectory(c.Query("directory")) + if err != nil { + TrackedServerError(err, s).AbortWithServerError(c) + return + } + + c.JSON(http.StatusOK, stats) +} + +// Renames (or moves) a file for a server. +func putServerRenameFile(c *gin.Context) { + s := GetServer(c.Param("server")) + + var data struct{ + RenameFrom string `json:"rename_from"` + RenameTo string `json:"rename_to"` + } + c.BindJSON(&data) + + if data.RenameFrom == "" || data.RenameTo == "" { + c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{ + "error": "Invalid paths were provided, did you forget to provide both a new and old path?", + }) + return + } + + if err := s.Filesystem.Rename(data.RenameFrom, data.RenameTo); err != nil { + TrackedServerError(err, s).AbortWithServerError(c) + return + } + + c.Status(http.StatusNoContent) +} + +// Copies a server file. +func postServerCopyFile(c *gin.Context) { + s := GetServer(c.Param("server")) + + var data struct { + Location string `json:"location"` + } + c.BindJSON(&data) + + if err := s.Filesystem.Copy(data.Location); err != nil { + TrackedServerError(err, s).AbortWithServerError(c) + return + } + + c.Status(http.StatusNoContent) +} + +// Deletes a server file. +func postServerDeleteFile(c *gin.Context) { + s := GetServer(c.Param("server")) + + var data struct { + Location string `json:"location"` + } + c.BindJSON(&data) + + if err := s.Filesystem.Delete(data.Location); err != nil { + TrackedServerError(err, s).AbortWithServerError(c) + return + } + + c.Status(http.StatusNoContent) +} + +// Writes the contents of the request to a file on a server. +func postServerWriteFile(c *gin.Context) { + s := GetServer(c.Param("server")) + + if err := s.Filesystem.Writefile(c.Query("file"), c.Request.Body); err != nil { + TrackedServerError(err, s).AbortWithServerError(c) + return + } + + c.Status(http.StatusNoContent) +} + +// Create a directory on a server. +func postServerCreateDirectory(c *gin.Context) { + s := GetServer(c.Param("server")) + + var data struct { + Name string `json:"name"` + Path string `json:"path"` + } + c.BindJSON(&data) + + if err := s.Filesystem.CreateDirectory(data.Name, data.Path); err != nil { + TrackedServerError(err, s).AbortWithServerError(c) + return + } + + c.Status(http.StatusNoContent) +} \ No newline at end of file diff --git a/router/router_server_ws.go b/router/router_server_ws.go new file mode 100644 index 0000000..665617d --- /dev/null +++ b/router/router_server_ws.go @@ -0,0 +1,60 @@ +package router + +import ( + "context" + "encoding/json" + "github.com/gin-gonic/gin" + ws "github.com/gorilla/websocket" + "github.com/pterodactyl/wings/router/websocket" + "go.uber.org/zap" +) + +// Upgrades a connection to a websocket and passes events along between. +func getServerWebsocket(c *gin.Context) { + s := GetServer(c.Param("server")) + handler, err := websocket.GetHandler(s, c.Writer, c.Request) + if err != nil { + TrackedServerError(err, s).AbortWithServerError(c) + return + } + defer handler.Connection.Close() + + // Create a context that can be canceled when the user disconnects from this + // socket that will also cancel listeners running in seperate threads. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go handler.ListenForServerEvents(ctx) + go handler.ListenForExpiration(ctx) + + for { + j := websocket.Message{} + + _, p, err := handler.Connection.ReadMessage() + if err != nil { + if !ws.IsCloseError( + err, + ws.CloseNormalClosure, + ws.CloseGoingAway, + ws.CloseNoStatusReceived, + ws.CloseServiceRestart, + ws.CloseAbnormalClosure, + ) { + zap.S().Warnw("error handling websocket message", zap.Error(err)) + } + break + } + + // Discard and JSON parse errors into the void and don't continue processing this + // specific socket request. If we did a break here the client would get disconnected + // from the socket, which is NOT what we want to do. + if err := json.Unmarshal(p, &j); err != nil { + continue + } + + if err := handler.HandleInbound(j); err != nil { + handler.SendErrorJson(err) + } + } +} + diff --git a/router/router_system.go b/router/router_system.go new file mode 100644 index 0000000..24ee31c --- /dev/null +++ b/router/router_system.go @@ -0,0 +1,64 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "github.com/pterodactyl/wings/installer" + "github.com/pterodactyl/wings/server" + "github.com/pterodactyl/wings/system" + "go.uber.org/zap" + "net/http" +) + +// Returns information about the system that wings is running on. +func getSystemInformation(c *gin.Context) { + i, err := system.GetSystemInformation() + if err != nil { + TrackedError(err).AbortWithServerError(c) + + return + } + + c.JSON(http.StatusOK, i) +} + +// Returns all of the servers that are registered and configured correctly on +// this wings instance. +func getAllServers(c *gin.Context) { + c.JSON(http.StatusOK, server.GetServers().All()) +} + +// Creates a new server on the wings daemon and begins the installation process +// for it. +func postCreateServer(c *gin.Context) { + var data []byte + c.Bind(&data) + + install, err := installer.New(data) + if err != nil { + TrackedError(err). + SetMessage("Failed to validate the data provided in the request."). + AbortWithStatus(http.StatusUnprocessableEntity, c) + return + } + + // Plop that server instance onto the request so that it can be referenced in + // requests from here-on out. + server.GetServers().Add(install.Server()) + + // Begin the installation process in the background to not block the request + // cycle. If there are any errors they will be logged and communicated back + // to the Panel where a reinstall may take place. + go func(i *installer.Installer) { + i.Execute() + + if err := i.Server().Install(); err != nil { + zap.S().Errorw( + "failed to run install process for server", + zap.String("server", i.Uuid()), + zap.Error(err), + ) + } + }(install) + + c.Status(http.StatusAccepted) +} diff --git a/router/websocket/listeners.go b/router/websocket/listeners.go new file mode 100644 index 0000000..9b79fbb --- /dev/null +++ b/router/websocket/listeners.go @@ -0,0 +1,76 @@ +package websocket + +import ( + "context" + "github.com/pterodactyl/wings/server" + "time" +) + +// Checks the time to expiration on the JWT every 30 seconds until the token has +// expired. If we are within 3 minutes of the token expiring, send a notice over +// the socket that it is expiring soon. If it has expired, send that notice as well. +func (h *Handler) ListenForExpiration(ctx context.Context) { + // Make a ticker and completion channel that is used to continuously poll the + // JWT stored in the session to send events to the socket when it is expiring. + ticker := time.NewTicker(time.Second * 30) + done := make(chan bool) + + // Whenever this function is complete, end the ticker, close out the channel, + // and then close the websocket connection. + defer func() { + ticker.Stop() + done <- true + }() + + for { + select { + case <-ctx.Done(): + case <-done: + return + case <-ticker.C: + { + if h.JWT != nil { + if h.JWT.ExpirationTime.Unix()-time.Now().Unix() <= 0 { + h.SendJson(&Message{Event: TokenExpiredEvent}) + } else if h.JWT.ExpirationTime.Unix()-time.Now().Unix() <= 180 { + h.SendJson(&Message{Event: TokenExpiringEvent}) + } + } + } + } + } +} + +// Listens for different events happening on a server and sends them along +// to the connected websocket. +func (h *Handler) ListenForServerEvents(ctx context.Context) { + events := []string{ + server.StatsEvent, + server.StatusEvent, + server.ConsoleOutputEvent, + server.InstallOutputEvent, + server.DaemonMessageEvent, + } + + eventChannel := make(chan server.Event) + for _, event := range events { + h.server.Events().Subscribe(event, eventChannel) + } + + select { + case <-ctx.Done(): + for _, event := range events { + h.server.Events().Unsubscribe(event, eventChannel) + } + + close(eventChannel) + default: + // Listen for different events emitted by the server and respond to them appropriately. + for d := range eventChannel { + h.SendJson(&Message{ + Event: d.Topic, + Args: []string{d.Data}, + }) + } + } +} diff --git a/router/websocket/message.go b/router/websocket/message.go new file mode 100644 index 0000000..a5fad3a --- /dev/null +++ b/router/websocket/message.go @@ -0,0 +1,26 @@ +package websocket + +const ( + AuthenticationSuccessEvent = "auth success" + TokenExpiringEvent = "token expiring" + TokenExpiredEvent = "token expired" + AuthenticationEvent = "auth" + SetStateEvent = "set state" + SendServerLogsEvent = "send logs" + SendCommandEvent = "send command" + ErrorEvent = "daemon error" +) + +type Message struct { + // The event to perform. Should be one of the following that are supported: + // + // - status : Returns the server's power state. + // - logs : Returns the server log data at the time of the request. + // - power : Performs a power action aganist the server based the data. + // - command : Performs a command on a server using the data field. + Event string `json:"event"` + + // The data to pass along, only used by power/command currently. Other requests + // should either omit the field or pass an empty value as it is ignored. + Args []string `json:"args,omitempty"` +} diff --git a/router/websocket/payload.go b/router/websocket/payload.go new file mode 100644 index 0000000..caedff7 --- /dev/null +++ b/router/websocket/payload.go @@ -0,0 +1,24 @@ +package websocket + +import ( + "encoding/json" + "github.com/gbrlsnchs/jwt/v3" +) + +type TokenPayload struct { + jwt.Payload + UserID json.Number `json:"user_id"` + ServerUUID string `json:"server_uuid"` + Permissions []string `json:"permissions"` +} + +// Checks if the given token payload has a permission string. +func (p *TokenPayload) HasPermission(permission string) bool { + for _, k := range p.Permissions { + if k == permission { + return true + } + } + + return false +} diff --git a/router/websocket/websocket.go b/router/websocket/websocket.go new file mode 100644 index 0000000..52d3f4b --- /dev/null +++ b/router/websocket/websocket.go @@ -0,0 +1,277 @@ +package websocket + +import ( + "fmt" + "github.com/gbrlsnchs/jwt/v3" + "github.com/google/uuid" + "github.com/gorilla/websocket" + "github.com/pkg/errors" + "github.com/pterodactyl/wings/config" + "github.com/pterodactyl/wings/server" + "go.uber.org/zap" + "net/http" + "os" + "strings" + "sync" + "time" +) + +var alg *jwt.HMACSHA + +const ( + PermissionConnect = "connect" + PermissionSendCommand = "send-command" + PermissionSendPower = "send-power" + PermissionReceiveErrors = "receive-errors" + PermissionReceiveInstall = "receive-install" +) + +type Handler struct { + Connection *websocket.Conn + JWT *TokenPayload `json:"-"` + server *server.Server + mutex sync.Mutex +} + +// Returns a new websocket handler using the context provided. +func GetHandler(s *server.Server, w http.ResponseWriter, r *http.Request) (*Handler, error) { + upgrader := websocket.Upgrader{ + // Ensure that the websocket request is originating from the Panel itself, + // and not some other location. + CheckOrigin: func(r *http.Request) bool { + return r.Header.Get("Origin") == config.Get().PanelLocation + }, + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return nil, err + } + + return &Handler{ + Connection: conn, + JWT: nil, + server: s, + mutex: sync.Mutex{}, + }, nil +} + +// Validates the provided JWT against the known secret for the Daemon and returns the +// parsed data. +// +// This function DOES NOT validate that the token is valid for the connected server, nor +// does it ensure that the user providing the token is able to actually do things. +func ParseJWT(token []byte) (*TokenPayload, error) { + var payload TokenPayload + if alg == nil { + alg = jwt.NewHS256([]byte(config.Get().AuthenticationToken)) + } + + now := time.Now() + verifyOptions := jwt.ValidatePayload( + &payload.Payload, + jwt.ExpirationTimeValidator(now), + ) + + _, err := jwt.Verify(token, alg, &payload, verifyOptions) + if err != nil { + return nil, err + } + + if !payload.HasPermission(PermissionConnect) { + return nil, errors.New("not authorized to connect to this socket") + } + + return &payload, nil +} + +func (h *Handler) SendJson(v *Message) error { + // Do not send JSON down the line if the JWT on the connection is not + // valid! + if err := h.TokenValid(); err != nil { + return nil + } + + // If we're sending installation output but the user does not have the required + // permissions to see the output, don't send it down the line. + if v.Event == server.InstallOutputEvent { + zap.S().Debugf("%+v", v.Args) + if h.JWT != nil && !h.JWT.HasPermission(PermissionReceiveInstall) { + return nil + } + } + + return h.unsafeSendJson(v) +} + +// Sends JSON over the websocket connection, ignoring the authentication state of the +// socket user. Do not call this directly unless you are positive a response should be +// sent back to the client! +func (h *Handler) unsafeSendJson(v interface{}) error { + h.mutex.Lock() + defer h.mutex.Unlock() + + return h.Connection.WriteJSON(v) +} + +// Checks if the JWT is still valid. +func (h *Handler) TokenValid() error { + if h.JWT == nil { + return errors.New("no jwt present") + } + + if err := jwt.ExpirationTimeValidator(time.Now())(&h.JWT.Payload); err != nil { + return err + } + + if !h.JWT.HasPermission(PermissionConnect) { + return errors.New("jwt does not have connect permission") + } + + if h.server.Uuid != h.JWT.ServerUUID { + return errors.New("jwt server uuid mismatch") + } + + return nil +} + +// Sends an error back to the connected websocket instance by checking the permissions +// of the token. If the user has the "receive-errors" grant we will send back the actual +// error message, otherwise we just send back a standard error message. +func (h *Handler) SendErrorJson(err error) error { + h.mutex.Lock() + defer h.mutex.Unlock() + + message := "an unexpected error was encountered while handling this request" + if h.JWT != nil { + if server.IsSuspendedError(err) || h.JWT.HasPermission(PermissionReceiveErrors) { + message = err.Error() + } + } + + m, u := h.GetErrorMessage(message) + + wsm := Message{Event: ErrorEvent} + wsm.Args = []string{m} + + if !server.IsSuspendedError(err) { + zap.S().Errorw( + "an error was encountered in the websocket process", + zap.String("server", h.server.Uuid), + zap.String("error_identifier", u.String()), + zap.Error(err), + ) + } + + return h.Connection.WriteJSON(wsm) +} + +// Converts an error message into a more readable representation and returns a UUID +// that can be cross-referenced to find the specific error that triggered. +func (h *Handler) GetErrorMessage(msg string) (string, uuid.UUID) { + u := uuid.Must(uuid.NewRandom()) + + m := fmt.Sprintf("Error Event [%s]: %s", u.String(), msg) + + return m, u +} + +// Handle the inbound socket request and route it to the proper server action. +func (h *Handler) HandleInbound(m Message) error { + if m.Event != AuthenticationEvent { + if err := h.TokenValid(); err != nil { + zap.S().Debugw("jwt token is no longer valid", zap.String("message", err.Error())) + + h.unsafeSendJson(Message{ + Event: ErrorEvent, + Args: []string{"could not authenticate client: " + err.Error()}, + }) + + return nil + } + } + + switch m.Event { + case AuthenticationEvent: + { + token, err := ParseJWT([]byte(strings.Join(m.Args, ""))) + if err != nil { + return err + } + + if token.HasPermission(PermissionConnect) { + h.JWT = token + } + + // On every authentication event, send the current server status back + // to the client. :) + h.server.Events().Publish(server.StatusEvent, h.server.State) + + h.unsafeSendJson(Message{ + Event: AuthenticationSuccessEvent, + Args: []string{}, + }) + + return nil + } + case SetStateEvent: + { + if !h.JWT.HasPermission(PermissionSendPower) { + return nil + } + + switch strings.Join(m.Args, "") { + case "start": + return h.server.Environment.Start() + case "stop": + return h.server.Environment.Stop() + case "restart": + { + if err := h.server.Environment.WaitForStop(60, false); err != nil { + return err + } + + return h.server.Environment.Start() + } + case "kill": + return h.server.Environment.Terminate(os.Kill) + } + + return nil + } + case SendServerLogsEvent: + { + if running, _ := h.server.Environment.IsRunning(); !running { + return nil + } + + logs, err := h.server.Environment.Readlog(1024 * 16) + if err != nil { + return err + } + + for _, line := range logs { + h.SendJson(&Message{ + Event: server.ConsoleOutputEvent, + Args: []string{line}, + }) + } + + return nil + } + case SendCommandEvent: + { + if !h.JWT.HasPermission(PermissionSendCommand) { + return nil + } + + if h.server.State == server.ProcessOfflineState { + return nil + } + + return h.server.Environment.SendCommand(strings.Join(m.Args, "")) + } + } + + return nil +} \ No newline at end of file diff --git a/server/power.go b/server/power.go new file mode 100644 index 0000000..454ac5b --- /dev/null +++ b/server/power.go @@ -0,0 +1,12 @@ +package server + +type PowerAction struct { + Action string `json:"action"` +} + +func (pr *PowerAction) IsValid() bool { + return pr.Action == "start" || + pr.Action == "stop" || + pr.Action == "kill" || + pr.Action == "restart" +} diff --git a/server/server.go b/server/server.go index d095d34..05eb264 100644 --- a/server/server.go +++ b/server/server.go @@ -394,3 +394,24 @@ func (s *Server) SetState(state string) error { func (s *Server) GetProcessConfiguration() (*api.ServerConfigurationResponse, *api.RequestError, error) { return api.NewRequester().GetServerConfiguration(s.Uuid) } + +// Helper function that can receieve a power action and then process the +// actions that need to occur for it. +func (s *Server) HandlePowerAction(action PowerAction) error { + switch action.Action { + case "start": + return s.Environment.Start() + case "restart": + if err := s.Environment.WaitForStop(60, false); err != nil { + return err + } + + return s.Environment.Start() + case "stop": + return s.Environment.Stop() + case "kill": + return s.Environment.Terminate(os.Kill) + default: + return errors.New("an invalid power action was provided") + } +} \ No newline at end of file diff --git a/const.go b/system/const.go similarity index 82% rename from const.go rename to system/const.go index ef3788e..435b270 100644 --- a/const.go +++ b/system/const.go @@ -1,4 +1,4 @@ -package main +package system const ( // The current version of this software. diff --git a/system.go b/system/system.go similarity index 80% rename from system.go rename to system/system.go index b201069..2559873 100644 --- a/system.go +++ b/system/system.go @@ -1,11 +1,11 @@ -package main +package system import ( "github.com/docker/docker/pkg/parsers/kernel" "runtime" ) -type SystemInformation struct { +type Information struct { Version string `json:"version"` KernelVersion string `json:"kernel_version"` Architecture string `json:"architecture"` @@ -13,13 +13,13 @@ type SystemInformation struct { CpuCount int `json:"cpu_count"` } -func GetSystemInformation() (*SystemInformation, error) { +func GetSystemInformation() (*Information, error) { k, err := kernel.GetKernelVersion() if err != nil { return nil, err } - s := &SystemInformation{ + s := &Information{ Version: Version, KernelVersion: k.String(), Architecture: runtime.GOARCH, diff --git a/websocket.go b/websocket.go deleted file mode 100644 index 3bfc144..0000000 --- a/websocket.go +++ /dev/null @@ -1,426 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "github.com/gbrlsnchs/jwt/v3" - "github.com/google/uuid" - "github.com/gorilla/websocket" - "github.com/julienschmidt/httprouter" - "github.com/pkg/errors" - "github.com/pterodactyl/wings/config" - "github.com/pterodactyl/wings/server" - "go.uber.org/zap" - "net/http" - "os" - "strings" - "sync" - "time" -) - -const ( - AuthenticationSuccessEvent = "auth success" - TokenExpiringEvent = "token expiring" - TokenExpiredEvent = "token expired" - AuthenticationEvent = "auth" - SetStateEvent = "set state" - SendServerLogsEvent = "send logs" - SendCommandEvent = "send command" - ErrorEvent = "daemon error" -) - -type WebsocketMessage struct { - // The event to perform. Should be one of the following that are supported: - // - // - status : Returns the server's power state. - // - logs : Returns the server log data at the time of the request. - // - power : Performs a power action aganist the server based the data. - // - command : Performs a command on a server using the data field. - Event string `json:"event"` - - // The data to pass along, only used by power/command currently. Other requests - // should either omit the field or pass an empty value as it is ignored. - Args []string `json:"args,omitempty"` - - // Is set to true when the request is originating from outside of the Daemon, - // otherwise set to false for outbound. - inbound bool -} - -type WebsocketHandler struct { - Server *server.Server - Mutex sync.Mutex - Connection *websocket.Conn - JWT *WebsocketTokenPayload -} - -type WebsocketTokenPayload struct { - jwt.Payload - UserID json.Number `json:"user_id"` - ServerUUID string `json:"server_uuid"` - Permissions []string `json:"permissions"` -} - -const ( - PermissionConnect = "connect" - PermissionSendCommand = "send-command" - PermissionSendPower = "send-power" - PermissionReceiveErrors = "receive-errors" - PermissionReceiveInstall = "receive-install" -) - -// Checks if the given token payload has a permission string. -func (wtp *WebsocketTokenPayload) HasPermission(permission string) bool { - for _, k := range wtp.Permissions { - if k == permission { - return true - } - } - - return false -} - -var alg *jwt.HMACSHA - -// Validates the provided JWT against the known secret for the Daemon and returns the -// parsed data. -// -// This function DOES NOT validate that the token is valid for the connected server, nor -// does it ensure that the user providing the token is able to actually do things. -func ParseJWT(token []byte) (*WebsocketTokenPayload, error) { - var payload WebsocketTokenPayload - if alg == nil { - alg = jwt.NewHS256([]byte(config.Get().AuthenticationToken)) - } - - now := time.Now() - verifyOptions := jwt.ValidatePayload( - &payload.Payload, - jwt.ExpirationTimeValidator(now), - ) - - _, err := jwt.Verify(token, alg, &payload, verifyOptions) - if err != nil { - return nil, err - } - - if !payload.HasPermission(PermissionConnect) { - return nil, errors.New("not authorized to connect to this socket") - } - - return &payload, nil -} - -// Checks if the JWT is still valid. -func (wsh *WebsocketHandler) TokenValid() error { - if wsh.JWT == nil { - return errors.New("no jwt present") - } - - if err := jwt.ExpirationTimeValidator(time.Now())(&wsh.JWT.Payload); err != nil { - return err - } - - if !wsh.JWT.HasPermission(PermissionConnect) { - return errors.New("jwt does not have connect permission") - } - - if wsh.Server.Uuid != wsh.JWT.ServerUUID { - return errors.New("jwt server uuid mismatch") - } - - return nil -} - -// Handle a request for a specific server websocket. This will handle inbound requests as well -// as ensure that any console output is also passed down the wire on the socket. -func (rt *Router) routeWebsocket(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - c, err := rt.upgrader.Upgrade(w, r, nil) - if err != nil { - zap.S().Errorw("error upgrading websocket", zap.Error(errors.WithStack(err))) - http.Error(w, "failed to upgrade websocket", http.StatusInternalServerError) - - return - } - - // Make a ticker and completion channel that is used to continuously poll the - // JWT stored in the session to send events to the socket when it is expiring. - ticker := time.NewTicker(time.Second * 30) - done := make(chan bool) - - // Whenever this function is complete, end the ticker, close out the channel, - // and then close the websocket connection. - defer func() { - ticker.Stop() - done <- true - c.Close() - }() - - s := rt.GetServer(ps.ByName("server")) - handler := WebsocketHandler{ - Server: s, - Mutex: sync.Mutex{}, - Connection: c, - JWT: nil, - } - - events := []string{ - server.StatsEvent, - server.StatusEvent, - server.ConsoleOutputEvent, - server.InstallOutputEvent, - server.DaemonMessageEvent, - } - - eventChannel := make(chan server.Event) - for _, event := range events { - s.Events().Subscribe(event, eventChannel) - } - - defer func() { - for _, event := range events { - s.Events().Unsubscribe(event, eventChannel) - } - - close(eventChannel) - }() - - // Listen for different events emitted by the server and respond to them appropriately. - go func() { - for d := range eventChannel { - handler.SendJson(&WebsocketMessage{ - Event: d.Topic, - Args: []string{d.Data}, - }) - } - }() - // Sit here and check the time to expiration on the JWT every 30 seconds until - // the token has expired. If we are within 3 minutes of the token expiring, send - // a notice over the socket that it is expiring soon. If it has expired, send that - // notice as well. - go func() { - for { - select { - case <-done: - return - case <-ticker.C: - { - if handler.JWT != nil { - if handler.JWT.ExpirationTime.Unix()-time.Now().Unix() <= 0 { - handler.SendJson(&WebsocketMessage{Event: TokenExpiredEvent}) - } else if handler.JWT.ExpirationTime.Unix()-time.Now().Unix() <= 180 { - handler.SendJson(&WebsocketMessage{Event: TokenExpiringEvent}) - } - } - } - } - } - }() - - for { - j := WebsocketMessage{inbound: true} - - _, p, err := c.ReadMessage() - if err != nil { - if !websocket.IsCloseError( - err, - websocket.CloseNormalClosure, - websocket.CloseGoingAway, - websocket.CloseNoStatusReceived, - websocket.CloseServiceRestart, - websocket.CloseAbnormalClosure, - ) { - zap.S().Errorw("error handling websocket message", zap.Error(err)) - } - break - } - - // Discard and JSON parse errors into the void and don't continue processing this - // specific socket request. If we did a break here the client would get disconnected - // from the socket, which is NOT what we want to do. - if err := json.Unmarshal(p, &j); err != nil { - continue - } - - if err := handler.HandleInbound(j); err != nil { - handler.SendErrorJson(err) - } - } -} - -// Perform a blocking send operation on the websocket since we want to avoid any -// concurrent writes to the connection, which would cause a runtime panic and cause -// the program to crash out. -func (wsh *WebsocketHandler) SendJson(v *WebsocketMessage) error { - // Do not send JSON down the line if the JWT on the connection is not - // valid! - if err := wsh.TokenValid(); err != nil { - return nil - } - - // If we're sending installation output but the user does not have the required - // permissions to see the output, don't send it down the line. - if v.Event == server.InstallOutputEvent { - zap.S().Debugf("%+v", v.Args) - if wsh.JWT != nil && !wsh.JWT.HasPermission(PermissionReceiveInstall) { - return nil - } - } - - return wsh.unsafeSendJson(v) -} - -// Sends JSON over the websocket connection, ignoring the authentication state of the -// socket user. Do not call this directly unless you are positive a response should be -// sent back to the client! -func (wsh *WebsocketHandler) unsafeSendJson(v interface{}) error { - wsh.Mutex.Lock() - defer wsh.Mutex.Unlock() - - return wsh.Connection.WriteJSON(v) -} - -// Sends an error back to the connected websocket instance by checking the permissions -// of the token. If the user has the "receive-errors" grant we will send back the actual -// error message, otherwise we just send back a standard error message. -func (wsh *WebsocketHandler) SendErrorJson(err error) error { - wsh.Mutex.Lock() - defer wsh.Mutex.Unlock() - - message := "an unexpected error was encountered while handling this request" - if wsh.JWT != nil { - if server.IsSuspendedError(err) || wsh.JWT.HasPermission(PermissionReceiveErrors) { - message = err.Error() - } - } - - m, u := wsh.GetErrorMessage(message) - - wsm := WebsocketMessage{Event: ErrorEvent} - wsm.Args = []string{m} - - if !server.IsSuspendedError(err) { - zap.S().Errorw( - "an error was encountered in the websocket process", - zap.String("server", wsh.Server.Uuid), - zap.String("error_identifier", u.String()), - zap.Error(err), - ) - } - - return wsh.Connection.WriteJSON(wsm) -} - -// Converts an error message into a more readable representation and returns a UUID -// that can be cross-referenced to find the specific error that triggered. -func (wsh *WebsocketHandler) GetErrorMessage(msg string) (string, uuid.UUID) { - u, _ := uuid.NewRandom() - - m := fmt.Sprintf("Error Event [%s]: %s", u.String(), msg) - - return m, u -} - -// Handle the inbound socket request and route it to the proper server action. -func (wsh *WebsocketHandler) HandleInbound(m WebsocketMessage) error { - if !m.inbound { - return errors.New("cannot handle websocket message, not an inbound connection") - } - - if m.Event != AuthenticationEvent { - if err := wsh.TokenValid(); err != nil { - zap.S().Debugw("jwt token is no longer valid", zap.String("message", err.Error())) - - wsh.unsafeSendJson(WebsocketMessage{ - Event: ErrorEvent, - Args: []string{"could not authenticate client: " + err.Error()}, - }) - - return nil - } - } - - switch m.Event { - case AuthenticationEvent: - { - token, err := ParseJWT([]byte(strings.Join(m.Args, ""))) - if err != nil { - return err - } - - if token.HasPermission(PermissionConnect) { - wsh.JWT = token - } - - // On every authentication event, send the current server status back - // to the client. :) - wsh.Server.Events().Publish(server.StatusEvent, wsh.Server.State) - - wsh.unsafeSendJson(WebsocketMessage{ - Event: AuthenticationSuccessEvent, - Args: []string{}, - }) - - return nil - } - case SetStateEvent: - { - if !wsh.JWT.HasPermission(PermissionSendPower) { - return nil - } - - switch strings.Join(m.Args, "") { - case "start": - return wsh.Server.Environment.Start() - case "stop": - return wsh.Server.Environment.Stop() - case "restart": - { - if err := wsh.Server.Environment.WaitForStop(60, false); err != nil { - return err - } - - return wsh.Server.Environment.Start() - } - case "kill": - return wsh.Server.Environment.Terminate(os.Kill) - } - - return nil - } - case SendServerLogsEvent: - { - if running, _ := wsh.Server.Environment.IsRunning(); !running { - return nil - } - - logs, err := wsh.Server.Environment.Readlog(1024 * 16) - if err != nil { - return err - } - - for _, line := range logs { - wsh.SendJson(&WebsocketMessage{ - Event: server.ConsoleOutputEvent, - Args: []string{line}, - }) - } - - return nil - } - case SendCommandEvent: - { - if !wsh.JWT.HasPermission(PermissionSendCommand) { - return nil - } - - if wsh.Server.State == server.ProcessOfflineState { - return nil - } - - return wsh.Server.Environment.SendCommand(strings.Join(m.Args, "")) - } - } - - return nil -} diff --git a/wings.go b/wings.go index adda06e..652e1e9 100644 --- a/wings.go +++ b/wings.go @@ -4,11 +4,12 @@ import ( "crypto/tls" "flag" "fmt" - "github.com/gorilla/websocket" "github.com/pkg/errors" "github.com/pterodactyl/wings/config" + "github.com/pterodactyl/wings/router" "github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/sftp" + "github.com/pterodactyl/wings/system" "github.com/remeh/sizedwaitgroup" "go.uber.org/zap" "net/http" @@ -149,30 +150,31 @@ func main() { zap.S().Errorw("failed to create archive directory", zap.Error(err)) } - r := &Router{ - token: c.AuthenticationToken, - upgrader: websocket.Upgrader{ - // Ensure that the websocket request is originating from the Panel itself, - // and not some other location. - CheckOrigin: func(r *http.Request) bool { - return r.Header.Get("Origin") == c.PanelLocation - }, - }, - } - - router := r.ConfigureRouter() zap.S().Infow("configuring webserver", zap.Bool("ssl", c.Api.Ssl.Enabled), zap.String("host", c.Api.Host), zap.Int("port", c.Api.Port)) + r := router.Configure() addr := fmt.Sprintf("%s:%d", c.Api.Host, c.Api.Port) + if c.Api.Ssl.Enabled { - if err := http.ListenAndServeTLS(addr, c.Api.Ssl.CertificateFile, c.Api.Ssl.KeyFile, router); err != nil { + if err := r.RunTLS(addr, c.Api.Ssl.CertificateFile, c.Api.Ssl.KeyFile); err != nil { zap.S().Fatalw("failed to configure HTTPS server", zap.Error(err)) } } else { - if err := http.ListenAndServe(addr, router); err != nil { + if err := r.Run(addr); err != nil { zap.S().Fatalw("failed to configure HTTP server", zap.Error(err)) } } + + // r := &Router{ + // token: c.AuthenticationToken, + // upgrader: websocket.Upgrader{ + // // Ensure that the websocket request is originating from the Panel itself, + // // and not some other location. + // CheckOrigin: func(r *http.Request) bool { + // return r.Header.Get("Origin") == c.PanelLocation + // }, + // }, + // } } // Configures the global logger for Zap so that we can call it from any location @@ -206,6 +208,6 @@ func printLogo() { fmt.Println(`\_____\ \/\/ / / / __ / ___/`) fmt.Println(` \___\ / / / / /_/ /___ /`) fmt.Println(` \___/\___/___/___/___/___ /______/`) - fmt.Println(` /_______/ v` + Version) + fmt.Println(` /_______/ v` + system.Version) fmt.Println() }