Files
threshold/internal/servers/worker/handler.go
T
Jeff Mitchell a6cca576a3 Attempt canceling sessions by the client (#831)
* Attempt canceling sessions by the client

In some circumstances (like a Ctrl-C), the client knows it's shutting
down and the TOFU token will be lost. In this case it makes sense to
cancel the session rather than have it time out. This introduces a
command enum to the handshake message which can be used for the client
to send commands to the worker -- in this case the only specified
command is to cancel the session. It doesn't occur if the session is
already timed out or if we can't authorize connections as this generally
means the session is already invalid.
2020-12-16 17:14:47 -05:00

292 lines
9.2 KiB
Go

package worker
import (
"context"
"net"
"net/http"
"strconv"
"github.com/hashicorp/boundary/globals"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/internal/proxy"
"github.com/hashicorp/shared-secure-libs/configutil"
"nhooyr.io/websocket"
"nhooyr.io/websocket/wspb"
)
type HandlerProperties struct {
ListenerConfig *configutil.Listener
}
// Handler returns an http.Handler for the API. This can be used on
// its own to mount the Worker API within another web server.
func (w *Worker) handler(props HandlerProperties) http.Handler {
// Create the muxer to handle the actual endpoints
mux := http.NewServeMux()
mux.Handle("/v1/proxy", w.handleProxy())
genericWrappedHandler := w.wrapGenericHandler(mux, props)
return genericWrappedHandler
}
func (w *Worker) handleProxy() http.HandlerFunc {
return http.HandlerFunc(func(wr http.ResponseWriter, r *http.Request) {
if r.TLS == nil {
w.logger.Error("no request TLS information found")
wr.WriteHeader(http.StatusInternalServerError)
return
}
sessionId := r.TLS.ServerName
clientIp, clientPort, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
w.logger.Error("unable to understand remote address", "error", err, "remote_addr", r.RemoteAddr)
wr.WriteHeader(http.StatusInternalServerError)
return
}
numPort, err := strconv.Atoi(clientPort)
if err != nil {
w.logger.Error("unable to understand remote port", "error", err)
wr.WriteHeader(http.StatusInternalServerError)
return
}
clientAddr := &net.TCPAddr{
IP: net.ParseIP(clientIp),
Port: numPort,
}
w.logger.Trace("received TLS connection")
siRaw, valid := w.sessionInfoMap.Load(sessionId)
if !valid {
w.logger.Error("session not found in info map", "session_id", sessionId)
wr.WriteHeader(http.StatusInternalServerError)
return
}
si := siRaw.(*sessionInfo)
si.RLock()
expiration := si.lookupSessionResponse.GetExpiration()
tofuToken := si.lookupSessionResponse.GetTofuToken()
version := si.lookupSessionResponse.GetVersion()
endpoint := si.lookupSessionResponse.GetEndpoint()
//userId := si.lookupSessionResponse.GetAuthorization()
sessStatus := si.status
si.RUnlock()
w.logger.Trace("found session in session info map")
opts := &websocket.AcceptOptions{
Subprotocols: []string{globals.TcpProxyV1},
}
conn, err := websocket.Accept(wr, r, opts)
if err != nil {
w.logger.Error("error during websocket upgrade", "error", err)
wr.WriteHeader(http.StatusInternalServerError)
return
}
// Later calls will cause this to noop if they return a different status
defer conn.Close(websocket.StatusNormalClosure, "done")
w.logger.Trace("websocket upgrade done")
connCtx, connCancel := context.WithDeadline(r.Context(), expiration.AsTime())
defer connCancel()
var handshake proxy.ClientHandshake
if err := wspb.Read(connCtx, conn, &handshake); err != nil {
w.logger.Error("error reading handshake from client", "error", err)
conn.Close(websocket.StatusPolicyViolation, "invalid handshake received")
return
}
if len(handshake.GetTofuToken()) < 20 {
w.logger.Error("invalid tofu token")
conn.Close(websocket.StatusUnsupportedData, "invalid tofu token")
return
}
w.logger.Trace("proxy handshake finished")
if tofuToken != "" {
if tofuToken != handshake.GetTofuToken() {
w.logger.Error("WARNING: mismatched tofu token", "session_id", sessionId)
conn.Close(websocket.StatusPolicyViolation, "tofu token not allowed")
return
}
} else {
if sessStatus != pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING {
w.logger.Error("no tofu token but not in correct session state", "error", err)
conn.Close(websocket.StatusInternalError, "refusing to activate session")
return
}
if handshake.Command == proxy.HANDSHAKECOMMAND_HANDSHAKECOMMAND_UNSPECIFIED {
w.logger.Trace("activating session")
sessStatus, err = w.activateSession(r.Context(), sessionId, handshake.GetTofuToken(), version)
if err != nil {
w.logger.Error("unable to validate session", "error", err)
conn.Close(websocket.StatusInternalError, "unable to activate session")
return
}
}
}
if handshake.Command == proxy.HANDSHAKECOMMAND_HANDSHAKECOMMAND_SESSION_CANCEL {
w.logger.Trace("canceling session at client request")
_, err := w.cancelSession(r.Context(), sessionId)
if err != nil {
w.logger.Error("unable to cancel session", "error", err)
conn.Close(websocket.StatusInternalError, "unable to cancel session")
return
}
conn.Close(websocket.StatusNormalClosure, "session canceled")
return
}
var ci *connInfo
var connsLeft int32
ci, connsLeft, err = w.authorizeConnection(r.Context(), sessionId)
if err != nil {
w.logger.Error("unable to authorize connection", "error", err)
conn.Close(websocket.StatusInternalError, "unable to authorize connection")
return
}
defer func() {
connectionId := ci.id
if err := w.closeConnections(r.Context(), map[string]string{
connectionId: si.id,
}); err != nil {
w.logger.Error("error marking connection closed", "error", err, "connection_id", connectionId)
}
}()
si.Lock()
ci.connCtx = connCtx
ci.connCancel = connCancel
si.connInfoMap[ci.id] = ci
si.status = sessStatus
connectionLimit := si.lookupSessionResponse.GetConnectionLimit()
si.Unlock()
w.logger.Trace("authorized connection", "connection_id", ci.id)
handshakeResult := &proxy.HandshakeResult{
Expiration: expiration,
ConnectionLimit: connectionLimit,
ConnectionsLeft: connsLeft,
}
if err := wspb.Write(connCtx, conn, handshakeResult); err != nil {
w.logger.Error("error sending handshake result to client", "error", err)
conn.Close(websocket.StatusProtocolError, "unable to send handshake result")
return
}
switch conn.Subprotocol() {
case globals.TcpProxyV1:
w.handleTcpProxyV1(connCtx, clientAddr, conn, si, ci.id, endpoint)
default:
conn.Close(websocket.StatusProtocolError, "unsupported-protocol")
return
}
})
}
func (w *Worker) wrapGenericHandler(h http.Handler, props HandlerProperties) http.Handler {
return http.HandlerFunc(func(wr http.ResponseWriter, r *http.Request) {
// Set the Cache-Control header for all responses returned
wr.Header().Set("Cache-Control", "no-store")
h.ServeHTTP(wr, r)
})
}
/*
func WrapForwardedForHandler(h http.Handler, authorizedAddrs []*sockaddr.SockAddrMarshaler, rejectNotPresent, rejectNonAuthz bool, hopSkips int) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
headers, headersOK := r.Header[textproto.CanonicalMIMEHeaderKey("X-Forwarded-For")]
if !headersOK || len(headers) == 0 {
if !rejectNotPresent {
h.ServeHTTP(w, r)
return
}
respondError(w, http.StatusBadRequest, fmt.Errorf("missing x-forwarded-for header and configured to reject when not present"))
return
}
host, port, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
// If not rejecting treat it like we just don't have a valid
// header because we can't do a comparison against an address we
// can't understand
if !rejectNotPresent {
h.ServeHTTP(w, r)
return
}
respondError(w, http.StatusBadRequest, errwrap.Wrapf("error parsing client hostport: {{err}}", err))
return
}
addr, err := sockaddr.NewIPAddr(host)
if err != nil {
// We treat this the same as the case above
if !rejectNotPresent {
h.ServeHTTP(w, r)
return
}
respondError(w, http.StatusBadRequest, errwrap.Wrapf("error parsing client address: {{err}}", err))
return
}
var found bool
for _, authz := range authorizedAddrs {
if authz.Contains(addr) {
found = true
break
}
}
if !found {
// If we didn't find it and aren't configured to reject, simply
// don't trust it
if !rejectNonAuthz {
h.ServeHTTP(w, r)
return
}
respondError(w, http.StatusBadRequest, fmt.Errorf("client address not authorized for x-forwarded-for and configured to reject connection"))
return
}
// At this point we have at least one value and it's authorized
// Split comma separated ones, which are common. This brings it in line
// to the multiple-header case.
var acc []string
for _, header := range headers {
vals := strings.Split(header, ",")
for _, v := range vals {
acc = append(acc, strings.TrimSpace(v))
}
}
indexToUse := len(acc) - 1 - hopSkips
if indexToUse < 0 {
// This is likely an error in either configuration or other
// infrastructure. We could either deny the request, or we
// could simply not trust the value. Denying the request is
// "safer" since if this logic is configured at all there may
// be an assumption it can always be trusted. Given that we can
// deny accepting the request at all if it's not from an
// authorized address, if we're at this point the address is
// authorized (or we've turned off explicit rejection) and we
// should assume that what comes in should be properly
// formatted.
respondError(w, http.StatusBadRequest, fmt.Errorf("malformed x-forwarded-for configuration or request, hops to skip (%d) would skip before earliest chain link (chain length %d)", hopSkips, len(headers)))
return
}
r.RemoteAddr = net.JoinHostPort(acc[indexToUse], port)
h.ServeHTTP(w, r)
return
})
}
*/