Skip to content

Commit

Permalink
validate mode
Browse files Browse the repository at this point in the history
  • Loading branch information
sgrimee committed Aug 3, 2016
1 parent 93113d6 commit 260e388
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 99 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## v0.2.0
- optional cryptographic validation of incoming webhooks (-validate option)
- server runs either standard or ssl, not both

## v0.1.0
- support encrypted websockets

Expand Down
3 changes: 1 addition & 2 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# To do

- enable webhook authentication secret for spark: https://developer.ciscospark.com/webhooks-explained.html
- encrypted websockets
- tests for SSL mode
- make healthz endpoint serve what is needs to serve to be a real healthz endpoint
42 changes: 0 additions & 42 deletions connections.go

This file was deleted.

2 changes: 1 addition & 1 deletion healthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func healthzServe(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
s := &HealthzResponse{
NbOpenHooks: connCount(),
NbOpenHooks: sessionsCount(),
}
if err := json.NewEncoder(w).Encode(s); err != nil {
panic(err)
Expand Down
15 changes: 15 additions & 0 deletions messages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

// json that is sent back to the websocket client open connection
type OpenHookResponse struct {
Url string `json:"url"`
}

// acceptable json to foward
// Fully open for now
type HookMsg interface{}

// json sent back on /status requests
type StatusResponse struct {
NbOpenHooks int `json:"nb_open_hooks"`
}
46 changes: 46 additions & 0 deletions sessions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

// concurrent management of open sessions

import (
"log"
"sync"

"golang.org/x/net/websocket"
)

type Session struct {
Connection *websocket.Conn
Secret []byte
}

var (
mu sync.Mutex
sessions = make(map[string]Session)
)

func session(id string) (Session, bool) {
mu.Lock()
defer mu.Unlock()
s, ok := sessions[id]
return s, ok
}

func setSession(id string, s Session) {
mu.Lock()
defer mu.Unlock()
sessions[id] = s
}

func deleteSession(id string) {
log.Printf("Removing websocket: %s\n", id)
mu.Lock()
defer mu.Unlock()
delete(sessions, id)
}

func sessionsCount() int {
mu.Lock()
defer mu.Unlock()
return len(sessions)
}
17 changes: 17 additions & 0 deletions signature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import (
"crypto/hmac"
"crypto/sha1"
)

// CheckMAC reports whether messageSig is a valid HMAC tag for message.
func validSignature(message, messageSig, secret []byte) bool {
mac := hmac.New(sha1.New, secret)
mac.Write(message)
expectedMAC := mac.Sum(nil)
// log.Printf("secret: %s", string(secret))
// log.Printf("messageSig: %x", string(messageSig))
// log.Printf("computedSig: %x", string(expectedMAC))
return hmac.Equal(messageSig, expectedMAC)
}
38 changes: 26 additions & 12 deletions webhooks.go
Original file line number Diff line number Diff line change
@@ -1,40 +1,48 @@
package main

import (
"encoding/hex"
"encoding/json"
"html"
"io"
"io/ioutil"
"log"
"net/http"

"golang.org/x/net/websocket"
)

const (
webhooksPath = "webhooks"
)

// acceptable json to foward
// Fully open for now
type HookMsg interface{}

// hookServe hanles an incoming webhook by reading json from it and
// passing it on to the corresponding open websocket
// then the webhook connection is closed
func hookServe(hw http.ResponseWriter, r *http.Request) {
log.Printf("Incoming webhook from %s: %q\n", r.RemoteAddr, html.EscapeString(r.URL.Path))
hSig := r.Header.Get("X-Spark-Signature")
sig, err := hex.DecodeString(hSig)
if err != nil {
log.Printf("Unable to decode signature: %q", hSig)
hw.WriteHeader(http.StatusNotAcceptable)
return
}
if config.Validate && sig == nil {
log.Println("Discarding webhook because non hex or missing X-Spark-Signature and Validation mode is on")
hw.WriteHeader(http.StatusNotAcceptable)
return
}
log.Printf(" with signature: %q\n", sig)
id := r.URL.Path[len("/"+webhooksPath+"/"):]
var ws *websocket.Conn
var s Session
var ok bool
if ws, ok = conn(id); !ok {
if s, ok = session(id); !ok {
log.Printf("Error: webhook %s not found.\n", id)
hw.WriteHeader(http.StatusNotFound)
return
}
if ws == nil {
if s.Connection == nil {
log.Printf("Error: webhook %s points to invalid websocket.\n", id)
deleteConn(id)
deleteSession(id)
hw.WriteHeader(http.StatusNotFound)
return
}
Expand All @@ -45,6 +53,12 @@ func hookServe(hw http.ResponseWriter, r *http.Request) {
if err := r.Body.Close(); err != nil {
panic(err)
}
log.Printf("Body: \n%s\n", body)
if config.Validate && !validSignature(body, sig, s.Secret) {
log.Println("Discarding webhook because X-Spark-Signature is not valid.")
hw.WriteHeader(http.StatusNotAcceptable)
return
}
// we parse the message as a means of validating its json
var hm HookMsg
if err := json.Unmarshal(body, &hm); err != nil {
Expand All @@ -62,9 +76,9 @@ func hookServe(hw http.ResponseWriter, r *http.Request) {
ev.Data = hm

hw.Header().Set("Content-Type", "application/json; charset=UTF-8")
if err = json.NewEncoder(ws).Encode(ev); err != nil {
if err = json.NewEncoder(s.Connection).Encode(ev); err != nil {
log.Printf("Could not send proxied json from %s to websocket: %s", id, err)
deleteConn(id)
deleteSession(id)
hw.WriteHeader(http.StatusNotFound)
}
hw.WriteHeader(http.StatusOK)
Expand Down
22 changes: 12 additions & 10 deletions websockets.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,21 @@ type SocketResponse struct {

// wsServe gemerates a private webhook endpoint for each incoming websocket
// The websocket is kept open so incoming webhook data can be proxied to it
func wsServe(ws *websocket.Conn) {
func wsServe(c *websocket.Conn) {
defer c.Close()
id := NewUid()
setConn(id, ws) // keep track of open sessions
secret := []byte(NewUid())
setSession(id, Session{c, secret})
defer deleteSession(id)
// send private webhook endpoint to client
data := SocketResponse{Url: fmt.Sprintf("http://%s:%d/%s/%s",
config.Host, config.Port, webhooksPath, id)}
log.Printf("Incoming websocket from %s, sending: %+v\n", ws.Request().RemoteAddr, data)
websocket.JSON.Send(ws, data)
data := SocketResponse{
Url: fmt.Sprintf("%s://%s:%d/%s/%s", scheme, config.Host, config.Port, webhooksPath, id),
Secret: string(secret),
}
log.Printf("Incoming websocket from %s, sending: %+v\n", c.Request().RemoteAddr, data)
websocket.JSON.Send(c, data)
// read forever on websocket to keep it open
var msg []byte
for _, err := ws.Read(msg); err == nil; time.Sleep(1 * time.Second) {
for _, err := c.Read(msg); err == nil; time.Sleep(1 * time.Second) {
}
log.Printf("Releasing websocket: %s\n", id)
deleteConn(id)
ws.Close()
}
36 changes: 17 additions & 19 deletions whproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,48 +17,46 @@ type Config struct {
CertFile,
KeyFile,
Host string
Port,
SSLPort int
Port int
Validate bool
}

var (
config Config
ssl = false
scheme = "http"
)

func main() {
flag.StringVar(&config.CertFile, "cert", "", "certificate file")
flag.StringVar(&config.KeyFile, "key", "", "key file")
flag.StringVar(&config.CertFile, "cert", "", "certificate file (with 'key', activates ssl)")
flag.StringVar(&config.KeyFile, "key", "", "key file (with 'cert', activates ssl)")
flag.StringVar(&config.Host, "host", "localhost", "hostname for webhook url")
flag.IntVar(&config.Port, "port", 12345, "port for the webhook server")
flag.IntVar(&config.SSLPort, "sslport", 12346, "SSL port for the webhook server")
flag.BoolVar(&config.Validate, "validate", false, "validate signature of incoming webhooks (WIP)")
showVer := flag.Bool("version", false, "show server version and exit")
flag.Parse()
if *showVer {
fmt.Println("Version: ", version)
return
}
ListenAndServe()
if (config.CertFile != "") && (config.KeyFile != "") {
ssl = true
scheme = "https"
}
ListenAndServe(ssl)
}

func ListenAndServe() {
var errs = make(chan error)

func ListenAndServe(ssl bool) {
http.Handle("/"+websocketPath, websocket.Handler(wsServe))
http.HandleFunc("/"+webhooksPath+"/", hookServe)
http.HandleFunc("/"+healthzPath, healthzServe)

go func() {
if ssl {
log.Printf("SSL Server starting on %s:%d\n", config.Host, config.Port)
log.Fatal(http.ListenAndServeTLS(fmt.Sprintf(":%d", config.Port),
config.CertFile, config.KeyFile, nil))
} else {
log.Printf("Server starting on %s:%d\n", config.Host, config.Port)
errs <- http.ListenAndServe(fmt.Sprintf(":%d", config.Port), nil)
}()
if (config.CertFile != "") && (config.KeyFile != "") {
go func() {
log.Printf("SSL Server starting on %s:%d\n", config.Host, config.SSLPort)
errs <- http.ListenAndServeTLS(fmt.Sprintf(":%d", config.SSLPort),
config.CertFile, config.KeyFile, nil)
}()
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", config.Port), nil))
}
log.Fatal(<-errs) // block until one of the servers exits
}
Loading

0 comments on commit 260e388

Please sign in to comment.