Commit abdadf1e authored by Matthew Holt's avatar Matthew Holt

Improvements to websocket middleware

parent d7ae9fb4
......@@ -10,7 +10,7 @@ import (
)
// 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 {
WSConfig
*http.Request
......@@ -21,33 +21,40 @@ type WebSocket struct {
// the command's stdin and stdout.
func (ws WebSocket) Handle(conn *websocket.Conn) {
cmd := exec.Command(ws.Command, ws.Arguments...)
cmd.Stdin = 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 {
// TODO
panic(err) // TODO
}
cmd.Env = metavars
err = cmd.Run()
if err != nil {
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
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)
if err != nil {
return err
return
}
serverHost, serverPort, err := net.SplitHostPort(ws.Host)
if err != nil {
return err
return
}
cmd.Env = []string{
metavars = []string{
`AUTH_TYPE=`, // Not used
`CONTENT_LENGTH=`, // Not used
`CONTENT_TYPE=`, // Not used
......@@ -62,7 +69,7 @@ func (ws WebSocket) buildEnv(cmd *exec.Cmd) error {
`REMOTE_USER=`, // Not used,
`REQUEST_METHOD=` + ws.Method,
`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_PORT=` + serverPort,
`SERVER_PROTOCOL=` + ws.Proto,
......@@ -75,8 +82,8 @@ func (ws WebSocket) buildEnv(cmd *exec.Cmd) error {
header = strings.ToUpper(header)
header = strings.Replace(header, "-", "_", -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 endpoints.
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 []WSConfig
}
// 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 {
Path string
Command 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
}
}
// Didn't match a websocket path, so pass-thru
ws.Next(w, r)
}
// New constructs and configures a new websockets middleware instance.
func New(c middleware.Controller) (middleware.Middleware, error) {
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() {
var val, path, command string
......@@ -57,38 +77,40 @@ func New(c middleware.Controller) (middleware.Middleware, error) {
}
val = c.Val()
// The rest of the arguments are the command
// Extra configuration may be in a block
hadBlock, err := optionalBlock()
if err != nil {
return nil, err
}
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()
for c.NextArg() {
command += " " + c.Val()
}
} else {
path = "/"
command = val
}
// Split command into the actual command and its arguments
var cmd string
var args []string
parts, err := shlex.Split(command)
// Okay, check again for optional block
hadBlock, err = optionalBlock()
if err != nil {
return nil, errors.New("Error parsing command for websocket use: " + err.Error())
} else if len(parts) == 0 {
return nil, errors.New("No command found for use by websocket")
return nil, err
}
}
cmd = parts[0]
if len(parts) > 1 {
args = parts[1:]
// Split command into the actual command and its arguments
cmd, args, err := parseCommandAndArgs(command)
if err != nil {
return nil, err
}
websocks = append(websocks, WSConfig{
Path: path,
Command: cmd,
Arguments: args,
Respawn: respawn,
})
}
......@@ -96,12 +118,30 @@ func New(c middleware.Controller) (middleware.Middleware, error) {
ServerSoftware = envServerSoftware
return func(next http.HandlerFunc) http.HandlerFunc {
// We don't use next because websockets aren't HTTP,
// so we don't invoke other middleware after this.
return WebSockets{Sockets: websocks}.ServeHTTP
return WebSockets{Next: next, Sockets: websocks}.ServeHTTP
}, 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 (
// See CGI spec, 4.1.4
GatewayInterface string
......@@ -112,5 +152,5 @@ var (
const (
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