Commit abdadf1e authored by Matthew Holt's avatar Matthew Holt

Improvements to websocket middleware

parent d7ae9fb4
...@@ -10,7 +10,7 @@ import ( ...@@ -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 { ...@@ -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) serverHost, serverPort, err := net.SplitHostPort(ws.Host)
if err != nil { if err != nil {
return err return
} }
cmd.Env = []string{ 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 { ...@@ -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 { ...@@ -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 ( ...@@ -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) { ...@@ -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) { ...@@ -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()
}
} else {
path = "/"
command = val
} }
// Split command into the actual command and its arguments if !hadBlock {
var cmd string // The next argument on this line will be the command or an open curly brace
var args []string if c.NextArg() {
path = val
command = c.Val()
} else {
path = "/"
command = val
}
parts, err := shlex.Split(command) // Okay, check again for optional block
if err != nil { hadBlock, err = optionalBlock()
return nil, errors.New("Error parsing command for websocket use: " + err.Error()) if err != nil {
} else if len(parts) == 0 { return nil, err
return nil, errors.New("No command found for use by websocket") }
} }
cmd = parts[0] // Split command into the actual command and its arguments
if len(parts) > 1 { cmd, args, err := parseCommandAndArgs(command)
args = parts[1:] if err != nil {
return nil, err
} }
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) { ...@@ -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 ( ...@@ -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
) )
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment