Port most of the HTTP code over to gin
This commit is contained in:
parent
223b9e05a1
commit
cf2ef1a173
|
@ -6,6 +6,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var alg *jwt.HMACSHA
|
||||||
|
|
||||||
// ArchiveTokenPayload represents an Archive Token Payload.
|
// ArchiveTokenPayload represents an Archive Token Payload.
|
||||||
type ArchiveTokenPayload struct {
|
type ArchiveTokenPayload struct {
|
||||||
jwt.Payload
|
jwt.Payload
|
||||||
|
|
21
go.mod
21
go.mod
|
@ -27,7 +27,10 @@ require (
|
||||||
github.com/gabriel-vasile/mimetype v0.1.4
|
github.com/gabriel-vasile/mimetype v0.1.4
|
||||||
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0
|
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0
|
||||||
github.com/ghodss/yaml v1.0.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/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/google/uuid v1.1.1
|
||||||
github.com/gorilla/websocket v1.4.0
|
github.com/gorilla/websocket v1.4.0
|
||||||
github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect
|
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/mattn/go-shellwords v1.0.10 // indirect
|
||||||
github.com/mholt/archiver/v3 v3.3.0
|
github.com/mholt/archiver/v3 v3.3.0
|
||||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db
|
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/ginkgo v1.8.0 // indirect
|
||||||
github.com/onsi/gomega v1.5.0 // indirect
|
github.com/onsi/gomega v1.5.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0-rc1 // 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/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce
|
||||||
github.com/sirupsen/logrus v1.0.5 // indirect
|
github.com/sirupsen/logrus v1.0.5 // indirect
|
||||||
github.com/smartystreets/goconvey v1.6.4 // 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/atomic v1.5.1 // indirect
|
||||||
go.uber.org/multierr v1.4.0 // indirect
|
go.uber.org/multierr v1.4.0 // indirect
|
||||||
go.uber.org/zap v1.13.0
|
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/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect
|
||||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 // indirect
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a // indirect
|
||||||
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab // indirect
|
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d // indirect
|
||||||
golang.org/x/text v0.3.2 // indirect
|
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // 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/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/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect
|
||||||
gopkg.in/ini.v1 v1.51.0
|
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
|
gotest.tools v2.2.0+incompatible // indirect
|
||||||
)
|
)
|
||||||
|
|
52
go.sum
52
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/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 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
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 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
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 h1:KRMr9A3qfbVM7iV/WcLY/rL5LICqwMHLhwRXKu99fXw=
|
||||||
github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
|
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 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
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 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
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.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
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/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/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 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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/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 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
|
||||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
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 h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
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=
|
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/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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
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 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE=
|
||||||
github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
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 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
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 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
|
||||||
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
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 h1:vWjhY8SQp5yzM9P6OJ/eZEkmi3UAbRrxCq48MxjAzig=
|
||||||
github.com/mholt/archiver/v3 v3.3.0/go.mod h1:YnQtqsp+94Rwd0D/rk5cnLrxusUBUXg+08Ebtr1Mqao=
|
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 h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
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 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
|
||||||
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
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 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
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.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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
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.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/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 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
|
||||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
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 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
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 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
||||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
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=
|
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-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-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-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 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-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-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 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-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 h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE=
|
||||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
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.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 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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
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-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 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-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 h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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-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 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-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 h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
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-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-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 h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM=
|
||||||
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
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-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 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-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-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 h1:Zc7EU2PqpsNeIfOoVA7hvQX4cS3YDJEs5KlfatT3hLo=
|
||||||
golang.org/x/tools v0.0.0-20191206204035-259af5ff87bd/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
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/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 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-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 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
|
||||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
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 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 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-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/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 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
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.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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
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 h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||||
|
|
514
http.go
514
http.go
|
@ -5,8 +5,6 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/buger/jsonparser"
|
"github.com/buger/jsonparser"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/julienschmidt/httprouter"
|
"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) {
|
func (rt *Router) routeServerBackup(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
s := rt.GetServer(ps.ByName("server"))
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
|
@ -543,74 +121,6 @@ func (rt *Router) routeServerBackup(w http.ResponseWriter, r *http.Request, ps h
|
||||||
w.WriteHeader(http.StatusAccepted)
|
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) {
|
func (rt *Router) routeRequestServerArchive(w http.ResponseWriter, _ *http.Request, ps httprouter.Params) {
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
s := rt.GetServer(ps.ByName("server"))
|
||||||
|
|
||||||
|
@ -886,32 +396,8 @@ func (rt *Router) ReaderToBytes(r io.Reader) []byte {
|
||||||
func (rt *Router) ConfigureRouter() *httprouter.Router {
|
func (rt *Router) ConfigureRouter() *httprouter.Router {
|
||||||
router := httprouter.New()
|
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("/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.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.POST("/api/servers/:server/archive", rt.AuthenticateRequest(rt.routeRequestServerArchive))
|
||||||
router.GET("/api/servers/:server/archive", rt.AuthenticateServer(rt.routeGetServerArchive))
|
router.GET("/api/servers/:server/archive", rt.AuthenticateServer(rt.routeGetServerArchive))
|
||||||
|
|
93
router/error.go
Normal file
93
router/error.go
Normal 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
67
router/middleware.go
Normal 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
50
router/router.go
Normal 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
|
||||||
|
}
|
1
router/router_download.go
Normal file
1
router/router_download.go
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package router
|
216
router/router_server.go
Normal file
216
router/router_server.go
Normal 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)
|
||||||
|
}
|
155
router/router_server_files.go
Normal file
155
router/router_server_files.go
Normal 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)
|
||||||
|
}
|
60
router/router_server_ws.go
Normal file
60
router/router_server_ws.go
Normal 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
64
router/router_system.go
Normal 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)
|
||||||
|
}
|
76
router/websocket/listeners.go
Normal file
76
router/websocket/listeners.go
Normal 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},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
router/websocket/message.go
Normal file
26
router/websocket/message.go
Normal 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"`
|
||||||
|
}
|
24
router/websocket/payload.go
Normal file
24
router/websocket/payload.go
Normal 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
|
||||||
|
}
|
277
router/websocket/websocket.go
Normal file
277
router/websocket/websocket.go
Normal 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
12
server/power.go
Normal 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"
|
||||||
|
}
|
|
@ -394,3 +394,24 @@ func (s *Server) SetState(state string) error {
|
||||||
func (s *Server) GetProcessConfiguration() (*api.ServerConfigurationResponse, *api.RequestError, error) {
|
func (s *Server) GetProcessConfiguration() (*api.ServerConfigurationResponse, *api.RequestError, error) {
|
||||||
return api.NewRequester().GetServerConfiguration(s.Uuid)
|
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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package system
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// The current version of this software.
|
// The current version of this software.
|
|
@ -1,11 +1,11 @@
|
||||||
package main
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/docker/docker/pkg/parsers/kernel"
|
"github.com/docker/docker/pkg/parsers/kernel"
|
||||||
"runtime"
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SystemInformation struct {
|
type Information struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
KernelVersion string `json:"kernel_version"`
|
KernelVersion string `json:"kernel_version"`
|
||||||
Architecture string `json:"architecture"`
|
Architecture string `json:"architecture"`
|
||||||
|
@ -13,13 +13,13 @@ type SystemInformation struct {
|
||||||
CpuCount int `json:"cpu_count"`
|
CpuCount int `json:"cpu_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetSystemInformation() (*SystemInformation, error) {
|
func GetSystemInformation() (*Information, error) {
|
||||||
k, err := kernel.GetKernelVersion()
|
k, err := kernel.GetKernelVersion()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &SystemInformation{
|
s := &Information{
|
||||||
Version: Version,
|
Version: Version,
|
||||||
KernelVersion: k.String(),
|
KernelVersion: k.String(),
|
||||||
Architecture: runtime.GOARCH,
|
Architecture: runtime.GOARCH,
|
426
websocket.go
426
websocket.go
|
@ -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
|
|
||||||
}
|
|
34
wings.go
34
wings.go
|
@ -4,11 +4,12 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/pterodactyl/wings/router"
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
"github.com/pterodactyl/wings/sftp"
|
"github.com/pterodactyl/wings/sftp"
|
||||||
|
"github.com/pterodactyl/wings/system"
|
||||||
"github.com/remeh/sizedwaitgroup"
|
"github.com/remeh/sizedwaitgroup"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -149,30 +150,31 @@ func main() {
|
||||||
zap.S().Errorw("failed to create archive directory", zap.Error(err))
|
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))
|
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)
|
addr := fmt.Sprintf("%s:%d", c.Api.Host, c.Api.Port)
|
||||||
|
|
||||||
if c.Api.Ssl.Enabled {
|
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))
|
zap.S().Fatalw("failed to configure HTTPS server", zap.Error(err))
|
||||||
}
|
}
|
||||||
} else {
|
} 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))
|
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
|
// 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(` \___\ / / / / /_/ /___ /`)
|
||||||
fmt.Println(` \___/\___/___/___/___/___ /______/`)
|
fmt.Println(` \___/\___/___/___/___/___ /______/`)
|
||||||
fmt.Println(` /_______/ v` + Version)
|
fmt.Println(` /_______/ v` + system.Version)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user