File structure organization.

Create a new package ipe and put everything n there except for the main function.
This commit is contained in:
claudemiro
2015-01-31 08:29:01 -03:00
parent c7f757e517
commit cd1fd8995d
22 changed files with 59 additions and 42 deletions
+220
View File
@@ -0,0 +1,220 @@
// 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 (
"errors"
"expvar"
"fmt"
"sync"
log "github.com/golang/glog"
)
// An App
type App struct {
sync.Mutex
Name string
AppID string
Key string
Secret string
OnlySSL bool
ApplicationDisabled bool
UserEvents bool
WebHooks bool
URLWebHook string
Channels map[string]*Channel `json:"-"`
Connections map[string]*Connection `json:"-"`
Stats *expvar.Map `json:"-"`
}
// Alloc memory for Connections and Channels
func (a *App) Init() {
a.Connections = make(map[string]*Connection)
a.Channels = make(map[string]*Channel)
a.Stats = expvar.NewMap(fmt.Sprintf("%s (%s)", a.Name, a.AppID))
}
// Only Presence channels
func (a *App) PresenceChannels() []*Channel {
var channels []*Channel
for _, c := range a.Channels {
if c.IsPresence() {
channels = append(channels, c)
}
}
return channels
}
// Only Private channels
func (a *App) PrivateChannels() []*Channel {
var channels []*Channel
for _, c := range a.Channels {
if c.IsPrivate() {
channels = append(channels, c)
}
}
return channels
}
// Only Public channels
func (a *App) PublicChannels() []*Channel {
var channels []*Channel
for _, c := range a.Channels {
if c.IsPublic() {
channels = append(channels, c)
}
}
return channels
}
// Disconnect Socket
func (a *App) Disconnect(socketID string) {
log.Infof("Disconnecting socket %+v", socketID)
conn, err := a.FindConnection(socketID)
if err != nil {
log.Infof("Socket not found, %+v", err)
return
}
// Unsubscribe from channels
for _, c := range a.Channels {
if c.IsSubscribed(conn) {
c.Unsubscribe(a, conn)
}
}
// Remove from app
a.Lock()
defer a.Unlock()
_, exists := a.Connections[conn.SocketID]
if !exists {
return
}
delete(a.Connections, conn.SocketID)
a.Stats.Add("TotalConnections", -1)
}
// Connect a new Subscriber
func (a *App) Connect(conn *Connection) {
log.Infof("Adding a new Connection %s to app %s", conn.SocketID, a.Name)
a.Lock()
defer a.Unlock()
a.Connections[conn.SocketID] = conn
a.Stats.Add("TotalConnections", 1)
}
// Find a Connection on this app
func (a *App) FindConnection(socketID string) (*Connection, error) {
conn, exists := a.Connections[socketID]
if exists {
return conn, nil
}
return nil, errors.New("Connection not found")
}
// DeleteChannel removes the channel from app
func (a *App) RemoveChannel(c *Channel) {
log.Infof("Remove the channel %s from app %s", c.ChannelID, a.Name)
a.Lock()
defer a.Unlock()
delete(a.Channels, c.ChannelID)
if c.IsPresence() {
a.Stats.Add("TotalPresenceChannels", -1)
}
if c.IsPrivate() {
a.Stats.Add("TotalPrivateChannels", -1)
}
if c.IsPublic() {
a.Stats.Add("TotalPublicChannels", -1)
}
a.Stats.Add("TotalChannels", -1)
}
// Add a new Channel to this APP
func (a *App) AddChannel(c *Channel) {
log.Infof("Adding a new channel %s to app %s", c.ChannelID, a.Name)
a.Lock()
defer a.Unlock()
a.Channels[c.ChannelID] = c
if c.IsPresence() {
a.Stats.Add("TotalPresenceChannels", 1)
}
if c.IsPrivate() {
a.Stats.Add("TotalPrivateChannels", 1)
}
if c.IsPublic() {
a.Stats.Add("TotalPublicChannels", 1)
}
a.Stats.Add("TotalChannels", 1)
}
// Returns a Channel from this app
// If not found then the channel is created and added to this app
func (a *App) FindOrCreateChannelByChannelID(n string) *Channel {
c, err := a.FindChannelByChannelID(n)
if err != nil {
c = NewChannel(n)
a.AddChannel(c)
}
return c
}
// Find the channel by channel ID
func (a *App) FindChannelByChannelID(n string) (*Channel, error) {
c, exists := a.Channels[n]
if exists {
return c, nil
}
return nil, errors.New("Channel does not exists")
}
func (a *App) Publish(c *Channel, event RawEvent, ignore string) error {
a.Stats.Add("TotalUniqueMessages", 1)
return c.Publish(a, event, ignore)
}
func (a *App) Unsubscribe(c *Channel, conn *Connection) error {
return c.Unsubscribe(a, conn)
}
func (a *App) Subscribe(c *Channel, conn *Connection, data string) error {
return c.Subscribe(a, conn, data)
}
+253
View File
@@ -0,0 +1,253 @@
// 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 (
"strconv"
"testing"
)
var id = 0
func newApp() *App {
a := App{Name: "Test", AppID: strconv.Itoa(id), Key: "123", Secret: "123", OnlySSL: false, ApplicationDisabled: false, UserEvents: true}
a.Init()
id++
return &a
}
func TestConnect(t *testing.T) {
app := newApp()
app.Connect(NewConnection("socketID", nil))
if len(app.Connections) != 1 {
t.Errorf("Connections must be 1, but was %d", len(app.Connections))
}
}
func TestDisconnect(t *testing.T) {
app := newApp()
app.Connect(NewConnection("socketID", nil))
app.Disconnect("socketID")
if len(app.Connections) != 0 {
t.Errorf("Connections must be 0, but was %d", len(app.Connections))
}
}
func TestFindConnection(t *testing.T) {
app := newApp()
app.Connect(NewConnection("socketID", nil))
if _, err := app.FindConnection("socketID"); err != nil {
t.Error("Must find Connection")
}
if _, err := app.FindConnection("NotFound"); err == nil {
t.Error("Must not found Connection")
}
}
func TestFindChannelByChannelID(t *testing.T) {
app := newApp()
channel := NewChannel("ID")
app.AddChannel(channel)
if _, err := app.FindChannelByChannelID("ID"); err != nil {
t.Error("Channel not found")
}
}
func TestFindOrCreateChannelByChannelID(t *testing.T) {
app := newApp()
if len(app.Channels) != 0 {
t.Error("Length of channels must be 0 before test")
}
app.FindOrCreateChannelByChannelID("ID")
if len(app.Channels) != 1 {
t.Error("Length of channels must be 1 after test")
}
}
func TestRemoveChannel(t *testing.T) {
app := newApp()
if len(app.Channels) != 0 {
t.Error("Length of channels must be 0 before test")
}
channel := NewChannel("ID")
app.AddChannel(channel)
if len(app.Channels) != 1 {
t.Error("Length of channels after insert must be 1")
}
app.RemoveChannel(channel)
if len(app.Channels) != 0 {
t.Error("Length of channels must be 0 after remove")
}
}
func Test_add_channels(t *testing.T) {
app := newApp()
// Public
if len(app.PublicChannels()) != 0 {
t.Error("Length of public channels must be 0 before test")
}
app.AddChannel(NewChannel("ID"))
if len(app.PublicChannels()) != 1 {
t.Error("Length os public channels after insert must be 1")
}
// Presence
if len(app.PresenceChannels()) != 0 {
t.Error("Length of presence channels must be 0 before test")
}
app.AddChannel(NewChannel("presence-test"))
if len(app.PresenceChannels()) != 1 {
t.Error("Length os presence channels after insert must be 1")
}
// Private
if len(app.PrivateChannels()) != 0 {
t.Error("Length of private channels must be 0 before test")
}
app.AddChannel(NewChannel("private-test"))
if len(app.PrivateChannels()) != 1 {
t.Error("Length os private channels after insert must be 1")
}
}
func Test_AllChannels(t *testing.T) {
app := newApp()
app.AddChannel(NewChannel("private-test"))
app.AddChannel(NewChannel("presence-test"))
app.AddChannel(NewChannel("test"))
if len(app.Channels) != 3 {
t.Error("Must have 3 channels")
}
}
func Test_New_Subscriber(t *testing.T) {
app := newApp()
if len(app.Connections) != 0 {
t.Error("Length of subscribers before test must be 0")
}
conn := NewConnection("1", nil)
app.Connect(conn)
if len(app.Connections) != 1 {
t.Error("Length os subscribers after test must be 1")
}
}
func Test_find_subscriber(t *testing.T) {
app := newApp()
conn := NewConnection("1", nil)
app.Connect(conn)
conn, err := app.FindConnection("1")
if err != nil {
t.Error(err)
}
if conn.SocketID != "1" {
t.Error("Wrong subscriber.")
}
// Find a wrong subscriber
conn, err = app.FindConnection("DoesNotExists")
if err == nil {
t.Error("Opps, Must be nil")
}
if conn != nil {
t.Error("Opps, Must be nil")
}
}
func Test_find_or_create_channels(t *testing.T) {
app := newApp()
// Public
if len(app.PublicChannels()) != 0 {
t.Error("Length of public channels must be 0 before test")
}
c := app.FindOrCreateChannelByChannelID("id")
if len(app.PublicChannels()) != 1 {
t.Error("Length os public channels after insert must be 1")
}
if c.ChannelID != "id" {
t.Error("Opps wrong channel")
}
// Presence
if len(app.PresenceChannels()) != 0 {
t.Error("Length of presence channels must be 0 before test")
}
c = app.FindOrCreateChannelByChannelID("presence-test")
if len(app.PresenceChannels()) != 1 {
t.Error("Length os presence channels after insert must be 1")
}
if c.ChannelID != "presence-test" {
t.Error("Opps wrong channel")
}
// Private
if len(app.PrivateChannels()) != 0 {
t.Error("Length of private channels must be 0 before test")
}
c = app.FindOrCreateChannelByChannelID("private-test")
if len(app.PrivateChannels()) != 1 {
t.Error("Length os private channels after insert must be 1")
}
if c.ChannelID != "private-test" {
t.Error("Opps wrong channel")
}
}
+77
View File
@@ -0,0 +1,77 @@
// 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)
}
})
}
+224
View File
@@ -0,0 +1,224 @@
// 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 (
"encoding/json"
"errors"
"strings"
"sync"
"time"
log "github.com/golang/glog"
)
// A Channel
type Channel struct {
sync.Mutex
CreatedAt time.Time
ChannelID string
Subscriptions map[string]*Subscription
}
// Return true if the channel has at least one subscriber
func (c *Channel) IsOccupied() bool {
return c.TotalSubscriptions() > 0
}
// Check if the type of the channel is presence or is private
func (c *Channel) IsPresenceOrPrivate() bool {
return c.IsPresence() || c.IsPrivate()
}
// Check if the type of the channel is public
func (c *Channel) IsPublic() bool {
return !c.IsPresenceOrPrivate()
}
// Check if the type of the channel is presence
func (c *Channel) IsPresence() bool {
return strings.HasPrefix(c.ChannelID, "presence-")
}
// Check if the type of the channel is private
func (c *Channel) IsPrivate() bool {
return strings.HasPrefix(c.ChannelID, "private-")
}
// Get the total of subscribers
func (c *Channel) TotalSubscriptions() int {
return len(c.Subscriptions)
}
// Get the total of users.
func (c *Channel) TotalUsers() int {
total := make(map[string]int)
for _, s := range c.Subscriptions {
total[s.Id]++
}
return len(total)
}
// Add a new subscriber to the channel
func (c *Channel) Subscribe(a *App, conn *Connection, channelData string) error {
log.Infof("Subscribing %s to channel %s", conn.SocketID, c.ChannelID)
c.Lock()
defer c.Unlock()
subscription := NewSubscription(conn, channelData)
c.Subscriptions[conn.SocketID] = subscription
if c.IsPresence() {
// User Info Data
var info struct {
UserID string `json:"user_id"`
UserInfo json.RawMessage `json:"user_info"`
}
log.Infof("%+v", channelData)
if err := json.Unmarshal([]byte(channelData), &info); err != nil {
log.Error(err)
return err
}
js, err := info.UserInfo.MarshalJSON()
if err != nil {
log.Error(err)
return err
}
// Update the Subscription
subscription.Id = info.UserID
subscription.Data = string(js)
// Publish pusher_internal:member_added
c.PublishMemberAddedEvent(a, channelData, subscription)
// WebHook
a.TriggerMemberAddedHook(c, subscription)
// pusher_internal:subscription_succeeded
data := make(map[string]SubscriptionSucceeedEventPresenceData)
data["presence"] = NewSubscriptionSucceedEventPresenceData(c)
js, err = json.Marshal(data)
if err != nil {
log.Error(err)
return err
}
conn.Publish(NewSubscriptionSucceededEvent(c.ChannelID, string(js)))
} else {
conn.Publish(NewSubscriptionSucceededEvent(c.ChannelID, "{}"))
}
// WebHook
if c.TotalSubscriptions() == 1 {
a.TriggerChannelOccupiedHook(c)
}
return nil
}
// IsSubscribed check if the user is subscribed
func (c *Channel) IsSubscribed(conn *Connection) bool {
_, exists := c.Subscriptions[conn.SocketID]
return exists
}
// Remove the subscriber from the channel
// It destroy the channel if the channels does not have any subscribers.
func (c *Channel) Unsubscribe(a *App, conn *Connection) error {
log.Infof("Unsubscribing %s from channel %s", conn.SocketID, c.ChannelID)
c.Lock()
defer c.Unlock()
subscription, exists := c.Subscriptions[conn.SocketID]
if !exists {
return errors.New("Subscription not found")
}
delete(c.Subscriptions, conn.SocketID)
if c.IsPresence() {
// Publish pusher_internal:member_removed
c.PublishMemberRemovedEvent(a, subscription)
// Webhook
a.TriggerMemberRemovedHook(c, subscription)
}
if !c.IsOccupied() {
// WebHook
a.TriggerChannelVacatedHook(c)
// Remove the empty Channel
a.RemoveChannel(c)
}
return nil
}
// Create a new Channel
func NewChannel(channelID string) *Channel {
log.Infof("Creating a new channel: %s", channelID)
return &Channel{ChannelID: channelID, CreatedAt: time.Now(), Subscriptions: make(map[string]*Subscription)}
}
// Publish a MemberAddedEvent to all subscriptions
func (c *Channel) PublishMemberAddedEvent(a *App, data string, subscription *Subscription) {
for _, subs := range c.Subscriptions {
if subs != subscription {
subs.Connection.Publish(NewMemberAddedEvent(c.ChannelID, data))
}
}
}
// Publish a MemberRemovedEvent to all subscriptions
func (c *Channel) PublishMemberRemovedEvent(a *App, subscription *Subscription) {
for _, subs := range c.Subscriptions {
if subs != subscription {
subs.Connection.Publish(NewMemberRemovedEvent(c.ChannelID, subscription))
}
}
}
// Publish messages to all Subscribers
func (c *Channel) Publish(a *App, event RawEvent, ignore string) error {
b, err := event.Data.MarshalJSON()
if err != nil {
return err
}
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
log.Infof("Publishing message %+v to channel %s", v, c.ChannelID)
for _, subs := range c.Subscriptions {
if subs.Connection.SocketID != ignore {
subs.Connection.Publish(NewResponseEvent(event.Event, event.Channel, v))
} else {
// Webhook
if strings.HasPrefix(event.Event, "client-") {
a.TriggerClientEventHook(c, subs, event.Event, v)
}
}
}
return nil
}
+98
View File
@@ -0,0 +1,98 @@
// 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 "testing"
func TestIsOccupied(t *testing.T) {
c := NewChannel("ID")
if c.IsOccupied() {
t.Error("Channels must be empty")
}
c.Subscriptions["ID"] = NewSubscription(NewConnection("ID", nil), "")
if !c.IsOccupied() {
t.Error("Channels must be empty")
}
}
func TestIsPrivate(t *testing.T) {
c := NewChannel("private-channel")
if !c.IsPrivate() {
t.Error("The Channel must be private")
}
}
func TestIsPresence(t *testing.T) {
c := NewChannel("presence-channel")
if !c.IsPresence() {
t.Error("The Channel must be presence")
}
}
func TestIsPublic(t *testing.T) {
c := NewChannel("channel")
if !c.IsPublic() {
t.Error("The Channel must be public")
}
}
func TestIsPrivateOrPresence(t *testing.T) {
c := NewChannel("private-channel")
if !c.IsPresenceOrPrivate() {
t.Error("The Channel must be private or presence")
}
c = NewChannel("presence-channel")
if !c.IsPresenceOrPrivate() {
t.Error("The Channel must be private or presence")
}
}
func TestTotalSubscriptions(t *testing.T) {
c := NewChannel("ID")
if c.TotalSubscriptions() != len(c.Subscriptions) {
t.Error("TotalSubscriptions must be equal to len of total subscriptions")
}
}
func TestTotalUsers(t *testing.T) {
c := NewChannel("ID")
c.Subscriptions["1"] = NewSubscription(NewConnection("ID", nil), "")
c.Subscriptions["2"] = NewSubscription(NewConnection("ID", nil), "")
if c.TotalSubscriptions() != len(c.Subscriptions) {
t.Error("TotalSubscriptions must be equal to len of total subscriptions")
}
if c.TotalUsers() != 1 {
t.Error("TotalUsers must be equal to 1")
}
}
func TestIsSubscribed(t *testing.T) {
c := NewChannel("ID")
conn := NewConnection("ID", nil)
if c.IsSubscribed(conn) {
t.Error("Must not be subscribed")
}
c.Subscriptions["ID"] = NewSubscription(conn, "")
if !c.IsSubscribed(conn) {
t.Error("Must be subscribed")
}
}
+30
View File
@@ -0,0 +1,30 @@
{
"Host": ":8080",
"Expvar": true,
"User": "Username",
"Password": "123456",
"Apps": [
{
"ApplicationDisabled": false,
"OnlySSL": false,
"Secret": "7ad3753142a6693b25b9",
"Key": "278d525bdf162c739803",
"Name": "App 1",
"AppID": "321",
"UserEvents": true,
"WebHooks": true,
"URLWebHook": "http://127.0.0.1:4567/php/hook.php"
},
{
"ApplicationDisabled": false,
"OnlySSL": false,
"Secret": "d6824d2fa32888931504",
"Key": "c8b30f611ffb13202976",
"Name": "App 2",
"AppID": "123",
"UserEvents": true,
"WebHooks": false,
"URLWebHook": "http://127.0.0.1:4567/php/hook.php"
}
]
}
+53
View File
@@ -0,0 +1,53 @@
// 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 (
"errors"
"strings"
)
// The config file
type ConfigFile struct {
Host string // The host, eg: :8080 will start on 0.0.0.0:8080
Expvar bool
User string
Password string
Apps []*App
}
// Error for App not found
var AppNotFoundError = errors.New("App not found")
// Initialize Apps
func (c *ConfigFile) Init() {
for _, app := range c.Apps {
app.Init()
}
}
func (c *ConfigFile) WasProvidedUserAndPassword() bool {
return len(strings.TrimSpace(c.User)) > 0 && len(strings.TrimSpace(c.Password)) > 0
}
// Returns an App with by appID
func (c *ConfigFile) GetAppByAppID(appID string) (*App, error) {
for _, app := range c.Apps {
if app.AppID == appID {
return app, nil
}
}
return &App{}, AppNotFoundError
}
// Returns an App with by key
func (c *ConfigFile) GetAppByKey(key string) (*App, error) {
for _, app := range c.Apps {
if app.Key == key {
return app, nil
}
}
return &App{}, AppNotFoundError
}
+32
View File
@@ -0,0 +1,32 @@
// 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 (
log "github.com/golang/glog"
"github.com/gorilla/websocket"
)
// An User Connection
type Connection struct {
SocketID string
Socket *websocket.Conn
}
// Create a new Subscriber
func NewConnection(socketID string, s *websocket.Conn) *Connection {
log.Infof("Creating a new Subscriber %+v", socketID)
return &Connection{SocketID: socketID, Socket: s}
}
// Publish the message to websocket atached to this client
func (conn *Connection) Publish(m interface{}) {
go func() {
if err := conn.Socket.WriteJSON(m); err != nil {
log.Errorf("Error publishing message to connection %+v, %s", conn, err)
}
}()
}
+5
View File
@@ -0,0 +1,5 @@
// 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
+47
View File
@@ -0,0 +1,47 @@
// 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
// Error Codes
const (
// 4000 - 4099
// Indicates an error resulting in the connection being closed by Pusher,
// and that attempting to reconnect using the same parameters will not succeed.
APPLICATION_ONLY_ACCEPTS_SSL = 4000
APPLICATION_DOES_NOT_EXISTS = 4001
APPLICATION_DISABLED = 4003
APPLICATION_IS_OVER_CONNECTION_QUOTA = 4004 // Not Implemented
PATH_NOT_FOUND = 4005 // Not Implemented
INVALID_VERSION_STRING_FORMAT = 4006
UNSUPPORTED_PROTOCOL_VERSION = 4007
NO_PROTOCOL_VERSION_SUPPLIED = 4008
// 4100 - 4199
// Indicates an error resulting in the connection being closed by Pusher,
// and the client may reconnect after 1s or more
OVER_CAPACITY = 4100 // Not Implemented
// 4200 - 4299
// Indicate an error resulting in the connection being closed by Pusher,
// and the client my reconnect immediately
GENERIC_RECONNECT_IMMEDIATELY = 4200
PONG_REPLY_NOT_RECEIVED = 4201 // Ping was sent to the client, but no reply was received
CLOSED_AFTER_INACTIVITY = 4202 // Client has been inactive for a long time (24 hours) and client does not suppot ping.
// 4300 - 4399
// Any other type of error
CLIENT_REJECTED_DUE_TO_RATE_LIMIT = 4301 // Not Implemented
// Pusher send null, This app use this error code to send the null value
// see ErrorEvent
GENERIC_ERROR = 0
)
// Only this version is supported
const SUPPORTED_PROTOCOL_VERSION = 7
// // Maximun event size permitted 20 kB
// See: http://blogs.gnome.org/cneumair/2008/09/30/1-kb-1024-bytes-no-1-kb-1000-bytes/
const MAX_DATA_EVENT_SIZE = 10 * 1000
+118
View File
@@ -0,0 +1,118 @@
// 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
// Base interface
type WebsocketError interface {
GetCode() int
GetMsg() string
}
// Base struct
type BaseWebsocketError struct {
Code int
Msg string
}
func (e BaseWebsocketError) GetCode() int {
return e.Code
}
func (e BaseWebsocketError) GetMsg() string {
return e.Msg
}
// Unsupprted protocol version
type UnsupportedProtocolVersionError struct {
BaseWebsocketError
}
func NewUnsupportedProtocolVersionError() UnsupportedProtocolVersionError {
return UnsupportedProtocolVersionError{
BaseWebsocketError{Code: UNSUPPORTED_PROTOCOL_VERSION, Msg: "Unsupported protocol version"},
}
}
// The application does not exists
// See the configuration file
type ApplicationDoesNotExistsError struct {
BaseWebsocketError
}
func NewApplicationDoesNotExistsError() ApplicationDoesNotExistsError {
return ApplicationDoesNotExistsError{
BaseWebsocketError{Code: APPLICATION_DOES_NOT_EXISTS, Msg: "Could not found an app with the given key"},
}
}
// The user did not send the protocol version
type NoProtocolVersionSuppliedError struct {
BaseWebsocketError
}
func NewNoProtocolVersionSuppliedError() NoProtocolVersionSuppliedError {
return NoProtocolVersionSuppliedError{
BaseWebsocketError{Code: NO_PROTOCOL_VERSION_SUPPLIED, Msg: "No protocol version supplied"},
}
}
// When the application is disabled.
// See the configuration file
type ApplicationDisabledError struct {
BaseWebsocketError
}
func NewApplicationDisabledError() NoProtocolVersionSuppliedError {
return NoProtocolVersionSuppliedError{
BaseWebsocketError{Code: APPLICATION_DISABLED, Msg: "Application disabled"},
}
}
// When the application only accepts SSL connections
type ApplicationOnlyAccepsSSLError struct {
BaseWebsocketError
}
func NewApplicationOnlyAccepsSSLError() ApplicationOnlyAccepsSSLError {
return ApplicationOnlyAccepsSSLError{
BaseWebsocketError{Code: APPLICATION_ONLY_ACCEPTS_SSL, Msg: "Application only accepts SSL connections, reconnect using wss://"},
}
}
// When the user send an invalid version
type InvalidVersionStringFormatError struct {
BaseWebsocketError
}
func NewInvalidVersionStringFormatError() InvalidVersionStringFormatError {
return InvalidVersionStringFormatError{
BaseWebsocketError{Code: INVALID_VERSION_STRING_FORMAT, Msg: "Invalid version string format"},
}
}
// Used when the error was internal
// * Decoding json
// * Writing to output
type GenericReconnectImmediatelyError struct {
BaseWebsocketError
}
func NewGenericReconnectImmediatelyError() GenericReconnectImmediatelyError {
return GenericReconnectImmediatelyError{
BaseWebsocketError{Code: GENERIC_RECONNECT_IMMEDIATELY, Msg: "Generic reconnect immediately"},
}
}
// When pusher wants to send an Generic error, it only send the message, the code become nil
// Currently I do not know how to send nil, so I send GENERIC_ERROR
type GenericError struct {
BaseWebsocketError
}
func NewGenericError(msg string) GenericError {
return GenericError{
BaseWebsocketError{Code: GENERIC_ERROR, Msg: msg},
}
}
+278
View File
@@ -0,0 +1,278 @@
// 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 (
"encoding/json"
log "github.com/golang/glog"
)
// {
// "event": "pusher:subscribe",
// "data": {
// "channel": "the channel",
// "auth": "the auth",
// "channelData": "extra data"
// }
// }
type SubscribeEventData struct {
Channel string `json:"channel"`
Auth string `json:"auth,omitempty"`
ChannelData string `json:"channel_data,omitempty"`
}
type SubscribeEvent struct {
Event string `json:"event"`
Data SubscribeEventData `json:"data"`
}
// Create a new subscribe event with the specified channel and data
func NewSubscribeEvent(channel, auth, channelData string) SubscribeEvent {
data := SubscribeEventData{Channel: channel, Auth: auth, ChannelData: channelData}
return SubscribeEvent{Event: "pusher:subscribe", Data: data}
}
type UnsubscribeEventData struct {
Channel string `json:"channel"`
}
// {
// "event": "pusher:unsubscribe",
// "data": {
// "channel": "The channel"
// }
// }
type UnsubscribeEvent struct {
Event string `json:"event"`
Data UnsubscribeEventData `json:"data"`
}
// Create a new unsubscribe event for the specified channel
func NewUnsubscribeEvent(channel string) UnsubscribeEvent {
data := UnsubscribeEventData{Channel: channel}
return UnsubscribeEvent{Event: "pusher:unsubscribe", Data: data}
}
// {
// "event": "pusher_internal:subscription_succeeded",
// "channel": "the channel"
// }
type SubscriptionSucceededEvent struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data string `json:"data"`
}
// Create a new subscription succeed event for the specified channel
func NewSubscriptionSucceededEvent(channel, data string) SubscriptionSucceededEvent {
return SubscriptionSucceededEvent{Event: "pusher_internal:subscription_succeeded", Channel: channel, Data: data}
}
// Data Subscription Succeed
// "{
// \"presence\": {
// \"ids\": [\"11814b369700141b222a3f3791cec2d9\",\"71dd6a29da2a4833336d2a964becf820\"],
// \"hash\": {
// \"11814b369700141b222a3f3791cec2d9\": {
// \"name\":\"Phil Leggetter\",
// \"twitter\": \"@leggetter\"
// },
// \"71dd6a29da2a4833336d2a964becf820\": {
// \"name\":\"Max Williams\",
// \"twitter\": \"@maxthelion\"
// }
// },
// \"count\": 2
// }
// }"
type SubscriptionSucceeedEventPresenceData struct {
Ids []string `json:"ids"`
Hash map[string]interface{} `json:"hash"`
Count int `json:"count"`
}
func NewSubscriptionSucceedEventPresenceData(c *Channel) SubscriptionSucceeedEventPresenceData {
event := SubscriptionSucceeedEventPresenceData{}
var ids []string
hash := make(map[string]interface{}, c.TotalSubscriptions())
for _, s := range c.Subscriptions {
// Do you have any other idea?
var js interface{}
json.Unmarshal([]byte(s.Data), &js)
hash[s.Id] = js
ids = append(ids, s.Id)
}
event.Ids = ids
event.Hash = hash
event.Count = c.TotalSubscriptions()
return event
}
// {
// "event": "pusher:pong",
// "data": {}
// }
type PongEvent struct {
Event string `json:"event"`
Data string `json:"data"`
}
// Create a new pong event
func NewPongEvent() PongEvent {
return PongEvent{Event: "pusher:pong", Data: "{}"}
}
// {
// "event": "pusher:ping",
// "data": {}
// }
type PingEvent struct {
Event string `json:"event"`
Data string `json:"data"`
}
// Create a new ping event
func NewPingEvent() PingEvent {
return PingEvent{Event: "pusher:ping", Data: "{}"}
}
// {
// "event": "pusher:error",
// "data": {
// "message": "A Message",
// "code": 4000
// }
// }
type ErrorEvent struct {
Event string `json:"event"`
Data interface{} `json:"data"`
}
// Create a new error event
// Pusher protocol is very strange in some parts
// It send null in some errors.
// So I created this GENERIC_ERROR thing, just to verify if the json must have null on the error code
func NewErrorEvent(code int, message string) ErrorEvent {
var data interface{}
if code == GENERIC_ERROR {
data = struct {
Code *int `json:"code"`
Message string `json:"message"`
}{
nil,
message,
}
} else {
data = struct {
Code int `json:"code"`
Message string `json:"message"`
}{
code,
message,
}
}
return ErrorEvent{Event: "pusher:error", Data: data}
}
// {
// "event" : "pusher:connection_established",
// "data" : {
// "socket_id" : "123456",
// "activity_timeout" : 120
// }
// }
type ConnectionEstablishedEventData struct {
SocketId string `json:"socket_id"`
ActivityTimeout int `json:"activity_timeout"`
}
type ConnectionEstablishedEvent struct {
Event string `json:"event"`
Data string `json:"data"`
}
// Create a new connection established event using the specified socketId
func NewConnectionEstablishedEvent(socketId string) ConnectionEstablishedEvent {
data := ConnectionEstablishedEventData{SocketId: socketId, ActivityTimeout: 120}
b, err := json.Marshal(data)
if err != nil {
panic("events: Could not Marshal json ConnectionEstablishedEvent")
}
return ConnectionEstablishedEvent{Event: "pusher:connection_established", Data: string(b)}
}
// {
// "event": "pusher_internal:member_added",
// "channel": "presence-example-channel",
// "data": String
// }
type MemberAddedEvent struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data string `json:"data"`
}
func NewMemberAddedEvent(channel, data string) MemberAddedEvent {
return MemberAddedEvent{Event: "pusher_internal:member_added", Channel: channel, Data: data}
}
// {
// "event": "pusher_internal:member_removed",
// "channel": "presence-example-channel",
// "data": String
// }
type MemberRemovedEvent struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data string `json:"data"`
}
func NewMemberRemovedEvent(channel string, s *Subscription) MemberRemovedEvent {
data, err := json.Marshal(struct {
UserID string `json:"user_id"`
}{
UserID: s.Id,
})
if err != nil {
log.Error(err)
}
return MemberRemovedEvent{Event: "pusher_internal:member_removed", Channel: channel, Data: string(data)}
}
// {
// "event": "client-?",
// "channel": "The channel",
// "data": {}
// }
type RawEvent struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data json.RawMessage `json:"data"`
}
type ResponseEvent struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data interface{} `json:"data"`
}
// The response event that is broadcasted to the client sockets
func NewResponseEvent(name, channel string, data interface{}) ResponseEvent {
return ResponseEvent{Event: name, Channel: channel, Data: data}
}
+34
View File
@@ -0,0 +1,34 @@
// 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)
})
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2015 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 (
"encoding/json"
"io/ioutil"
"net/http"
)
// Conf holds the global configuration state
var Conf ConfigFile
// Start Parse the configuration file and starts the ipe server
func Start(configfile string) error {
file, err := ioutil.ReadFile(configfile)
if err != nil {
return err
}
if err := json.Unmarshal(file, &Conf); err != nil {
return err
}
Conf.Init()
router := NewRouter()
if err := http.ListenAndServe(Conf.Host, router); err != nil {
return err
}
return nil
}
+315
View File
@@ -0,0 +1,315 @@
// 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 (
"encoding/json"
"fmt"
"net/http"
"strings"
log "github.com/golang/glog"
"github.com/gorilla/mux"
)
// 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.
//
// The body should contain a Hash of parameters encoded as JSON where data parameter itself is JSON encoded.
//
// Not Implemented:
// Note that these parameters may be provided in the query string, although this is discouraged.
//
// Example:
//
// {"name":"foo","channels":["project-3"],"data":"{\"some\":\"data\"}"}
//
// Response is an empty JSON hash.
//
// POST /apps/{app_id}/events
func PostEvents(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
appID := vars["app_id"]
app, err := Conf.GetAppByAppID(appID)
if err != nil {
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
}
var input struct {
Name string `json:"name"`
Data json.RawMessage `json:"data"`
Channels []string `json:"channels,omitempty"`
Channel string `json:"channel,omitempty"`
SocketID string `json:"socket_id,omitempty"`
}
err = json.NewDecoder(r.Body).Decode(&input)
if err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
// The event data should not be larger than 10KB.
if len(input.Data) > MAX_DATA_EVENT_SIZE {
http.Error(w, "Request too large.", http.StatusRequestEntityTooLarge)
return
}
log.Info(input.Channels)
if len(input.Channel) > 0 && len(input.Channels) == 0 {
input.Channels = append(input.Channels, input.Channel)
}
for _, c := range input.Channels {
channel := app.FindOrCreateChannelByChannelID(c)
app.Publish(channel, RawEvent{Event: input.Name, Channel: c, Data: input.Data}, input.SocketID)
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte("{}"))
}
// Allows fetching a hash of occupied channels (optionally filtered by prefix),
// and optionally one or more attributes for each channel.
//
// Notes:
// 'user_count' is the only attribute documented on the Pusher API
//
// Example:
// {
// "channels": {
// "presence-foobar": {
// user_count: 42
// },
// "presence-another": {
// user_count: 123
// }
// }
// }
//
// GET /apps/{app_id}/channels
func GetChannels(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
vars := mux.Vars(r)
appID := vars["app_id"]
filter := params.Get("filter_by_prefix")
info := params.Get("info")
attributes := strings.Split(info, ",")
requestedUserCount := false
for _, a := range attributes {
if a == "user_count" {
requestedUserCount = true
}
}
// If an attribute such as user_count is requested, and the request is not limited
// to presence channels, the API will return an error (400 code)
if requestedUserCount && filter != "presence-" {
http.Error(w, "Attribute user_count is restricted to presence channels", http.StatusBadRequest)
return
}
app, err := Conf.GetAppByAppID(appID)
if err != nil {
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
}
channels := make(map[string]interface{})
switch filter {
case "presence-":
for _, c := range app.PresenceChannels() {
if requestedUserCount {
channels[c.ChannelID] = struct {
UserCount int `json:"user_count"`
}{
c.TotalUsers(),
}
} else {
channels[c.ChannelID] = struct{}{}
}
}
case "public-":
for _, c := range app.PublicChannels() {
channels[c.ChannelID] = struct{}{}
}
case "private-":
for _, c := range app.PrivateChannels() {
channels[c.ChannelID] = struct{}{}
}
default:
for _, c := range app.Channels {
channels[c.ChannelID] = struct{}{}
}
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
js := make(map[string]interface{}, 1)
js["channels"] = channels
if err := json.NewEncoder(w).Encode(js); err != nil {
log.Error(err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
// Fetch info for one channel
//
// Example:
// {
// occupied: true,
// user_count: 42,
// subscription_count: 42
// }
//
// GET /apps/{app_id}/channels/{channel_name}
func GetChannel(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
params := r.URL.Query()
vars := mux.Vars(r)
appID := vars["app_id"]
app, err := Conf.GetAppByAppID(appID)
if err != nil {
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
}
channelName := vars["channel_name"]
// Channel name could not be empty
if strings.TrimSpace(channelName) == "" {
http.Error(w, "Empty channel name", http.StatusBadRequest)
return
}
info := params.Get("info")
attributes := strings.Split(info, ",")
// Attributes requested
requestedUserCount := false
requestedSubscriptionCount := false
for _, a := range attributes {
switch a {
case "subscription_count":
requestedSubscriptionCount = true
case "user_count":
requestedUserCount = true
}
}
channel, err := app.FindChannelByChannelID(channelName)
// Channel exists?
if err != nil {
http.Error(w, fmt.Sprintf("Could not find a channel with id %s", channelName), http.StatusBadRequest)
return
}
// If an attribute such as user_count is requested, and the request is not limited
// to presence channels, the API will return an error (400 code)
if requestedUserCount && !channel.IsPresence() {
http.Error(w, "Attribute user_count is restricted to presence channels", http.StatusBadRequest)
return
}
// Output
dtoChannel := struct {
Occupied bool `json:"occupied"`
UserCount int `json:"user_count,omitempty"`
SubscriptionCount int `json:"subscription_count,omitempty"`
}{Occupied: channel.IsOccupied()}
switch {
case requestedSubscriptionCount && requestedUserCount:
dtoChannel.UserCount = channel.TotalUsers()
dtoChannel.SubscriptionCount = channel.TotalSubscriptions()
case requestedUserCount:
dtoChannel.UserCount = channel.TotalUsers()
case requestedSubscriptionCount:
dtoChannel.SubscriptionCount = channel.TotalSubscriptions()
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
if err := json.NewEncoder(w).Encode(dtoChannel); err != nil {
log.Error(err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
// Allowed only for presence-channels
//
// Example:
// {
// "users": [
// { "id": "1" },
// { "id": "2" }
// ]
// }
//
// GET /apps/{app_id}/channels/{channel_name}/users
func GetChannelUsers(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
appID := vars["app_id"]
channelName := vars["channel_name"]
isPresence := strings.HasPrefix(channelName, "presence-")
if !isPresence {
http.Error(w, "This api endpoint is restricted to presence channels.", http.StatusBadRequest)
return
}
app, err := Conf.GetAppByAppID(appID)
if err != nil {
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
}
// Get the channel
channel, err := app.FindChannelByChannelID(channelName)
// Channel exists?
if err != nil {
http.Error(w, fmt.Sprintf("Could not find a channel with id %s", channelName), http.StatusBadRequest)
return
}
result := make(map[string][]interface{})
var users []interface{}
for _, s := range channel.Subscriptions {
users = append(users, struct {
Id string `json:"id"`
}{s.Id})
}
result["users"] = users
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
if err := json.NewEncoder(w).Encode(result); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
log.Error(err)
}
}
+158
View File
@@ -0,0 +1,158 @@
// 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 main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func init() {
// Conf = NewConfig(":8080", "123456", "APPID", "Secret", false, false)
channel := NewChannel("presence-c1", "")
channel.addSubscriber(Subscriber{Id: 1, SocketID: "Sock1", Data: "Data1"})
channel.addSubscriber(Subscriber{Id: 2, SocketID: "Sock2", Data: "Data2"})
PresenceChannels["presence-c1"] = channel
PrivateChannels["private-c3"] = NewChannel("private-c3", "")
PublicChannels["c2"] = NewChannel("c2", "")
}
// All Channels
func Test_GetChannels_all(t *testing.T) {
r, _ := http.NewRequest("GET", "/apps/APPID/channels", nil)
w := httptest.NewRecorder()
NewRouter().ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("Must return OK: %s returned", w.Code)
}
channels := make(map[string]interface{})
json.Unmarshal(w.Body.Bytes(), &channels)
if len(channels) != 3 {
t.Error("Must return 3 channels")
}
}
// Only presence channels
func Test_GetChannels_filter_by_presence_prefix(t *testing.T) {
r, _ := http.NewRequest("GET", "/apps/APPID/channels?filter_by_prefix=presence-", nil)
w := httptest.NewRecorder()
NewRouter().ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("Must return OK: %s returned", w.Code)
}
channels := make(map[string]interface{})
json.Unmarshal(w.Body.Bytes(), &channels)
if len(channels) != 1 {
t.Error("Must return 1 channel")
}
}
// Only presence channels and user_count
func Test_GetChannels_filter_by_presence_prefix_and_user_count(t *testing.T) {
r, _ := http.NewRequest("GET", "/apps/APPID/channels?filter_by_prefix=presence-&info=user_count", nil)
w := httptest.NewRecorder()
NewRouter().ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("Must return OK: %s returned", w.Code)
}
channels := make(map[string]struct {
UserCount int `json:"user_count"`
})
json.Unmarshal(w.Body.Bytes(), &channels)
if len(channels) != 1 {
t.Error("Must return 1 channel")
}
channel, exists := channels["presence-c1"]
if !exists {
t.Error("Channel must exist.")
}
if channel.UserCount != 2 {
t.Error("Must be 2 users")
}
}
// User count only alowed in Presence channels
func Test_GetChannels_filter_by_private_prefix_and_info_user_count(t *testing.T) {
r, _ := http.NewRequest("GET", "/apps/APPID/channels?filter_by_prefix=private-&info=user_count", nil)
w := httptest.NewRecorder()
NewRouter().ServeHTTP(w, r)
if w.Code != http.StatusBadRequest {
t.Errorf("Must return BadRequest: %s returned", w.Code)
}
}
func Test_GetChannels_filter_by_public_prefix(t *testing.T) {
r, _ := http.NewRequest("GET", "/apps/APPID/channels?filter_by_prefix=public-", nil)
w := httptest.NewRecorder()
NewRouter().ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("Must return OK: %s returned", w.Code)
}
channels := make(map[string]interface{})
json.Unmarshal(w.Body.Bytes(), &channels)
if len(channels) != 1 {
t.Error("Must return 1 channel")
}
_, exists := channels["c2"]
if !exists {
t.Error("Channel must exist.")
}
}
func Test_GetChannels_filter_by_private_prefix(t *testing.T) {
r, _ := http.NewRequest("GET", "/apps/APPID/channels?filter_by_prefix=private-", nil)
w := httptest.NewRecorder()
NewRouter().ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("Must return OK: %s returned", w.Code)
}
channels := make(map[string]interface{})
json.Unmarshal(w.Body.Bytes(), &channels)
if len(channels) != 1 {
t.Error("Must return 1 channel")
}
_, exists := channels["private-c3"]
if !exists {
t.Error("Channel must exist.")
}
}
+42
View File
@@ -0,0 +1,42 @@
// 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 (
_ "expvar"
"net/http"
"github.com/goji/httpauth"
"github.com/gorilla/mux"
)
// NewRouter is a function that returns a new configured Router
// It add the necessary middlewares
func NewRouter() *mux.Router {
router := mux.NewRouter().StrictSlash(true)
if Conf.Expvar {
if !Conf.WasProvidedUserAndPassword() {
panic("Your are exporting debug variables and looks like you forget to define an User and a Password")
}
router.Handle("/debug/vars", httpauth.SimpleBasicAuth(Conf.User, Conf.Password)(http.DefaultServeMux))
}
for _, route := range routes {
var handler http.Handler
handler = route.HandlerFunc
if route.RequiresRestAuth {
handler = RestAuthenticationHandler(handler)
handler = RestCheckAppDisabledHandler(handler)
}
router.Methods(route.Method).Path(route.Pattern).Name(route.Name).Handler(handler)
}
return router
}
+58
View File
@@ -0,0 +1,58 @@
// 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
}
type Routes []Route
var routes = Routes{
Route{
"PostEvents",
"POST",
"/apps/{app_id}/events",
PostEvents,
true,
},
Route{
"GetChannels",
"GET",
"/apps/{app_id}/channels",
GetChannels,
true,
},
Route{
"GetChannel",
"GET",
"/apps/{app_id}/channels/{channel_name}",
GetChannel,
true,
},
Route{
"GetChannelUsers",
"GET",
"/apps/{app_id}/channels/{channel_name}/users",
GetChannelUsers,
true,
},
Route{
"Websocket",
"GET",
"/app/{key}",
Websocket,
false,
},
}
+17
View File
@@ -0,0 +1,17 @@
// 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
// A Channel Subscription
type Subscription struct {
Connection *Connection
Id string
Data string
}
// Create a new Subscription
func NewSubscription(conn *Connection, data string) *Subscription {
return &Subscription{Connection: conn, Data: data}
}
+164
View File
@@ -0,0 +1,164 @@
// 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 (
"bytes"
"encoding/json"
"net/http"
"time"
"github.com/dimiro1/ipe/utils"
log "github.com/golang/glog"
)
// A WebHook is sent as a HTTP POST request to the url which you specify.
// The POST request payload (body) contains a JSON document, and follows the following format:
// {
// "time_ms": 1327078148132
// "events": [
// { "name": "event_name", "some": "data" }
// ]
// }
//
// Security
// Encryption
//
// You may use a HTTP or a HTTPS url for WebHooks. In most cases HTTP is sufficient, but HTTPS can be useful if your data is sensitive or if you wish to protect against replay attacks for example.
// Authentication
//
// Since anyone could in principle send WebHooks to your application, its important to verify that these WebHooks originated from Pusher. Valid WebHooks will therefore contain these headers which contain a HMAC signature of the WebHook payload (body):
//
// X-Pusher-Key: A Pusher app may have multiple tokens. The oldest active token will be used, identified by this key.
// X-Pusher-Signature: A HMAC SHA256 hex digest formed by signing the POST payload (body) with the tokens secret.
type WebHook struct {
TimeMs int64 `json:"time_ms"`
Events []HookEvent `json:"events"`
}
type HookEvent struct {
Name string `json:"name"`
Channel string `json:"channel"`
Event string `json:"event,omitempty"`
Data interface{} `json:"data,omitempty"`
SocketID string `json:"socket_id,omitempty"`
UserId string `json:"user_id,omitempty"`
}
func NewChannelOcuppiedHook(channel *Channel) HookEvent {
return HookEvent{Name: "channel_occupied", Channel: channel.ChannelID}
}
func NewChannelVacatedHook(channel *Channel) HookEvent {
return HookEvent{Name: "channel_vacated", Channel: channel.ChannelID}
}
func NewMemberAddedHook(channel *Channel, s *Subscription) HookEvent {
return HookEvent{Name: "member_added", Channel: channel.ChannelID, UserId: s.Id}
}
func NewMemberRemovedHook(channel *Channel, s *Subscription) HookEvent {
return HookEvent{Name: "member_removed", Channel: channel.ChannelID, UserId: s.Id}
}
func NewClientHook(channel *Channel, s *Subscription, event string, data interface{}) HookEvent {
return HookEvent{Name: "client_event", Channel: channel.ChannelID, Event: event, Data: data, SocketID: s.Connection.SocketID}
}
// channel_occupied
// { "name": "channel_occupied", "channel": "test_channel" }
func (a *App) TriggerChannelOccupiedHook(c *Channel) {
event := NewChannelOcuppiedHook(c)
triggerHook(event.Name, a, c, event)
}
// channel_vacated
// { "name": "channel_vacated", "channel": "test_channel" }
func (a *App) TriggerChannelVacatedHook(c *Channel) {
event := NewChannelVacatedHook(c)
triggerHook(event.Name, a, c, event)
}
// {
// "name": "client_event",
// "channel": "name of the channel the event was published on",
// "event": "name of the event",
// "data": "data associated with the event",
// "socket_id": "socket_id of the sending socket",
// "user_id": "user_id associated with the sending socket" # Only for presence channels
// }
func (a *App) TriggerClientEventHook(c *Channel, s *Subscription, client_event string, data interface{}) {
event := NewClientHook(c, s, client_event, data)
if c.IsPresence() {
event.UserId = s.Id
}
triggerHook(event.Name, a, c, event)
}
// {
// "name": "member_added",
// "channel": "presence-your_channel_name",
// "user_id": "a_user_id"
// }
func (a *App) TriggerMemberAddedHook(c *Channel, s *Subscription) {
event := NewMemberAddedHook(c, s)
triggerHook(event.Name, a, c, event)
}
// {
// "name": "member_removed",
// "channel": "presence-your_channel_name",
// "user_id": "a_user_id"
// }
func (a *App) TriggerMemberRemovedHook(c *Channel, s *Subscription) {
event := NewMemberRemovedHook(c, s)
triggerHook(event.Name, a, c, event)
}
func triggerHook(name string, app *App, c *Channel, event HookEvent) {
if !app.WebHooks {
log.Infof("Webhooks are not enabled for app: %s", app.Name)
return
}
go func() {
log.Infof("Triggering %s event", name)
hook := WebHook{TimeMs: time.Now().Unix()}
hook.Events = append(hook.Events, event)
var js []byte
var err error
js, err = json.Marshal(hook)
if err != nil {
log.Errorf("Error decoding json: %+v", err)
return
}
var req *http.Request
req, err = http.NewRequest("POST", app.URLWebHook, bytes.NewReader(js))
if err != nil {
log.Errorf("Error creating request: %+v", err)
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Pusher-Key", app.Key)
req.Header.Set("X-Pusher-Signature", utils.HashMAC(js, []byte(app.Secret)))
log.V(1).Infof("%+v", req.Header)
log.V(1).Infof("%+v", string(js))
if _, err := http.DefaultClient.Do(req); err != nil {
log.Errorf("Error posting %s event: %+v", name, err)
}
}()
}
+247
View File
@@ -0,0 +1,247 @@
// 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 (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
log "github.com/golang/glog"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/dimiro1/ipe/utils"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
// Handle open Subscriber.
func onOpen(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, sessionID string, app *App) WebsocketError {
params := r.URL.Query()
p := params.Get("protocol")
protocol, err := strconv.Atoi(p)
if err != nil {
return NewInvalidVersionStringFormatError()
}
switch {
case strings.TrimSpace(p) == "":
return NewNoProtocolVersionSuppliedError()
case protocol != SUPPORTED_PROTOCOL_VERSION:
return NewUnsupportedProtocolVersionError()
case app.ApplicationDisabled:
return NewApplicationDisabledError()
case r.TLS != nil:
if app.OnlySSL {
return NewApplicationOnlyAccepsSSLError()
}
}
// Create the new Subscriber
connection := NewConnection(sessionID, conn)
app.Connect(connection)
// Everything went fine. Huhu.
if err := conn.WriteJSON(NewConnectionEstablishedEvent(connection.SocketID)); err != nil {
return NewGenericReconnectImmediatelyError()
}
return nil
}
// Handle the close event
func onClose(sessionID string, app *App) {
app.Disconnect(sessionID)
}
// Handle messages
//
// If there is an unrecoverable error then break the loop,
// otherwise just keep going.
func onMessage(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, sessionID string, app *App) {
var event struct {
Event string `json:"event"`
}
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Errorf("%+v", err)
switch err {
case io.EOF:
onClose(sessionID, app)
default:
emitWSError(NewGenericReconnectImmediatelyError(), conn)
}
break
}
if err := json.Unmarshal(message, &event); err != nil {
emitWSError(NewGenericReconnectImmediatelyError(), conn)
break
}
log.Infof("websockets: Handling %s event", event.Event)
switch event.Event {
case "pusher:ping":
if err := conn.WriteJSON(NewPongEvent()); err != nil {
emitWSError(NewGenericReconnectImmediatelyError(), conn)
}
case "pusher:subscribe":
subscribeEvent := SubscribeEvent{}
if err := json.Unmarshal(message, &subscribeEvent); err != nil {
emitWSError(NewGenericReconnectImmediatelyError(), conn)
break
}
connection, err := app.FindConnection(sessionID)
if err != nil {
emitWSError(NewGenericReconnectImmediatelyError(), conn)
break
}
channelName := strings.TrimSpace(subscribeEvent.Data.Channel)
isPresence := strings.HasPrefix(channelName, "presence-")
isPrivate := strings.HasPrefix(channelName, "private-")
if isPresence || isPrivate {
toSign := []string{connection.SocketID, channelName}
if isPresence {
toSign = append(toSign, subscribeEvent.Data.ChannelData)
}
expectedAuthKey := fmt.Sprintf("%s:%s", app.Key, utils.HashMAC([]byte(strings.Join(toSign, ":")), []byte(app.Secret)))
if subscribeEvent.Data.Auth != expectedAuthKey {
emitWSError(NewGenericError(fmt.Sprintf("Auth value for subscription to %s is invalid", channelName)), conn)
continue
}
}
channel := app.FindOrCreateChannelByChannelID(channelName)
log.Info(subscribeEvent.Data.ChannelData)
if err := app.Subscribe(channel, connection, subscribeEvent.Data.ChannelData); err != nil {
emitWSError(NewGenericReconnectImmediatelyError(), conn)
}
case "pusher:unsubscribe":
unsubscribeEvent := UnsubscribeEvent{}
if err := json.Unmarshal(message, &unsubscribeEvent); err != nil {
emitWSError(NewGenericReconnectImmediatelyError(), conn)
}
connection, err := app.FindConnection(sessionID)
if err != nil {
emitWSError(NewGenericError(fmt.Sprintf("Could not find a connection with the id %s", sessionID)), conn)
}
channel, err := app.FindChannelByChannelID(unsubscribeEvent.Data.Channel)
if err != nil {
emitWSError(NewGenericError(fmt.Sprintf("Could not find a channel with the id %s", unsubscribeEvent.Data.Channel)), conn)
}
if err := app.Unsubscribe(channel, connection); err != nil {
emitWSError(NewGenericReconnectImmediatelyError(), conn)
break
}
default: // CLient Events ??
// see http://pusher.com/docs/client_api_guide/client_events#trigger-events
if strings.HasPrefix(event.Event, "client-") {
if !app.UserEvents {
emitWSError(NewGenericError("To send client events, you must enable this feature in the Settings."), conn)
}
clientEvent := RawEvent{}
if err := json.Unmarshal(message, &clientEvent); err != nil {
log.Error(err)
emitWSError(NewGenericReconnectImmediatelyError(), conn)
break
}
channel, err := app.FindChannelByChannelID(clientEvent.Channel)
if !channel.IsPresenceOrPrivate() {
emitWSError(NewGenericError("Client event rejected - only supported on private and presence channels"), conn)
break
}
if err != nil {
emitWSError(NewGenericError(fmt.Sprintf("Could not find a channel with the id %s", clientEvent.Channel)), conn)
}
if err := app.Publish(channel, clientEvent, sessionID); err != nil {
log.Error(err)
emitWSError(NewGenericReconnectImmediatelyError(), conn)
break
}
}
} // switch
} // For
}
// Websocket GET /app/{key}
func Websocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
defer func() {
if conn != nil {
conn.Close()
}
}()
if err != nil {
log.Error(err)
return
}
vars := mux.Vars(r)
appKey := vars["key"]
app, err := Conf.GetAppByKey(appKey)
if err != nil {
log.Error(err)
emitWSError(NewApplicationDoesNotExistsError(), conn)
return
}
sessionID := utils.RandomHash()
if err := onOpen(conn, w, r, sessionID, app); err != nil {
emitWSError(err, conn)
return
}
onMessage(conn, w, r, sessionID, app)
}
// Emit an Websocket ErrorEvent
func emitWSError(err WebsocketError, conn *websocket.Conn) {
event := NewErrorEvent(err.GetCode(), err.GetMsg())
if err := conn.WriteJSON(event); err != nil {
log.Error(err)
}
}