15 Commits
25 changed files with 754 additions and 375 deletions
+2 -1
View File
@@ -157,4 +157,5 @@ ignore_http/*
config.json config.json
*.pem *.pem
build build
.vscode/*
debug
+18 -16
View File
@@ -47,24 +47,24 @@ $ go install github.com/dimiro1/ipe
## The server ## The server
```json ```javascript
{ {
"Host": ":8080", "Host": ":8080", // Required
"SSL": false, "SSL": false, // Required but can be false
"SSLHost": ":4433", "SSLHost": ":4433", // Required if SSL is true
"SSLKeyFile": "A key.pem file", "SSLKeyFile": "A key.pem file", // Required if SSL is true
"SSLCertFile": "A cert.pem file", "SSLCertFile": "A cert.pem file", // Required if SSL is true
"Apps": [ "Apps": [ // Required, A Json arrays with multiple apps
{ {
"ApplicationDisabled": false, "ApplicationDisabled": false, // Required but can be false
"Secret": "A really secret random string", "Secret": "A really secret random string", // Required
"Key": "A random Key string", "Key": "A random Key string", // Required
"OnlySSL": false, "OnlySSL": false, // Required but can be false
"Name": "The app name", "Name": "The app name", // Required
"AppID": "The app ID", "AppID": "The app ID", // Required
"UserEvents": true, "UserEvents": true, // Required but can be false
"WebHooks": true, "WebHooks": true, // Required but can be false
"URLWebHook": "Some URL to send webhooks" "URLWebHook": "Some URL to send webhooks" // Required if WebHooks is true
} }
] ]
} }
@@ -79,6 +79,8 @@ $ go install github.com/dimiro1/ipe
var pusher = new Pusher(APP_KEY, { var pusher = new Pusher(APP_KEY, {
wsHost: 'localhost', wsHost: 'localhost',
wsPort: 8080, wsPort: 8080,
wssPort: 4433, // Required if encrypted is true
encrypted: false, // Optional. the application must use only SSL connections
enabledTransports: ["ws", "flash"], enabledTransports: ["ws", "flash"],
disabledTransports: ["flash"] disabledTransports: ["flash"]
}); });
+1 -1
View File
@@ -4,7 +4,7 @@
require 'rake/clean' require 'rake/clean'
VERSION = 'v1.0.0' VERSION = 'v1.1.0'
GITHASH = `git rev-parse --short HEAD` GITHASH = `git rev-parse --short HEAD`
DATE = Time.now.strftime '%Y%m%d%H%M%S' DATE = Time.now.strftime '%Y%m%d%H%M%S'
+1 -1
View File
@@ -1,2 +1,2 @@
client: go run client.go client: go run client.go
server: go run ../main.go -config ./functional-config.json -logtostderr server: go run ../main.go -config ./functional-config.json -alsologtostderr
+2 -1
View File
@@ -17,7 +17,8 @@ function getPusher(auth) {
wsPort: PORT, wsPort: PORT,
authEndpoint: auth, authEndpoint: auth,
enabledTransports: ["ws"], enabledTransports: ["ws"],
disabledTransports: ["flash"] disabledTransports: ["flash"],
cluster: "hello", // Should be ignored
}); });
} }
+19 -5
View File
@@ -27,17 +27,31 @@ type app struct {
WebHooks bool WebHooks bool
URLWebHook string URLWebHook string
Channels map[string]*channel `json:"-"` Channels map[string]*channel
Connections map[string]*connection `json:"-"` Connections map[string]*connection
Stats *expvar.Map `json:"-"` Stats *expvar.Map
} }
// Alloc memory for Connections and Channels func newApp(name, appID, key, secret string, onlySSL, disabled, userEvents, webHooks bool, webHookURL string) *app {
func (a *app) Init() {
a := &app{
Name: name,
AppID: appID,
Key: key,
Secret: secret,
OnlySSL: onlySSL,
ApplicationDisabled: disabled,
UserEvents: userEvents,
WebHooks: webHooks,
URLWebHook: webHookURL,
}
a.Connections = make(map[string]*connection) a.Connections = make(map[string]*connection)
a.Channels = make(map[string]*channel) a.Channels = make(map[string]*channel)
a.Stats = expvar.NewMap(fmt.Sprintf("%s (%s)", a.Name, a.AppID)) a.Stats = expvar.NewMap(fmt.Sprintf("%s (%s)", a.Name, a.AppID))
return a
} }
// Only Presence channels // Only Presence channels
+51 -52
View File
@@ -11,172 +11,171 @@ import (
var id = 0 var id = 0
func newApp() *app { func newTestApp() *app {
a := app{Name: "Test", AppID: strconv.Itoa(id), Key: "123", Secret: "123", OnlySSL: false, ApplicationDisabled: false, UserEvents: true}
a.Init()
a := newApp("Test", strconv.Itoa(id), "123", "123", false, false, true, false, "")
id++ id++
return &a
return a
} }
func TestConnect(t *testing.T) { func TestConnect(t *testing.T) {
app := newApp() app := newTestApp()
app.Connect(newConnection("socketID", nil)) app.Connect(newConnection("socketID", mockSocket{}))
if len(app.Connections) != 1 { if len(app.Connections) != 1 {
t.Errorf("Connections must be 1, but was %d", len(app.Connections)) t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 1)
} }
} }
func TestDisconnect(t *testing.T) { func TestDisconnect(t *testing.T) {
app := newApp() app := newTestApp()
app.Connect(newConnection("socketID", nil)) app.Connect(newConnection("socketID", mockSocket{}))
app.Disconnect("socketID") app.Disconnect("socketID")
if len(app.Connections) != 0 { if len(app.Connections) != 0 {
t.Errorf("Connections must be 0, but was %d", len(app.Connections)) t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 0)
} }
} }
func TestFindConnection(t *testing.T) { func TestFindConnection(t *testing.T) {
app := newApp() app := newTestApp()
app.Connect(newConnection("socketID", nil)) app.Connect(newConnection("socketID", mockSocket{}))
if _, err := app.FindConnection("socketID"); err != nil { if _, err := app.FindConnection("socketID"); err != nil {
t.Error("Must find Connection") t.Errorf("app.FindConnection('socketID') == _, %q, wants %v", err, nil)
} }
if _, err := app.FindConnection("NotFound"); err == nil { if _, err := app.FindConnection("NotFound"); err == nil {
t.Error("Must not found Connection") t.Errorf("app.FindConnection('socketID') == _, %q, wants !nil", err)
} }
} }
func TestFindChannelByChannelID(t *testing.T) { func TestFindChannelByChannelID(t *testing.T) {
app := newApp() app := newTestApp()
channel := newChannel("ID") channel := newChannel("ID")
app.AddChannel(channel) app.AddChannel(channel)
if _, err := app.FindChannelByChannelID("ID"); err != nil { if _, err := app.FindChannelByChannelID("ID"); err != nil {
t.Error("Channel not found") t.Errorf("app.FindChannelByChannelID('ID') == _, %q, wants %v", err, nil)
} }
} }
func TestFindOrCreateChannelByChannelID(t *testing.T) { func TestFindOrCreateChannelByChannelID(t *testing.T) {
app := newApp() app := newTestApp()
if len(app.Channels) != 0 { if len(app.Channels) != 0 {
t.Error("Length of channels must be 0 before test") t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0)
} }
app.FindOrCreateChannelByChannelID("ID") app.FindOrCreateChannelByChannelID("ID")
if len(app.Channels) != 1 { if len(app.Channels) != 1 {
t.Error("Length of channels must be 1 after test") t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 1)
} }
} }
func TestRemoveChannel(t *testing.T) { func TestRemoveChannel(t *testing.T) {
app := newApp() app := newTestApp()
if len(app.Channels) != 0 { if len(app.Channels) != 0 {
t.Error("Length of channels must be 0 before test") t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0)
} }
channel := newChannel("ID") channel := newChannel("ID")
app.AddChannel(channel) app.AddChannel(channel)
if len(app.Channels) != 1 { if len(app.Channels) != 1 {
t.Error("Length of channels after insert must be 1") t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 1)
} }
app.RemoveChannel(channel) app.RemoveChannel(channel)
if len(app.Channels) != 0 { if len(app.Channels) != 0 {
t.Error("Length of channels must be 0 after remove") t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0)
} }
} }
func Test_add_channels(t *testing.T) { func Test_add_channels(t *testing.T) {
app := newApp() app := newTestApp()
// Public // Public
if len(app.PublicChannels()) != 0 { if len(app.PublicChannels()) != 0 {
t.Error("Length of public channels must be 0 before test") t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
} }
app.AddChannel(newChannel("ID")) app.AddChannel(newChannel("ID"))
if len(app.PublicChannels()) != 1 { if len(app.PublicChannels()) != 1 {
t.Error("Length os public channels after insert must be 1") t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
} }
// Presence // Presence
if len(app.PresenceChannels()) != 0 { if len(app.PresenceChannels()) != 0 {
t.Error("Length of presence channels must be 0 before test") t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
} }
app.AddChannel(newChannel("presence-test")) app.AddChannel(newChannel("presence-test"))
if len(app.PresenceChannels()) != 1 { if len(app.PresenceChannels()) != 1 {
t.Error("Length os presence channels after insert must be 1") t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
} }
// Private // Private
if len(app.PrivateChannels()) != 0 { if len(app.PrivateChannels()) != 0 {
t.Error("Length of private channels must be 0 before test") t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
} }
app.AddChannel(newChannel("private-test")) app.AddChannel(newChannel("private-test"))
if len(app.PrivateChannels()) != 1 { if len(app.PrivateChannels()) != 1 {
t.Error("Length os private channels after insert must be 1") t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
} }
} }
func Test_AllChannels(t *testing.T) { func Test_AllChannels(t *testing.T) {
app := newApp() app := newTestApp()
app.AddChannel(newChannel("private-test")) app.AddChannel(newChannel("private-test"))
app.AddChannel(newChannel("presence-test")) app.AddChannel(newChannel("presence-test"))
app.AddChannel(newChannel("test")) app.AddChannel(newChannel("test"))
if len(app.Channels) != 3 { if len(app.Channels) != 3 {
t.Error("Must have 3 channels") t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 3)
} }
} }
func Test_New_Subscriber(t *testing.T) { func Test_New_Subscriber(t *testing.T) {
app := newApp() app := newTestApp()
if len(app.Connections) != 0 { if len(app.Connections) != 0 {
t.Error("Length of subscribers before test must be 0") t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 0)
} }
conn := newConnection("1", nil) conn := newConnection("1", mockSocket{})
app.Connect(conn) app.Connect(conn)
if len(app.Connections) != 1 { if len(app.Connections) != 1 {
t.Error("Length os subscribers after test must be 1") t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 1)
} }
} }
func Test_find_subscriber(t *testing.T) { func Test_find_subscriber(t *testing.T) {
app := newApp() app := newTestApp()
conn := newConnection("1", nil) conn := newConnection("1", mockSocket{})
app.Connect(conn) app.Connect(conn)
conn, err := app.FindConnection("1") conn, err := app.FindConnection("1")
@@ -186,7 +185,7 @@ func Test_find_subscriber(t *testing.T) {
} }
if conn.SocketID != "1" { if conn.SocketID != "1" {
t.Error("Wrong subscriber.") t.Errorf("conn.SocketID == %s, wants %s", conn.SocketID, "1")
} }
// Find a wrong subscriber // Find a wrong subscriber
@@ -194,60 +193,60 @@ func Test_find_subscriber(t *testing.T) {
conn, err = app.FindConnection("DoesNotExists") conn, err = app.FindConnection("DoesNotExists")
if err == nil { if err == nil {
t.Error("Opps, Must be nil") t.Errorf("err == %q, wants !nil", err)
} }
if conn != nil { if conn != nil {
t.Error("Opps, Must be nil") t.Errorf("conn == %q, wants nil", conn)
} }
} }
func Test_find_or_create_channels(t *testing.T) { func Test_find_or_create_channels(t *testing.T) {
app := newApp() app := newTestApp()
// Public // Public
if len(app.PublicChannels()) != 0 { if len(app.PublicChannels()) != 0 {
t.Error("Length of public channels must be 0 before test") t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
} }
c := app.FindOrCreateChannelByChannelID("id") c := app.FindOrCreateChannelByChannelID("id")
if len(app.PublicChannels()) != 1 { if len(app.PublicChannels()) != 1 {
t.Error("Length os public channels after insert must be 1") t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
} }
if c.ChannelID != "id" { if c.ChannelID != "id" {
t.Error("Opps wrong channel") t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "id")
} }
// Presence // Presence
if len(app.PresenceChannels()) != 0 { if len(app.PresenceChannels()) != 0 {
t.Error("Length of presence channels must be 0 before test") t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
} }
c = app.FindOrCreateChannelByChannelID("presence-test") c = app.FindOrCreateChannelByChannelID("presence-test")
if len(app.PresenceChannels()) != 1 { if len(app.PresenceChannels()) != 1 {
t.Error("Length os presence channels after insert must be 1") t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
} }
if c.ChannelID != "presence-test" { if c.ChannelID != "presence-test" {
t.Error("Opps wrong channel") t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "presence-test")
} }
// Private // Private
if len(app.PrivateChannels()) != 0 { if len(app.PrivateChannels()) != 0 {
t.Error("Length of private channels must be 0 before test") t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
} }
c = app.FindOrCreateChannelByChannelID("private-test") c = app.FindOrCreateChannelByChannelID("private-test")
if len(app.PrivateChannels()) != 1 { if len(app.PrivateChannels()) != 1 {
t.Error("Length os private channels after insert must be 1") t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
} }
if c.ChannelID != "private-test" { if c.ChannelID != "private-test" {
t.Error("Opps wrong channel") t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "private-test")
} }
} }
-77
View File
@@ -1,77 +0,0 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import (
"net/http"
"net/url"
"sort"
"strings"
log "github.com/golang/glog"
"github.com/gorilla/mux"
"github.com/dimiro1/ipe/utils"
)
// Prepare Querystring
func prepareQueryString(params url.Values) string {
var keys []string
for key := range params {
keys = append(keys, strings.ToLower(key))
}
sort.Strings(keys)
var pieces []string
for _, key := range keys {
pieces = append(pieces, key+"="+params.Get(key))
}
return strings.Join(pieces, "&")
}
// Authenticate pusher
// see: https://gist.github.com/mloughran/376898
//
// The signature is a HMAC SHA256 hex digest.
// This is generated by signing a string made up of the following components concatenated with newline characters \n.
//
// * The uppercase request method (e.g. POST)
// * The request path (e.g. /some/resource)
// * The query parameters sorted by key, with keys converted to lowercase, then joined as in the query string.
// Note that the string must not be url escaped (e.g. given the keys auth_key: foo, Name: Something else, you get auth_key=foo&name=Something else)
func restAuthenticationHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
appID := vars["app_id"]
app, err := conf.GetAppByAppID(appID)
if err != nil {
log.Error(err)
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
params := r.URL.Query()
signature := params.Get("auth_signature")
params.Del("auth_signature")
queryString := prepareQueryString(params)
toSign := strings.ToUpper(r.Method) + "\n" + r.URL.Path + "\n" + queryString
if utils.HashMAC([]byte(toSign), []byte(app.Secret)) == signature {
h.ServeHTTP(w, r)
} else {
log.Error("Not authorized")
http.Error(w, "Not authorized", http.StatusUnauthorized)
}
})
}
+16 -16
View File
@@ -10,13 +10,13 @@ func TestIsOccupied(t *testing.T) {
c := newChannel("ID") c := newChannel("ID")
if c.IsOccupied() { if c.IsOccupied() {
t.Error("Channels must be empty") t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), false)
} }
c.Subscriptions["ID"] = newSubscription(newConnection("ID", nil), "") c.Subscriptions["ID"] = newSubscription(newConnection("ID", mockSocket{}), "")
if !c.IsOccupied() { if !c.IsOccupied() {
t.Error("Channels must be empty") t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), true)
} }
} }
@@ -24,7 +24,7 @@ func TestIsPrivate(t *testing.T) {
c := newChannel("private-channel") c := newChannel("private-channel")
if !c.IsPrivate() { if !c.IsPrivate() {
t.Error("The Channel must be private") t.Errorf("c.IsPrivate() == %t, wants %t", c.IsPrivate(), true)
} }
} }
@@ -32,7 +32,7 @@ func TestIsPresence(t *testing.T) {
c := newChannel("presence-channel") c := newChannel("presence-channel")
if !c.IsPresence() { if !c.IsPresence() {
t.Error("The Channel must be presence") t.Errorf("c.IsPresence() == %t, wants %t", c.IsPresence(), true)
} }
} }
@@ -40,7 +40,7 @@ func TestIsPublic(t *testing.T) {
c := newChannel("channel") c := newChannel("channel")
if !c.IsPublic() { if !c.IsPublic() {
t.Error("The Channel must be public") t.Errorf("c.IsPublic() == %t, wants %t", c.IsPublic(), true)
} }
} }
@@ -48,13 +48,13 @@ func TestIsPrivateOrPresence(t *testing.T) {
c := newChannel("private-channel") c := newChannel("private-channel")
if !c.IsPresenceOrPrivate() { if !c.IsPresenceOrPrivate() {
t.Error("The Channel must be private or presence") t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
} }
c = newChannel("presence-channel") c = newChannel("presence-channel")
if !c.IsPresenceOrPrivate() { if !c.IsPresenceOrPrivate() {
t.Error("The Channel must be private or presence") t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
} }
} }
@@ -62,37 +62,37 @@ func TestTotalSubscriptions(t *testing.T) {
c := newChannel("ID") c := newChannel("ID")
if c.TotalSubscriptions() != len(c.Subscriptions) { if c.TotalSubscriptions() != len(c.Subscriptions) {
t.Error("TotalSubscriptions must be equal to len of total subscriptions") t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.Subscriptions))
} }
} }
func TestTotalUsers(t *testing.T) { func TestTotalUsers(t *testing.T) {
c := newChannel("ID") c := newChannel("ID")
c.Subscriptions["1"] = newSubscription(newConnection("ID", nil), "") c.Subscriptions["1"] = newSubscription(newConnection("ID", mockSocket{}), "")
c.Subscriptions["2"] = newSubscription(newConnection("ID", nil), "") c.Subscriptions["2"] = newSubscription(newConnection("ID", mockSocket{}), "")
if c.TotalSubscriptions() != len(c.Subscriptions) { if c.TotalSubscriptions() != len(c.Subscriptions) {
t.Error("TotalSubscriptions must be equal to len of total subscriptions") t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.Subscriptions))
} }
if c.TotalUsers() != 1 { if c.TotalUsers() != 1 {
t.Error("TotalUsers must be equal to 1") t.Errorf("c.TotalUsers() == %d, wants %d", c.TotalUsers(), 1)
} }
} }
func TestIsSubscribed(t *testing.T) { func TestIsSubscribed(t *testing.T) {
c := newChannel("ID") c := newChannel("ID")
conn := newConnection("ID", nil) conn := newConnection("ID", mockSocket{})
if c.IsSubscribed(conn) { if c.IsSubscribed(conn) {
t.Error("Must not be subscribed") t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), false)
} }
c.Subscriptions["ID"] = newSubscription(conn, "") c.Subscriptions["ID"] = newSubscription(conn, "")
if !c.IsSubscribed(conn) { if !c.IsSubscribed(conn) {
t.Error("Must be subscribed") t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), true)
} }
} }
+24 -35
View File
@@ -1,54 +1,43 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved. // Copyright 2014, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package ipe package ipe
import (
"errors"
"strings"
)
// The config file // The config file
type configFile struct { type configFile struct {
Host string // The host, eg: :8080 will start on 0.0.0.0:8080 Host string // The host, eg: :8080 will start on 0.0.0.0:8080
User string User string
Password string
SSL bool SSL bool
SSLHost string SSLHost string
SSLKeyFile string SSLKeyFile string
SSLCertFile string SSLCertFile string
Apps []*app Apps []configApp
} }
// Initialize Apps type configApp struct {
func (c *configFile) Init() { Name string
for _, app := range c.Apps { AppID string
app.Init() Key string
} Secret string
OnlySSL bool
ApplicationDisabled bool
UserEvents bool
WebHooks bool
URLWebHook string
} }
func (c *configFile) WasProvidedUserAndPassword() bool { func newAppFromConfig(a configApp) *app {
return len(strings.TrimSpace(c.User)) > 0 && len(strings.TrimSpace(c.Password)) > 0 return newApp(
} a.Name,
a.AppID,
// Returns an App with by appID a.Key,
func (c *configFile) GetAppByAppID(appID string) (*app, error) { a.Secret,
for _, a := range c.Apps { a.OnlySSL,
if a.AppID == appID { a.ApplicationDisabled,
return a, nil a.UserEvents,
} a.WebHooks,
} a.URLWebHook,
return &app{}, errors.New("App not found") )
}
// Returns an App with by key
func (c *configFile) GetAppByKey(key string) (*app, error) {
for _, a := range c.Apps {
if a.Key == key {
return a, nil
}
}
return &app{}, errors.New("App not found")
} }
+16 -8
View File
@@ -8,18 +8,30 @@ import (
"time" "time"
log "github.com/golang/glog" log "github.com/golang/glog"
"github.com/gorilla/websocket"
) )
// socket interface to write to the client
type socket interface {
WriteJSON(interface{}) error
}
// mockSocket is a mock implementation of socket
// used in the test suite
type mockSocket struct{}
func (s mockSocket) WriteJSON(i interface{}) error {
return nil
}
// An User Connection // An User Connection
type connection struct { type connection struct {
SocketID string SocketID string
Socket *websocket.Conn Socket socket
CreatedAt time.Time CreatedAt time.Time
} }
// Create a new Subscriber // Create a new Subscriber
func newConnection(socketID string, s *websocket.Conn) *connection { func newConnection(socketID string, s socket) *connection {
log.Infof("Creating a new Subscriber %+v", socketID) log.Infof("Creating a new Subscriber %+v", socketID)
return &connection{SocketID: socketID, Socket: s, CreatedAt: time.Now()} return &connection{SocketID: socketID, Socket: s, CreatedAt: time.Now()}
@@ -27,9 +39,5 @@ func newConnection(socketID string, s *websocket.Conn) *connection {
// Publish the message to websocket atached to this client // Publish the message to websocket atached to this client
func (conn *connection) Publish(m interface{}) { func (conn *connection) Publish(m interface{}) {
go func() { conn.Socket.WriteJSON(m)
if err := conn.Socket.WriteJSON(m); err != nil {
log.Errorf("Error publishing message to connection %+v, %s", conn, err)
}
}()
} }
+5 -9
View File
@@ -4,27 +4,23 @@
package ipe package ipe
import ( import "testing"
"testing"
"github.com/gorilla/websocket"
)
func TestNewConnection(t *testing.T) { func TestNewConnection(t *testing.T) {
expectedSocketID := "socketID" expectedSocketID := "socketID"
expectedSocket := &websocket.Conn{} expectedSocket := mockSocket{}
c := newConnection(expectedSocketID, expectedSocket) c := newConnection(expectedSocketID, expectedSocket)
if c.SocketID != expectedSocketID { if c.SocketID != expectedSocketID {
t.Errorf("Expected: %s but got %s", expectedSocketID, c.SocketID) t.Errorf("c.SocketID == %s, wants %s", c.SocketID, expectedSocketID)
} }
if c.Socket != expectedSocket { if c.Socket != expectedSocket {
t.Errorf("Expected: %+v but got %+v", expectedSocket, c.Socket) t.Errorf("c.Socket == %v, wants %v", c.Socket, expectedSocket)
} }
if c.CreatedAt.IsZero() { if c.CreatedAt.IsZero() {
t.Errorf("Expected %s to not be zero", c.CreatedAt) t.Errorf("c.CreatedAt.IsZero() == %t, wants %t", c.CreatedAt.IsZero(), false)
} }
} }
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import "net/http"
type applicationContext struct {
DB db
}
// url params
type params map[string]string
func (p params) Get(key string) string {
return p[key]
}
// A contextHandler responds to an HTTP request with custom application context.
type contextHandler interface {
ServeWithContext(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request)
}
type contextHandlerFunc func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request)
func (c contextHandlerFunc) ServeWithContext(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
c(ctx, p, w, r)
}
+58
View File
@@ -0,0 +1,58 @@
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import (
"errors"
"sync"
)
// db represents a app database
// For now it there is only one memory database implementation
// but in the future I can write a sql implementation
type db interface {
GetAppByAppID(appID string) (*app, error)
GetAppByKey(key string) (*app, error)
AddApp(*app) error
}
// memdb is a in memory implementation of db interface
type memdb struct {
sync.Mutex
Apps []*app
}
func newMemdb() *memdb {
return &memdb{}
}
func (db *memdb) AddApp(a *app) error {
db.Lock()
defer db.Unlock()
db.Apps = append(db.Apps, a)
return nil
}
// GetAppByAppID returns an App with by appID
func (db *memdb) GetAppByAppID(appID string) (*app, error) {
for _, a := range db.Apps {
if a.AppID == appID {
return a, nil
}
}
return nil, errors.New("App not found")
}
// GetAppByKey returns an App with by key
func (db *memdb) GetAppByKey(key string) (*app, error) {
for _, a := range db.Apps {
if a.Key == key {
return a, nil
}
}
return nil, errors.New("App not found")
}
+59
View File
@@ -0,0 +1,59 @@
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import "testing"
func Test_db_GetAppByAppID(t *testing.T) {
app := &app{AppID: "123456", Name: "Example"}
db := newMemdb()
db.AddApp(app)
a, err := db.GetAppByAppID("123456")
if err != nil {
t.Errorf("GetAppByAppID(%q) == %q, want %q", "123456", a, app)
}
}
func Test_db_GetAppByAppID__error(t *testing.T) {
app := &app{AppID: "123456", Name: "Example"}
db := newMemdb()
db.AddApp(app)
a, err := db.GetAppByAppID("not-found")
if err == nil {
t.Errorf("GetAppByAppID(%q) == %q, want %q", "123456", a, app)
}
}
func Test_db_GetAppByKey(t *testing.T) {
app := &app{AppID: "123456", Name: "Example", Key: "654321"}
db := newMemdb()
db.AddApp(app)
a, err := db.GetAppByKey("654321")
if err != nil {
t.Errorf("GetAppByKey(%q) == %q, want %q", "654321", a, app)
}
}
func Test_db_GetAppByKey__error(t *testing.T) {
app := &app{AppID: "123456", Name: "Example", Key: "654321"}
db := newMemdb()
db.AddApp(app)
a, err := db.GetAppByKey("not-found")
if err == nil {
t.Errorf("GetAppByKey(%q) == %q, want %v", "not-found", a, nil)
}
}
-34
View File
@@ -1,34 +0,0 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
// Check if the application is disabled
func restCheckAppDisabledHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
appID := vars["app_id"]
currentApp, err := conf.GetAppByAppID(appID)
if err != nil {
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusForbidden)
return
}
if currentApp.ApplicationDisabled {
http.Error(w, "Application disabled", http.StatusForbidden)
return
}
h.ServeHTTP(w, r)
})
}
+108 -26
View File
@@ -8,13 +8,100 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"sort"
"strings" "strings"
"github.com/dimiro1/ipe/utils"
log "github.com/golang/glog" log "github.com/golang/glog"
"github.com/gorilla/mux"
"github.com/dimiro1/ipe/utils"
) )
// Prepare Querystring
func prepareQueryString(params url.Values) string {
var keys []string
for key := range params {
keys = append(keys, strings.ToLower(key))
}
sort.Strings(keys)
var pieces []string
for _, key := range keys {
pieces = append(pieces, key+"="+params.Get(key))
}
return strings.Join(pieces, "&")
}
// Authenticate pusher
// see: https://gist.github.com/mloughran/376898
//
// The signature is a HMAC SHA256 hex digest.
// This is generated by signing a string made up of the following components concatenated with newline characters \n.
//
// * The uppercase request method (e.g. POST)
// * The request path (e.g. /some/resource)
// * The query parameters sorted by key, with keys converted to lowercase, then joined as in the query string.
// Note that the string must not be url escaped (e.g. given the keys auth_key: foo, Name: Something else, you get auth_key=foo&name=Something else)
func restAuthenticationHandler(ctx *applicationContext, h contextHandler) contextHandler {
return contextHandlerFunc(func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
appID := p.Get("app_id")
app, err := ctx.DB.GetAppByAppID(appID)
if err != nil {
log.Error(err)
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
query := r.URL.Query()
signature := query.Get("auth_signature")
query.Del("auth_signature")
queryString := prepareQueryString(query)
toSign := strings.ToUpper(r.Method) + "\n" + r.URL.Path + "\n" + queryString
if utils.HashMAC([]byte(toSign), []byte(app.Secret)) == signature {
h.ServeWithContext(ctx, p, w, r)
} else {
log.Error("Not authorized")
http.Error(w, "Not authorized", http.StatusUnauthorized)
}
})
}
// Check if the application is disabled
func restCheckAppDisabledHandler(ctx *applicationContext, h contextHandler) contextHandler {
return contextHandlerFunc(func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
appID := p.Get("app_id")
currentApp, err := ctx.DB.GetAppByAppID(appID)
if err != nil {
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusForbidden)
return
}
if currentApp.ApplicationDisabled {
http.Error(w, "Application disabled", http.StatusForbidden)
return
}
h.ServeWithContext(ctx, p, w, r)
})
}
// commonHandlers combine restCheckAppDisabledHandler and restAuthenticationHandler handlers
func commonHandlers(ctx *applicationContext, h contextHandlerFunc) contextHandler {
return restCheckAppDisabledHandler(ctx, restAuthenticationHandler(ctx, h))
}
// An event consists of a name and data (typically JSON) which may be sent to all subscribers to a particular channel or channels. // An event consists of a name and data (typically JSON) which may be sent to all subscribers to a particular channel or channels.
// This is conventionally known as triggering an event. // This is conventionally known as triggering an event.
// //
@@ -30,11 +117,10 @@ import (
// Response is an empty JSON hash. // Response is an empty JSON hash.
// //
// POST /apps/{app_id}/events // POST /apps/{app_id}/events
func postEvents(w http.ResponseWriter, r *http.Request) { func postEvents(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) appID := p.Get("app_id")
appID := vars["app_id"]
app, err := conf.GetAppByAppID(appID) app, err := ctx.DB.GetAppByAppID(appID)
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest) http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
@@ -96,13 +182,12 @@ func postEvents(w http.ResponseWriter, r *http.Request) {
// } // }
// //
// GET /apps/{app_id}/channels // GET /apps/{app_id}/channels
func getChannels(w http.ResponseWriter, r *http.Request) { func getChannels(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
params := r.URL.Query() query := r.URL.Query()
vars := mux.Vars(r)
appID := vars["app_id"] appID := p.Get("app_id")
filter := params.Get("filter_by_prefix") filter := query.Get("filter_by_prefix")
info := params.Get("info") info := query.Get("info")
attributes := strings.Split(info, ",") attributes := strings.Split(info, ",")
@@ -121,7 +206,7 @@ func getChannels(w http.ResponseWriter, r *http.Request) {
return return
} }
app, err := conf.GetAppByAppID(appID) app, err := ctx.DB.GetAppByAppID(appID)
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest) http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
@@ -177,20 +262,19 @@ func getChannels(w http.ResponseWriter, r *http.Request) {
// } // }
// //
// GET /apps/{app_id}/channels/{channel_name} // GET /apps/{app_id}/channels/{channel_name}
func getChannel(w http.ResponseWriter, r *http.Request) { func getChannel(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json;charset=UTF-8") w.Header().Set("Content-Type", "application/json;charset=UTF-8")
params := r.URL.Query() query := r.URL.Query()
vars := mux.Vars(r)
appID := vars["app_id"] appID := p.Get("app_id")
app, err := conf.GetAppByAppID(appID) app, err := ctx.DB.GetAppByAppID(appID)
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest) http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
} }
channelName := vars["channel_name"] channelName := p.Get("channel_name")
// Channel name could not be empty // Channel name could not be empty
if strings.TrimSpace(channelName) == "" { if strings.TrimSpace(channelName) == "" {
@@ -198,7 +282,7 @@ func getChannel(w http.ResponseWriter, r *http.Request) {
return return
} }
info := params.Get("info") info := query.Get("info")
attributes := strings.Split(info, ",") attributes := strings.Split(info, ",")
// Attributes requested // Attributes requested
@@ -267,11 +351,9 @@ func getChannel(w http.ResponseWriter, r *http.Request) {
// } // }
// //
// GET /apps/{app_id}/channels/{channel_name}/users // GET /apps/{app_id}/channels/{channel_name}/users
func getChannelUsers(w http.ResponseWriter, r *http.Request) { func getChannelUsers(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) appID := p.Get("app_id")
channelName := p.Get("channel_name")
appID := vars["app_id"]
channelName := vars["channel_name"]
isPresence := utils.IsPresenceChannel(channelName) isPresence := utils.IsPresenceChannel(channelName)
@@ -280,7 +362,7 @@ func getChannelUsers(w http.ResponseWriter, r *http.Request) {
return return
} }
app, err := conf.GetAppByAppID(appID) app, err := ctx.DB.GetAppByAppID(appID)
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest) http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
+207
View File
@@ -0,0 +1,207 @@
package ipe
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
var (
testApp *app
ctx *applicationContext
)
func init() {
testApp = newTestApp()
channel := newChannel("presence-c1")
testApp.AddChannel(channel)
testApp.AddChannel(newChannel("c2"))
testApp.AddChannel(newChannel("private-c3"))
conn := newConnection("123.456", mockSocket{})
testApp.Subscribe(channel, conn, "{}")
conn = newConnection("321.654", mockSocket{})
testApp.Subscribe(channel, conn, "{}")
db := newMemdb()
db.AddApp(testApp)
ctx = &applicationContext{DB: db}
}
// All Channels
func Test_getChannels_all(t *testing.T) {
appID := testApp.AppID
p := map[string]string{}
p["app_id"] = appID
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels", appID), nil)
w := httptest.NewRecorder()
getChannels(ctx, params(p), w, r)
if w.Code != http.StatusOK {
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
}
data := make(map[string]interface{})
json.Unmarshal(w.Body.Bytes(), &data)
channels := data["channels"].(map[string]interface{})
if len(channels) != 3 {
t.Errorf("len(%q) == %d, want %d", channels, len(channels), 3)
}
}
func Test_getChannels_filter_by_presence_prefix(t *testing.T) {
appID := testApp.AppID
p := map[string]string{}
p["app_id"] = appID
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-", appID), nil)
w := httptest.NewRecorder()
getChannels(ctx, params(p), w, r)
if w.Code != http.StatusOK {
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
}
data := make(map[string]interface{})
json.Unmarshal(w.Body.Bytes(), &data)
channels := data["channels"].(map[string]interface{})
if len(channels) != 1 {
t.Errorf("len(%q) == %d, want %d", channels, len(channels), 1)
}
}
// Only presence channels and user_count
func Test_getChannels_filter_by_presence_prefix_and_user_count(t *testing.T) {
appID := testApp.AppID
p := map[string]string{}
p["app_id"] = appID
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-&info=user_count", appID), nil)
w := httptest.NewRecorder()
getChannels(ctx, params(p), w, r)
if w.Code != http.StatusOK {
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
}
data := make(map[string]interface{})
json.Unmarshal(w.Body.Bytes(), &data)
channels := data["channels"].(map[string]interface{})
if len(channels) != 1 {
t.Errorf("len(%q) == %d, want %d", channels, len(channels), 1)
}
c, exists := channels["presence-c1"]
if !exists {
t.Errorf("!exists == %t, want %t", !exists, false)
}
_channel := c.(map[string]interface{})
if _channel["user_count"] != float64(1) {
t.Errorf("_channel['user_count'] == %f, want %d", _channel["user_count"], 1)
}
}
// User count only alowed in Presence channels
func Test_getChannels_filter_by_private_prefix_and_info_user_count(t *testing.T) {
appID := testApp.AppID
p := map[string]string{}
p["app_id"] = appID
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-&info=user_count", appID), nil)
w := httptest.NewRecorder()
getChannels(ctx, params(p), w, r)
if w.Code != http.StatusBadRequest {
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusBadRequest)
}
}
func Test_getChannels_filter_by_public_prefix(t *testing.T) {
appID := testApp.AppID
p := map[string]string{}
p["app_id"] = appID
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=public-", appID), nil)
w := httptest.NewRecorder()
getChannels(ctx, params(p), w, r)
if w.Code != http.StatusOK {
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
}
data := make(map[string]interface{})
json.Unmarshal(w.Body.Bytes(), &data)
channels := data["channels"].(map[string]interface{})
if len(channels) != 1 {
t.Errorf("len(%q) == %d, want %d", channels, len(channels), 1)
}
_, exists := channels["c2"]
if !exists {
t.Errorf("!exists == %t, want %t", !exists, false)
}
}
func Test_getChannels_filter_by_private_prefix(t *testing.T) {
appID := testApp.AppID
p := map[string]string{}
p["app_id"] = appID
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-", appID), nil)
w := httptest.NewRecorder()
getChannels(ctx, params(p), w, r)
if w.Code != http.StatusOK {
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
}
data := make(map[string]interface{})
json.Unmarshal(w.Body.Bytes(), &data)
channels := data["channels"].(map[string]interface{})
if len(channels) != 1 {
t.Errorf("len(%q) == %d, want %d", channels, len(channels), 1)
}
_, exists := channels["private-c3"]
if !exists {
t.Errorf("!exists == %t, want %t", !exists, false)
}
}
+28 -7
View File
@@ -14,25 +14,46 @@ import (
log "github.com/golang/glog" log "github.com/golang/glog"
) )
// Conf holds the global configuration state
var conf configFile
// Start Parse the configuration file and starts the ipe server // Start Parse the configuration file and starts the ipe server
// It Panic if could not start the HTTP or HTTPS server // It Panic if could not start the HTTP or HTTPS server
func Start(configfile string) { func Start(filename string) {
var conf configFile
rand.Seed(time.Now().Unix()) rand.Seed(time.Now().Unix())
file, err := os.Open(configfile) file, err := os.Open(filename)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
// Reading config
if err := json.NewDecoder(file).Decode(&conf); err != nil { if err := json.NewDecoder(file).Decode(&conf); err != nil {
log.Fatal(err) log.Fatal(err)
} }
conf.Init() // Using a in memory database
router := newRouter() db := newMemdb()
// Adding applications
for _, a := range conf.Apps {
db.AddApp(newAppFromConfig(a))
}
// Creating the global application context
ctx := &applicationContext{DB: db}
// The router
router := newRouter(ctx)
router.POST("/apps/{app_id}/events", commonHandlers(ctx, postEvents))
router.GET("/apps/{app_id}/channels", commonHandlers(ctx, getChannels))
router.GET("/apps/{app_id}/channels/{channel_name}", commonHandlers(ctx, getChannel))
router.GET("/apps/{app_id}/channels/{channel_name}/users", commonHandlers(ctx, getChannelUsers))
router.GET("/app/{key}", contextHandlerFunc(wsHandler))
if conf.SSL { if conf.SSL {
go func() { go func() {
+25 -15
View File
@@ -1,4 +1,4 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved. // Copyright 2014, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@@ -10,23 +10,33 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
// newRouter is a function that returns a new configured Router type router struct {
// It add the necessary middlewares ctx *applicationContext
func newRouter() *mux.Router { mux *mux.Router
router := mux.NewRouter().StrictSlash(true) routes map[string]contextHandler
}
for _, route := range routes { func newRouter(ctx *applicationContext) *router {
var handler http.Handler return &router{
ctx: ctx,
mux: mux.NewRouter().StrictSlash(true),
}
}
handler = route.HandlerFunc func (a *router) GET(path string, handler contextHandler) {
a.Handle("GET", path, handler)
}
if route.RequiresRestAuth { func (a *router) POST(path string, handler contextHandler) {
handler = restAuthenticationHandler(handler) a.Handle("POST", path, handler)
handler = restCheckAppDisabledHandler(handler) }
}
router.Methods(route.Method).Path(route.Pattern).Name(route.Name).Handler(handler) func (a *router) Handle(method, path string, handler contextHandler) {
} a.mux.Methods(method).Path(path).HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handler.ServeWithContext(a.ctx, params(mux.Vars(r)), w, r)
})
}
return router func (a router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
a.mux.ServeHTTP(w, r)
} }
-56
View File
@@ -1,56 +0,0 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import (
"net/http"
)
// A route
type route struct {
Name string
Method string
Pattern string
HandlerFunc http.HandlerFunc
RequiresRestAuth bool
}
var routes = []route{
{
"PostEvents",
"POST",
"/apps/{app_id}/events",
postEvents,
true,
},
{
"GetChannels",
"GET",
"/apps/{app_id}/channels",
getChannels,
true,
},
{
"GetChannel",
"GET",
"/apps/{app_id}/channels/{channel_name}",
getChannel,
true,
},
{
"GetChannelUsers",
"GET",
"/apps/{app_id}/channels/{channel_name}/users",
getChannelUsers,
true,
},
{
"Websocket",
"GET",
"/app/{key}",
wsHandler,
false,
},
}
+8 -1
View File
@@ -158,7 +158,14 @@ func triggerHook(name string, a *app, c *channel, event hookEvent) {
log.V(1).Infof("%+v", req.Header) log.V(1).Infof("%+v", req.Header)
log.V(1).Infof("%+v", string(js)) log.V(1).Infof("%+v", string(js))
if _, err := http.DefaultClient.Do(req); err != nil { resp, err := http.DefaultClient.Do(req)
// See: http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#close_http_resp_body
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
log.Errorf("Error posting %s event: %+v", name, err) log.Errorf("Error posting %s event: %+v", name, err)
} }
}() }()
+3 -5
View File
@@ -13,7 +13,6 @@ import (
"strings" "strings"
log "github.com/golang/glog" log "github.com/golang/glog"
"github.com/gorilla/mux"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/dimiro1/ipe/utils" "github.com/dimiro1/ipe/utils"
@@ -207,7 +206,7 @@ func onMessage(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, ses
} }
// Websocket GET /app/{key} // Websocket GET /app/{key}
func wsHandler(w http.ResponseWriter, r *http.Request) { func wsHandler(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil) conn, err := upgrader.Upgrade(w, r, nil)
defer func() { defer func() {
if conn != nil { if conn != nil {
@@ -220,10 +219,9 @@ func wsHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
vars := mux.Vars(r) appKey := p.Get("key")
appKey := vars["key"]
app, err := conf.GetAppByKey(appKey) app, err := ctx.DB.GetAppByKey(appKey)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
+5 -1
View File
@@ -15,7 +15,11 @@ import (
"strings" "strings"
) )
var validChannelName *regexp.Regexp = regexp.MustCompile("^[A-Za-z0-9_\\-=@,.;]+$") var validChannelName *regexp.Regexp
func init() {
validChannelName = regexp.MustCompile("^[A-Za-z0-9_\\-=@,.;]+$")
}
// HashMAC Calculates the MAC signing with the given key and returns the hexadecimal encoded Result // HashMAC Calculates the MAC signing with the given key and returns the hexadecimal encoded Result
func HashMAC(message, key []byte) string { func HashMAC(message, key []byte) string {
+69 -8
View File
@@ -30,19 +30,80 @@ func TestGenerateSession(t *testing.T) {
} }
func TestIsValidChannelName(t *testing.T) { func TestIsValidChannelName(t *testing.T) {
if IsChannelNameValid("#@#hhh**sasas") { name := "#@#hhh**sasas"
t.Errorf("Invalid Channel Name") ok := IsChannelNameValid(name)
if ok {
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, false)
} }
if !IsChannelNameValid("private-hello") { name = "private-hello"
t.Errorf("Must be Valid Channel Name") ok = IsChannelNameValid(name)
if !ok {
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, true)
} }
if !IsChannelNameValid("presence-hello") { name = "presence-hello"
t.Errorf("Must be Valid Channel Name") ok = IsChannelNameValid(name)
if !ok {
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, true)
} }
if !IsChannelNameValid("public") { name = "public"
t.Errorf("Must be Valid Channel Name") ok = IsChannelNameValid(name)
if !ok {
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, true)
}
}
func TestIsPrivateChannel_valid(t *testing.T) {
name := "private-hello"
ok := IsPrivateChannel(name)
if !ok {
t.Errorf("IsPrivateChannel(%s) == %t, wants %t", name, ok, true)
}
}
func TestIsPrivateChannel_invalid(t *testing.T) {
name := "hello"
ok := IsPrivateChannel(name)
if ok {
t.Errorf("IsPrivateChannel(%s) == %t, wants %t", name, ok, false)
}
}
func TestIsIsPresenceChannel_valid(t *testing.T) {
name := "presence-hello"
ok := IsPresenceChannel(name)
if !ok {
t.Errorf("IsPresenceChannel(%s) == %t, wants %t", name, ok, true)
}
}
func TestIsPresenceChannel_invalid(t *testing.T) {
name := "hello"
ok := IsPresenceChannel(name)
if ok {
t.Errorf("IsPresenceChannel(%s) == %t, wants %t", name, ok, false)
}
}
func TestHashMAC(t *testing.T) {
message := []byte("hello world")
key := []byte("my super secret key")
digest := HashMAC(message, key)
// See: http://www.freeformatter.com/hmac-generator.html
expected := "0811b8affc185a01e1a65b80089ebb1f7f68d287fc3b64581da9ec99136ad1db"
if digest != expected {
t.Errorf("HashMAC(%s, %q) == %s, wants %s", message, key, digest, expected)
} }
} }