mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-24 03:05:49 +03:00
Merge branch 'master' into configfix
This commit is contained in:
commit
a518049fa2
18 changed files with 437 additions and 187 deletions
|
@ -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
|
||||
|
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 := `<!DOCTYPE html>
|
||||
expectedBody := []byte(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>first_post</title>
|
||||
|
@ -104,9 +105,10 @@ func TestMarkdownStaticGen(t *testing.T) {
|
|||
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
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)
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
}, {
|
||||
|
|
|
@ -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)
|
||||
|
|
138
middleware/commands_test.go
Normal file
138
middleware/commands_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
229
middleware/websocket/websocket.go
Normal file
229
middleware/websocket/websocket.go
Normal file
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
Loading…
Reference in a new issue