mirror of
https://github.com/gravitational/teleport
synced 2024-10-21 01:34:01 +00:00
(web) csrf protection of public API
This commit is contained in:
parent
763b92f0c3
commit
07783385d3
141
lib/httplib/csrf/csrf.go
Normal file
141
lib/httplib/csrf/csrf.go
Normal file
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
Copyright 2016 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
"github.com/gravitational/trace"
|
||||
)
|
||||
|
||||
const (
|
||||
// CookieName is a name of the cookie
|
||||
CookieName = "grv_csrf"
|
||||
// HeaderName is the default HTTP request header to inspect
|
||||
HeaderName = "X-CSRF-Token"
|
||||
// tokenLenBytes is CSRF token length in bytes.
|
||||
tokenLenBytes = 32
|
||||
// defaultMaxAge is the default MaxAge for cookies.
|
||||
defaultMaxAge = 0
|
||||
)
|
||||
|
||||
// AddCSRFProtection adds CSRF token into the user session via secure cookie,
|
||||
// it implements "double submit cookie" approach to check against CSRF attacks
|
||||
// https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29_Prevention_Cheat_Sheet#Double_Submit_Cookie
|
||||
func AddCSRFProtection(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||
encodedToken := ""
|
||||
token, err := extractFromCookie(r)
|
||||
// if there was an error retrieving the token, the token doesn't exist
|
||||
if err != nil || len(token) == 0 {
|
||||
encodedToken, err = utils.CryptoRandomHex(tokenLenBytes)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
}
|
||||
} else {
|
||||
encodedToken = hex.EncodeToString(token)
|
||||
}
|
||||
|
||||
save(encodedToken, w)
|
||||
return encodedToken, nil
|
||||
}
|
||||
|
||||
// VerifyToken checks if the cookie value and request value match.
|
||||
func VerifyToken(w http.ResponseWriter, r *http.Request) error {
|
||||
realToken, err := extractFromCookie(r)
|
||||
if err != nil {
|
||||
return trace.BadParameter("cannot retrieve CSRF token from cookie", err)
|
||||
}
|
||||
|
||||
if len(realToken) != tokenLenBytes {
|
||||
return trace.BadParameter("invalid CSRF cookie token length, expected %v, got %v", tokenLenBytes, len(realToken))
|
||||
}
|
||||
|
||||
requestToken, err := extractFromRequest(r)
|
||||
if err != nil {
|
||||
return trace.BadParameter("cannot retrieve CSRF token from HTTP header", err)
|
||||
}
|
||||
|
||||
// compare the request token against the real token
|
||||
if !compareTokens(requestToken, realToken) {
|
||||
return trace.BadParameter("request and cookie CSRF tokens do not match")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractFromCookie retrieves a CSRF token from the session cookie.
|
||||
func extractFromCookie(r *http.Request) ([]byte, error) {
|
||||
cookie, err := r.Cookie(CookieName)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
token, err := decode(cookie.Value)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// extractFromRequest returns the issued token from HTTP header.
|
||||
func extractFromRequest(r *http.Request) ([]byte, error) {
|
||||
issued := r.Header.Get(HeaderName)
|
||||
decoded, err := decode(issued)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
// save stores encoded CSRF token in the session cookie.
|
||||
func save(encodedToken string, w http.ResponseWriter) string {
|
||||
cookie := &http.Cookie{
|
||||
Name: CookieName,
|
||||
Value: encodedToken,
|
||||
MaxAge: defaultMaxAge,
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
Path: "/",
|
||||
}
|
||||
|
||||
// write the authenticated cookie to the response.
|
||||
http.SetCookie(w, cookie)
|
||||
w.Header().Add("Vary", "Cookie")
|
||||
return encodedToken
|
||||
}
|
||||
|
||||
// compare securely (constant-time) compares request token against the real token
|
||||
// from the session.
|
||||
func compareTokens(a, b []byte) bool {
|
||||
// this is required as subtle.ConstantTimeCompare does not check for equal
|
||||
// lengths in Go versions prior to 1.3.
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
return subtle.ConstantTimeCompare(a, b) == 1
|
||||
}
|
||||
|
||||
// decode decodes a cookie using base64.
|
||||
func decode(value string) ([]byte, error) {
|
||||
return hex.DecodeString(value)
|
||||
}
|
|
@ -40,6 +40,7 @@ import (
|
|||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/events"
|
||||
"github.com/gravitational/teleport/lib/httplib"
|
||||
"github.com/gravitational/teleport/lib/httplib/csrf"
|
||||
"github.com/gravitational/teleport/lib/reversetunnel"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/session"
|
||||
|
@ -150,7 +151,7 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
|
|||
h.GET("/webapi/ping/:connector", httplib.MakeHandler(h.pingWithConnector))
|
||||
|
||||
// Web sessions
|
||||
h.POST("/webapi/sessions", httplib.MakeHandler(h.createSession))
|
||||
h.POST("/webapi/sessions", h.WithCSRFProtection(httplib.MakeHandler(h.createSession)))
|
||||
h.DELETE("/webapi/sessions", h.WithAuth(h.deleteSession))
|
||||
h.POST("/webapi/sessions/renew", h.WithAuth(h.renewSession))
|
||||
|
||||
|
@ -259,13 +260,24 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
|
|||
// serve Web UI:
|
||||
if strings.HasPrefix(r.URL.Path, "/web/app") {
|
||||
http.StripPrefix("/web", http.FileServer(staticFS)).ServeHTTP(w, r)
|
||||
|
||||
} else if strings.HasPrefix(r.URL.Path, "/web/config.js") {
|
||||
writeSettings.ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/web") {
|
||||
ctx, err := h.AuthenticateRequest(w, r, false)
|
||||
csrfToken, err := csrf.AddCSRFProtection(w, r)
|
||||
if err != nil {
|
||||
log.Errorf("failed to generate CSRF token %v", err)
|
||||
}
|
||||
|
||||
session := struct {
|
||||
Session string
|
||||
}{Session: base64.StdEncoding.EncodeToString([]byte("{}"))}
|
||||
XCSRF string
|
||||
}{
|
||||
XCSRF: csrfToken,
|
||||
Session: base64.StdEncoding.EncodeToString([]byte("{}")),
|
||||
}
|
||||
|
||||
ctx, err := h.AuthenticateRequest(w, r, false)
|
||||
if err == nil {
|
||||
re, err := NewSessionResponse(ctx)
|
||||
if err == nil {
|
||||
|
@ -281,11 +293,13 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
|
|||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
h.NotFound = routingHandler
|
||||
plugin := GetPlugin()
|
||||
if plugin != nil {
|
||||
plugin.AddHandlers(h)
|
||||
}
|
||||
|
||||
return &RewritingHandler{
|
||||
Handler: httplib.RewritePaths(h,
|
||||
httplib.Rewrite("/webapi/sites/([^/]+)/sessions/(.*)", "/webapi/sites/$1/namespaces/default/sessions/$2"),
|
||||
|
@ -298,11 +312,11 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
|
|||
}
|
||||
|
||||
// Close closes associated session cache operations
|
||||
func (m *Handler) Close() error {
|
||||
return m.auth.Close()
|
||||
func (h *Handler) Close() error {
|
||||
return h.auth.Close()
|
||||
}
|
||||
|
||||
func (m *Handler) getUserStatus(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c *SessionContext) (interface{}, error) {
|
||||
func (h *Handler) getUserStatus(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c *SessionContext) (interface{}, error) {
|
||||
return ok(), nil
|
||||
}
|
||||
|
||||
|
@ -310,7 +324,7 @@ func (m *Handler) getUserStatus(w http.ResponseWriter, r *http.Request, _ httpro
|
|||
//
|
||||
// GET /webapi/user/context
|
||||
//
|
||||
func (m *Handler) getUserContext(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c *SessionContext) (interface{}, error) {
|
||||
func (h *Handler) getUserContext(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c *SessionContext) (interface{}, error) {
|
||||
clt, err := c.GetClient()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
@ -436,10 +450,10 @@ func defaultAuthenticationSettings(authClient auth.ClientI) (client.Authenticati
|
|||
return as, nil
|
||||
}
|
||||
|
||||
func (m *Handler) ping(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
func (h *Handler) ping(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
var err error
|
||||
|
||||
defaultSettings, err := defaultAuthenticationSettings(m.cfg.ProxyClient)
|
||||
defaultSettings, err := defaultAuthenticationSettings(h.cfg.ProxyClient)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
@ -450,8 +464,8 @@ func (m *Handler) ping(w http.ResponseWriter, r *http.Request, p httprouter.Para
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (m *Handler) pingWithConnector(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
authClient := m.cfg.ProxyClient
|
||||
func (h *Handler) pingWithConnector(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
authClient := h.cfg.ProxyClient
|
||||
connectorName := p.ByName("connector")
|
||||
|
||||
cap, err := authClient.GetAuthPreference()
|
||||
|
@ -460,7 +474,7 @@ func (m *Handler) pingWithConnector(w http.ResponseWriter, r *http.Request, p ht
|
|||
}
|
||||
|
||||
if connectorName == teleport.Local {
|
||||
as, err := localSettings(m.cfg.ProxyClient, cap)
|
||||
as, err := localSettings(h.cfg.ProxyClient, cap)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
@ -501,8 +515,8 @@ type webConfig struct {
|
|||
}
|
||||
|
||||
// getConfigurationSettings returns configuration for the web application.
|
||||
func (m *Handler) getConfigurationSettings(w http.ResponseWriter, r *http.Request) (interface{}, error) {
|
||||
as, err := defaultAuthenticationSettings(m.cfg.ProxyClient)
|
||||
func (h *Handler) getConfigurationSettings(w http.ResponseWriter, r *http.Request) (interface{}, error) {
|
||||
as, err := defaultAuthenticationSettings(h.cfg.ProxyClient)
|
||||
if err != nil {
|
||||
log.Infof("Cannot retrieve cluster auth preferences: %v", err)
|
||||
}
|
||||
|
@ -521,7 +535,7 @@ func (m *Handler) getConfigurationSettings(w http.ResponseWriter, r *http.Reques
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *Handler) oidcLoginWeb(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
func (h *Handler) oidcLoginWeb(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
log.Infof("oidcLoginWeb start")
|
||||
|
||||
query := r.URL.Query()
|
||||
|
@ -533,7 +547,7 @@ func (m *Handler) oidcLoginWeb(w http.ResponseWriter, r *http.Request, p httprou
|
|||
if connectorID == "" {
|
||||
return nil, trace.BadParameter("missing connector_id query parameter")
|
||||
}
|
||||
response, err := m.cfg.ProxyClient.CreateOIDCAuthRequest(
|
||||
response, err := h.cfg.ProxyClient.CreateOIDCAuthRequest(
|
||||
services.OIDCAuthRequest{
|
||||
ConnectorID: connectorID,
|
||||
CreateWebSession: true,
|
||||
|
@ -547,7 +561,7 @@ func (m *Handler) oidcLoginWeb(w http.ResponseWriter, r *http.Request, p httprou
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *Handler) oidcLoginConsole(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
func (h *Handler) oidcLoginConsole(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
log.Debugf("oidcLoginConsole start")
|
||||
var req *client.SSOLoginConsoleReq
|
||||
if err := httplib.ReadJSON(r, &req); err != nil {
|
||||
|
@ -562,7 +576,7 @@ func (m *Handler) oidcLoginConsole(w http.ResponseWriter, r *http.Request, p htt
|
|||
if req.ConnectorID == "" {
|
||||
return nil, trace.BadParameter("missing ConnectorID")
|
||||
}
|
||||
response, err := m.cfg.ProxyClient.CreateOIDCAuthRequest(
|
||||
response, err := h.cfg.ProxyClient.CreateOIDCAuthRequest(
|
||||
services.OIDCAuthRequest{
|
||||
ConnectorID: req.ConnectorID,
|
||||
ClientRedirectURL: req.RedirectURL,
|
||||
|
@ -577,10 +591,10 @@ func (m *Handler) oidcLoginConsole(w http.ResponseWriter, r *http.Request, p htt
|
|||
return &client.SSOLoginConsoleResponse{RedirectURL: response.RedirectURL}, nil
|
||||
}
|
||||
|
||||
func (m *Handler) oidcCallback(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
func (h *Handler) oidcCallback(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
log.Debugf("oidcCallback start")
|
||||
|
||||
response, err := m.cfg.ProxyClient.ValidateOIDCAuthCallback(r.URL.Query())
|
||||
response, err := h.cfg.ProxyClient.ValidateOIDCAuthCallback(r.URL.Query())
|
||||
if err != nil {
|
||||
log.Warningf("[OIDC] Error while processing callback: %v", err)
|
||||
|
||||
|
@ -757,7 +771,7 @@ func NewSessionResponse(ctx *SessionContext) (*CreateSessionResponse, error) {
|
|||
//
|
||||
// {"type": "bearer", "token": "bearer token", "user": {"name": "alex", "allowed_logins": ["admin", "bob"]}, "expires_in": 20}
|
||||
//
|
||||
func (m *Handler) createSession(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
func (h *Handler) createSession(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
var req *createSessionReq
|
||||
if err := httplib.ReadJSON(r, &req); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
@ -765,7 +779,7 @@ func (m *Handler) createSession(w http.ResponseWriter, r *http.Request, p httpro
|
|||
|
||||
// get cluster preferences to see if we should login
|
||||
// with password or password+otp
|
||||
authClient := m.cfg.ProxyClient
|
||||
authClient := h.cfg.ProxyClient
|
||||
cap, err := authClient.GetAuthPreference()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
@ -775,9 +789,9 @@ func (m *Handler) createSession(w http.ResponseWriter, r *http.Request, p httpro
|
|||
|
||||
switch cap.GetSecondFactor() {
|
||||
case teleport.OFF:
|
||||
webSession, err = m.auth.AuthWithoutOTP(req.User, req.Pass)
|
||||
webSession, err = h.auth.AuthWithoutOTP(req.User, req.Pass)
|
||||
case teleport.OTP, teleport.HOTP, teleport.TOTP:
|
||||
webSession, err = m.auth.AuthWithOTP(req.User, req.Pass, req.SecondFactorToken)
|
||||
webSession, err = h.auth.AuthWithOTP(req.User, req.Pass, req.SecondFactorToken)
|
||||
default:
|
||||
return nil, trace.AccessDenied("unknown second factor type: %q", cap.GetSecondFactor())
|
||||
}
|
||||
|
@ -789,7 +803,7 @@ func (m *Handler) createSession(w http.ResponseWriter, r *http.Request, p httpro
|
|||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
ctx, err := m.auth.ValidateSession(req.User, webSession.GetName())
|
||||
ctx, err := h.auth.ValidateSession(req.User, webSession.GetName())
|
||||
if err != nil {
|
||||
return nil, trace.AccessDenied("need auth")
|
||||
}
|
||||
|
@ -805,7 +819,7 @@ func (m *Handler) createSession(w http.ResponseWriter, r *http.Request, p httpro
|
|||
//
|
||||
// {"message": "ok"}
|
||||
//
|
||||
func (m *Handler) deleteSession(w http.ResponseWriter, r *http.Request, _ httprouter.Params, ctx *SessionContext) (interface{}, error) {
|
||||
func (h *Handler) deleteSession(w http.ResponseWriter, r *http.Request, _ httprouter.Params, ctx *SessionContext) (interface{}, error) {
|
||||
if err := ctx.Invalidate(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
@ -826,7 +840,7 @@ func (m *Handler) deleteSession(w http.ResponseWriter, r *http.Request, _ httpro
|
|||
// {"type": "bearer", "token": "bearer token", "user": {"name": "alex", "allowed_logins": ["admin", "bob"]}, "expires_in": 20}
|
||||
//
|
||||
//
|
||||
func (m *Handler) renewSession(w http.ResponseWriter, r *http.Request, _ httprouter.Params, ctx *SessionContext) (interface{}, error) {
|
||||
func (h *Handler) renewSession(w http.ResponseWriter, r *http.Request, _ httprouter.Params, ctx *SessionContext) (interface{}, error) {
|
||||
newSess, err := ctx.ExtendWebSession()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
@ -859,9 +873,9 @@ type renderUserInviteResponse struct {
|
|||
// {"invite_token": "token", "user": "alex", qr: "base64-encoded-qr-code image"}
|
||||
//
|
||||
//
|
||||
func (m *Handler) renderUserInvite(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
func (h *Handler) renderUserInvite(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
token := p[0].Value
|
||||
user, qrCodeBytes, err := m.auth.GetUserInviteInfo(token)
|
||||
user, qrCodeBytes, err := h.auth.GetUserInviteInfo(token)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
@ -881,9 +895,9 @@ func (m *Handler) renderUserInvite(w http.ResponseWriter, r *http.Request, p htt
|
|||
//
|
||||
// {"version":"U2F_V2","challenge":"randombase64string","appId":"https://mycorp.com:3080"}
|
||||
//
|
||||
func (m *Handler) u2fRegisterRequest(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
func (h *Handler) u2fRegisterRequest(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
token := p[0].Value
|
||||
u2fRegisterRequest, err := m.auth.GetUserInviteU2FRegisterRequest(token)
|
||||
u2fRegisterRequest, err := h.auth.GetUserInviteU2FRegisterRequest(token)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
@ -901,12 +915,12 @@ func (m *Handler) u2fRegisterRequest(w http.ResponseWriter, r *http.Request, p h
|
|||
//
|
||||
// {"version":"U2F_V2","challenge":"randombase64string","keyHandle":"longbase64string","appId":"https://mycorp.com:3080"}
|
||||
//
|
||||
func (m *Handler) u2fSignRequest(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
func (h *Handler) u2fSignRequest(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
var req *client.U2fSignRequestReq
|
||||
if err := httplib.ReadJSON(r, &req); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
u2fSignReq, err := m.auth.GetU2FSignRequest(req.User, req.Pass)
|
||||
u2fSignReq, err := h.auth.GetU2FSignRequest(req.User, req.Pass)
|
||||
if err != nil {
|
||||
return nil, trace.AccessDenied("bad auth credentials")
|
||||
}
|
||||
|
@ -930,20 +944,20 @@ type u2fSignResponseReq struct {
|
|||
//
|
||||
// {"type": "bearer", "token": "bearer token", "user": {"name": "alex", "allowed_logins": ["admin", "bob"]}, "expires_in": 20}
|
||||
//
|
||||
func (m *Handler) createSessionWithU2FSignResponse(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
func (h *Handler) createSessionWithU2FSignResponse(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
var req *u2fSignResponseReq
|
||||
if err := httplib.ReadJSON(r, &req); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
sess, err := m.auth.AuthWithU2FSignResponse(req.User, &req.U2FSignResponse)
|
||||
sess, err := h.auth.AuthWithU2FSignResponse(req.User, &req.U2FSignResponse)
|
||||
if err != nil {
|
||||
return nil, trace.AccessDenied("bad auth credentials")
|
||||
}
|
||||
if err := SetSession(w, req.User, sess.GetName()); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
ctx, err := m.auth.ValidateSession(req.User, sess.GetName())
|
||||
ctx, err := h.auth.ValidateSession(req.User, sess.GetName())
|
||||
if err != nil {
|
||||
return nil, trace.AccessDenied("need auth")
|
||||
}
|
||||
|
@ -966,16 +980,16 @@ type createNewUserReq struct {
|
|||
// Sucessful response: (session cookie is set)
|
||||
//
|
||||
// {"type": "bearer", "token": "bearer token", "user": "alex", "expires_in": 20}
|
||||
func (m *Handler) createNewUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
func (h *Handler) createNewUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
var req *createNewUserReq
|
||||
if err := httplib.ReadJSON(r, &req); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
sess, err := m.auth.CreateNewUser(req.InviteToken, req.Pass, req.SecondFactorToken)
|
||||
sess, err := h.auth.CreateNewUser(req.InviteToken, req.Pass, req.SecondFactorToken)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
ctx, err := m.auth.ValidateSession(sess.GetUser(), sess.GetName())
|
||||
ctx, err := h.auth.ValidateSession(sess.GetUser(), sess.GetName())
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
@ -1001,16 +1015,16 @@ type createNewU2FUserReq struct {
|
|||
// Sucessful response: (session cookie is set)
|
||||
//
|
||||
// {"type": "bearer", "token": "bearer token", "user": "alex", "expires_in": 20}
|
||||
func (m *Handler) createNewU2FUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
func (h *Handler) createNewU2FUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
var req *createNewU2FUserReq
|
||||
if err := httplib.ReadJSON(r, &req); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
sess, err := m.auth.CreateNewU2FUser(req.InviteToken, req.Pass, req.U2FRegisterResponse)
|
||||
sess, err := h.auth.CreateNewU2FUser(req.InviteToken, req.Pass, req.U2FRegisterResponse)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
ctx, err := m.auth.ValidateSession(sess.GetUser(), sess.GetName())
|
||||
ctx, err := h.auth.ValidateSession(sess.GetUser(), sess.GetName())
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
@ -1068,7 +1082,7 @@ Sucessful response:
|
|||
|
||||
{"namespaces": [{..namespace resource...}]}
|
||||
*/
|
||||
func (m *Handler) getSiteNamespaces(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
func (h *Handler) getSiteNamespaces(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
log.Debugf("[web] GET /namespaces")
|
||||
clt, err := site.GetClient()
|
||||
if err != nil {
|
||||
|
@ -1088,7 +1102,7 @@ type nodeWithSessions struct {
|
|||
Sessions []session.Session `json:"sessions"`
|
||||
}
|
||||
|
||||
func (m *Handler) siteNodesGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, c *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
func (h *Handler) siteNodesGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, c *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
log.Debugf("[web] GET /nodes")
|
||||
clt, err := site.GetClient()
|
||||
if err != nil {
|
||||
|
@ -1120,7 +1134,7 @@ func (m *Handler) siteNodesGet(w http.ResponseWriter, r *http.Request, p httprou
|
|||
//
|
||||
// Sucessful response is a websocket stream that allows read write to the server
|
||||
//
|
||||
func (m *Handler) siteNodeConnect(
|
||||
func (h *Handler) siteNodeConnect(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
p httprouter.Params,
|
||||
|
@ -1146,7 +1160,7 @@ func (m *Handler) siteNodeConnect(
|
|||
req.Namespace, req.Server, req.Login, req.SessionID)
|
||||
|
||||
req.Namespace = namespace
|
||||
req.ProxyHostPort = m.ProxyHostPort()
|
||||
req.ProxyHostPort = h.ProxyHostPort()
|
||||
req.Cluster = site.GetName()
|
||||
|
||||
clt, err := ctx.GetUserClient(site)
|
||||
|
@ -1182,7 +1196,7 @@ type sessionStreamEvent struct {
|
|||
// Sucessful response is a websocket stream that allows read write to the server and returns
|
||||
// json events
|
||||
//
|
||||
func (m *Handler) siteSessionStream(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
func (h *Handler) siteSessionStream(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
sessionID, err := session.ParseID(p.ByName("sid"))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
@ -1194,7 +1208,7 @@ func (m *Handler) siteSessionStream(w http.ResponseWriter, r *http.Request, p ht
|
|||
}
|
||||
|
||||
connect, err := newSessionStreamHandler(namespace,
|
||||
*sessionID, ctx, site, m.sessionStreamPollPeriod)
|
||||
*sessionID, ctx, site, h.sessionStreamPollPeriod)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
@ -1230,7 +1244,7 @@ type siteSessionGenerateResponse struct {
|
|||
//
|
||||
// {"session": {"id": "session-id", "terminal_params": {"w": 100, "h": 100}, "login": "centos"}}
|
||||
//
|
||||
func (m *Handler) siteSessionGenerate(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
func (h *Handler) siteSessionGenerate(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
namespace := p.ByName("namespace")
|
||||
if !services.IsValidNamespace(namespace) {
|
||||
return nil, trace.BadParameter("invalid namespace %q", namespace)
|
||||
|
@ -1264,7 +1278,7 @@ type siteSessionUpdateReq struct {
|
|||
//
|
||||
// {"message": "ok"}
|
||||
//
|
||||
func (m *Handler) siteSessionUpdate(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
func (h *Handler) siteSessionUpdate(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
sessionID, err := session.ParseID(p.ByName("sid"))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
@ -1306,7 +1320,7 @@ type siteSessionsGetResponse struct {
|
|||
// Response body:
|
||||
//
|
||||
// {"sessions": [{"id": "sid", "terminal_params": {"w": 100, "h": 100}, "parties": [], "login": "bob"}, ...] }
|
||||
func (m *Handler) siteSessionsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
func (h *Handler) siteSessionsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
clt, err := ctx.GetUserClient(site)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
@ -1332,7 +1346,7 @@ func (m *Handler) siteSessionsGet(w http.ResponseWriter, r *http.Request, p http
|
|||
//
|
||||
// {"session": {"id": "sid", "terminal_params": {"w": 100, "h": 100}, "parties": [], "login": "bob"}}
|
||||
//
|
||||
func (m *Handler) siteSessionGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
func (h *Handler) siteSessionGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
sessionID, err := session.ParseID(p.ByName("sid"))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
@ -1369,7 +1383,7 @@ const maxStreamBytes = 5 * 1024 * 1024
|
|||
// the default backend performs exact search: ?key=value means "event
|
||||
// with a field 'key' with value 'value'
|
||||
//
|
||||
func (m *Handler) siteEventsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
func (h *Handler) siteEventsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
query := r.URL.Query()
|
||||
log.Infof("web.getEvents(%v)", r.URL.RawQuery)
|
||||
|
||||
|
@ -1422,14 +1436,14 @@ type siteSessionStreamGetResponse struct {
|
|||
// It returns the binary stream unencoded, directly in the respose body,
|
||||
// with Content-Type of application/octet-stream, gzipped with up to 95%
|
||||
// compression ratio.
|
||||
func (m *Handler) siteSessionStreamGet(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
func (h *Handler) siteSessionStreamGet(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
var site reversetunnel.RemoteSite
|
||||
onError := func(err error) {
|
||||
w.Header().Set("Content-Type", "text/json")
|
||||
trace.WriteError(w, err)
|
||||
}
|
||||
// authenticate first:
|
||||
ctx, err := m.AuthenticateRequest(w, r, true)
|
||||
ctx, err := h.AuthenticateRequest(w, r, true)
|
||||
if err != nil {
|
||||
log.Info(err)
|
||||
// clear session just in case if the authentication request is not valid
|
||||
|
@ -1440,14 +1454,14 @@ func (m *Handler) siteSessionStreamGet(w http.ResponseWriter, r *http.Request, p
|
|||
// get the site interface:
|
||||
siteName := p.ByName("site")
|
||||
if siteName == currentSiteShortcut {
|
||||
sites := m.cfg.Proxy.GetSites()
|
||||
sites := h.cfg.Proxy.GetSites()
|
||||
if len(sites) < 1 {
|
||||
onError(trace.NotFound("no active sites"))
|
||||
return
|
||||
}
|
||||
siteName = sites[0].GetName()
|
||||
}
|
||||
site, err = m.cfg.Proxy.GetSite(siteName)
|
||||
site, err = h.cfg.Proxy.GetSite(siteName)
|
||||
if err != nil {
|
||||
onError(err)
|
||||
return
|
||||
|
@ -1525,7 +1539,7 @@ type eventsListGetResponse struct {
|
|||
//
|
||||
// {"events": [{...}, {...}, ...}
|
||||
//
|
||||
func (m *Handler) siteSessionEventsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
func (h *Handler) siteSessionEventsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
sessionID, err := session.ParseID(p.ByName("sid"))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
@ -1717,7 +1731,19 @@ func (h *Handler) WithAuth(fn ContextHandler) httprouter.Handle {
|
|||
})
|
||||
}
|
||||
|
||||
// authenticateRequest authenticates request using combination of a session cookie
|
||||
// WithCSRFProtection ensures that request to unauthenticated API is checked against CSRF attacks
|
||||
func (h *Handler) WithCSRFProtection(fn httprouter.Handle) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
err := csrf.VerifyToken(w, r)
|
||||
if err != nil {
|
||||
trace.WriteError(w, trace.AccessDenied("failed to validate CSRF token", err))
|
||||
return
|
||||
}
|
||||
fn(w, r, p)
|
||||
}
|
||||
}
|
||||
|
||||
// AuthenticateRequest authenticates request using combination of a session cookie
|
||||
// and bearer token
|
||||
func (h *Handler) AuthenticateRequest(w http.ResponseWriter, r *http.Request, checkBearerToken bool) (*SessionContext, error) {
|
||||
const missingCookieMsg = "missing session cookie"
|
||||
|
@ -1778,10 +1804,6 @@ func ok() interface{} {
|
|||
return message("ok")
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
http.Server
|
||||
}
|
||||
|
||||
// CreateSignupLink generates and returns a URL which is given to a new
|
||||
// user to complete registration with Teleport via Web UI
|
||||
func CreateSignupLink(hostPort string, token string) string {
|
||||
|
|
|
@ -49,6 +49,8 @@ import (
|
|||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/events"
|
||||
"github.com/gravitational/teleport/lib/fixtures"
|
||||
"github.com/gravitational/teleport/lib/httplib"
|
||||
"github.com/gravitational/teleport/lib/httplib/csrf"
|
||||
"github.com/gravitational/teleport/lib/reversetunnel"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/services/local"
|
||||
|
@ -589,12 +591,14 @@ func (s *WebSuite) authPack(c *C) *authPack {
|
|||
c.Assert(err, IsNil)
|
||||
|
||||
clt := s.client()
|
||||
|
||||
re, err := clt.PostJSON(clt.Endpoint("webapi", "sessions"), createSessionReq{
|
||||
req := createSessionReq{
|
||||
User: user,
|
||||
Pass: pass,
|
||||
SecondFactorToken: validToken,
|
||||
})
|
||||
}
|
||||
|
||||
csrfToken := "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992"
|
||||
re, err := s.login(clt, csrfToken, csrfToken, req)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
var rawSess *createSessionResponseRaw
|
||||
|
@ -648,6 +652,30 @@ func (s *WebSuite) TestNamespace(c *C) {
|
|||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *WebSuite) TestCRSF(c *C) {
|
||||
type input struct {
|
||||
reqToken string
|
||||
cookieToken string
|
||||
}
|
||||
|
||||
encodedToken1 := "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992"
|
||||
encodedToken2 := "bf355921bbf3ef3672a03e410d4194077dfa5fe863c652521763b3e7f81e7b11"
|
||||
invalid := []input{
|
||||
{reqToken: encodedToken2, cookieToken: encodedToken1},
|
||||
{reqToken: "", cookieToken: encodedToken1},
|
||||
{reqToken: "", cookieToken: ""},
|
||||
{reqToken: encodedToken1, cookieToken: ""},
|
||||
}
|
||||
|
||||
clt := s.client()
|
||||
// invalid
|
||||
for i := range invalid {
|
||||
_, err := s.login(clt, invalid[i].cookieToken, invalid[i].reqToken, nil)
|
||||
c.Assert(err, NotNil)
|
||||
c.Assert(trace.IsAccessDenied(err), Equals, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WebSuite) TestWebSessionsRenew(c *C) {
|
||||
pack := s.authPack(c)
|
||||
|
||||
|
@ -1344,3 +1372,23 @@ func (s *WebSuite) makeTerminalHandler(login string, server string, v2Servers []
|
|||
}), nil)
|
||||
|
||||
}
|
||||
|
||||
func (s *WebSuite) login(clt *client.WebClient, cookieToken string, reqToken string, reqData interface{}) (*roundtrip.Response, error) {
|
||||
return httplib.ConvertResponse(clt.RoundTrip(func() (*http.Response, error) {
|
||||
data, err := json.Marshal(reqData)
|
||||
req, err := http.NewRequest("POST", clt.Endpoint("webapi", "sessions"), bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cookie := &http.Cookie{
|
||||
Name: csrf.CookieName,
|
||||
Value: cookieToken,
|
||||
}
|
||||
|
||||
req.AddCookie(cookie)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set(csrf.HeaderName, reqToken)
|
||||
return clt.HTTPClient().Do(req)
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -60,8 +60,6 @@ func (s *StaticSuite) TestZipFS(c *check.C) {
|
|||
fs, err := readZipArchive("../../fixtures/assets.zip")
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(fs, check.NotNil)
|
||||
|
||||
checkFS(fs, c)
|
||||
}
|
||||
|
||||
func checkFS(fs http.FileSystem, c *check.C) {
|
||||
|
@ -70,7 +68,7 @@ func checkFS(fs http.FileSystem, c *check.C) {
|
|||
c.Assert(err, check.IsNil)
|
||||
bytes, err := ioutil.ReadAll(f)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(bytes), check.Equals, 813)
|
||||
c.Assert(len(bytes), check.Equals, 880)
|
||||
c.Assert(f.Close(), check.IsNil)
|
||||
|
||||
// seek + read
|
||||
|
@ -84,7 +82,7 @@ func checkFS(fs http.FileSystem, c *check.C) {
|
|||
|
||||
bytes, err = ioutil.ReadAll(f)
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(len(bytes), check.Equals, 803)
|
||||
c.Assert(len(bytes), check.Equals, 870)
|
||||
|
||||
n, err = f.Seek(-50, io.SeekEnd)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
## web client
|
||||
|
||||
Build (to create new /dist files)
|
||||
To build and generate a new /dist
|
||||
|
||||
1. `install nodejs >= 5.0.0`
|
||||
2. `npm install`
|
||||
3. `npm run build`
|
||||
1. `install nodejs >= 8.0.0`
|
||||
2. `$ npm install`
|
||||
3. `$ npm run build`
|
||||
|
||||
To run a dev server (development)
|
||||
To run a dev server
|
||||
|
||||
1. `npm run start`
|
||||
1. `$ npm run start -- --proxy=https://host:port`
|
||||
2. `open https://localhost:8081/web`
|
||||
|
||||
|
|
|
@ -14,58 +14,118 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
var fs = require('fs');
|
||||
var uri = require('url');
|
||||
var WebpackDevServer = require("webpack-dev-server");
|
||||
var webpackConfig = require('./webpack/webpack.config.dev.js');
|
||||
var express = require('express');
|
||||
var webpack = require('webpack');
|
||||
var proxy = require('http-proxy').createProxyServer();
|
||||
var changeProxyResponse = require('./devServerUtils');
|
||||
|
||||
var PROXY_TARGET = '0.0.0.0:5080/';
|
||||
// parse target URL
|
||||
var argv = require('optimist')
|
||||
.usage('Usage: $0 -proxy [url]')
|
||||
.demand(['proxy'])
|
||||
.argv;
|
||||
|
||||
var urlObj = uri.parse(argv.proxy)
|
||||
|
||||
if (!urlObj.host) {
|
||||
console.error('invalid URL: ' + argv.proxy);
|
||||
return;
|
||||
}
|
||||
|
||||
var PROXY_TARGET = urlObj.host;
|
||||
var ROOT = '/web';
|
||||
var PORT = '8081';
|
||||
var WEBPACK_CLIENT_ENTRY = 'webpack-dev-server/client?https://localhost:' + PORT;
|
||||
var WEBPACK_CLIENT_ENTRY = 'webpack-dev-server/client?https://0.0.0.0:' + PORT;
|
||||
var WEBPACK_SRV_ENTRY = 'webpack/hot/dev-server';
|
||||
|
||||
webpackConfig.entry.app.unshift(WEBPACK_CLIENT_ENTRY, WEBPACK_SRV_ENTRY);
|
||||
webpackConfig.entry.styles.unshift(WEBPACK_CLIENT_ENTRY, WEBPACK_SRV_ENTRY);
|
||||
|
||||
var compiler = webpack(webpackConfig);
|
||||
|
||||
function getTargetOptions() {
|
||||
return {
|
||||
target: "https://"+PROXY_TARGET,
|
||||
secure: false,
|
||||
changeOrigin: true
|
||||
changeOrigin: true,
|
||||
xfwd: true
|
||||
}
|
||||
}
|
||||
|
||||
var compiler = webpack(webpackConfig);
|
||||
|
||||
var server = new WebpackDevServer(compiler, {
|
||||
proxy: {
|
||||
'/web/config.js': getTargetOptions(),
|
||||
'/v1/webapi/*': getTargetOptions()
|
||||
proxy:{
|
||||
'/web/grafana/*': getTargetOptions(),
|
||||
'/web/config.*': getTargetOptions(),
|
||||
'/pack/v1/*': getTargetOptions(),
|
||||
'/portalapi/*': getTargetOptions(),
|
||||
'/portal*': getTargetOptions(),
|
||||
'/proxy/*': getTargetOptions(),
|
||||
'/v1/*': getTargetOptions(),
|
||||
'/app/*': getTargetOptions(),
|
||||
'/sites/v1/*': getTargetOptions()
|
||||
},
|
||||
publicPath: ROOT +'/app',
|
||||
hot: true,
|
||||
https: true,
|
||||
disableHostCheck: true,
|
||||
https: true,
|
||||
inline: true,
|
||||
headers: { 'X-Custom-Header': 'yes' },
|
||||
//stats: { colors: true },
|
||||
stats: 'errors-only'
|
||||
});
|
||||
|
||||
// tell webpack dev server to proxy below sockets requests to actual server
|
||||
server.listeningApp.on('upgrade', function(req, socket) {
|
||||
server.listeningApp.on('upgrade', function(req, socket) {
|
||||
console.log('proxying ws', req.url);
|
||||
proxy.ws(req, socket, {
|
||||
target: 'wss://' + PROXY_TARGET,
|
||||
secure: false
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var htmlToSend = fs.readFileSync(__dirname + "//dist//index.html", 'utf8')
|
||||
|
||||
// to enable Hot Module Reload we need to serve local index.html.
|
||||
// since local index.html has no embeded TOKEN, we need to:
|
||||
// 1) make a proxy request
|
||||
// 2) modify proxy response by replacing server index.html with the local
|
||||
// 3) insert embeded by server token into the local
|
||||
server.app.use(changeProxyResponse(
|
||||
(req, res) => {
|
||||
// return true if you want to modify the response later
|
||||
var contentType = res.getHeader('Content-Type');
|
||||
if (contentType && contentType.startsWith('text/html')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
(req, res, body) => {
|
||||
// body is a Buffer with the current response; return Buffer or string with the modified response
|
||||
// can also return a Promise.
|
||||
var str = body.toString();
|
||||
htmlToSend = replaceToken(new RegExp(/<meta name="grv_csrf_token" .*\>/), str, htmlToSend);
|
||||
htmlToSend = replaceToken(new RegExp(/<meta name="grv_bearer_token" .*\>/), str, htmlToSend);
|
||||
return htmlToSend;
|
||||
}
|
||||
));
|
||||
|
||||
function replaceToken(regex, takeFrom, insertTo){
|
||||
var value = takeFrom.match(regex);
|
||||
if(value){
|
||||
return insertTo.replace(regex, value[0]);
|
||||
}
|
||||
return insertTo;
|
||||
}
|
||||
|
||||
server.app.use(ROOT, express.static(__dirname + "//dist"));
|
||||
server.app.get(ROOT +'/*', function (req, res) {
|
||||
res.sendfile(__dirname + "//dist//index.html");
|
||||
proxy.web(req, res, getTargetOptions());
|
||||
});
|
||||
|
||||
server.listen(PORT, "0.0.0.0", function() {
|
||||
console.log('Dev Server is up and running: https://location:' + PORT +'/web');
|
||||
console.log('Dev Server is up and running: https://location:' + PORT + '/web/');
|
||||
});
|
||||
|
|
114
web/devServerUtils.js
Normal file
114
web/devServerUtils.js
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
Copyright 2015 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
//
|
||||
// taken and modified from https://github.com/mo22/express-modify-response
|
||||
//
|
||||
module.exports = function expressModifyResponse(checkCallback, modifyCallback) {
|
||||
return function expressModifyResponse(req, res, next) {
|
||||
var _end = res.end;
|
||||
var _write = res.write;
|
||||
var checked = false;
|
||||
var buffers = [];
|
||||
var addBuffer = (chunk, encoding) => {
|
||||
if (chunk === undefined)
|
||||
return;
|
||||
if (typeof chunk === 'string') {
|
||||
chunk = new Buffer(chunk, encoding);
|
||||
}
|
||||
buffers.push(chunk);
|
||||
};
|
||||
|
||||
var _writeHead = res.writeHead;
|
||||
|
||||
res.writeHead = function () {
|
||||
// writeHead supports (statusCode, headers) as well as (statusCode,
|
||||
// statusMessage, headers)
|
||||
var headers = (arguments.length > 2)
|
||||
? arguments[2]
|
||||
: arguments[1];
|
||||
var contentType = this.getHeader('content-type');
|
||||
|
||||
if ((typeof contentType != 'undefined') && (contentType.indexOf('text/html') == 0)) {
|
||||
res.isHtml = true;
|
||||
|
||||
// Strip off the content length since it will change.
|
||||
res.removeHeader('Content-Length');
|
||||
|
||||
if (headers) {
|
||||
delete headers['content-length'];
|
||||
}
|
||||
}
|
||||
|
||||
_writeHead.apply(res, arguments);
|
||||
};
|
||||
|
||||
res.write = function write(chunk, encoding) {
|
||||
if (!checked) {
|
||||
checked = true;
|
||||
var hook = checkCallback(req, res);
|
||||
if (!hook) {
|
||||
res.end = _end;
|
||||
res.write = _write;
|
||||
return res.write(chunk, encoding);
|
||||
} else {
|
||||
addBuffer(chunk, encoding);
|
||||
}
|
||||
} else {
|
||||
addBuffer(chunk, encoding);
|
||||
}
|
||||
};
|
||||
|
||||
res.end = function end(chunk, encoding) {
|
||||
if (!checked) {
|
||||
checked = true;
|
||||
var hook = checkCallback(req, res);
|
||||
if (!hook) {
|
||||
res.end = _end;
|
||||
res.write = _write;
|
||||
return res.end(chunk, encoding);
|
||||
} else {
|
||||
addBuffer(chunk, encoding);
|
||||
}
|
||||
} else {
|
||||
addBuffer(chunk, encoding);
|
||||
}
|
||||
var buffer = Buffer.concat(buffers);
|
||||
|
||||
Promise
|
||||
.resolve(modifyCallback(req, res, buffer))
|
||||
.then((result) => {
|
||||
if (typeof result === 'string') {
|
||||
result = new Buffer(result, 'utf-8');
|
||||
}
|
||||
|
||||
if (res.getHeader('Content-Length')) {
|
||||
res.setHeader('Content-Length', String(result.length));
|
||||
}
|
||||
res.end = _end;
|
||||
res.write = _write;
|
||||
res.write(result);
|
||||
res.end();
|
||||
})
|
||||
.catch((e) => {
|
||||
// handle?
|
||||
next(e);
|
||||
});
|
||||
};
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
|
@ -2011,6 +2011,7 @@ webpackJsonp([0],[
|
|||
contentType: 'application/json; charset=utf-8',
|
||||
dataType: 'json',
|
||||
beforeSend: function beforeSend(xhr) {
|
||||
xhr.setRequestHeader('X-CSRF-Token', getXCSRFToken());
|
||||
if (withToken) {
|
||||
var bearerToken = _localStorage2.default.getBearerToken() || {};
|
||||
var accessToken = bearerToken.accessToken;
|
||||
|
@ -2045,6 +2046,11 @@ webpackJsonp([0],[
|
|||
}
|
||||
};
|
||||
|
||||
var getXCSRFToken = function getXCSRFToken() {
|
||||
var metaTag = document.querySelector('[name=grv_csrf_token]');
|
||||
return metaTag ? metaTag.content : '';
|
||||
};
|
||||
|
||||
exports.default = api;
|
||||
module.exports = exports['default'];
|
||||
|
||||
|
@ -2976,10 +2982,10 @@ webpackJsonp([0],[
|
|||
return token;
|
||||
},
|
||||
_extractBearerTokenFromHtml: function _extractBearerTokenFromHtml() {
|
||||
var el = document.querySelector("#bearer_token");
|
||||
var el = document.querySelector('[name=grv_bearer_token]');
|
||||
var token = null;
|
||||
if (el !== null) {
|
||||
var encodedToken = el.textContent || '';
|
||||
var encodedToken = el.content || '';
|
||||
if (encodedToken.length > EMPTY_TOKEN_CONTENT_LENGTH) {
|
||||
var decoded = window.atob(encodedToken);
|
||||
var json = JSON.parse(decoded);
|
|
@ -1,4 +1,4 @@
|
|||
!function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={exports:{},id:n,loaded:!1};return t[n].call(i.exports,i,i.exports,e),i.loaded=!0,i.exports}var n=window.webpackJsonp;window.webpackJsonp=function(o,a){for(var s,u,l=0,c=[];l<o.length;l++)u=o[l],i[u]&&c.push.apply(c,i[u]),i[u]=0;for(s in a)t[s]=a[s];for(n&&n(o,a);c.length;)c.shift().call(null,e);if(a[0])return r[0]=0,e(0)};var r={},i={2:0};return e.e=function(t,n){if(0===i[t])return n.call(null,e);if(void 0!==i[t])i[t].push(n);else{i[t]=[n];var r=document.getElementsByTagName("head")[0],o=document.createElement("script");o.type="text/javascript",o.charset="utf-8",o.async=!0,o.src=e.p+""+{0:"fc0baa6ecc1984e3945a",1:"19f124bc7fa95f2638fd"}[t]+".js",r.appendChild(o)}},e.m=t,e.c=r,e.p="/web/app",e(0)}([function(t,e,n){t.exports=n(560)},,function(t,e,n){"use strict";t.exports=n(3)},function(t,e,n){"use strict";var r=n(4),i=n(5),o=n(17),a=n(20),s=n(21),u=n(26),l=n(9),c=n(27),d=n(29),h=n(30),f=(n(11),l.createElement),p=l.createFactory,m=l.cloneElement,_=r,v={Children:{map:i.map,forEach:i.forEach,count:i.count,toArray:i.toArray,only:h},Component:o,PureComponent:a,createElement:f,cloneElement:m,isValidElement:l.isValidElement,PropTypes:c,createClass:s.createClass,createFactory:p,createMixin:function(t){return t},DOM:u,version:d,__spread:_};t.exports=v},function(t,e){/*
|
||||
!function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={exports:{},id:n,loaded:!1};return t[n].call(i.exports,i,i.exports,e),i.loaded=!0,i.exports}var n=window.webpackJsonp;window.webpackJsonp=function(o,a){for(var s,u,l=0,c=[];l<o.length;l++)u=o[l],i[u]&&c.push.apply(c,i[u]),i[u]=0;for(s in a)t[s]=a[s];for(n&&n(o,a);c.length;)c.shift().call(null,e);if(a[0])return r[0]=0,e(0)};var r={},i={2:0};return e.e=function(t,n){if(0===i[t])return n.call(null,e);if(void 0!==i[t])i[t].push(n);else{i[t]=[n];var r=document.getElementsByTagName("head")[0],o=document.createElement("script");o.type="text/javascript",o.charset="utf-8",o.async=!0,o.src=e.p+""+{0:"660778fd716afb85cfa8",1:"19f124bc7fa95f2638fd"}[t]+".js",r.appendChild(o)}},e.m=t,e.c=r,e.p="/web/app",e(0)}([function(t,e,n){t.exports=n(560)},,function(t,e,n){"use strict";t.exports=n(3)},function(t,e,n){"use strict";var r=n(4),i=n(5),o=n(17),a=n(20),s=n(21),u=n(26),l=n(9),c=n(27),d=n(29),h=n(30),f=(n(11),l.createElement),p=l.createFactory,m=l.cloneElement,_=r,v={Children:{map:i.map,forEach:i.forEach,count:i.count,toArray:i.toArray,only:h},Component:o,PureComponent:a,createElement:f,cloneElement:m,isValidElement:l.isValidElement,PropTypes:c,createClass:s.createClass,createFactory:p,createMixin:function(t){return t},DOM:u,version:d,__spread:_};t.exports=v},function(t,e){/*
|
||||
object-assign
|
||||
(c) Sindre Sorhus
|
||||
@license MIT
|
7
web/dist/index.html
vendored
7
web/dist/index.html
vendored
|
@ -4,11 +4,12 @@
|
|||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="grv_csrf_token" content="{{ .XCSRF }}" />
|
||||
<meta name="grv_bearer_token" content="{{ .Session }}" />
|
||||
<title>Teleport by Gravitational</title>
|
||||
<script src="/web/config.js"></script>
|
||||
<link rel="shortcut icon" href="/web/app/favicon.ico"><link href="/web/app/vendor.7c7e8bde8015ae63511442e683de6a37.css" rel="stylesheet"></head>
|
||||
<body class="grv">
|
||||
<div id="app"></div>
|
||||
<div id="bearer_token" style="display: none;">{{.Session}}</div>
|
||||
<script type="text/javascript" src="/web/app/vendor.b83772a8a4dadb99e5c4.js"></script><script type="text/javascript" src="/web/app/styles.b83772a8a4dadb99e5c4.js"></script><script type="text/javascript" src="/web/app/app.b83772a8a4dadb99e5c4.js"></script></body>
|
||||
<div id="app"></div>
|
||||
<script type="text/javascript" src="/web/app/vendor.d0f1230171ff8c0e4540.js"></script><script type="text/javascript" src="/web/app/styles.d0f1230171ff8c0e4540.js"></script><script type="text/javascript" src="/web/app/app.d0f1230171ff8c0e4540.js"></script></body>
|
||||
</html>
|
||||
|
|
|
@ -41,13 +41,14 @@ const api = {
|
|||
type: 'GET',
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
dataType: 'json',
|
||||
beforeSend: function(xhr) {
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader('X-CSRF-Token', getXCSRFToken());
|
||||
if (withToken) {
|
||||
const bearerToken = localStorage.getBearerToken() || {};
|
||||
const { accessToken } = bearerToken;
|
||||
xhr.setRequestHeader('Authorization','Bearer ' + accessToken);
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $.ajax($.extend({}, defaultCfg, cfg));
|
||||
|
@ -76,4 +77,9 @@ const api = {
|
|||
}
|
||||
}
|
||||
|
||||
const getXCSRFToken = () => {
|
||||
const metaTag = document.querySelector('[name=grv_csrf_token]');
|
||||
return metaTag ? metaTag.content : ''
|
||||
}
|
||||
|
||||
export default api;
|
||||
|
|
|
@ -87,10 +87,10 @@ const session = {
|
|||
},
|
||||
|
||||
_extractBearerTokenFromHtml() {
|
||||
const el = document.querySelector("#bearer_token")
|
||||
const el = document.querySelector('[name=grv_bearer_token]');
|
||||
let token = null;
|
||||
if (el !== null) {
|
||||
let encodedToken = el.textContent || '';
|
||||
let encodedToken = el.content || '';
|
||||
if (encodedToken.length > EMPTY_TOKEN_CONTENT_LENGTH) {
|
||||
let decoded = window.atob(encodedToken);
|
||||
let json = JSON.parse(decoded);
|
||||
|
|
|
@ -4,11 +4,12 @@
|
|||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="grv_csrf_token" content="{{ .XCSRF }}" />
|
||||
<meta name="grv_bearer_token" content="{{ .Session }}" />
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<script src="/web/config.js"></script>
|
||||
</head>
|
||||
<body class="grv">
|
||||
<div id="app"></div>
|
||||
<div id="bearer_token" style="display: none;">{{.Session}}</div>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in a new issue