File structure organization.
Create a new package ipe and put everything n there except for the main function.
This commit is contained in:
+220
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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
@@ -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, it’s 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 token’s 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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user