Skip to content

Commit

Permalink
Initial TLS support for joewalnes#17
Browse files Browse the repository at this point in the history
* added command line flags for ssl mode
* HTTPS=on variable to environment
* HTTPS variable to examples
* default port 443 or 80 depending on ssl flag

Also added support for re-writing SERVER_SOFTWARE in cgi handler.

Also fixes joewalnes#32 bug where Firefox was not able to render devconsole due to use of non standard-prescribed innerText property.
  • Loading branch information
Alex Sergeyev committed Feb 28, 2014
1 parent 7a2919a commit 2df0c30
Show file tree
Hide file tree
Showing 15 changed files with 117 additions and 34 deletions.
42 changes: 36 additions & 6 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import (
)

type Config struct {
BasePath string // Base URL path. e.g. "/"
Addr []string // TCP addresses to listen on. e.g. ":1234", "1.2.3.4:1234" or "[::1]:1234"
LogLevel libwebsocketd.LogLevel
BasePath string // Base URL path. e.g. "/"
Addr []string // TCP addresses to listen on. e.g. ":1234", "1.2.3.4:1234" or "[::1]:1234"
LogLevel libwebsocketd.LogLevel
CertFile, KeyFile string
*libwebsocketd.Config
}

Expand All @@ -44,7 +45,7 @@ func parseCommandLine() Config {
flag.Var(&addrlist, "address", "Interfaces to bind to (e.g. 127.0.0.1 or [::1]).")

// server config options
portFlag := flag.Int("port", 80, "HTTP port to listen on")
portFlag := flag.Int("port", 0, "HTTP port to listen on")
versionFlag := flag.Bool("version", false, "Print version and exit")
licenseFlag := flag.Bool("license", false, "Print license and exit")
logLevelFlag := flag.String("loglevel", "access", "Log level, one of: debug, trace, access, info, error, fatal")
Expand All @@ -56,16 +57,28 @@ func parseCommandLine() Config {
staticDirFlag := flag.String("staticdir", "", "Serve static content from this directory over HTTP")
cgiDirFlag := flag.String("cgidir", "", "Serve CGI scripts from this directory over HTTP")
devConsoleFlag := flag.Bool("devconsole", false, "Enable development console (cannot be used in conjunction with --staticdir)")
sslFlag := flag.Bool("ssl", false, "Use TLS on listening socket (see also --sslcert and --sslkey)")
sslCert := flag.String("sslcert", "", "Should point to certificate PEM file when --ssl is used")
sslKey := flag.String("sslkey", "", "Should point to certificate private key file when --ssl is used")

flag.Parse()

port := *portFlag
if port == 0 {
if *sslFlag {
port = 443
} else {
port = 80
}
}

if socknum := len(addrlist); socknum != 0 {
mainConfig.Addr = make([]string, socknum)
for i, addrSingle := range addrlist {
mainConfig.Addr[i] = fmt.Sprintf("%s:%d", addrSingle, *portFlag)
mainConfig.Addr[i] = fmt.Sprintf("%s:%d", addrSingle, port)
}
} else {
mainConfig.Addr = []string{fmt.Sprintf(":%d", *portFlag)}
mainConfig.Addr = []string{fmt.Sprintf(":%d", port)}
}
mainConfig.BasePath = *basePathFlag

Expand Down Expand Up @@ -94,6 +107,7 @@ func parseCommandLine() Config {
}

config.ReverseLookup = *reverseLookupFlag
config.Ssl = *sslFlag
config.ScriptDir = *scriptDirFlag
config.StaticDir = *staticDirFlag
config.CgiDir = *cgiDirFlag
Expand All @@ -117,6 +131,22 @@ func parseCommandLine() Config {
os.Exit(2)
}

// Reading SSL options
if config.Ssl {
if *sslCert == "" || *sslKey == "" {
fmt.Fprintf(os.Stderr, "Please specify both --sslcert and --sslkey when requesting --ssl.\n")
os.Exit(1)
}
} else {
if *sslCert != "" || *sslKey != "" {
fmt.Fprintf(os.Stderr, "You should not be using --ssl* flags when there is no --ssl option.\n")
os.Exit(1)
}
}

mainConfig.CertFile = *sslCert
mainConfig.KeyFile = *sslKey

args := flag.Args()
if len(args) < 1 && config.ScriptDir == "" && config.StaticDir == "" && config.CgiDir == "" {
fmt.Fprintf(os.Stderr, "Please specify COMMAND or provide --dir, --staticdir or --cgidir argument.\n")
Expand Down
1 change: 1 addition & 0 deletions examples/bash/dump-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ NAMES="""
SERVER_PROTOCOL
SERVER_SOFTWARE
UNIQUE_ID
HTTPS
"""

for NAME in ${NAMES}
Expand Down
3 changes: 2 additions & 1 deletion examples/cgi-bin/dump-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ NAMES="""
SERVER_PROTOCOL
SERVER_SOFTWARE
UNIQUE_ID
HTTPS
"""

echo "Content-type: text/plain"
Expand All @@ -39,4 +40,4 @@ do
done

# Additional HTTP headers
env | grep '^HTTP_'
env | egrep '^(HTTP|SSL)_'
1 change: 1 addition & 0 deletions examples/perl/dump-env.pl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
SERVER_PROTOCOL
SERVER_SOFTWARE
UNIQUE_ID
HTTPS
);

for my $name (@names) {
Expand Down
3 changes: 2 additions & 1 deletion examples/php/dump-env.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
'SERVER_PORT',
'SERVER_PROTOCOL',
'SERVER_SOFTWARE',
'UNIQUE_ID'
'UNIQUE_ID',
'HTTPS'
);

foreach ($names as $name) {
Expand Down
3 changes: 2 additions & 1 deletion examples/python/dump-env.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
'SERVER_PORT',
'SERVER_PROTOCOL',
'SERVER_SOFTWARE',
'UNIQUE_ID'
'UNIQUE_ID',
'HTTPS'
]
for var_name in var_names:
print '%s=%s' % (var_name, os.environ.get(var_name, '<unset>'))
Expand Down
1 change: 1 addition & 0 deletions examples/ruby/dump-env.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
'SERVER_PROTOCOL',
'SERVER_SOFTWARE',
'UNIQUE_ID',
'HTTPS',
]

names.each do |name|
Expand Down
3 changes: 2 additions & 1 deletion examples/windows-jscript/dump-env.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ var names = [
'SERVER_PORT',
'SERVER_PROTOCOL',
'SERVER_SOFTWARE',
'UNIQUE_ID'
'UNIQUE_ID',
'HTTPS'
];

var shell = WScript.CreateObject("WScript.Shell");
Expand Down
3 changes: 2 additions & 1 deletion examples/windows-vbscript/dump-env.vbs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ names = Array(_
"SERVER_PORT", _
"SERVER_PROTOCOL", _
"SERVER_SOFTWARE", _
"UNIQUE_ID"_
"UNIQUE_ID", _
"HTTPS"_
)

set shell = WScript.CreateObject("WScript.Shell")
Expand Down
5 changes: 5 additions & 0 deletions help.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ Options:
Use square brackets to specify IPv6 address.
Default: "" (all)
--ssl Listen for HTTPS socket instead of regular HTTP.
--sslcert=FILE All three options must be used together or all of
--sslkey=FILE them should be omitted.
--basepath=PATH Base path in URLs to serve from.
Default: / (root of domain)
Expand Down
1 change: 1 addition & 0 deletions libwebsocketd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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).
Ssl bool // websocketd works with --ssl which means TLS is in use
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).
Expand Down
6 changes: 3 additions & 3 deletions libwebsocketd/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Full documentation at http://websocketd.com/
<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">
<input class="url" type="text" value="{{addr}}" spellcheck="false">
</div>
</header>
Expand Down Expand Up @@ -216,8 +216,8 @@ Full documentation at http://websocketd.com/
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-type').textContent = type;
el.querySelector('.message-data').textContent = data || '';
el.querySelector('.message-data').innerHTML += '&nbsp;';
el.scrollIntoView(true);
}
Expand Down
46 changes: 34 additions & 12 deletions libwebsocketd/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import (
"strconv"
"strings"
"time"

"code.google.com/p/go.net/websocket"
)

const (
Expand Down Expand Up @@ -49,27 +47,37 @@ func remoteDetails(req *http.Request, config *Config) (string, string, string, e
return remoteAddr, remoteHost, remotePort, nil
}

func createEnv(ws *websocket.Conn, config *Config, urlInfo *URLInfo, id string) ([]string, error) {
req := ws.Request()
func createEnv(req *http.Request, config *Config, urlInfo *URLInfo, id string, log *LogScope) ([]string, error) {
headers := req.Header

url := req.URL

remoteAddr, remoteHost, remotePort, err := remoteDetails(ws.Request(), config)
remoteAddr, remoteHost, remotePort, err := remoteDetails(req, config)
if err != nil {
return nil, err
}

serverName, serverPort, err := net.SplitHostPort(req.Host)
if err != nil {
if !strings.Contains(req.Host, ":") {
// Without hijacking socket connection we cannot know port for sure.
if addrerr, ok := err.(*net.AddrError); ok && strings.Contains(addrerr.Err, "missing port") {
serverName = req.Host
serverPort = "80"
if config.Ssl {
serverPort = "443"
} else {
serverPort = "80"
}
} else {
return nil, err
// this does mean that we cannot detect port from Host: header... Just keep going with ""
serverPort = ""
}
}

standardEnvCount := 20
if config.Ssl {
standardEnvCount += 1
}

parentEnv := os.Environ()
env := make([]string, 0, len(headers)+standardEnvCount+len(parentEnv)+len(config.Env))
for _, v := range parentEnv {
Expand Down Expand Up @@ -114,15 +122,28 @@ func createEnv(ws *websocket.Conn, config *Config, urlInfo *URLInfo, id string)
// CONTENT_LENGTH, CONTENT_TYPE
// -- makes no sense for WebSocket connections.
//
// HTTPS, SSL_*
// -- SSL not supported
// SSL_*
// -- SSL variables are not supported, HTTPS=on added for websocketd running with --ssl

if config.Ssl {
env = appendEnv(env, "HTTPS", "on")
}

for k, _ := range headers {
env = appendEnv(env, fmt.Sprintf("HTTP_%s", headerDashToUnderscore.Replace(k)), headers[k]...)
if log.MinLevel == LogDebug {
for k, v := range env {
log.Debug("env", "Std. variable: %s %v", k, v)
}
}

for k, hdrs := range headers {
header := fmt.Sprintf("HTTP_%s", headerDashToUnderscore.Replace(k))
env = appendEnv(env, header, hdrs...)
log.Debug("env", "Header variable %s=%v", header, hdrs)
}

for _, v := range config.Env {
env = append(env, v)
log.Debug("env", "External variable: %s %v", env, v)
}

return env, nil
Expand All @@ -133,6 +154,7 @@ 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)))
Expand Down
19 changes: 14 additions & 5 deletions libwebsocketd/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,15 @@ type HttpWsMuxHandler struct {

// Main HTTP handler. Muxes between WebSocket handler, DevConsole or 404.
func (h HttpWsMuxHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
log := h.Log.NewLevel(h.Log.LogFunc)

hdrs := req.Header

log := h.Log.NewLevel(h.Log.LogFunc)
log.Associate("url", fmt.Sprintf("http://%s%s", req.Host, req.URL.RequestURI()))
schema := "http"
if h.Config.Ssl {
schema = "https"
}
log.Associate("url", fmt.Sprintf("%s://%s%s", schema, req.Host, req.URL.RequestURI()))

_, remoteHost, _, err := remoteDetails(req, h.Config)
if err != nil {
Expand Down Expand Up @@ -61,18 +66,22 @@ func (h HttpWsMuxHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {

// Dev console (if enabled)
if h.Config.DevConsole {
content := strings.Replace(ConsoleContent, "{{license}}", License, -1)
content := ConsoleContent
content = strings.Replace(content, "{{license}}", License, -1)
content = strings.Replace(content, "{{addr}}", fmt.Sprintf("%s://%s", schema, req.Host), -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,
Env: []string{
"SERVER_SOFTWARE=" + h.Config.ServerSoftware,
},
}
log.Associate("cgiscript", filePath)
log.Access("http", "CGI")
Expand Down Expand Up @@ -113,7 +122,7 @@ func acceptWebSocket(ws *websocket.Conn, config *Config, log *LogScope) {
}
log.Debug("session", "URLInfo: %s", urlInfo)

env, err := createEnv(ws, config, urlInfo, id)
env, err := createEnv(ws.Request(), config, urlInfo, id, log)
if err != nil {
log.Error("process", "Could not create ENV: %s", err)
return
Expand Down
14 changes: 11 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,24 @@ func main() {
}

rejects := make(chan error, 1)
wsschema, httpschema := "ws", "http"
if config.Ssl {
wsschema, httpschema = "wss", "https"
}
for _, addrSingle := range config.Addr {
log.Info("server", "Starting WebSocket server : ws://%s%s", addrSingle, config.BasePath)
log.Info("server", "Starting WebSocket server : %s://%s%s", wsschema, addrSingle, config.BasePath)
if config.DevConsole {
log.Info("server", "Developer console enabled : http://%s/", addrSingle)
log.Info("server", "Developer console enabled : %s://%s/", httpschema, addrSingle)
}
// ListenAndServe is blocking function. Let's run it in
// go routine, reporting result to control channel.
// Since it's blocking it'll never return non-error.
go func(addr string) {
rejects <- http.ListenAndServe(addr, nil)
if config.Ssl {
rejects <- http.ListenAndServeTLS(addr, config.CertFile, config.KeyFile, nil)
} else {
rejects <- http.ListenAndServe(addr, nil)
}
}(addrSingle)
}
select {
Expand Down

0 comments on commit 2df0c30

Please sign in to comment.