diff --git a/config/setup/basicauth_test.go b/config/setup/basicauth_test.go
index 7588f0f0..a94d6e69 100644
--- a/config/setup/basicauth_test.go
+++ b/config/setup/basicauth_test.go
@@ -38,7 +38,7 @@ func TestBasicAuthParse(t *testing.T) {
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
var skipHtpassword bool
- htfh, err := ioutil.TempFile("", "basicauth-")
+ htfh, err := ioutil.TempFile(".", "basicauth-")
if err != nil {
t.Logf("Error creating temp file (%v), will skip htpassword test", err)
skipHtpassword = true
diff --git a/config/setup/controller.go b/config/setup/controller.go
index 357365e2..3b78dbcd 100644
--- a/config/setup/controller.go
+++ b/config/setup/controller.go
@@ -30,7 +30,9 @@ type Controller struct {
// add-ons can use this as a convenience.
func NewTestController(input string) *Controller {
return &Controller{
- Config: &server.Config{},
+ Config: &server.Config{
+ Root: ".",
+ },
Dispenser: parse.NewDispenser("Testfile", strings.NewReader(input)),
}
}
diff --git a/config/setup/errors.go b/config/setup/errors.go
index bc131976..5aa09b9f 100644
--- a/config/setup/errors.go
+++ b/config/setup/errors.go
@@ -5,7 +5,7 @@ import (
"io"
"log"
"os"
- "path"
+ "path/filepath"
"strconv"
"github.com/hashicorp/go-syslog"
@@ -105,7 +105,7 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
}
} else {
// Error page; ensure it exists
- where = path.Join(c.Root, where)
+ where = filepath.Join(c.Root, where)
f, err := os.Open(where)
if err != nil {
fmt.Println("Warning: Unable to open error page '" + where + "': " + err.Error())
diff --git a/config/setup/markdown.go b/config/setup/markdown.go
index eb24a595..65344a74 100644
--- a/config/setup/markdown.go
+++ b/config/setup/markdown.go
@@ -114,11 +114,11 @@ func loadParams(c *Controller, mdc *markdown.Config) error {
if _, ok := mdc.Templates[markdown.DefaultTemplate]; ok {
return c.Err("only one default template is allowed, use alias.")
}
- fpath := filepath.Clean(c.Root + string(filepath.Separator) + tArgs[0])
+ fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[0]))
mdc.Templates[markdown.DefaultTemplate] = fpath
return nil
case 2:
- fpath := filepath.Clean(c.Root + string(filepath.Separator) + tArgs[1])
+ fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[1]))
mdc.Templates[tArgs[0]] = fpath
return nil
default:
diff --git a/config/setup/markdown_test.go b/config/setup/markdown_test.go
index 1ada8a1f..5bf012b0 100644
--- a/config/setup/markdown_test.go
+++ b/config/setup/markdown_test.go
@@ -1,6 +1,7 @@
package setup
import (
+ "bytes"
"fmt"
"io/ioutil"
"net/http"
@@ -92,7 +93,7 @@ func TestMarkdownStaticGen(t *testing.T) {
t.Fatalf("An error occured when getting the file content: %v", err)
}
- expectedBody := `
+ expectedBody := []byte(`
first_post
@@ -104,9 +105,10 @@ func TestMarkdownStaticGen(t *testing.T) {
-`
- if string(html) != expectedBody {
- t.Fatalf("Expected file content: %v got: %v", expectedBody, html)
+`)
+
+ if !bytes.Equal(html, expectedBody) {
+ t.Fatalf("Expected file content: %s got: %s", string(expectedBody), string(html))
}
fp := filepath.Join(c.Root, markdown.DefaultStaticDir)
diff --git a/config/setup/root_test.go b/config/setup/root_test.go
index f34e05d2..8b38e6d0 100644
--- a/config/setup/root_test.go
+++ b/config/setup/root_test.go
@@ -26,9 +26,12 @@ func TestRoot(t *testing.T) {
if err != nil {
t.Fatalf("BeforeTest: Failed to create temp file for testing! Error was: %v", err)
}
- defer os.Remove(existingFile.Name())
+ defer func() {
+ existingFile.Close()
+ os.Remove(existingFile.Name())
+ }()
- unaccessiblePath := filepath.Join(existingFile.Name(), "some_name")
+ inaccessiblePath := getInaccessiblePath(existingFile.Name())
tests := []struct {
input string
@@ -48,7 +51,7 @@ func TestRoot(t *testing.T) {
`root `, true, "", parseErrContent,
},
{
- fmt.Sprintf(`root %s`, unaccessiblePath), true, "", unableToAccessErrContent,
+ fmt.Sprintf(`root %s`, inaccessiblePath), true, "", unableToAccessErrContent,
},
{
fmt.Sprintf(`root {
@@ -60,8 +63,9 @@ func TestRoot(t *testing.T) {
for i, test := range tests {
c := NewTestController(test.input)
mid, err := Root(c)
+
if test.shouldErr && err == nil {
- t.Errorf("Test %d: Expected error but found nil for input %s", i, test.input)
+ t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
@@ -97,3 +101,8 @@ func getTempDirPath() (string, error) {
return tempDir, nil
}
+
+func getInaccessiblePath(file string) string {
+ // null byte in filename is not allowed on Windows AND unix
+ return filepath.Join("C:", "file\x00name")
+}
diff --git a/config/setup/websocket.go b/config/setup/websocket.go
index 9178bd13..33df76d7 100644
--- a/config/setup/websocket.go
+++ b/config/setup/websocket.go
@@ -2,26 +2,26 @@ package setup
import (
"github.com/mholt/caddy/middleware"
- "github.com/mholt/caddy/middleware/websockets"
+ "github.com/mholt/caddy/middleware/websocket"
)
-// WebSocket configures a new WebSockets middleware instance.
+// WebSocket configures a new WebSocket middleware instance.
func WebSocket(c *Controller) (middleware.Middleware, error) {
websocks, err := webSocketParse(c)
if err != nil {
return nil, err
}
- websockets.GatewayInterface = c.AppName + "-CGI/1.1"
- websockets.ServerSoftware = c.AppName + "/" + c.AppVersion
+ websocket.GatewayInterface = c.AppName + "-CGI/1.1"
+ websocket.ServerSoftware = c.AppName + "/" + c.AppVersion
return func(next middleware.Handler) middleware.Handler {
- return websockets.WebSockets{Next: next, Sockets: websocks}
+ return websocket.WebSocket{Next: next, Sockets: websocks}
}, nil
}
-func webSocketParse(c *Controller) ([]websockets.Config, error) {
- var websocks []websockets.Config
+func webSocketParse(c *Controller) ([]websocket.Config, error) {
+ var websocks []websocket.Config
var respawn bool
optionalBlock := func() (hadBlock bool, err error) {
@@ -74,7 +74,7 @@ func webSocketParse(c *Controller) ([]websockets.Config, error) {
return nil, err
}
- websocks = append(websocks, websockets.Config{
+ websocks = append(websocks, websocket.Config{
Path: path,
Command: cmd,
Arguments: args,
diff --git a/config/setup/websocket_test.go b/config/setup/websocket_test.go
index 86af253d..750f2a1d 100644
--- a/config/setup/websocket_test.go
+++ b/config/setup/websocket_test.go
@@ -1,8 +1,9 @@
package setup
import (
- "github.com/mholt/caddy/middleware/websockets"
"testing"
+
+ "github.com/mholt/caddy/middleware/websocket"
)
func TestWebSocket(t *testing.T) {
@@ -20,10 +21,10 @@ func TestWebSocket(t *testing.T) {
}
handler := mid(EmptyNext)
- myHandler, ok := handler.(websockets.WebSockets)
+ myHandler, ok := handler.(websocket.WebSocket)
if !ok {
- t.Fatalf("Expected handler to be type WebSockets, got: %#v", handler)
+ t.Fatalf("Expected handler to be type WebSocket, got: %#v", handler)
}
if myHandler.Sockets[0].Path != "/" {
@@ -38,15 +39,15 @@ func TestWebSocketParse(t *testing.T) {
tests := []struct {
inputWebSocketConfig string
shouldErr bool
- expectedWebSocketConfig []websockets.Config
+ expectedWebSocketConfig []websocket.Config
}{
- {`websocket /api1 cat`, false, []websockets.Config{{
+ {`websocket /api1 cat`, false, []websocket.Config{{
Path: "/api1",
Command: "cat",
}}},
{`websocket /api3 cat
- websocket /api4 cat `, false, []websockets.Config{{
+ websocket /api4 cat `, false, []websocket.Config{{
Path: "/api3",
Command: "cat",
}, {
diff --git a/middleware/basicauth/basicauth_test.go b/middleware/basicauth/basicauth_test.go
index aa1fc244..aad5ed39 100644
--- a/middleware/basicauth/basicauth_test.go
+++ b/middleware/basicauth/basicauth_test.go
@@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"os"
+ "path/filepath"
"testing"
"github.com/mholt/caddy/middleware"
@@ -124,15 +125,18 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
t.Skipf("Error creating temp file (%v), will skip htpassword test")
return
}
+ defer os.Remove(htfh.Name())
if _, err = htfh.Write([]byte(htpasswdFile)); err != nil {
t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err)
}
htfh.Close()
- defer os.Remove(htfh.Name())
for i, username := range []string{"sha1", "md5"} {
rule := Rule{Username: username, Resources: []string{"/testing"}}
- if rule.Password, err = GetHtpasswdMatcher(htfh.Name(), rule.Username, "/"); err != nil {
+
+ siteRoot := filepath.Dir(htfh.Name())
+ filename := filepath.Base(htfh.Name())
+ if rule.Password, err = GetHtpasswdMatcher(filename, rule.Username, siteRoot); err != nil {
t.Fatalf("GetHtpasswdMatcher(%q, %q): %v", htfh.Name(), rule.Username, err)
}
t.Logf("%d. username=%q password=%v", i, rule.Username, rule.Password)
diff --git a/middleware/commands_test.go b/middleware/commands_test.go
new file mode 100644
index 00000000..3a5b3334
--- /dev/null
+++ b/middleware/commands_test.go
@@ -0,0 +1,138 @@
+package middleware
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+)
+
+func TestSplitCommandAndArgs(t *testing.T) {
+ var parseErrorContent = "error parsing command:"
+ var noCommandErrContent = "no command contained in"
+
+ tests := []struct {
+ input string
+ expectedCommand string
+ expectedArgs []string
+ expectedErrContent string
+ }{
+ // Test case 0 - emtpy command
+ {
+ input: ``,
+ expectedCommand: ``,
+ expectedArgs: nil,
+ expectedErrContent: noCommandErrContent,
+ },
+ // Test case 1 - command without arguments
+ {
+ input: `command`,
+ expectedCommand: `command`,
+ expectedArgs: nil,
+ expectedErrContent: ``,
+ },
+ // Test case 2 - command with single argument
+ {
+ input: `command arg1`,
+ expectedCommand: `command`,
+ expectedArgs: []string{`arg1`},
+ expectedErrContent: ``,
+ },
+ // Test case 3 - command with multiple arguments
+ {
+ input: `command arg1 arg2`,
+ expectedCommand: `command`,
+ expectedArgs: []string{`arg1`, `arg2`},
+ expectedErrContent: ``,
+ },
+ // Test case 4 - command with single argument with space character - in quotes
+ {
+ input: `command "arg1 arg1"`,
+ expectedCommand: `command`,
+ expectedArgs: []string{`arg1 arg1`},
+ expectedErrContent: ``,
+ },
+ // Test case 4 - command with single argument with space character - escaped
+ {
+ input: `command arg1\ arg1`,
+ expectedCommand: `command`,
+ expectedArgs: []string{`arg1 arg1`},
+ expectedErrContent: ``,
+ },
+ // Test case 6 - command with escaped quote character
+ {
+ input: `command "arg1 \" arg1"`,
+ expectedCommand: `command`,
+ expectedArgs: []string{`arg1 " arg1`},
+ expectedErrContent: ``,
+ },
+ // Test case 7 - command with escaped backslash
+ {
+ input: `command '\arg1'`,
+ expectedCommand: `command`,
+ expectedArgs: []string{`\arg1`},
+ expectedErrContent: ``,
+ },
+ // Test case 8 - command with comments
+ {
+ input: `command arg1 #comment1 comment2`,
+ expectedCommand: `command`,
+ expectedArgs: []string{`arg1`},
+ expectedErrContent: "",
+ },
+ // Test case 9 - command with multiple spaces and tab character
+ {
+ input: "command arg1 arg2\targ3",
+ expectedCommand: `command`,
+ expectedArgs: []string{`arg1`, `arg2`, "arg3"},
+ expectedErrContent: "",
+ },
+ // Test case 10 - command with unclosed quotes
+ {
+ input: `command "arg1 arg2`,
+ expectedCommand: "",
+ expectedArgs: nil,
+ expectedErrContent: parseErrorContent,
+ },
+ // Test case 11 - command with unclosed quotes
+ {
+ input: `command 'arg1 arg2"`,
+ expectedCommand: "",
+ expectedArgs: nil,
+ expectedErrContent: parseErrorContent,
+ },
+ }
+
+ for i, test := range tests {
+ errorPrefix := fmt.Sprintf("Test [%d]: ", i)
+ errorSuffix := fmt.Sprintf(" Command to parse: [%s]", test.input)
+ actualCommand, actualArgs, actualErr := SplitCommandAndArgs(test.input)
+
+ // test if error matches expectation
+ if test.expectedErrContent != "" {
+ if actualErr == nil {
+ t.Errorf(errorPrefix+"Expected error with content [%s], found no error."+errorSuffix, test.expectedErrContent)
+ } else if !strings.Contains(actualErr.Error(), test.expectedErrContent) {
+ t.Errorf(errorPrefix+"Expected error with content [%s], found [%v]."+errorSuffix, test.expectedErrContent, actualErr)
+ }
+ } else if actualErr != nil {
+ t.Errorf(errorPrefix+"Expected no error, found [%v]."+errorSuffix, actualErr)
+ }
+
+ // test if command matches
+ if test.expectedCommand != actualCommand {
+ t.Errorf("Expected command: [%s], actual: [%s]."+errorSuffix, test.expectedCommand, actualCommand)
+ }
+
+ // test if arguments match
+ if len(test.expectedArgs) != len(actualArgs) {
+ t.Errorf("Wrong number of arguments! Expected [%v], actual [%v]."+errorSuffix, test.expectedArgs, actualArgs)
+ }
+
+ for j, actualArg := range actualArgs {
+ expectedArg := test.expectedArgs[j]
+ if actualArg != expectedArg {
+ t.Errorf(errorPrefix+"Argument at position [%d] differ! Expected [%s], actual [%s]"+errorSuffix, j, expectedArg, actualArg)
+ }
+ }
+ }
+}
diff --git a/middleware/markdown/generator.go b/middleware/markdown/generator.go
index c1dfd67a..a02cb3b0 100644
--- a/middleware/markdown/generator.go
+++ b/middleware/markdown/generator.go
@@ -70,7 +70,7 @@ func generateLinks(md Markdown, cfg *Config) (bool, error) {
return generated, g.lastErr
}
-// generateStaticFiles generates static html files from markdowns.
+// generateStaticHTML generates static HTML files from markdowns.
func generateStaticHTML(md Markdown, cfg *Config) error {
// If generated site already exists, clear it out
_, err := os.Stat(cfg.StaticDir)
@@ -98,6 +98,7 @@ func generateStaticHTML(md Markdown, cfg *Config) error {
if err != nil {
return err
}
+ reqPath = filepath.ToSlash(reqPath)
reqPath = "/" + reqPath
// Generate the static file
diff --git a/middleware/markdown/page.go b/middleware/markdown/page.go
index 2b7816ba..3185d8ec 100644
--- a/middleware/markdown/page.go
+++ b/middleware/markdown/page.go
@@ -116,7 +116,7 @@ func (l *linkGen) generateLinks(md Markdown, cfg *Config) bool {
if err != nil {
return err
}
- reqPath = "/" + reqPath
+ reqPath = "/" + filepath.ToSlash(reqPath)
parser := findParser(body)
if parser == nil {
diff --git a/middleware/markdown/process.go b/middleware/markdown/process.go
index 93fe5147..0fb48dba 100644
--- a/middleware/markdown/process.go
+++ b/middleware/markdown/process.go
@@ -134,7 +134,10 @@ func (md Markdown) generatePage(c *Config, requestPath string, content []byte) e
}
}
- filePath := filepath.Join(c.StaticDir, requestPath)
+ // the URL will always use "/" as a path separator,
+ // convert that to a native path to support OS that
+ // use different path separators
+ filePath := filepath.Join(c.StaticDir, filepath.FromSlash(requestPath))
// If it is index file, use the directory instead
if md.IsIndexFile(filepath.Base(requestPath)) {
@@ -154,7 +157,7 @@ func (md Markdown) generatePage(c *Config, requestPath string, content []byte) e
}
c.Lock()
- c.StaticFiles[requestPath] = filePath
+ c.StaticFiles[requestPath] = filepath.ToSlash(filePath)
c.Unlock()
}
diff --git a/middleware/middleware.go b/middleware/middleware.go
index 0310ba2f..ba7699ce 100644
--- a/middleware/middleware.go
+++ b/middleware/middleware.go
@@ -3,7 +3,7 @@ package middleware
import (
"net/http"
- "path/filepath"
+ "path"
)
type (
@@ -57,12 +57,19 @@ func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err
// and false is returned. fpath must end in a forward slash '/'
// otherwise no index files will be tried (directory paths must end
// in a forward slash according to HTTP).
+//
+// All paths passed into and returned from this function use '/' as the
+// path separator, just like URLs. IndexFle handles path manipulation
+// internally for systems that use different path separators.
func IndexFile(root http.FileSystem, fpath string, indexFiles []string) (string, bool) {
if fpath[len(fpath)-1] != '/' || root == nil {
return "", false
}
for _, indexFile := range indexFiles {
- fp := filepath.Join(fpath, indexFile)
+ // func (http.FileSystem).Open wants all paths separated by "/",
+ // regardless of operating system convention, so use
+ // path.Join instead of filepath.Join
+ fp := path.Join(fpath, indexFile)
f, err := root.Open(fp)
if err == nil {
f.Close()
diff --git a/middleware/middleware_test.go b/middleware/middleware_test.go
index e5b238e6..700beed8 100644
--- a/middleware/middleware_test.go
+++ b/middleware/middleware_test.go
@@ -15,9 +15,12 @@ func TestIndexfile(t *testing.T) {
expectedBoolValue bool //return value
}{
{
- http.Dir("./templates/testdata"), "/images/", []string{"img.htm"},
+ http.Dir("./templates/testdata"),
+ "/images/",
+ []string{"img.htm"},
false,
- "/images/img.htm", true,
+ "/images/img.htm",
+ true,
},
}
for i, test := range tests {
diff --git a/middleware/websocket/websocket.go b/middleware/websocket/websocket.go
new file mode 100644
index 00000000..f344fe51
--- /dev/null
+++ b/middleware/websocket/websocket.go
@@ -0,0 +1,229 @@
+// Package websocket implements a WebSocket server by executing
+// a command and piping its input and output through the WebSocket
+// connection.
+package websocket
+
+import (
+ "io"
+ "net"
+ "net/http"
+ "os/exec"
+ "strings"
+ "time"
+
+ "github.com/gorilla/websocket"
+ "github.com/mholt/caddy/middleware"
+)
+
+const (
+ // Time allowed to write a message to the peer.
+ writeWait = 10 * time.Second
+
+ // Time allowed to read the next pong message from the peer.
+ pongWait = 60 * time.Second
+
+ // Send pings to peer with this period. Must be less than pongWait.
+ pingPeriod = (pongWait * 9) / 10
+
+ // Maximum message size allowed from peer.
+ maxMessageSize = 1024 * 1024 * 10 // 10 MB default.
+)
+
+var (
+ // GatewayInterface is the dialect of CGI being used by the server
+ // to communicate with the script. See CGI spec, 4.1.4
+ GatewayInterface string
+
+ // ServerSoftware is the name and version of the information server
+ // software making the CGI request. See CGI spec, 4.1.17
+ ServerSoftware string
+)
+
+type (
+ // WebSocket is a type that holds configuration for the
+ // websocket middleware generally, like a list of all the
+ // websocket endpoints.
+ WebSocket struct {
+ // Next is the next HTTP handler in the chain for when the path doesn't match
+ Next middleware.Handler
+
+ // Sockets holds all the web socket endpoint configurations
+ Sockets []Config
+ }
+
+ // Config holds the configuration for a single websocket
+ // endpoint which may serve multiple websocket connections.
+ Config struct {
+ Path string
+ Command string
+ Arguments []string
+ Respawn bool // TODO: Not used, but parser supports it until we decide on it
+ }
+)
+
+// ServeHTTP converts the HTTP request to a WebSocket connection and serves it up.
+func (ws WebSocket) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
+ for _, sockconfig := range ws.Sockets {
+ if middleware.Path(r.URL.Path).Matches(sockconfig.Path) {
+ return serveWS(w, r, &sockconfig)
+ }
+ }
+
+ // Didn't match a websocket path, so pass-thru
+ return ws.Next.ServeHTTP(w, r)
+}
+
+// serveWS is used for setting and upgrading the HTTP connection to a websocket connection.
+// It also spawns the child process that is associated with matched HTTP path/url.
+func serveWS(w http.ResponseWriter, r *http.Request, config *Config) (int, error) {
+ upgrader := websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+ CheckOrigin: func(r *http.Request) bool { return true },
+ }
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+ defer conn.Close()
+
+ cmd := exec.Command(config.Command, config.Arguments...)
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return http.StatusBadGateway, err
+ }
+
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ return http.StatusBadGateway, err
+ }
+
+ metavars, err := buildEnv(cmd.Path, r)
+ if err != nil {
+ return http.StatusBadGateway, err
+ }
+
+ cmd.Env = metavars
+
+ if err := cmd.Start(); err != nil {
+ return http.StatusBadGateway, err
+ }
+
+ reader(conn, stdout, stdin)
+
+ return 0, nil
+}
+
+// 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
+// cmdPath should be the path of the command being run.
+// The returned string slice can be set to the command's Env property.
+func buildEnv(cmdPath string, r *http.Request) (metavars []string, err error) {
+ remoteHost, remotePort, err := net.SplitHostPort(r.RemoteAddr)
+ if err != nil {
+ return
+ }
+
+ serverHost, serverPort, err := net.SplitHostPort(r.Host)
+ if err != nil {
+ return
+ }
+
+ metavars = []string{
+ `AUTH_TYPE=`, // Not used
+ `CONTENT_LENGTH=`, // Not used
+ `CONTENT_TYPE=`, // Not used
+ `GATEWAY_INTERFACE=` + GatewayInterface,
+ `PATH_INFO=`, // TODO
+ `PATH_TRANSLATED=`, // TODO
+ `QUERY_STRING=` + r.URL.RawQuery,
+ `REMOTE_ADDR=` + remoteHost,
+ `REMOTE_HOST=` + remoteHost, // Host lookups are slow - don't do them
+ `REMOTE_IDENT=`, // Not used
+ `REMOTE_PORT=` + remotePort,
+ `REMOTE_USER=`, // Not used,
+ `REQUEST_METHOD=` + r.Method,
+ `REQUEST_URI=` + r.RequestURI,
+ `SCRIPT_NAME=` + cmdPath, // path of the program being executed
+ `SERVER_NAME=` + serverHost,
+ `SERVER_PORT=` + serverPort,
+ `SERVER_PROTOCOL=` + r.Proto,
+ `SERVER_SOFTWARE=` + ServerSoftware,
+ }
+
+ // Add each HTTP header to the environment as well
+ for header, values := range r.Header {
+ value := strings.Join(values, ", ")
+ header = strings.ToUpper(header)
+ header = strings.Replace(header, "-", "_", -1)
+ value = strings.Replace(value, "\n", " ", -1)
+ metavars = append(metavars, "HTTP_"+header+"="+value)
+ }
+
+ return
+}
+
+// reader is the guts of this package. It takes the stdin and stdout pipes
+// of the cmd we created in ServeWS and pipes them between the client and server
+// over websockets.
+func reader(conn *websocket.Conn, stdout io.ReadCloser, stdin io.WriteCloser) {
+ // Setup our connection's websocket ping/pong handlers from our const values.
+ conn.SetReadLimit(maxMessageSize)
+ conn.SetReadDeadline(time.Now().Add(pongWait))
+ conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
+ tickerChan := make(chan bool)
+ defer func() { tickerChan <- true }() // make sure to close the ticker when we are done.
+ go ticker(conn, tickerChan)
+
+ for {
+ msgType, r, err := conn.NextReader()
+ if err != nil {
+ if msgType == -1 {
+ return // we got a disconnect from the client. We are good to close.
+ }
+ conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, ""), time.Time{})
+ return
+ }
+
+ w, err := conn.NextWriter(msgType)
+ if err != nil {
+ conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, ""), time.Time{})
+ return
+ }
+
+ if _, err := io.Copy(stdin, r); err != nil {
+ conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, ""), time.Time{})
+ return
+ }
+
+ go func() {
+ if _, err := io.Copy(w, stdout); err != nil {
+ conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, ""), time.Time{})
+ return
+ }
+ if err := w.Close(); err != nil {
+ conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, ""), time.Time{})
+ return
+ }
+ }()
+ }
+}
+
+// ticker is start by the reader. Basically it is the method that simulates the websocket
+// between the server and client to keep it alive with ping messages.
+func ticker(conn *websocket.Conn, c chan bool) {
+ ticker := time.NewTicker(pingPeriod)
+ defer func() {
+ ticker.Stop()
+ close(c)
+ }()
+
+ for { // blocking loop with select to wait for stimulation.
+ select {
+ case <-ticker.C:
+ conn.WriteMessage(websocket.PingMessage, nil)
+ case <-c:
+ return // clean up this routine.
+ }
+ }
+}
diff --git a/middleware/websockets/websocket.go b/middleware/websockets/websocket.go
deleted file mode 100644
index 4c843f05..00000000
--- a/middleware/websockets/websocket.go
+++ /dev/null
@@ -1,89 +0,0 @@
-package websockets
-
-import (
- "net"
- "net/http"
- "os/exec"
- "strings"
-
- "golang.org/x/net/websocket"
-)
-
-// WebSocket represents a web socket server instance. A WebSocket
-// is instantiated for each new websocket request/connection.
-type WebSocket struct {
- Config
- *http.Request
-}
-
-// Handle handles a WebSocket connection. It launches the
-// specified command and streams input and output through
-// 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
-
- metavars, err := ws.buildEnv(cmd.Path)
- if err != nil {
- panic(err) // TODO
- }
-
- cmd.Env = metavars
-
- err = cmd.Run()
- if err != nil {
- panic(err)
- }
-}
-
-// 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
-// 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
- }
-
- serverHost, serverPort, err := net.SplitHostPort(ws.Host)
- if err != nil {
- return
- }
-
- metavars = []string{
- `AUTH_TYPE=`, // Not used
- `CONTENT_LENGTH=`, // Not used
- `CONTENT_TYPE=`, // Not used
- `GATEWAY_INTERFACE=` + GatewayInterface,
- `PATH_INFO=`, // TODO
- `PATH_TRANSLATED=`, // TODO
- `QUERY_STRING=` + ws.URL.RawQuery,
- `REMOTE_ADDR=` + remoteHost,
- `REMOTE_HOST=` + remoteHost, // Host lookups are slow - don't do them
- `REMOTE_IDENT=`, // Not used
- `REMOTE_PORT=` + remotePort,
- `REMOTE_USER=`, // Not used,
- `REQUEST_METHOD=` + ws.Method,
- `REQUEST_URI=` + ws.RequestURI,
- `SCRIPT_NAME=` + cmdPath, // path of the program being executed
- `SERVER_NAME=` + serverHost,
- `SERVER_PORT=` + serverPort,
- `SERVER_PROTOCOL=` + ws.Proto,
- `SERVER_SOFTWARE=` + ServerSoftware,
- }
-
- // Add each HTTP header to the environment as well
- for header, values := range ws.Header {
- value := strings.Join(values, ", ")
- header = strings.ToUpper(header)
- header = strings.Replace(header, "-", "_", -1)
- value = strings.Replace(value, "\n", " ", -1)
- metavars = append(metavars, "HTTP_"+header+"="+value)
- }
-
- return
-}
diff --git a/middleware/websockets/websockets.go b/middleware/websockets/websockets.go
deleted file mode 100644
index 81e40510..00000000
--- a/middleware/websockets/websockets.go
+++ /dev/null
@@ -1,60 +0,0 @@
-// Package websockets implements a WebSocket server by executing
-// a command and piping its input and output through the WebSocket
-// connection.
-package websockets
-
-import (
- "net/http"
-
- "github.com/mholt/caddy/middleware"
- "golang.org/x/net/websocket"
-)
-
-type (
- // WebSockets is a type that holds configuration for the
- // 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 middleware.Handler
-
- // Sockets holds all the web socket endpoint configurations
- Sockets []Config
- }
-
- // Config holds the configuration for a single websocket
- // endpoint which may serve multiple websocket connections.
- Config struct {
- Path string
- Command string
- Arguments []string
- Respawn bool // TODO: Not used, but parser supports it until we decide on it
- }
-)
-
-// ServeHTTP converts the HTTP request to a WebSocket connection and serves it up.
-func (ws WebSockets) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
- for _, sockconfig := range ws.Sockets {
- if middleware.Path(r.URL.Path).Matches(sockconfig.Path) {
- socket := WebSocket{
- Config: sockconfig,
- Request: r,
- }
- websocket.Handler(socket.Handle).ServeHTTP(w, r)
- return 0, nil
- }
- }
-
- // Didn't match a websocket path, so pass-thru
- return ws.Next.ServeHTTP(w, r)
-}
-
-var (
- // GatewayInterface is the dialect of CGI being used by the server
- // to communicate with the script. See CGI spec, 4.1.4
- GatewayInterface string
-
- // ServerSoftware is the name and version of the information server
- // software making the CGI request. See CGI spec, 4.1.17
- ServerSoftware string
-)