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