Skip to content

Instantly share code, notes, and snippets.

@wzdf1982
Created December 16, 2013 09:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wzdf1982/7984648 to your computer and use it in GitHub Desktop.
Save wzdf1982/7984648 to your computer and use it in GitHub Desktop.
libwebsocketd
// 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").
}
// 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">&times;</button>
<button class="connect" title="Connect" style="display:none">&#x2714;</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 &#xbb;</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 += '&nbsp;';
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>
`
)
// 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
}
// 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, ", ")))
}
// 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
}
}
}
}
// 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
}
// 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")
}
}
// 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.
`
)
// 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)
}
// 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]
}
// 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