Port most of the HTTP code over to gin

This commit is contained in:
Dane Everitt 2020-04-05 18:00:33 -07:00
parent 223b9e05a1
commit cf2ef1a173
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
22 changed files with 1233 additions and 968 deletions

View File

@ -6,6 +6,8 @@ import (
"time"
)
var alg *jwt.HMACSHA
// ArchiveTokenPayload represents an Archive Token Payload.
type ArchiveTokenPayload struct {
jwt.Payload

21
go.mod
View File

@ -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
)

52
go.sum
View File

@ -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=

514
http.go
View File

@ -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))

93
router/error.go Normal file
View File

@ -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)
}

67
router/middleware.go Normal file
View File

@ -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()
}

50
router/router.go Normal file
View File

@ -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
}

View File

@ -0,0 +1 @@
package router

216
router/router_server.go Normal file
View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}
}

64
router/router_system.go Normal file
View File

@ -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)
}

View File

@ -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},
})
}
}
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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
}

12
server/power.go Normal file
View File

@ -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"
}

View File

@ -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")
}
}

View File

@ -1,4 +1,4 @@
package main
package system
const (
// The current version of this software.

View File

@ -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,

View File

@ -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
}

View File

@ -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()
}