diff --git a/go.mod b/go.mod index b50a0d2..3cb4fc1 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,8 @@ require ( github.com/docker/go-connections v0.4.0 github.com/docker/go-units v0.3.3 // indirect github.com/gabriel-vasile/mimetype v0.1.4 + github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0 github.com/gogo/protobuf v1.2.1 // indirect - github.com/google/go-cmp v0.2.0 // indirect github.com/gorilla/websocket v1.4.0 github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect github.com/julienschmidt/httprouter v1.2.0 @@ -31,9 +31,15 @@ require ( go.uber.org/atomic v1.3.2 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.9.1 - golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480 // indirect - golang.org/x/net v0.0.0-20180906233101-161cd47e91fd + golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 // indirect + golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac // indirect + golang.org/x/net v0.0.0-20190923162816-aa69164e4478 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect + golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe // indirect + golang.org/x/text v0.3.2 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect + golang.org/x/tools v0.0.0-20190925020647-22afafe3322a // indirect + golang.org/x/tools/gopls v0.1.7 // indirect gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect diff --git a/go.sum b/go.sum index e61e2af..a43f454 100644 --- a/go.sum +++ b/go.sum @@ -20,12 +20,15 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gabriel-vasile/mimetype v0.1.4 h1:5mcsq3+DXypREUkW+1juhjeKmE/XnWgs+paHMJn7lf8= github.com/gabriel-vasile/mimetype v0.1.4/go.mod h1:kMJbg3SlWZCsj4R73F1WDzbT9AyGCOVmUtIxxwO5pmI= +github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0 h1:7KeiSrO5puFH1+vdAdbpiie2TrNnkvFc/eOQzT60Z2k= +github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0/go.mod h1:D1+3UtCYAJ1os1PI+zhTVEj6Tb+IHJvXjXKz83OstmM= github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI= @@ -41,6 +44,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/olebedev/emitter v0.0.0-20190110104742-e8d1457e6aee h1:IquUs3fIykn10zWDIyddanhpTqBvAHMaPnFhQuyYw5U= github.com/olebedev/emitter v0.0.0-20190110104742-e8d1457e6aee/go.mod h1:eT2/Pcsim3XBjbvldGiJBvvgiqZkAFyiOJJsDKXs/ts= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -66,21 +70,49 @@ go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480 h1:O5YqonU5IWby+w98jVUG9h7zlCWCcH4RHyPVReBmhzk= golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190710153321-831012c29e42/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20190918214516-5a1a30219888/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190925020647-22afafe3322a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools/gopls v0.1.3/go.mod h1:vrCQzOKxvuiZLjCKSmbbov04oeBQQOb4VQqwYK2PWIY= +golang.org/x/tools/gopls v0.1.7/go.mod h1:PE3vTwT0ejw3a2L2fFgSJkxlEbA8Slbk+Lsy9hTmbG8= +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= gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/http.go b/http.go index 593834e..bfbcdf6 100644 --- a/http.go +++ b/http.go @@ -405,7 +405,7 @@ func (rt *Router) ConfigureRouter() *httprouter.Router { router.GET("/", rt.routeIndex) router.GET("/api/servers", rt.AuthenticateToken(rt.routeAllServers)) router.GET("/api/servers/:server", rt.AuthenticateRequest(rt.routeServer)) - router.GET("/api/servers/:server/ws/:token", rt.AuthenticateServer(rt.AuthenticateWebsocket(rt.routeWebsocket))) + 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)) diff --git a/websocket.go b/websocket.go index 1d4da15..66f4310 100644 --- a/websocket.go +++ b/websocket.go @@ -1,9 +1,9 @@ package main import ( - "bytes" "encoding/json" "errors" + "github.com/gbrlsnchs/jwt/v3" "github.com/gorilla/websocket" "github.com/julienschmidt/httprouter" "github.com/pterodactyl/wings/config" @@ -13,6 +13,7 @@ import ( "os" "strings" "sync" + "time" ) const ( @@ -34,6 +35,13 @@ type WebsocketMessage struct { // should either omit the field or pass an empty value as it is ignored. Args []string `json:"args,omitempty"` + // The authentication JWT passed along with every call to the websocket that + // should be used to validate the user's authenticity and ability to perform + // whatever action they're doing. + Token string `json:"token,omitempty"` + + // Is set to true when the request is originating from outside of the Daemon, + // otherwise set to false for outbound. inbound bool } @@ -41,56 +49,100 @@ type WebsocketHandler struct { Server *server.Server Mutex sync.Mutex Connection *websocket.Conn + JWT *WebsocketTokenPayload } -type socketCredentials struct { - ServerUuid string `json:"server_uuid"` +type WebsocketTokenPayload struct { + jwt.Payload + UserID json.Number `json:"user_id"` + ServerUUID string `json:"server_uuid"` + Permissions []string `json:"permissions"` } -func (rt *Router) AuthenticateWebsocket(h httprouter.Handle) httprouter.Handle { - return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - s := rt.Servers.Get(ps.ByName("server")) +const ( + PermissionConnect = "connect" + PermissionSendCommand = "send-command" + PermissionSendPower = "send-power" +) - j, err := json.Marshal(socketCredentials{ServerUuid: s.Uuid}) - if err != nil { - zap.S().Errorw("failed to marshal json", zap.Error(err)) - http.Error(w, "failed to marshal json", http.StatusInternalServerError) - return +// 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 } - - url := strings.TrimRight(config.Get().PanelLocation, "/") + "/api/remote/websocket/" + ps.ByName("token") - req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(j)) - if err != nil { - zap.S().Errorw("failed to generate a new HTTP request when validating websocket credentials", zap.Error(err)) - http.Error(w, "failed to generate HTTP request", http.StatusInternalServerError) - return - } - - req.Header.Set("Accept", "application/json") - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+config.Get().AuthenticationToken) - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - zap.S().Errorw("failed to perform client HTTP request", zap.Error(err)) - http.Error(w, "failed to perform client HTTP request", http.StatusInternalServerError) - return - } - - defer resp.Body.Close() - if resp.StatusCode != http.StatusNoContent { - http.Error(w, "failed to validate token with server", resp.StatusCode) - return - } - - h(w, r, ps) } + + 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)) + } + + _, err := jwt.Verify(token, alg, &payload) + if err != nil { + return nil, err + } + + // Check the time of the JWT becoming valid does not exceed more than 15 seconds + // compared to the system time. This accounts for clock drift to some degree. + if time.Now().Unix() - payload.NotBefore.Unix() <= -15 { + return nil, errors.New("jwt violates nbf") + } + + // Compare the expiration time of the token to the current system time. Include + // up to 15 seconds of clock drift, and if it has expired return an error and + // do not process the action. + if time.Now().Unix() - payload.ExpirationTime.Unix() > 15 { + return nil, errors.New("jwt violates exp") + } + + 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 time.Now().Unix() - wsh.JWT.ExpirationTime.Unix() > 15 { + return errors.New("jwt violates nbf") + } + + 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) { + token, err := ParseJWT([]byte(r.URL.Query().Get("token"))) + if err != nil { + return + } + c, err := rt.upgrader.Upgrade(w, r, nil) if err != nil { zap.S().Error(err) @@ -103,6 +155,7 @@ func (rt *Router) routeWebsocket(w http.ResponseWriter, r *http.Request, ps http Server: s, Mutex: sync.Mutex{}, Connection: c, + JWT: token, } handleOutput := func(data string) { @@ -185,9 +238,19 @@ func (wsh *WebsocketHandler) HandleInbound(m WebsocketMessage) error { return errors.New("cannot handle websocket message, not an inbound connection") } + if err := wsh.TokenValid(); err != nil { + zap.S().Debugw("jwt token is no longer valid", zap.String("message", err.Error())) + + return nil + } + switch m.Event { case SetStateEvent: { + if !wsh.JWT.HasPermission(PermissionSendPower) { + return nil + } + var err error switch strings.Join(m.Args, "") { case "start": @@ -229,6 +292,10 @@ func (wsh *WebsocketHandler) HandleInbound(m WebsocketMessage) error { } case SendCommandEvent: { + if !wsh.JWT.HasPermission(PermissionSendCommand) { + return nil + } + return wsh.Server.Environment.SendCommand(strings.Join(m.Args, "")) } }