First import
This commit is contained in:
+122
@@ -0,0 +1,122 @@
|
||||
# Created by https://www.gitignore.io
|
||||
|
||||
### Go ###
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
|
||||
### SublimeText ###
|
||||
# cache files for sublime text
|
||||
*.tmlanguage.cache
|
||||
*.tmPreferences.cache
|
||||
*.stTheme.cache
|
||||
|
||||
# workspace files are user-specific
|
||||
*.sublime-workspace
|
||||
|
||||
# project files should be checked into the repository, unless a significant
|
||||
# proportion of contributors will probably not be using SublimeText
|
||||
# *.sublime-project
|
||||
|
||||
# sftp configuration file
|
||||
sftp-config.json
|
||||
|
||||
|
||||
### OSX ###
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear on external disk
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
|
||||
### Intellij ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
|
||||
|
||||
*.iml
|
||||
|
||||
## Directory-based project format:
|
||||
.idea/
|
||||
# if you remove the above rule, at least ignore the following:
|
||||
|
||||
# User-specific stuff:
|
||||
# .idea/workspace.xml
|
||||
# .idea/tasks.xml
|
||||
# .idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
# .idea/dataSources.ids
|
||||
# .idea/dataSources.xml
|
||||
# .idea/sqlDataSources.xml
|
||||
# .idea/dynamic.xml
|
||||
# .idea/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
# .idea/gradle.xml
|
||||
# .idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
# .idea/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
|
||||
# Project Specific
|
||||
|
||||
ignore_http/*
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2015 Claudemiro Alves Feitosa Neto <dimiro1@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,10 @@
|
||||
default: install-debug
|
||||
|
||||
install-debug:
|
||||
go install -ldflags "-w" github.com/dimiro1/ipe
|
||||
|
||||
run-debug: install-debug
|
||||
${GOPATH}/bin/ipe --config ${GOPATH}/src/github.com/dimiro1/ipe/config-example.json
|
||||
|
||||
test:
|
||||
go test github.com/dimiro1/ipe
|
||||
@@ -0,0 +1,17 @@
|
||||
Tarefas IPE
|
||||
|
||||
** DONE Autenticação API Rest
|
||||
** DONE Autenticação Websockets
|
||||
** TODO Console de log
|
||||
** TODO Ping e Pong
|
||||
** TODO Escrever testes automatizados
|
||||
** TODO Refatorar partes do código, remover repetições
|
||||
** TODO SSL
|
||||
** TODO Otimizações
|
||||
** TODO Segurança, tempo de expiração, etc
|
||||
** TODO Coletor de conexões abandonadas. Tempo de vida e go routine checking from time to time.
|
||||
|
||||
Objetivos
|
||||
|
||||
** TODO Implementação Funcional.
|
||||
** TODO Testes automatizados após o projeto pronto.
|
||||
@@ -0,0 +1,110 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Applications
|
||||
type App struct {
|
||||
Name string
|
||||
AppID string
|
||||
Key string
|
||||
Secret string
|
||||
OnlySSL bool
|
||||
ApplicationDisabled bool
|
||||
|
||||
PublicChannels []*Channel `json:"-"`
|
||||
PresenceChannels []*Channel `json:"-"`
|
||||
PrivateChannels []*Channel `json:"-"`
|
||||
|
||||
Connections []*Connection `json:"-"`
|
||||
}
|
||||
|
||||
func (a *App) AllChannels() []*Channel {
|
||||
var channels []*Channel
|
||||
|
||||
for _, c := range a.PrivateChannels {
|
||||
channels = append(channels, c)
|
||||
}
|
||||
|
||||
for _, c := range a.PublicChannels {
|
||||
channels = append(channels, c)
|
||||
}
|
||||
|
||||
for _, c := range a.PresenceChannels {
|
||||
channels = append(channels, c)
|
||||
}
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
// Create a new Connection
|
||||
func (a *App) AddConnection(c *Connection) {
|
||||
a.Connections = append(a.Connections, c)
|
||||
}
|
||||
|
||||
func (a *App) FindConnection(socketID string) (*Connection, error) {
|
||||
for _, c := range a.Connections {
|
||||
if c.SocketID == socketID {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("Connection not found")
|
||||
}
|
||||
|
||||
func (a *App) AddChannel(c *Channel) {
|
||||
if c.isPresence() {
|
||||
a.PresenceChannels = append(a.PresenceChannels, c)
|
||||
} else if c.isPrivate() {
|
||||
a.PrivateChannels = append(a.PrivateChannels, c)
|
||||
} else {
|
||||
a.PublicChannels = append(a.PublicChannels, c)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) FindOrCreateChannelByChannelID(n, data string) *Channel {
|
||||
channel, err := a.FindChannelByChannelID(n)
|
||||
|
||||
if err != nil {
|
||||
channel = NewChannel(n, data)
|
||||
|
||||
a.AddChannel(channel)
|
||||
}
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
// Find the channel by channel ID
|
||||
func (a *App) FindChannelByChannelID(n string) (*Channel, error) {
|
||||
isPrivate := strings.HasPrefix(n, "private-")
|
||||
isPresence := strings.HasPrefix(n, "presence-")
|
||||
|
||||
// Get the channel
|
||||
if isPresence {
|
||||
for _, channel := range a.PresenceChannels {
|
||||
if channel.ChannelID == n {
|
||||
return channel, nil
|
||||
}
|
||||
}
|
||||
} else if isPrivate {
|
||||
for _, channel := range a.PrivateChannels {
|
||||
if channel.ChannelID == n {
|
||||
return channel, nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, channel := range a.PublicChannels {
|
||||
if channel.ChannelID == n {
|
||||
return channel, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("Channel does not exists")
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
// 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 (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func newApp() App {
|
||||
return App{Name: "Test", AppID: "123", Key: "123", Secret: "123", OnlySSL: false, ApplicationDisabled: false}
|
||||
}
|
||||
|
||||
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_New_Connection(t *testing.T) {
|
||||
app := newApp()
|
||||
|
||||
if len(app.Connections) != 0 {
|
||||
t.Error("Length of connections before test must be 0")
|
||||
}
|
||||
|
||||
app.NewConnection("1", "", nil)
|
||||
|
||||
if len(app.Connections) != 1 {
|
||||
t.Error("Length os connections after test must be 1")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_find_connection(t *testing.T) {
|
||||
app := newApp()
|
||||
app.NewConnection("1", "", nil)
|
||||
|
||||
conn, err := app.FindConnection("1")
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if conn.SocketID != "1" {
|
||||
t.Error("Wrong connection.")
|
||||
}
|
||||
|
||||
// Find a wrong connection
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// How to test this???
|
||||
func TestRandomID(t *testing.T) {
|
||||
n := randomID()
|
||||
|
||||
if n < 0 {
|
||||
t.Error("Must be greater than or equal zero")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// 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 (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Calculate the Mac
|
||||
func HashMAC(message, key []byte) string {
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write(message)
|
||||
expectedMAC := mac.Sum(nil)
|
||||
|
||||
return hex.EncodeToString(expectedMAC)
|
||||
}
|
||||
|
||||
// Verify the message
|
||||
func checkMAC(message, messageMAC, key []byte) bool {
|
||||
return string(messageMAC) == HashMAC(message, key)
|
||||
}
|
||||
|
||||
// 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"]
|
||||
|
||||
currentApp, err := Conf.GetAppByAppID(appID)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
params := r.URL.Query()
|
||||
|
||||
authSignature := params.Get("auth_signature")
|
||||
params.Del("auth_signature")
|
||||
|
||||
queryString := prepareQueryString(params)
|
||||
|
||||
toSign := strings.ToUpper(r.Method) + "\n" + r.URL.Path + "\n" + queryString
|
||||
|
||||
if checkMAC([]byte(toSign), []byte(authSignature), []byte(currentApp.Secret)) {
|
||||
h.ServeHTTP(w, r)
|
||||
} else {
|
||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Host": ":8080",
|
||||
"SessionSecret": "372843uhudsbdfy4ure8dbyrty73uhf%7327#vbˆB%bB66BˆVFTV#12g2",
|
||||
"SessionName": "ipe",
|
||||
"Apps": [
|
||||
{
|
||||
"ApplicationDisabled": false,
|
||||
"OnlySSL": false,
|
||||
"Secret": "7ad3773142a6692b25b8",
|
||||
"Key": "278d425bdf160c739803",
|
||||
"Name": "Teste",
|
||||
"AppID": "1"
|
||||
},
|
||||
{
|
||||
"ApplicationDisabled": false,
|
||||
"OnlySSL": false,
|
||||
"Secret": "2189389a989a9ab8921f",
|
||||
"Key": "6a266506823423cf2a1f",
|
||||
"Name": "Apresentacao",
|
||||
"AppID": "2"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type ConfigFile struct {
|
||||
Host string
|
||||
SessionName string
|
||||
SessionSecret string
|
||||
Apps []*App
|
||||
}
|
||||
|
||||
func (c ConfigFile) GetAppByAppID(appID string) (*App, error) {
|
||||
for _, app := range c.Apps {
|
||||
if app.AppID == appID {
|
||||
return app, nil
|
||||
}
|
||||
}
|
||||
return &App{}, errors.New("App not found")
|
||||
}
|
||||
|
||||
func (c ConfigFile) GetAppByKey(key string) (*App, error) {
|
||||
for _, app := range c.Apps {
|
||||
if app.Key == key {
|
||||
return app, nil
|
||||
}
|
||||
}
|
||||
return &App{}, errors.New("App not found")
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"github.com/gorilla/websocket"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// A subscriber
|
||||
type Connection struct {
|
||||
Id int
|
||||
SocketID string
|
||||
Data string // Extra data attached to this subscriber
|
||||
Socket *websocket.Conn
|
||||
Messages chan []byte
|
||||
}
|
||||
|
||||
// A channel
|
||||
type Channel struct {
|
||||
ChannelID string
|
||||
Data string
|
||||
Connections []*Connection
|
||||
Messages chan []byte
|
||||
}
|
||||
|
||||
// Return true if the channel has at least one subscriber
|
||||
func (c Channel) isOccupied() bool {
|
||||
return c.totalConnections() > 0
|
||||
}
|
||||
|
||||
// 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) totalConnections() int {
|
||||
return len(c.Connections)
|
||||
}
|
||||
|
||||
// Get the total of users.
|
||||
// For now, totalUsers is equal to totalSubscribers
|
||||
func (c Channel) totalUsers() int {
|
||||
return c.totalConnections()
|
||||
}
|
||||
|
||||
// Add a new subscriber to the channel
|
||||
func (c *Channel) Subscribe(conn *Connection) {
|
||||
c.Connections = append(c.Connections, conn)
|
||||
}
|
||||
|
||||
// Remove the subscriber from the channel
|
||||
func (c *Channel) Unsubscribe(conn *Connection) error {
|
||||
index := -1
|
||||
|
||||
for i, c := range c.Connections {
|
||||
if c == conn {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if index == -1 {
|
||||
return errors.New("Connections not found")
|
||||
}
|
||||
|
||||
c.Connections = append(c.Connections[:index], c.Connections[index+1:]...)
|
||||
|
||||
// Remove Channel if necessary
|
||||
// Close sockets and Channels
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new Channel
|
||||
func NewChannel(channelID, data string) *Channel {
|
||||
c := &Channel{ChannelID: channelID, Data: data, Messages: make(chan []byte)}
|
||||
c.Listen()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
var mu = &sync.Mutex{}
|
||||
var currentID = 0
|
||||
|
||||
// Generate a New ID
|
||||
func newID() int {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
currentID += 1
|
||||
|
||||
return currentID
|
||||
}
|
||||
|
||||
// Create a new Connection
|
||||
func NewConnection(socketID, data string, socket *websocket.Conn) *Connection {
|
||||
id := newID()
|
||||
|
||||
connection := &Connection{Id: id, SocketID: socketID, Data: data, Socket: socket, Messages: make(chan []byte)}
|
||||
connection.Listen()
|
||||
|
||||
return connection
|
||||
}
|
||||
|
||||
func (c *Channel) Listen() {
|
||||
go func() {
|
||||
select {
|
||||
case message := <-c.Messages:
|
||||
// err := c.Socket.WriteMessage(websocket.TextMessage, message)
|
||||
// log.Println(err)
|
||||
log.Println(message)
|
||||
}
|
||||
|
||||
}()
|
||||
}
|
||||
|
||||
// Listen Messages
|
||||
func (c *Connection) Listen() {
|
||||
go func() {
|
||||
select {
|
||||
case message := <-c.Messages:
|
||||
// err := c.Socket.WriteMessage(websocket.TextMessage, message)
|
||||
// log.Println(err)
|
||||
log.Println(message)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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 ()
|
||||
|
||||
// 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
|
||||
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 send -1 on the error
|
||||
GENERIC_ERROR = -1
|
||||
)
|
||||
|
||||
// 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
|
||||
@@ -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 main
|
||||
|
||||
// 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},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// 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
|
||||
|
||||
// {
|
||||
// "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:"channelData,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"`
|
||||
}
|
||||
|
||||
// Create a new subscription succeed event for the specified channel
|
||||
func NewSubscriptionSucceededEvent(channel string) SubscriptionSucceededEvent {
|
||||
return SubscriptionSucceededEvent{Event: "pusher_internal:subscription_succeeded", Channel: channel}
|
||||
}
|
||||
|
||||
// {
|
||||
// "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 ErrorEventData struct {
|
||||
Message string `json:"message"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
type ErrorEvent struct {
|
||||
Event string `json:"event"`
|
||||
Data ErrorEventData `json:"data"`
|
||||
}
|
||||
|
||||
// Create a new error event
|
||||
func NewErrorEvent(code int, message string) ErrorEvent {
|
||||
data := ErrorEventData{Message: message, Code: code}
|
||||
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 ConnectionEstablishedEventData `json:"data"`
|
||||
}
|
||||
|
||||
// Create a new connection established event using the specified socketId
|
||||
func NewConnectionEstablishedEvent(socketId string) ConnectionEstablishedEvent {
|
||||
data := ConnectionEstablishedEventData{SocketId: socketId, ActivityTimeout: 120}
|
||||
return ConnectionEstablishedEvent{Event: "pusher:connection_established", Data: data}
|
||||
}
|
||||
|
||||
// {
|
||||
// "event": "client-?",
|
||||
// "channel": "The channel",
|
||||
// "data": {
|
||||
// "message": "A Message"
|
||||
// }
|
||||
// }
|
||||
type ClientEventData struct {
|
||||
Message string `json:"data"`
|
||||
}
|
||||
|
||||
type ClientEvent struct {
|
||||
Event string `json:"event"`
|
||||
Channel string `json:"channel"`
|
||||
Data ClientEventData `json:"data"`
|
||||
}
|
||||
|
||||
// Create a new custom client event
|
||||
func NewClientEvent(name, channel, message string) ClientEvent {
|
||||
data := ClientEventData{Message: message}
|
||||
return ClientEvent{Event: "pusher:client-" + name, Channel: channel, Data: data}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
|
||||
func LogHandler(h http.Handler) http.Handler {
|
||||
return handlers.CombinedLoggingHandler(os.Stdout, h)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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"
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var Conf ConfigFile
|
||||
|
||||
func main() {
|
||||
var filename = flag.String("config", "config.json", "Config file location")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
file, err := ioutil.ReadFile(*filename)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(file, &Conf); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
router := NewRouter()
|
||||
|
||||
log.Printf("Starting Ipê using config file: '%s'", *filename)
|
||||
|
||||
if err := http.ListenAndServe(Conf.Host, router); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
var input struct {
|
||||
Name string `json:"name"`
|
||||
Data string `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
|
||||
}
|
||||
|
||||
// Trigger events
|
||||
|
||||
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.AllChannels() {
|
||||
channels[c.ChannelID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(channels); err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// Check the kind of 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
|
||||
}
|
||||
|
||||
// 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.totalConnections()
|
||||
|
||||
case requestedUserCount:
|
||||
dtoChannel.UserCount = channel.totalUsers()
|
||||
|
||||
case requestedSubscriptionCount:
|
||||
dtoChannel.SubscriptionCount = channel.totalConnections()
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(dtoChannel); err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Allowed only to 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.Connections {
|
||||
users = append(users, struct {
|
||||
Id int `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.Println(err)
|
||||
}
|
||||
}
|
||||
+158
@@ -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,31 @@
|
||||
// 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 (
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func NewRouter() *mux.Router {
|
||||
router := mux.NewRouter().StrictSlash(true)
|
||||
|
||||
for _, route := range routes {
|
||||
var handler http.Handler
|
||||
|
||||
handler = route.HandlerFunc
|
||||
|
||||
if route.RequiresRestAuth {
|
||||
handler = RestAuthenticationHandler(handler)
|
||||
handler = RestCheckAppDisabledHandler(handler)
|
||||
}
|
||||
|
||||
handler = LogHandler(handler)
|
||||
|
||||
router.Methods(route.Method).Path(route.Pattern).Name(route.Name).Handler(handler)
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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 (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/gorilla/websocket"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
// Handle open connection.
|
||||
func onOpen(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, session *sessions.Session, 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 connection
|
||||
connection := NewConnection(session.ID, "", conn)
|
||||
app.AddConnection(connection)
|
||||
|
||||
// Everything went fine. Huhu.
|
||||
if err := conn.WriteJSON(NewConnectionEstablishedEvent(connection.SocketID)); err != nil {
|
||||
return NewGenericReconnectImmediatelyError()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle messages
|
||||
func onMessage(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, session *sessions.Session, app *App) WebsocketError {
|
||||
var event struct {
|
||||
Event string `json:"event"`
|
||||
}
|
||||
|
||||
for {
|
||||
_, message, err := conn.ReadMessage()
|
||||
|
||||
if err != nil {
|
||||
return NewGenericReconnectImmediatelyError()
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(message, &event); err != nil {
|
||||
return NewGenericReconnectImmediatelyError()
|
||||
}
|
||||
|
||||
switch event.Event {
|
||||
case "pusher:ping":
|
||||
if err := conn.WriteJSON(NewPongEvent()); err != nil {
|
||||
return NewGenericReconnectImmediatelyError()
|
||||
}
|
||||
case "pusher:subscribe":
|
||||
subscribeEvent := SubscribeEvent{}
|
||||
|
||||
if err := json.Unmarshal(message, &subscribeEvent); err != nil {
|
||||
return NewGenericReconnectImmediatelyError()
|
||||
}
|
||||
|
||||
connection, err := app.FindConnection(session.ID)
|
||||
|
||||
if err != nil {
|
||||
return NewGenericReconnectImmediatelyError()
|
||||
}
|
||||
|
||||
channelName := strings.TrimSpace(subscribeEvent.Data.Channel)
|
||||
|
||||
// Authentication
|
||||
if strings.HasPrefix(channelName, "presence-") {
|
||||
toSign := fmt.Sprintf("%s:%s:%s", connection.SocketID, channelName, subscribeEvent.Data.ChannelData)
|
||||
|
||||
if subscribeEvent.Data.Auth != HashMAC([]byte(toSign), []byte(app.Secret)) {
|
||||
return NewGenericError(fmt.Sprintf("Auth value for subscription to %s is invalid", channelName))
|
||||
}
|
||||
} else if strings.HasPrefix(channelName, "private-") {
|
||||
toSign := fmt.Sprintf("%s:%s", connection.SocketID, channelName)
|
||||
|
||||
if subscribeEvent.Data.Auth != HashMAC([]byte(toSign), []byte(app.Secret)) {
|
||||
return NewGenericError(fmt.Sprintf("Auth value for subscription to %s is invalid", channelName))
|
||||
}
|
||||
}
|
||||
|
||||
channel := app.FindOrCreateChannelByChannelID(channelName, subscribeEvent.Data.ChannelData)
|
||||
channel.Subscribe(connection)
|
||||
|
||||
if err := conn.WriteJSON(NewSubscriptionSucceededEvent(channel.ChannelID)); err != nil {
|
||||
return NewGenericReconnectImmediatelyError()
|
||||
}
|
||||
case "pusher:unsubscribe":
|
||||
unsubscribeEvent := UnsubscribeEvent{}
|
||||
|
||||
if err := json.Unmarshal(message, &unsubscribeEvent); err != nil {
|
||||
return NewGenericReconnectImmediatelyError()
|
||||
}
|
||||
|
||||
connection, err := app.FindConnection(session.ID)
|
||||
|
||||
if err != nil {
|
||||
return NewGenericError(fmt.Sprintf("Could not find a connection with the id %s", session.ID))
|
||||
}
|
||||
|
||||
channel, err := app.FindChannelByChannelID(unsubscribeEvent.Data.Channel)
|
||||
|
||||
if err != nil {
|
||||
return NewGenericError(fmt.Sprintf("Could not find a channel with the id %s", unsubscribeEvent.Data.Channel))
|
||||
}
|
||||
|
||||
if err := channel.Unsubscribe(connection); err != nil {
|
||||
return NewGenericReconnectImmediatelyError()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
// Client Events
|
||||
}
|
||||
}
|
||||
|
||||
// Websocket GET /app/{key}
|
||||
func Websocket(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
emitWSError(NewGenericReconnectImmediatelyError(), conn)
|
||||
return
|
||||
}
|
||||
|
||||
var store = sessions.NewFilesystemStore("", []byte(Conf.SessionSecret))
|
||||
session, err := store.Get(r, Conf.SessionName)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
emitWSError(NewGenericReconnectImmediatelyError(), conn)
|
||||
return
|
||||
}
|
||||
|
||||
if err := session.Save(r, w); err != nil {
|
||||
log.Println(err)
|
||||
emitWSError(NewGenericReconnectImmediatelyError(), conn)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
appKey := vars["key"]
|
||||
|
||||
app, err := Conf.GetAppByKey(appKey)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
emitWSError(NewApplicationDoesNotExistsError(), conn)
|
||||
return
|
||||
}
|
||||
|
||||
if err := onOpen(conn, w, r, session, app); err != nil {
|
||||
emitWSError(err, conn)
|
||||
return
|
||||
}
|
||||
|
||||
if err := onMessage(conn, w, r, session, app); err != nil {
|
||||
emitWSError(err, conn)
|
||||
|
||||
// Find the connection in app and destroy it
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 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.Println(err)
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user