mirror of
https://github.com/caddyserver/caddy.git
synced 2025-02-24 23:57:05 -05:00
Improvements to websocket middleware
This commit is contained in:
parent
d7ae9fb4a2
commit
abdadf1ee1
2 changed files with 87 additions and 40 deletions
|
@ -10,7 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// WebSocket represents a web socket server instance. A WebSocket
|
// WebSocket represents a web socket server instance. A WebSocket
|
||||||
// struct is instantiated for each new websocket request.
|
// is instantiated for each new websocket request/connection.
|
||||||
type WebSocket struct {
|
type WebSocket struct {
|
||||||
WSConfig
|
WSConfig
|
||||||
*http.Request
|
*http.Request
|
||||||
|
@ -21,33 +21,40 @@ type WebSocket struct {
|
||||||
// the command's stdin and stdout.
|
// the command's stdin and stdout.
|
||||||
func (ws WebSocket) Handle(conn *websocket.Conn) {
|
func (ws WebSocket) Handle(conn *websocket.Conn) {
|
||||||
cmd := exec.Command(ws.Command, ws.Arguments...)
|
cmd := exec.Command(ws.Command, ws.Arguments...)
|
||||||
|
|
||||||
cmd.Stdin = conn
|
cmd.Stdin = conn
|
||||||
cmd.Stdout = conn
|
cmd.Stdout = conn
|
||||||
|
cmd.Stderr = conn // TODO: Make this configurable from the Caddyfile
|
||||||
|
|
||||||
err := ws.buildEnv(cmd)
|
metavars, err := ws.buildEnv(cmd.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO
|
panic(err) // TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Env = metavars
|
||||||
|
|
||||||
err = cmd.Run()
|
err = cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildEnv sets the meta-variables for the child process according
|
// buildEnv creates the meta-variables for the child process according
|
||||||
// to the CGI 1.1 specification: http://tools.ietf.org/html/rfc3875#section-4.1
|
// to the CGI 1.1 specification: http://tools.ietf.org/html/rfc3875#section-4.1
|
||||||
func (ws WebSocket) buildEnv(cmd *exec.Cmd) error {
|
// cmdPath should be the path of the command being run.
|
||||||
|
// The returned string slice can be set to the command's Env property.
|
||||||
|
func (ws WebSocket) buildEnv(cmdPath string) (metavars []string, err error) {
|
||||||
remoteHost, remotePort, err := net.SplitHostPort(ws.RemoteAddr)
|
remoteHost, remotePort, err := net.SplitHostPort(ws.RemoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
|
||||||
serverHost, serverPort, err := net.SplitHostPort(ws.Host)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Env = []string{
|
serverHost, serverPort, err := net.SplitHostPort(ws.Host)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
metavars = []string{
|
||||||
`AUTH_TYPE=`, // Not used
|
`AUTH_TYPE=`, // Not used
|
||||||
`CONTENT_LENGTH=`, // Not used
|
`CONTENT_LENGTH=`, // Not used
|
||||||
`CONTENT_TYPE=`, // Not used
|
`CONTENT_TYPE=`, // Not used
|
||||||
|
@ -62,7 +69,7 @@ func (ws WebSocket) buildEnv(cmd *exec.Cmd) error {
|
||||||
`REMOTE_USER=`, // Not used,
|
`REMOTE_USER=`, // Not used,
|
||||||
`REQUEST_METHOD=` + ws.Method,
|
`REQUEST_METHOD=` + ws.Method,
|
||||||
`REQUEST_URI=` + ws.RequestURI,
|
`REQUEST_URI=` + ws.RequestURI,
|
||||||
`SCRIPT_NAME=`, // TODO - absolute path to program being executed?
|
`SCRIPT_NAME=` + cmdPath, // path of the program being executed
|
||||||
`SERVER_NAME=` + serverHost,
|
`SERVER_NAME=` + serverHost,
|
||||||
`SERVER_PORT=` + serverPort,
|
`SERVER_PORT=` + serverPort,
|
||||||
`SERVER_PROTOCOL=` + ws.Proto,
|
`SERVER_PROTOCOL=` + ws.Proto,
|
||||||
|
@ -75,8 +82,8 @@ func (ws WebSocket) buildEnv(cmd *exec.Cmd) error {
|
||||||
header = strings.ToUpper(header)
|
header = strings.ToUpper(header)
|
||||||
header = strings.Replace(header, "-", "_", -1)
|
header = strings.Replace(header, "-", "_", -1)
|
||||||
value = strings.Replace(value, "\n", " ", -1)
|
value = strings.Replace(value, "\n", " ", -1)
|
||||||
cmd.Env = append(cmd.Env, "HTTP_"+header+"="+value)
|
metavars = append(metavars, "HTTP_"+header+"="+value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,16 +17,20 @@ type (
|
||||||
// websocket middleware generally, like a list of all the
|
// websocket middleware generally, like a list of all the
|
||||||
// websocket endpoints.
|
// websocket endpoints.
|
||||||
WebSockets struct {
|
WebSockets struct {
|
||||||
|
// Next is the next HTTP handler in the chain for when the path doesn't match
|
||||||
|
Next http.HandlerFunc
|
||||||
|
|
||||||
// Sockets holds all the web socket endpoint configurations
|
// Sockets holds all the web socket endpoint configurations
|
||||||
Sockets []WSConfig
|
Sockets []WSConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// WSConfig holds the configuration for a single websocket
|
// WSConfig holds the configuration for a single websocket
|
||||||
// endpoint which may serve zero or more websocket connections.
|
// endpoint which may serve multiple websocket connections.
|
||||||
WSConfig struct {
|
WSConfig struct {
|
||||||
Path string
|
Path string
|
||||||
Command string
|
Command string
|
||||||
Arguments []string
|
Arguments []string
|
||||||
|
Respawn bool // TODO: Not used, but parser supports it until we decide on it
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,11 +46,27 @@ func (ws WebSockets) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Didn't match a websocket path, so pass-thru
|
||||||
|
ws.Next(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New constructs and configures a new websockets middleware instance.
|
// New constructs and configures a new websockets middleware instance.
|
||||||
func New(c middleware.Controller) (middleware.Middleware, error) {
|
func New(c middleware.Controller) (middleware.Middleware, error) {
|
||||||
var websocks []WSConfig
|
var websocks []WSConfig
|
||||||
|
var respawn bool
|
||||||
|
|
||||||
|
optionalBlock := func() (hadBlock bool, err error) {
|
||||||
|
for c.NextBlock() {
|
||||||
|
hadBlock = true
|
||||||
|
if c.Val() == "respawn" {
|
||||||
|
respawn = true
|
||||||
|
} else {
|
||||||
|
return true, c.Err("Expected websocket configuration parameter in block")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for c.Next() {
|
for c.Next() {
|
||||||
var val, path, command string
|
var val, path, command string
|
||||||
|
@ -57,38 +77,40 @@ func New(c middleware.Controller) (middleware.Middleware, error) {
|
||||||
}
|
}
|
||||||
val = c.Val()
|
val = c.Val()
|
||||||
|
|
||||||
// The rest of the arguments are the command
|
// Extra configuration may be in a block
|
||||||
if c.NextArg() {
|
hadBlock, err := optionalBlock()
|
||||||
path = val
|
if err != nil {
|
||||||
command = c.Val()
|
return nil, err
|
||||||
for c.NextArg() {
|
}
|
||||||
command += " " + c.Val()
|
|
||||||
|
if !hadBlock {
|
||||||
|
// The next argument on this line will be the command or an open curly brace
|
||||||
|
if c.NextArg() {
|
||||||
|
path = val
|
||||||
|
command = c.Val()
|
||||||
|
} else {
|
||||||
|
path = "/"
|
||||||
|
command = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Okay, check again for optional block
|
||||||
|
hadBlock, err = optionalBlock()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
path = "/"
|
|
||||||
command = val
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split command into the actual command and its arguments
|
// Split command into the actual command and its arguments
|
||||||
var cmd string
|
cmd, args, err := parseCommandAndArgs(command)
|
||||||
var args []string
|
|
||||||
|
|
||||||
parts, err := shlex.Split(command)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("Error parsing command for websocket use: " + err.Error())
|
return nil, err
|
||||||
} else if len(parts) == 0 {
|
|
||||||
return nil, errors.New("No command found for use by websocket")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd = parts[0]
|
|
||||||
if len(parts) > 1 {
|
|
||||||
args = parts[1:]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
websocks = append(websocks, WSConfig{
|
websocks = append(websocks, WSConfig{
|
||||||
Path: path,
|
Path: path,
|
||||||
Command: cmd,
|
Command: cmd,
|
||||||
Arguments: args,
|
Arguments: args,
|
||||||
|
Respawn: respawn,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,12 +118,30 @@ func New(c middleware.Controller) (middleware.Middleware, error) {
|
||||||
ServerSoftware = envServerSoftware
|
ServerSoftware = envServerSoftware
|
||||||
|
|
||||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
return func(next http.HandlerFunc) http.HandlerFunc {
|
||||||
// We don't use next because websockets aren't HTTP,
|
return WebSockets{Next: next, Sockets: websocks}.ServeHTTP
|
||||||
// so we don't invoke other middleware after this.
|
|
||||||
return WebSockets{Sockets: websocks}.ServeHTTP
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseCommandAndArgs takes a command string and parses it
|
||||||
|
// shell-style into the command and its separate arguments.
|
||||||
|
func parseCommandAndArgs(command string) (cmd string, args []string, err error) {
|
||||||
|
parts, err := shlex.Split(command)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("Error parsing command for websocket: " + err.Error())
|
||||||
|
return
|
||||||
|
} else if len(parts) == 0 {
|
||||||
|
err = errors.New("No command found for use by websocket")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = parts[0]
|
||||||
|
if len(parts) > 1 {
|
||||||
|
args = parts[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// See CGI spec, 4.1.4
|
// See CGI spec, 4.1.4
|
||||||
GatewayInterface string
|
GatewayInterface string
|
||||||
|
@ -112,5 +152,5 @@ var (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
envGatewayInterface = "caddy-CGI/1.1"
|
envGatewayInterface = "caddy-CGI/1.1"
|
||||||
envServerSoftware = "caddy/0.1.0"
|
envServerSoftware = "caddy/?.?.?" // TODO
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Reference in a new issue