lnrpc: add macaroon workaround for WebSockets in browsers
For security reasons, browsers are limited in the header fields they can send when opening a WebSocket connection. Specifically, the macaroon cannot be sent in the Grpc-Metadata-Macaroon header field as that would be possible for normal REST requests. Instead we only have the special field "Sec-Websocket-Protocol" that can be used to transport custom data. We allow the macaroon to be sent there and transform it into a proper header field for the target request.
This commit is contained in:
1 file changed
+56
-7
@@ -21,6 +21,16 @@ const (
|
|||||||
// This is necessary because the WebSocket API specifies that a
|
// This is necessary because the WebSocket API specifies that a
|
||||||
// handshake request must always be done through a GET request.
|
// handshake request must always be done through a GET request.
|
||||||
MethodOverrideParam = "method"
|
MethodOverrideParam = "method"
|
||||||
|
|
||||||
|
// HeaderWebSocketProtocol is the name of the WebSocket protocol
|
||||||
|
// exchange header field that we use to transport additional header
|
||||||
|
// fields.
|
||||||
|
HeaderWebSocketProtocol = "Sec-Websocket-Protocol"
|
||||||
|
|
||||||
|
// WebSocketProtocolDelimiter is the delimiter we use between the
|
||||||
|
// additional header field and its value. We use the plus symbol because
|
||||||
|
// the default delimiters aren't allowed in the protocol names.
|
||||||
|
WebSocketProtocolDelimiter = "+"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -32,6 +42,13 @@ var (
|
|||||||
"Referer": true,
|
"Referer": true,
|
||||||
"Grpc-Metadata-Macaroon": true,
|
"Grpc-Metadata-Macaroon": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// defaultProtocolsToAllow are additional header fields that we allow
|
||||||
|
// to be transported inside of the Sec-Websocket-Protocol field to be
|
||||||
|
// forwarded to the backend.
|
||||||
|
defaultProtocolsToAllow = map[string]bool{
|
||||||
|
"Grpc-Metadata-Macaroon": true,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewWebSocketProxy attempts to expose the underlying handler as a response-
|
// NewWebSocketProxy attempts to expose the underlying handler as a response-
|
||||||
@@ -101,13 +118,13 @@ func (p *WebsocketProxy) upgradeToWebSocketProxy(w http.ResponseWriter,
|
|||||||
p.logger.Errorf("WS: error preparing request:", err)
|
p.logger.Errorf("WS: error preparing request:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for header := range r.Header {
|
|
||||||
headerName := textproto.CanonicalMIMEHeaderKey(header)
|
// Allow certain headers to be forwarded, either from source headers
|
||||||
forward, ok := defaultHeadersToForward[headerName]
|
// or the special Sec-Websocket-Protocol header field.
|
||||||
if ok && forward {
|
forwardHeaders(r.Header, request.Header)
|
||||||
request.Header.Set(headerName, r.Header.Get(header))
|
|
||||||
}
|
// Also allow the target request method to be overwritten, as all
|
||||||
}
|
// WebSocket establishment calls MUST be GET requests.
|
||||||
if m := r.URL.Query().Get(MethodOverrideParam); m != "" {
|
if m := r.URL.Query().Get(MethodOverrideParam); m != "" {
|
||||||
request.Method = m
|
request.Method = m
|
||||||
}
|
}
|
||||||
@@ -182,6 +199,38 @@ func (p *WebsocketProxy) upgradeToWebSocketProxy(w http.ResponseWriter,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// forwardHeaders forwards certain allowed header fields from the source request
|
||||||
|
// to the target request. Because browsers are limited in what header fields
|
||||||
|
// they can send on the WebSocket setup call, we also allow additional fields to
|
||||||
|
// be transported in the special Sec-Websocket-Protocol field.
|
||||||
|
func forwardHeaders(source, target http.Header) {
|
||||||
|
// Forward allowed header fields directly.
|
||||||
|
for header := range source {
|
||||||
|
headerName := textproto.CanonicalMIMEHeaderKey(header)
|
||||||
|
forward, ok := defaultHeadersToForward[headerName]
|
||||||
|
if ok && forward {
|
||||||
|
target.Set(headerName, source.Get(header))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browser aren't allowed to set custom header fields on WebSocket
|
||||||
|
// requests. We need to allow them to submit the macaroon as a WS
|
||||||
|
// protocol, which is the only allowed header. Set any "protocols" we
|
||||||
|
// declare valid as header fields on the forwarded request.
|
||||||
|
protocol := source.Get(HeaderWebSocketProtocol)
|
||||||
|
for key := range defaultProtocolsToAllow {
|
||||||
|
if strings.HasPrefix(protocol, key) {
|
||||||
|
// The format is "<protocol name>+<value>". We know the
|
||||||
|
// protocol string starts with the name so we only need
|
||||||
|
// to set the value.
|
||||||
|
values := strings.Split(
|
||||||
|
protocol, WebSocketProtocolDelimiter,
|
||||||
|
)
|
||||||
|
target.Set(key, values[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// newRequestForwardingReader creates a new request forwarding pipe.
|
// newRequestForwardingReader creates a new request forwarding pipe.
|
||||||
func newRequestForwardingReader() *requestForwardingReader {
|
func newRequestForwardingReader() *requestForwardingReader {
|
||||||
r, w := io.Pipe()
|
r, w := io.Pipe()
|
||||||
|
|||||||
Reference in new issue
Block a user