Created
December 16, 2013 09:55
-
-
Save wzdf1982/7984648 to your computer and use it in GitHub Desktop.
libwebsocketd
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2013 Joe Walnes and the websocketd team. | |
// All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
package libwebsocketd | |
import ( | |
"time" | |
) | |
type Config struct { | |
CommandName string // Command to execute. | |
CommandArgs []string // Additional args to pass to command. | |
ReverseLookup bool // Perform reverse DNS lookups on hostnames (useful, but slower). | |
ScriptDir string // Base directory for websocket scripts. | |
UsingScriptDir bool // Are we running with a script dir. | |
StartupTime time.Time // Server startup time (used for dev console caching). | |
StaticDir string // If set, static files will be served from this dir over HTTP. | |
CgiDir string // If set, CGI scripts will be served from this dir over HTTP. | |
DevConsole bool // Enable dev console. This disables StaticDir and CgiDir. | |
ServerSoftware string // Value to pass to SERVER_SOFTWARE environment variable (e.g. websocketd/1.2.3). | |
Env []string // Additional environment variables to pass to process ("key=value"). | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2013 Joe Walnes and the websocketd team. | |
// All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
package libwebsocketd | |
// Although this isn't particularly elegant, it's the simplest | |
// way to embed the console content into the binary. | |
// Note that the console is served by a single HTML file containing | |
// all CSS and JS inline. | |
// We can get by without jQuery or Bootstrap for this one ;). | |
const ( | |
ConsoleContent = ` | |
<!-- | |
websocketd console | |
Full documentation at http://websocketd.com/ | |
{{license}} | |
--> | |
<!DOCTYPE html> | |
<meta charset="utf8"> | |
<title>websocketd console</title> | |
<style> | |
.template { | |
display: none !important; | |
} | |
body, input { | |
font-family: dejavu sans mono, Menlo, Monaco, Consolas, Lucida Console, tahoma, arial; | |
font-size: 13px; | |
} | |
body { | |
margin: 0; | |
} | |
.header { | |
background-color: #efefef; | |
padding: 2px; | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
height: 32px; | |
} | |
.header button { | |
font-size: 19px; | |
width: 30px; | |
margin: 2px 2px 0 2px; | |
padding: 0; | |
float: left; | |
} | |
.header .url-holder { | |
position: absolute; | |
left: 38px; | |
top: 4px; | |
right: 14px; | |
bottom: 9px; | |
} | |
.header .url { | |
border: 1px solid #999; | |
background-color: #fff; | |
width: 100%; | |
height: 100%; | |
border-radius: 2px; | |
padding-left: 4px; | |
padding-right: 4px; | |
} | |
.messages { | |
overflow-y: scroll; | |
position: absolute; | |
left: 0; | |
right: 0; | |
top: 36px; | |
bottom: 0; | |
border-top: 1px solid #ccc; | |
} | |
.message { | |
border-bottom: 1px solid #bbb; | |
padding: 2px; | |
} | |
.message-type { | |
font-weight: bold; | |
position: absolute; | |
width: 80px; | |
display: block; | |
} | |
.message-data { | |
margin-left: 90px; | |
display: block; | |
word-wrap: break-word; | |
} | |
.type-input, | |
.type-send { | |
background-color: #ffe; | |
} | |
.type-onmessage { | |
background-color: #eef; | |
} | |
.type-open, | |
.type-onopen { | |
background-color: #efe; | |
} | |
.type-close, | |
.type-onclose { | |
background-color: #fee; | |
} | |
.type-onerror, | |
.type-exception { | |
background-color: #333; | |
color: #f99; | |
} | |
.type-send .message-type, | |
.type-onmessage .message-type { | |
opacity: 0.2; | |
} | |
.type-input .message-type { | |
color: #090; | |
} | |
.send-input { | |
width: 100%; | |
border: 0; | |
padding: 0; | |
margin: -1px; | |
background-color: inherit; | |
} | |
.send-input:focus { | |
outline: none; | |
} | |
</style> | |
<header class="header"> | |
<button class="disconnect" title="Disconnect" style="display:none">×</button> | |
<button class="connect" title="Connect" style="display:none">✔</button> | |
<div class="url-holder"> | |
<input class="url" type="text" value="ws://localhost:1234/" spellcheck="false"> | |
</div> | |
</header> | |
<section class="messages"> | |
<div class="message template"> | |
<span class="message-type"></span> | |
<span class="message-data"></span> | |
</div> | |
<div class="message type-input"> | |
<span class="message-type">send »</span> | |
<span class="message-data"><input type="text" class="send-input" spellcheck="false"></span> | |
</div> | |
</section> | |
<script> | |
var ws = null; | |
function ready() { | |
select('.connect').style.display = 'block'; | |
select('.disconnect').style.display = 'none'; | |
select('.connect').addEventListener('click', function() { | |
connect(select('.url').value); | |
}); | |
select('.disconnect').addEventListener('click', function() { | |
disconnect(); | |
}); | |
select('.url').focus(); | |
select('.url').addEventListener('keydown', function(ev) { | |
if (ev.keyIdentifier == 'Enter') { | |
updatePageUrl(); | |
connect(select('.url').value); | |
} | |
}); | |
select('.url').addEventListener('change', updatePageUrl); | |
select('.send-input').addEventListener('keydown', function(ev) { | |
if (ev.keyIdentifier == 'Enter') { | |
var msg = select('.send-input').value; | |
select('.send-input').value = ''; | |
send(msg); | |
} | |
if (ev.keyIdentifier == 'Up') { | |
moveThroughSendHistory(1); | |
} | |
if (ev.keyIdentifier == 'Down') { | |
moveThroughSendHistory(-1); | |
} | |
}); | |
window.addEventListener('popstate', updateWebSocketUrl); | |
} | |
function updatePageUrl() { | |
var match = select('.url').value.match(new RegExp('^(ws)(s)?://([^/]*)(/.*)$')); | |
if (match) { | |
var pageUrlSuffix = match[4]; | |
if (history.state != pageUrlSuffix) { | |
history.pushState(pageUrlSuffix, pageUrlSuffix, pageUrlSuffix); | |
} | |
} | |
} | |
function updateWebSocketUrl() { | |
var match = location.href.match(new RegExp('^(http)(s)?://([^/]*)(/.*)$')); | |
if (match) { | |
var wsUrl = 'ws' + (match[2] || '') + '://' + match[3] + match[4]; | |
select('.url').value = wsUrl; | |
} | |
} | |
function appendMessage(type, data) { | |
var template = select('.message.template'); | |
var el = template.parentElement.insertBefore(template.cloneNode(true), select('.message.type-input')); | |
el.classList.remove('template'); | |
el.classList.add('type-' + type.toLowerCase()); | |
el.querySelector('.message-type').innerText = type; | |
el.querySelector('.message-data').innerText = data || ''; | |
el.querySelector('.message-data').innerHTML += ' '; | |
el.scrollIntoView(true); | |
} | |
function connect(url) { | |
function action() { | |
appendMessage('open', url); | |
try { | |
ws = new WebSocket(url); | |
} catch (ex) { | |
appendMessage('exception', 'Cannot connect: ' + ex); | |
return; | |
} | |
select('.connect').style.display = 'none'; | |
select('.disconnect').style.display = 'block'; | |
ws.addEventListener('open', function(ev) { | |
appendMessage('onopen'); | |
}); | |
ws.addEventListener('close', function(ev) { | |
select('.connect').style.display = 'block'; | |
select('.disconnect').style.display = 'none'; | |
appendMessage('onclose', '[Clean: ' + ev.wasClean + ', Code: ' + ev.code + ', Reason: ' + (ev.reason || 'none') + ']'); | |
ws = null; | |
select('.url').focus(); | |
}); | |
ws.addEventListener('message', function(ev) { | |
appendMessage('onmessage', ev.data); | |
}); | |
ws.addEventListener('error', function(ev) { | |
appendMessage('onerror'); | |
}); | |
select('.send-input').focus(); | |
} | |
if (ws) { | |
ws.addEventListener('close', function(ev) { | |
action(); | |
}); | |
disconnect(); | |
} else { | |
action(); | |
} | |
} | |
function disconnect() { | |
if (ws) { | |
appendMessage('close'); | |
ws.close(); | |
} | |
} | |
function send(msg) { | |
appendToSendHistory(msg); | |
appendMessage('send', msg); | |
if (ws) { | |
try { | |
ws.send(msg); | |
} catch (ex) { | |
appendMessage('exception', 'Cannot send: ' + ex); | |
} | |
} else { | |
appendMessage('exception', 'Cannot send: Not connected'); | |
} | |
} | |
function select(selector) { | |
return document.querySelector(selector); | |
} | |
var maxSendHistorySize = 100; | |
currentSendHistoryPosition = -1, | |
sendHistoryRollback = ''; | |
function appendToSendHistory(msg) { | |
currentSendHistoryPosition = -1; | |
sendHistoryRollback = ''; | |
var sendHistory = JSON.parse(localStorage['websocketdconsole.sendhistory'] || '[]'); | |
if (sendHistory[0] !== msg) { | |
sendHistory.unshift(msg); | |
while (sendHistory.length > maxSendHistorySize) { | |
sendHistory.pop(); | |
} | |
localStorage['websocketdconsole.sendhistory'] = JSON.stringify(sendHistory); | |
} | |
} | |
function moveThroughSendHistory(offset) { | |
if (currentSendHistoryPosition == -1) { | |
sendHistoryRollback = select('.send-input').value; | |
} | |
var sendHistory = JSON.parse(localStorage['websocketdconsole.sendhistory'] || '[]'); | |
currentSendHistoryPosition += offset; | |
currentSendHistoryPosition = Math.max(-1, Math.min(sendHistory.length - 1, currentSendHistoryPosition)); | |
var el = select('.send-input'); | |
el.value = currentSendHistoryPosition == -1 | |
? sendHistoryRollback | |
: sendHistory[currentSendHistoryPosition]; | |
setTimeout(function() { | |
el.setSelectionRange(el.value.length, el.value.length); | |
}, 0); | |
} | |
document.addEventListener("DOMContentLoaded", ready, false); | |
</script> | |
` | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2013 Joe Walnes and the websocketd team. | |
// All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
package libwebsocketd | |
type Endpoint interface { | |
Terminate() | |
Output() chan string | |
Send(msg string) bool | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2013 Joe Walnes and the websocketd team. | |
// All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
package libwebsocketd | |
import ( | |
"fmt" | |
"net" | |
"net/http" | |
"os" | |
"strconv" | |
"strings" | |
"time" | |
"code.google.com/p/go.net/websocket" | |
) | |
const ( | |
gatewayInterface = "websocketd-CGI/0.1" | |
) | |
var headerNewlineToSpace = strings.NewReplacer("\n", " ", "\r", " ") | |
var headerDashToUnderscore = strings.NewReplacer("-", "_") | |
func generateId() string { | |
return strconv.FormatInt(time.Now().UnixNano(), 10) | |
} | |
func remoteDetails(req *http.Request, config *Config) (string, string, string, error) { | |
remoteAddr, remotePort, err := net.SplitHostPort(req.RemoteAddr) | |
if err != nil { | |
return "", "", "", err | |
} | |
var remoteHost string | |
if config.ReverseLookup { | |
remoteHosts, err := net.LookupAddr(remoteAddr) | |
if err != nil || len(remoteHosts) == 0 { | |
remoteHost = remoteAddr | |
} else { | |
remoteHost = remoteHosts[0] | |
} | |
} else { | |
remoteHost = remoteAddr | |
} | |
return remoteAddr, remoteHost, remotePort, nil | |
} | |
func createEnv(ws *websocket.Conn, config *Config, urlInfo *URLInfo, id string) ([]string, error) { | |
req := ws.Request() | |
headers := req.Header | |
url := req.URL | |
remoteAddr, remoteHost, remotePort, err := remoteDetails(ws.Request(), config) | |
if err != nil { | |
return nil, err | |
} | |
serverName, serverPort, err := net.SplitHostPort(req.Host) | |
if err != nil { | |
if !strings.Contains(req.Host, ":") { | |
serverName = req.Host | |
serverPort = "80" | |
} else { | |
return nil, err | |
} | |
} | |
standardEnvCount := 20 | |
parentEnv := os.Environ() | |
env := make([]string, 0, len(headers)+standardEnvCount+len(parentEnv)+len(config.Env)) | |
for _, v := range parentEnv { | |
env = append(env, v) | |
} | |
// IMPORTANT ---> Adding a header? Make sure standardHeaderCount (above) is up to date. | |
// Standard CGI specification headers. | |
// As defined in http://tools.ietf.org/html/rfc3875 | |
env = appendEnv(env, "REMOTE_ADDR", remoteAddr) | |
env = appendEnv(env, "REMOTE_HOST", remoteHost) | |
env = appendEnv(env, "SERVER_NAME", serverName) | |
env = appendEnv(env, "SERVER_PORT", serverPort) | |
env = appendEnv(env, "SERVER_PROTOCOL", req.Proto) | |
env = appendEnv(env, "SERVER_SOFTWARE", config.ServerSoftware) | |
env = appendEnv(env, "GATEWAY_INTERFACE", gatewayInterface) | |
env = appendEnv(env, "REQUEST_METHOD", req.Method) | |
env = appendEnv(env, "SCRIPT_NAME", urlInfo.ScriptPath) | |
env = appendEnv(env, "PATH_INFO", urlInfo.PathInfo) | |
env = appendEnv(env, "PATH_TRANSLATED", url.Path) | |
env = appendEnv(env, "QUERY_STRING", url.RawQuery) | |
// Not supported, but we explicitly clear them so we don't get leaks from parent environment. | |
env = appendEnv(env, "AUTH_TYPE", "") | |
env = appendEnv(env, "CONTENT_LENGTH", "") | |
env = appendEnv(env, "CONTENT_TYPE", "") | |
env = appendEnv(env, "REMOTE_IDENT", "") | |
env = appendEnv(env, "REMOTE_USER", "") | |
// Non standard, but commonly used headers. | |
env = appendEnv(env, "UNIQUE_ID", id) // Based on Apache mod_unique_id. | |
env = appendEnv(env, "REMOTE_PORT", remotePort) | |
env = appendEnv(env, "REQUEST_URI", url.RequestURI()) // e.g. /foo/blah?a=b | |
// The following variables are part of the CGI specification, but are optional | |
// and not set by websocketd: | |
// | |
// AUTH_TYPE, REMOTE_USER, REMOTE_IDENT | |
// -- Authentication left to the underlying programs. | |
// | |
// CONTENT_LENGTH, CONTENT_TYPE | |
// -- makes no sense for WebSocket connections. | |
// | |
// HTTPS, SSL_* | |
// -- SSL not supported | |
for k, _ := range headers { | |
env = appendEnv(env, fmt.Sprintf("HTTP_%s", headerDashToUnderscore.Replace(k)), headers[k]...) | |
} | |
for _, v := range config.Env { | |
env = append(env, v) | |
} | |
return env, nil | |
} | |
// Adapted from net/http/header.go | |
func appendEnv(env []string, k string, v ...string) []string { | |
if len(v) == 0 { | |
return env | |
} | |
vCleaned := make([]string, 0, len(v)) | |
for _, val := range v { | |
vCleaned = append(vCleaned, strings.TrimSpace(headerNewlineToSpace.Replace(val))) | |
} | |
return append(env, fmt.Sprintf("%s=%s", | |
strings.ToUpper(k), | |
strings.Join(vCleaned, ", "))) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2013 Joe Walnes and the websocketd team. | |
// All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
package libwebsocketd | |
import ( | |
"code.google.com/p/go.net/websocket" | |
"fmt" | |
"net/http" | |
"net/http/cgi" | |
"os" | |
"path" | |
"path/filepath" | |
"strconv" | |
"strings" | |
) | |
type HttpWsMuxHandler struct { | |
Config *Config | |
Log *LogScope | |
} | |
// Main HTTP handler. Muxes between WebSocket handler, DevConsole or 404. | |
func (h HttpWsMuxHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | |
hdrs := req.Header | |
log := h.Log.NewLevel(h.Log.LogFunc) | |
log.Associate("url", fmt.Sprintf("http://%s%s", req.RemoteAddr, req.URL.RequestURI())) | |
_, remoteHost, _, err := remoteDetails(req, h.Config) | |
if err != nil { | |
log.Error("session", "Could not understand remote address '%s': %s", req.RemoteAddr, err) | |
return | |
} | |
log.Associate("remote", remoteHost) | |
// WebSocket | |
if strings.ToLower(hdrs.Get("Upgrade")) == "websocket" && strings.ToLower(hdrs.Get("Connection")) == "upgrade" { | |
if hdrs.Get("Origin") == "null" { | |
// Fix up mismatch between how Chrome reports Origin | |
// when using file:// url (using the string "null"), and | |
// how the WebSocket library expects to see it. | |
hdrs.Set("Origin", "file:") | |
} | |
if h.Config.CommandName != "" || h.Config.UsingScriptDir { | |
wsHandler := websocket.Handler(func(ws *websocket.Conn) { | |
acceptWebSocket(ws, h.Config, log) | |
}) | |
wsHandler.ServeHTTP(w, req) | |
return | |
} | |
} | |
// Dev console (if enabled) | |
if h.Config.DevConsole { | |
content := strings.Replace(ConsoleContent, "{{license}}", License, -1) | |
http.ServeContent(w, req, ".html", h.Config.StartupTime, strings.NewReader(content)) | |
return | |
} | |
// CGI scripts | |
if h.Config.CgiDir != "" { | |
filePath := path.Join(h.Config.CgiDir, fmt.Sprintf(".%s", filepath.FromSlash(req.URL.Path))) | |
if fi, err := os.Stat(filePath); err == nil && !fi.IsDir() { | |
cgiHandler := &cgi.Handler{ | |
Path: filePath, | |
} | |
log.Associate("cgiscript", filePath) | |
log.Access("http", "CGI") | |
cgiHandler.ServeHTTP(w, req) | |
return | |
} | |
} | |
// Static files | |
if h.Config.StaticDir != "" { | |
handler := http.FileServer(http.Dir(h.Config.StaticDir)) | |
log.Access("http", "STATIC") | |
handler.ServeHTTP(w, req) | |
return | |
} | |
// 404 | |
log.Access("http", "NOT FOUND") | |
http.NotFound(w, req) | |
} | |
func acceptWebSocket(ws *websocket.Conn, config *Config, log *LogScope) { | |
defer ws.Close() | |
req := ws.Request() | |
id := generateId() | |
log.Associate("id", id) | |
log.Associate("origin", req.Header.Get("Origin")) | |
log.Access("session", "CONNECT") | |
defer log.Access("session", "DISCONNECT") | |
urlInfo, err := parsePath(ws.Request().URL.Path, config) | |
if err != nil { | |
log.Access("session", "NOT FOUND: %s", err) | |
return | |
} | |
log.Debug("session", "URLInfo: %s", urlInfo) | |
env, err := createEnv(ws, config, urlInfo, id) | |
if err != nil { | |
log.Error("process", "Could not create ENV: %s", err) | |
return | |
} | |
commandName := config.CommandName | |
if config.UsingScriptDir { | |
commandName = urlInfo.FilePath | |
} | |
log.Associate("command", commandName) | |
launched, err := launchCmd(commandName, config.CommandArgs, env) | |
if err != nil { | |
log.Error("process", "Could not launch process %s %s (%s)", commandName, strings.Join(config.CommandArgs, " "), err) | |
return | |
} | |
log.Associate("pid", strconv.Itoa(launched.cmd.Process.Pid)) | |
process := NewProcessEndpoint(launched, log) | |
wsEndpoint := NewWebSocketEndpoint(ws, log) | |
defer process.Terminate() | |
go process.ReadOutput(launched.stdout, config) | |
go wsEndpoint.ReadOutput(config) | |
go process.pipeStdErr(config) | |
pipeEndpoints(process, wsEndpoint, log) | |
} | |
func pipeEndpoints(process Endpoint, wsEndpoint *WebSocketEndpoint, log *LogScope) { | |
for { | |
select { | |
case msgFromProcess, ok := <-process.Output(): | |
if ok { | |
log.Trace("send<-", "%s", msgFromProcess) | |
if !wsEndpoint.Send(msgFromProcess) { | |
return | |
} | |
} else { | |
// TODO: Log exit code. Mechanism differs on different platforms. | |
log.Trace("process", "Process terminated") | |
return | |
} | |
case msgFromSocket, ok := <-wsEndpoint.Output(): | |
if ok { | |
log.Trace("recv->", "%s", msgFromSocket) | |
process.Send(msgFromSocket) | |
} else { | |
log.Trace("websocket", "WebSocket connection closed") | |
return | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2013 Joe Walnes and the websocketd team. | |
// All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
package libwebsocketd | |
import ( | |
"errors" | |
"io" | |
"os" | |
"os/exec" | |
"path/filepath" | |
"strings" | |
) | |
var ScriptNotFoundError = errors.New("script not found") | |
type URLInfo struct { | |
ScriptPath string | |
PathInfo string | |
FilePath string | |
} | |
func parsePath(path string, config *Config) (*URLInfo, error) { | |
if !config.UsingScriptDir { | |
return &URLInfo{"/", path, ""}, nil | |
} | |
parts := strings.Split(path[1:], "/") | |
urlInfo := &URLInfo{} | |
for i, part := range parts { | |
urlInfo.ScriptPath = strings.Join([]string{urlInfo.ScriptPath, part}, "/") | |
urlInfo.FilePath = filepath.Join(config.ScriptDir, urlInfo.ScriptPath) | |
isLastPart := i == len(parts)-1 | |
statInfo, err := os.Stat(urlInfo.FilePath) | |
// not a valid path | |
if err != nil { | |
return nil, ScriptNotFoundError | |
} | |
// at the end of url but is a dir | |
if isLastPart && statInfo.IsDir() { | |
return nil, ScriptNotFoundError | |
} | |
// we've hit a dir, carry on looking | |
if statInfo.IsDir() { | |
continue | |
} | |
// no extra args | |
if isLastPart { | |
return urlInfo, nil | |
} | |
// build path info from extra parts of url | |
urlInfo.PathInfo = "/" + strings.Join(parts[i+1:], "/") | |
return urlInfo, nil | |
} | |
panic("parsePath") | |
} | |
type LaunchedProcess struct { | |
cmd *exec.Cmd | |
stdin io.WriteCloser | |
stdout io.ReadCloser | |
stderr io.ReadCloser | |
} | |
func launchCmd(commandName string, commandArgs []string, env []string) (*LaunchedProcess, error) { | |
cmd := exec.Command(commandName, commandArgs...) | |
cmd.Env = env | |
stdout, err := cmd.StdoutPipe() | |
if err != nil { | |
return nil, err | |
} | |
stderr, err := cmd.StderrPipe() | |
if err != nil { | |
return nil, err | |
} | |
stdin, err := cmd.StdinPipe() | |
if err != nil { | |
return nil, err | |
} | |
err = cmd.Start() | |
if err != nil { | |
return nil, err | |
} | |
return &LaunchedProcess{cmd, stdin, stdout, stderr}, err | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2013 Joe Walnes and the websocketd team. | |
// All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
package libwebsocketd | |
import ( | |
"io/ioutil" | |
"os" | |
"path/filepath" | |
"testing" | |
) | |
func TestParsePathWithScriptDir(t *testing.T) { | |
baseDir, _ := ioutil.TempDir("", "websockets") | |
scriptDir := filepath.Join(baseDir, "foo", "bar") | |
scriptPath := filepath.Join(scriptDir, "baz.sh") | |
defer os.RemoveAll(baseDir) | |
if err := os.MkdirAll(scriptDir, os.ModePerm); err != nil { | |
t.Error("could not create ", scriptDir) | |
} | |
if _, err := os.Create(scriptPath); err != nil { | |
t.Error("could not create ", scriptPath) | |
} | |
config := new(Config) | |
config.UsingScriptDir = true | |
config.ScriptDir = baseDir | |
var res *URLInfo | |
var err error | |
// simple url | |
res, err = parsePath("/foo/bar/baz.sh", config) | |
if err != nil { | |
t.Error(err) | |
} | |
if res.ScriptPath != "/foo/bar/baz.sh" { | |
t.Error("scriptPath") | |
} | |
if res.PathInfo != "" { | |
t.Error("pathInfo") | |
} | |
if res.FilePath != scriptPath { | |
t.Error("filePath") | |
} | |
// url with extra path info | |
res, err = parsePath("/foo/bar/baz.sh/some/extra/stuff", config) | |
if err != nil { | |
t.Error(err) | |
} | |
if res.ScriptPath != "/foo/bar/baz.sh" { | |
t.Error("scriptPath") | |
} | |
if res.PathInfo != "/some/extra/stuff" { | |
t.Error("pathInfo") | |
} | |
if res.FilePath != scriptPath { | |
t.Error("filePath") | |
} | |
// non-existing file | |
_, err = parsePath("/foo/bar/bang.sh", config) | |
if err == nil { | |
t.Error("non-existing file should fail") | |
} | |
if err != ScriptNotFoundError { | |
t.Error("should fail with script not found") | |
} | |
// non-existing dir | |
_, err = parsePath("/hoohar/bang.sh", config) | |
if err == nil { | |
t.Error("non-existing dir should fail") | |
} | |
if err != ScriptNotFoundError { | |
t.Error("should fail with script not found") | |
} | |
} | |
func TestParsePathExplicitScript(t *testing.T) { | |
config := new(Config) | |
config.UsingScriptDir = false | |
res, err := parsePath("/some/path", config) | |
if err != nil { | |
t.Error(err) | |
} | |
if res.ScriptPath != "/" { | |
t.Error("scriptPath") | |
} | |
if res.PathInfo != "/some/path" { | |
t.Error("pathInfo") | |
} | |
if res.FilePath != "" { | |
t.Error("filePath") | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2013 Joe Walnes and the websocketd team. | |
// All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
package libwebsocketd | |
const ( | |
License = ` | |
Copyright (c) 2013, Joe Walnes and the websocketd authors. | |
All rights reserved. | |
Redistribution and use in source and binary forms, with or without | |
modification, are permitted provided that the following conditions are met: | |
1. Redistributions of source code must retain the above copyright notice, this | |
list of conditions and the following disclaimer. | |
2. Redistributions in binary form must reproduce the above copyright notice, | |
this list of conditions and the following disclaimer in the documentation | |
and/or other materials provided with the distribution. | |
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | |
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | |
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
` | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2013 Joe Walnes and the websocketd team. | |
// All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
package libwebsocketd | |
import ( | |
"sync" | |
"time" | |
) | |
type LogLevel int | |
const ( | |
LogDebug = iota | |
LogTrace | |
LogAccess | |
LogInfo | |
LogError | |
LogFatal | |
) | |
type LogFunc func(logScope *LogScope, level LogLevel, levelName string, category string, msg string, args ...interface{}) | |
type LogScope struct { | |
Parent *LogScope // Parent scope | |
MinLevel LogLevel // Minimum log level to write out. | |
Mutex *sync.Mutex // Should be shared across all LogScopes that write to the same destination. | |
Associated []AssocPair // Additional data associated with scope | |
LogFunc LogFunc | |
} | |
type AssocPair struct { | |
Key string | |
Value string | |
} | |
func (l *LogScope) Associate(key string, value string) { | |
l.Associated = append(l.Associated, AssocPair{key, value}) | |
} | |
func (l *LogScope) Debug(category string, msg string, args ...interface{}) { | |
l.LogFunc(l, LogDebug, "DEBUG", category, msg, args...) | |
} | |
func (l *LogScope) Trace(category string, msg string, args ...interface{}) { | |
l.LogFunc(l, LogTrace, "TRACE", category, msg, args...) | |
} | |
func (l *LogScope) Access(category string, msg string, args ...interface{}) { | |
l.LogFunc(l, LogAccess, "ACCESS", category, msg, args...) | |
} | |
func (l *LogScope) Info(category string, msg string, args ...interface{}) { | |
l.LogFunc(l, LogInfo, "INFO", category, msg, args...) | |
} | |
func (l *LogScope) Error(category string, msg string, args ...interface{}) { | |
l.LogFunc(l, LogError, "ERROR", category, msg, args...) | |
} | |
func (l *LogScope) Fatal(category string, msg string, args ...interface{}) { | |
l.LogFunc(l, LogFatal, "FATAL", category, msg, args...) | |
} | |
func (parent *LogScope) NewLevel(logFunc LogFunc) *LogScope { | |
return &LogScope{ | |
Parent: parent, | |
MinLevel: parent.MinLevel, | |
Mutex: parent.Mutex, | |
Associated: make([]AssocPair, 0), | |
LogFunc: logFunc} | |
} | |
func RootLogScope(minLevel LogLevel, logFunc LogFunc) *LogScope { | |
return &LogScope{ | |
Parent: nil, | |
MinLevel: minLevel, | |
Mutex: &sync.Mutex{}, | |
Associated: make([]AssocPair, 0), | |
LogFunc: logFunc} | |
} | |
func Timestamp() string { | |
return time.Now().Format(time.RFC1123Z) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2013 Joe Walnes and the websocketd team. | |
// All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
package libwebsocketd | |
import ( | |
"bufio" | |
"io" | |
"syscall" | |
) | |
type ProcessEndpoint struct { | |
process *LaunchedProcess | |
bufferedIn *bufio.Writer | |
output chan string | |
log *LogScope | |
} | |
func NewProcessEndpoint(process *LaunchedProcess, log *LogScope) *ProcessEndpoint { | |
return &ProcessEndpoint{ | |
process: process, | |
bufferedIn: bufio.NewWriter(process.stdin), | |
output: make(chan string), | |
log: log} | |
} | |
func (pe *ProcessEndpoint) Terminate() { | |
pe.process.stdin.Close() | |
err := pe.process.cmd.Process.Signal(syscall.SIGINT) | |
if err != nil { | |
pe.log.Debug("process", "Failed to Interupt process %v: %s, attempting to kill", pe.process.cmd.Process.Pid, err) | |
err = pe.process.cmd.Process.Kill() | |
if err != nil { | |
pe.log.Debug("process", "Failed to Kill process %v: %s", pe.process.cmd.Process.Pid, err) | |
} | |
} | |
pe.process.cmd.Wait() | |
if err != nil { | |
pe.log.Debug("process", "Failed to reap process %v: %s", pe.process.cmd.Process.Pid, err) | |
} | |
} | |
func (pe *ProcessEndpoint) Output() chan string { | |
return pe.output | |
} | |
func (pe *ProcessEndpoint) Send(msg string) bool { | |
pe.bufferedIn.WriteString(msg) | |
pe.bufferedIn.WriteString("\n") | |
pe.bufferedIn.Flush() | |
return true | |
} | |
func (pe *ProcessEndpoint) ReadOutput(input io.ReadCloser, config *Config) { | |
bufin := bufio.NewReader(input) | |
for { | |
str, err := bufin.ReadString('\n') | |
if err != nil { | |
if err != io.EOF { | |
pe.log.Error("process", "Unexpected STDOUT read from process: %s", err) | |
} else { | |
pe.log.Debug("process", "Process STDOUT closed") | |
} | |
break | |
} | |
pe.output <- trimEOL(str) | |
} | |
close(pe.output) | |
} | |
func (pe *ProcessEndpoint) pipeStdErr(config *Config) { | |
bufstderr := bufio.NewReader(pe.process.stderr) | |
for { | |
str, err := bufstderr.ReadString('\n') | |
if err != nil { | |
if err != io.EOF { | |
pe.log.Error("process", "Unexpected STDERR read from process: %s", err) | |
} else { | |
pe.log.Debug("process", "Process STDERR closed") | |
} | |
break | |
} | |
pe.log.Error("stderr", "%s", trimEOL(str)) | |
} | |
} | |
func trimEOL(s string) string { | |
// Handles unixy style \n and windowsy style \r\n | |
trimCount := 0 | |
if len(s) > 0 && s[len(s)-1] == '\n' { | |
trimCount = 1 | |
if len(s) > 1 && s[len(s)-2] == '\r' { | |
trimCount = 2 | |
} | |
} | |
if trimCount == 0 { | |
return s | |
} | |
return s[0 : len(s)-trimCount] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2013 Joe Walnes and the websocketd team. | |
// All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
package libwebsocketd | |
import ( | |
"io" | |
"code.google.com/p/go.net/websocket" | |
) | |
type WebSocketEndpoint struct { | |
ws *websocket.Conn | |
output chan string | |
log *LogScope | |
} | |
func NewWebSocketEndpoint(ws *websocket.Conn, log *LogScope) *WebSocketEndpoint { | |
return &WebSocketEndpoint{ | |
ws: ws, | |
output: make(chan string), | |
log: log} | |
} | |
func (we *WebSocketEndpoint) Terminate() { | |
} | |
func (we *WebSocketEndpoint) Output() chan string { | |
return we.output | |
} | |
func (we *WebSocketEndpoint) Send(msg string) bool { | |
err := websocket.Message.Send(we.ws, msg) | |
if err != nil { | |
we.log.Trace("websocket", "Cannot send: %s", err) | |
return false | |
} | |
return true | |
} | |
func (we *WebSocketEndpoint) ReadOutput(config *Config) { | |
for { | |
var msg string | |
err := websocket.Message.Receive(we.ws, &msg) | |
if err != nil { | |
if err != io.EOF { | |
we.log.Debug("websocket", "Cannot receive: %s", err) | |
} | |
break | |
} | |
we.output <- msg | |
} | |
close(we.output) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment