Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7deb3edc3 | |||
| ea6e116836 | |||
| 3cb9eb27e1 | |||
| 0ff81caae4 | |||
| 0de46888eb | |||
| 05c088f158 | |||
| 3c51d0b614 | |||
| 1a9fb1a706 | |||
| d412759c16 | |||
| f4781ee483 | |||
| 964df8a5dd | |||
| 8da763ec2f | |||
| 4523549f71 | |||
| fe1eb8c5eb | |||
| 481311061f | |||
| b117386591 | |||
| 746b978e52 | |||
| da48528e51 | |||
| 065d6ab286 | |||
| 1d1ffa00b5 | |||
| 983f490737 | |||
| 5c9528bce4 | |||
| 8fb96f3cbf | |||
| 9d732fb01a | |||
| 0bd4fa02e9 | |||
| 4a305b32a6 | |||
| 4c5d5302ec | |||
| 994e8e00f1 | |||
| 9ad14daeeb | |||
| 0ac6c7a55c |
@@ -155,6 +155,7 @@ flymake*
|
|||||||
|
|
||||||
ignore_http/*
|
ignore_http/*
|
||||||
config.json
|
config.json
|
||||||
|
config.yml
|
||||||
*.pem
|
*.pem
|
||||||
build
|
build
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
FROM golang:alpine as builder
|
||||||
|
RUN mkdir /build
|
||||||
|
ADD . /build/
|
||||||
|
WORKDIR /build
|
||||||
|
RUN apk add git gcc musl-dev
|
||||||
|
RUN go build -o ipe ./cmd
|
||||||
|
FROM alpine
|
||||||
|
USER root
|
||||||
|
RUN mkdir -p /config
|
||||||
|
RUN adduser -S -D -H -h /app appuser
|
||||||
|
COPY ./entrypoint.sh /app/entrypoint.sh
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
USER appuser
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /build/ipe /app/
|
||||||
|
COPY --from=builder /build/config-example.yml /app/config-example.yml
|
||||||
|
VOLUME /config
|
||||||
|
CMD ["/bin/sh", "/app/entrypoint.sh"]
|
||||||
|
EXPOSE 4343
|
||||||
|
EXPOSE 8080
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
[](https://goreportcard.com/report/github.com/dimiro1/ipe)
|
[](https://goreportcard.com/report/github.com/dimiro1/ipe)
|
||||||
|
|
||||||
|
Try browsing [the code on Sourcegraph](https://sourcegraph.com/github.com/dimiro1/ipe)!
|
||||||
|
|
||||||
# IPÊ
|
# IPÊ
|
||||||
|
|
||||||
This software is written in Go - the WYSIWYG lang
|
An open source Pusher server implementation compatible with Pusher client libraries written in Go.
|
||||||
|
|
||||||
# Why I wrote this software?
|
# Why I wrote this software?
|
||||||
|
|
||||||
1. I wanted to learn Go and I needed a non trivial application;
|
1. I wanted to learn Go and I needed a non trivial application;
|
||||||
2. I use Pusher in some projects;
|
2. I use Pusher in some projects;
|
||||||
3. I really like Pusher;
|
3. I really like Pusher;
|
||||||
|
4. I was using Pusher on some projects behind a firewall;
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
@@ -18,7 +21,7 @@ This software is written in Go - the WYSIWYG lang
|
|||||||
* Web Hooks;
|
* Web Hooks;
|
||||||
* Client events;
|
* Client events;
|
||||||
* Complete REST API;
|
* Complete REST API;
|
||||||
* Easy instalation;
|
* Easy installation;
|
||||||
* A single binary without dependencies;
|
* A single binary without dependencies;
|
||||||
* Easy configuration;
|
* Easy configuration;
|
||||||
* Protocol version 7;
|
* Protocol version 7;
|
||||||
@@ -29,8 +32,6 @@ This software is written in Go - the WYSIWYG lang
|
|||||||
|
|
||||||
You can download pre built binaries from the [releases tab](https://github.com/dimiro1/ipe/releases).
|
You can download pre built binaries from the [releases tab](https://github.com/dimiro1/ipe/releases).
|
||||||
|
|
||||||
I do not have a Windows machine, so I can only distribute binaries for amd64 linux and amd64 darwin.
|
|
||||||
|
|
||||||
# Building
|
# Building
|
||||||
|
|
||||||
```console
|
```console
|
||||||
@@ -43,31 +44,39 @@ or simply
|
|||||||
$ go install github.com/dimiro1/ipe
|
$ go install github.com/dimiro1/ipe
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Building from a local checkout
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ git clone https://github.com/dimiro1/ipe.git
|
||||||
|
$ cd ipe/cmd
|
||||||
|
$ go build -o ipe
|
||||||
|
```
|
||||||
|
|
||||||
# How to configure?
|
# How to configure?
|
||||||
|
|
||||||
## The server
|
## The server
|
||||||
|
|
||||||
```javascript
|
```yaml
|
||||||
{
|
|
||||||
"Host": ":8080", // Required
|
---
|
||||||
"SSL": false, // Required but can be false
|
host: ":8080"
|
||||||
"SSLHost": ":4433", // Required if SSL is true
|
profiling: false
|
||||||
"SSLKeyFile": "A key.pem file", // Required if SSL is true
|
ssl:
|
||||||
"SSLCertFile": "A cert.pem file", // Required if SSL is true
|
enabled: false
|
||||||
"Apps": [ // Required, A Json arrays with multiple apps
|
host: ":4343"
|
||||||
{
|
key_file: "key.pem"
|
||||||
"ApplicationDisabled": false, // Required but can be false
|
cert_file: "cert.pem"
|
||||||
"Secret": "A really secret random string", // Required
|
apps:
|
||||||
"Key": "A random Key string", // Required
|
- name: "Sample Application"
|
||||||
"OnlySSL": false, // Required but can be false
|
enabled: true
|
||||||
"Name": "The app name", // Required
|
only_ssl: false
|
||||||
"AppID": "The app ID", // Required
|
key: "278d525bdf162c739803"
|
||||||
"UserEvents": true, // Required but can be false
|
secret: "${APP_SECRET}" # Expand env vars
|
||||||
"WebHooks": true, // Required but can be false
|
app_id: "1"
|
||||||
"URLWebHook": "Some URL to send webhooks" // Required if WebHooks is true
|
user_events: true
|
||||||
}
|
webhooks:
|
||||||
]
|
enabled: true
|
||||||
}
|
url: "http://127.0.0.1:5000/hook"
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -76,7 +85,7 @@ $ go install github.com/dimiro1/ipe
|
|||||||
### Client javascript library
|
### Client javascript library
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var pusher = new Pusher(APP_KEY, {
|
let pusher = new Pusher(APP_KEY, {
|
||||||
wsHost: 'localhost',
|
wsHost: 'localhost',
|
||||||
wsPort: 8080,
|
wsPort: 8080,
|
||||||
wssPort: 4433, // Required if encrypted is true
|
wssPort: 4433, // Required if encrypted is true
|
||||||
@@ -101,10 +110,29 @@ PHP
|
|||||||
$pusher = new Pusher(APP_KEY, APP_SECRET, APP_ID, DEBUG, "http://localhost", "8080");
|
$pusher = new Pusher(APP_KEY, APP_SECRET, APP_ID, DEBUG, "http://localhost", "8080");
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Go
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/pusher/pusher-http-go"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
client := pusher.Client{
|
||||||
|
AppId: "APP_ID",
|
||||||
|
Key: "APP_KEY",
|
||||||
|
Secret: "APP_SECRET",
|
||||||
|
Host: ":8080",
|
||||||
|
}
|
||||||
|
|
||||||
|
// use the client
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
NodeJS
|
NodeJS
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var pusher = new Pusher({
|
let pusher = new Pusher({
|
||||||
appId: APP_ID,
|
appId: APP_ID,
|
||||||
key: APP_KEY,
|
key: APP_KEY,
|
||||||
secret: APP_SECRET
|
secret: APP_SECRET
|
||||||
@@ -137,7 +165,7 @@ Feel free to fork this repo.
|
|||||||
|
|
||||||
# Pusher
|
# Pusher
|
||||||
|
|
||||||
Pusher is an excelent service, their service is very reliable. I recomend for everyone.
|
Pusher is an excellent service, their service is very reliable. I recommend for everyone.
|
||||||
|
|
||||||
# Where this name came from?
|
# Where this name came from?
|
||||||
|
|
||||||
@@ -151,7 +179,7 @@ Claudemiro Alves Feitosa Neto
|
|||||||
|
|
||||||
# LICENSE
|
# LICENSE
|
||||||
|
|
||||||
Copyright 2014, 2015, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
Copyright 2014, 2018 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
Use of this source code is governed by a MIT-style
|
Use of this source code is governed by a MIT-style
|
||||||
license that can be found in the LICENSE file.
|
license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
# Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
|
||||||
# Use of this source code is governed by a MIT-style
|
|
||||||
# license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
require 'rake/clean'
|
|
||||||
|
|
||||||
VERSION = 'v1.2.0'
|
|
||||||
GITHASH = `git rev-parse --short HEAD`
|
|
||||||
DATE = Time.now.strftime '%Y%m%d%H%M%S'
|
|
||||||
|
|
||||||
CLOBBER.include 'build'
|
|
||||||
|
|
||||||
task :default => [:'run-debug']
|
|
||||||
|
|
||||||
desc 'Build a debug version'
|
|
||||||
task :debug do
|
|
||||||
sh "GO15VENDOREXPERIMENT=1 go install -ldflags '-w -X main.version=DEBUG -X main.buildstamp=DEBUG -X main.githash=DEBUG' github.com/dimiro1/ipe"
|
|
||||||
end
|
|
||||||
|
|
||||||
desc 'Build and run debug version'
|
|
||||||
task :'run-debug' => :debug do
|
|
||||||
sh '$GOPATH/bin/ipe --config $GOPATH/src/github.com/dimiro1/ipe/config.json -logtostderr=true -v=2'
|
|
||||||
end
|
|
||||||
|
|
||||||
desc 'Run test suite'
|
|
||||||
task :test do
|
|
||||||
sh 'GO15VENDOREXPERIMENT=1 go test . `glide nv`'
|
|
||||||
end
|
|
||||||
|
|
||||||
desc 'Download the dependencies'
|
|
||||||
task :'deps' do
|
|
||||||
sh 'glide install -v -s'
|
|
||||||
end
|
|
||||||
|
|
||||||
desc 'Generate distributions'
|
|
||||||
task :distribute => [:linux, :darwin]
|
|
||||||
|
|
||||||
desc 'Generate a linux distribution'
|
|
||||||
task :linux do
|
|
||||||
Rake::Task['build'].invoke 'linux'
|
|
||||||
end
|
|
||||||
|
|
||||||
desc 'Generate a darwin distribution'
|
|
||||||
task :darwin do
|
|
||||||
Rake::Task['build'].invoke 'darwin'
|
|
||||||
end
|
|
||||||
|
|
||||||
task :build, [:os] do |t, args|
|
|
||||||
t.reenable
|
|
||||||
os = args[:os]
|
|
||||||
|
|
||||||
sh "mkdir -p build/#{os}"
|
|
||||||
sh "GO15VENDOREXPERIMENT=1 GOOS=#{os} GOARCH=amd64 go build -ldflags '-X main.version=#{VERSION} -X main.buildstamp=#{DATE} -X main.githash=#{GITHASH}' -o build/#{os}/ipe github.com/dimiro1/ipe"
|
|
||||||
sh "cp ipe/config-example.json build/#{os}/config.json"
|
|
||||||
sh "cp LICENSE build/#{os}/"
|
|
||||||
sh "cp README.md build/#{os}/"
|
|
||||||
sh "tar -C build/#{os} -czf build/ipe_#{VERSION}_#{os}_amd64.tar.gz ."
|
|
||||||
end
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
IPÊ
|
|
||||||
---
|
|
||||||
|
|
||||||
* TODO [12/14]
|
|
||||||
* [X] Autenticação API Rest
|
|
||||||
* [X] Autenticação Websockets
|
|
||||||
* [X] Ping e Pong
|
|
||||||
* [ ] Escrever testes automatizados
|
|
||||||
* [X] SSL
|
|
||||||
* [X] Expvar - Canais, inscritos
|
|
||||||
* [X] Otimizações [3/3]
|
|
||||||
* [X] Refatorar partes do código, remover repetições
|
|
||||||
* [X] Alterar tipos de dados de slices para mapas em alguns locais.
|
|
||||||
* [X] Remover Canais vazios.
|
|
||||||
* [ ] Segurança, tempo de expiração, etc
|
|
||||||
* [X] Dados extra na conexão do usuário. Ver Websockets onOpen
|
|
||||||
* [X] Webhooks [5/5]
|
|
||||||
* [X] Member added
|
|
||||||
* [X] Member removed
|
|
||||||
* [X] Channel Occupied
|
|
||||||
* [X] Channel vacated
|
|
||||||
* [X] Clients Events
|
|
||||||
* [X] Events Presence channels [3/3]
|
|
||||||
* [X] pusher_internal:subscription_succeeded para canais de presença
|
|
||||||
* [X] pusher_internal:member_added
|
|
||||||
* [X] pusher_internal:member_removed
|
|
||||||
* [X] Remover inscrições quando o web socket for fechado
|
|
||||||
* [X] Alterar os dados extras da inscrição são relacionados ao canal e não diretamente a inscrição
|
|
||||||
* [X] Escrever um README
|
|
||||||
|
|
||||||
* Dúvidas
|
|
||||||
* [X] Atualmente o ID do usuário em canais de presença está como string, o protocolo não deixa claro, já ví exemplos onde a chave é um inteiro. Nesse caso essa chave terá que ser um RawMessage, O campo Data inteiro??
|
|
||||||
|
|
||||||
* Objetivos [7/7]
|
|
||||||
* [X] Implementação Funcional.
|
|
||||||
* [X] WebHooks
|
|
||||||
* [X] Presence channels
|
|
||||||
* [X] Private Channels
|
|
||||||
* [X] Public Channels
|
|
||||||
* [X] Easy Instalation
|
|
||||||
* [X] Easy configuration
|
|
||||||
+144
-130
@@ -2,7 +2,7 @@
|
|||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package ipe
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -12,17 +12,19 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
goji "goji.io"
|
|
||||||
|
|
||||||
"goji.io/pat"
|
|
||||||
|
|
||||||
log "github.com/golang/glog"
|
log "github.com/golang/glog"
|
||||||
"golang.org/x/net/context"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/dimiro1/ipe/utils"
|
"ipe/events"
|
||||||
|
"ipe/storage"
|
||||||
|
"ipe/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Prepare Querystring
|
// // Maximum event size permitted 10 kB
|
||||||
|
// See: http://blogs.gnome.org/cneumair/2008/09/30/1-kb-1024-bytes-no-1-kb-1000-bytes/
|
||||||
|
const maxDataEventSize = 10 * 1000
|
||||||
|
|
||||||
|
// Prepare QueryString
|
||||||
func prepareQueryString(params url.Values) string {
|
func prepareQueryString(params url.Values) string {
|
||||||
var keys []string
|
var keys []string
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ func prepareQueryString(params url.Values) string {
|
|||||||
return strings.Join(pieces, "&")
|
return strings.Join(pieces, "&")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate pusher
|
// Authentication Authenticate pusher
|
||||||
// see: https://gist.github.com/mloughran/376898
|
// see: https://gist.github.com/mloughran/376898
|
||||||
//
|
//
|
||||||
// The signature is a HMAC SHA256 hex digest.
|
// The signature is a HMAC SHA256 hex digest.
|
||||||
@@ -51,82 +53,79 @@ func prepareQueryString(params url.Values) string {
|
|||||||
// * The request path (e.g. /some/resource)
|
// * 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.
|
// * 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)
|
// 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(DB db, next goji.Handler) goji.HandlerFunc {
|
func Authentication(storage storage.Storage) func(http.Handler) http.Handler {
|
||||||
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
return func(next http.Handler) http.Handler {
|
||||||
appID := pat.Param(ctx, "app_id")
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
pathVars = mux.Vars(r)
|
||||||
|
appID = pathVars["app_id"]
|
||||||
|
)
|
||||||
|
|
||||||
app, err := DB.GetAppByAppID(appID)
|
app, err := storage.GetAppByAppID(appID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
query := r.URL.Query()
|
|
||||||
|
|
||||||
signature := query.Get("auth_signature")
|
|
||||||
query.Del("auth_signature")
|
|
||||||
|
|
||||||
queryString := prepareQueryString(query)
|
|
||||||
|
|
||||||
toSign := strings.ToUpper(r.Method) + "\n" + r.URL.Path + "\n" + queryString
|
|
||||||
|
|
||||||
if utils.HashMAC([]byte(toSign), []byte(app.Secret)) == signature {
|
|
||||||
next.ServeHTTPC(ctx, w, r)
|
|
||||||
} else {
|
|
||||||
log.Error("Not authorized")
|
|
||||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the application is disabled
|
|
||||||
func restCheckAppDisabledHandler(DB db, next goji.Handler) goji.HandlerFunc {
|
|
||||||
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
appID := pat.Param(ctx, "app_id")
|
|
||||||
|
|
||||||
currentApp, err := DB.GetAppByAppID(appID)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if currentApp.ApplicationDisabled {
|
|
||||||
http.Error(w, "Application disabled", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTPC(ctx, w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func recoverHandler(next goji.Handler) goji.HandlerFunc {
|
|
||||||
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
log.Errorf("Please verify the url parameters error was: %s", r)
|
|
||||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
next.ServeHTTPC(ctx, w, r)
|
query := r.URL.Query()
|
||||||
|
|
||||||
|
signature := query.Get("auth_signature")
|
||||||
|
query.Del("auth_signature")
|
||||||
|
|
||||||
|
queryString := prepareQueryString(query)
|
||||||
|
|
||||||
|
toSign := strings.ToUpper(r.Method) + "\n" + r.URL.Path + "\n" + queryString
|
||||||
|
|
||||||
|
if utils.HashMAC([]byte(toSign), []byte(app.Secret)) == signature {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
} else {
|
||||||
|
log.Error("Not authorized")
|
||||||
|
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// commonHandlers combine restCheckAppDisabledHandler and restAuthenticationHandler handlers
|
// CheckAppDisabled Check if the application is disabled
|
||||||
func commonHandlers(DB db, next goji.Handler) goji.HandlerFunc {
|
func CheckAppDisabled(storage storage.Storage) func(http.Handler) http.Handler {
|
||||||
return recoverHandler(restCheckAppDisabledHandler(DB, restAuthenticationHandler(DB, next)))
|
return func(next http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
pathVars = mux.Vars(r)
|
||||||
|
appID = pathVars["app_id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
currentApp, err := storage.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.Enabled {
|
||||||
|
http.Error(w, "Application disabled", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPostEventsHandler(DB db) goji.HandlerFunc {
|
// PostEvents handle post events
|
||||||
return commonHandlers(DB, &postEventsHandler{DB})
|
type PostEvents struct{ storage storage.Storage }
|
||||||
|
|
||||||
|
// NewPostEvents return a new PostEvents handler
|
||||||
|
func NewPostEvents(storage storage.Storage) *PostEvents {
|
||||||
|
return &PostEvents{storage: storage}
|
||||||
}
|
}
|
||||||
|
|
||||||
type postEventsHandler struct{ DB db }
|
// ServeHTTP An event consists of a name and data (typically JSON) which may be sent to all subscribers to a particular channel or channels.
|
||||||
|
|
||||||
// ServeHTTPC An event consists of a name and data (typically JSON) which may be sent to all subscribers to a particular channel or channels.
|
|
||||||
// This is conventionally known as triggering an event.
|
// This is conventionally known as triggering an event.
|
||||||
//
|
//
|
||||||
// The body should contain a Hash of parameters encoded as JSON where data parameter itself is JSON encoded.
|
// The body should contain a Hash of parameters encoded as JSON where data parameter itself is JSON encoded.
|
||||||
@@ -141,10 +140,13 @@ type postEventsHandler struct{ DB db }
|
|||||||
// Response is an empty JSON hash.
|
// Response is an empty JSON hash.
|
||||||
//
|
//
|
||||||
// POST /apps/{app_id}/events
|
// POST /apps/{app_id}/events
|
||||||
func (h *postEventsHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
func (h *PostEvents) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
appID := pat.Param(ctx, "app_id")
|
var (
|
||||||
|
pathVars = mux.Vars(r)
|
||||||
|
appID = pathVars["app_id"]
|
||||||
|
)
|
||||||
|
|
||||||
app, err := h.DB.GetAppByAppID(appID)
|
app, err := h.storage.GetAppByAppID(appID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
||||||
@@ -179,21 +181,28 @@ func (h *postEventsHandler) ServeHTTPC(ctx context.Context, w http.ResponseWrite
|
|||||||
for _, c := range input.Channels {
|
for _, c := range input.Channels {
|
||||||
channel := app.FindOrCreateChannelByChannelID(c)
|
channel := app.FindOrCreateChannelByChannelID(c)
|
||||||
|
|
||||||
app.Publish(channel, rawEvent{Event: input.Name, Channel: c, Data: input.Data}, input.SocketID)
|
if err := app.Publish(channel, events.Raw{Event: input.Name, Channel: c, Data: input.Data}, input.SocketID); err != nil {
|
||||||
|
log.Errorf("error publishing event %+v", err)
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte("{}"))
|
if _, err := w.Write([]byte("{}")); err != nil {
|
||||||
|
log.Errorf("unexpected error while writing into response %+v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGetChannelsHandler(DB db) goji.HandlerFunc {
|
// GetChannels handle get channels
|
||||||
return commonHandlers(DB, &getChannelsHandler{DB})
|
type GetChannels struct{ storage storage.Storage }
|
||||||
|
|
||||||
|
// NewGetChannels return a new GetChannels handler
|
||||||
|
func NewGetChannels(storage storage.Storage) *GetChannels {
|
||||||
|
return &GetChannels{storage: storage}
|
||||||
}
|
}
|
||||||
|
|
||||||
type getChannelsHandler struct{ DB db }
|
// ServeHTTP Allows fetching a hash of occupied channels (optionally filtered by prefix),
|
||||||
|
|
||||||
// Allows fetching a hash of occupied channels (optionally filtered by prefix),
|
|
||||||
// and optionally one or more attributes for each channel.
|
// and optionally one or more attributes for each channel.
|
||||||
//
|
//
|
||||||
// Notes:
|
// Notes:
|
||||||
@@ -212,14 +221,15 @@ type getChannelsHandler struct{ DB db }
|
|||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// GET /apps/{app_id}/channels
|
// GET /apps/{app_id}/channels
|
||||||
func (h *getChannelsHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
func (h *GetChannels) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
query := r.URL.Query()
|
var (
|
||||||
|
pathVars = mux.Vars(r)
|
||||||
appID := pat.Param(ctx, "app_id")
|
queryVars = r.URL.Query()
|
||||||
filter := query.Get("filter_by_prefix")
|
appID = pathVars["app_id"]
|
||||||
info := query.Get("info")
|
filter = queryVars.Get("filter_by_prefix")
|
||||||
|
info = queryVars.Get("info")
|
||||||
attributes := strings.Split(info, ",")
|
attributes = strings.Split(info, ",")
|
||||||
|
)
|
||||||
|
|
||||||
requestedUserCount := false
|
requestedUserCount := false
|
||||||
|
|
||||||
@@ -236,7 +246,7 @@ func (h *getChannelsHandler) ServeHTTPC(ctx context.Context, w http.ResponseWrit
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app, err := h.DB.GetAppByAppID(appID)
|
app, err := h.storage.GetAppByAppID(appID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
||||||
@@ -248,30 +258,30 @@ func (h *getChannelsHandler) ServeHTTPC(ctx context.Context, w http.ResponseWrit
|
|||||||
case "presence-":
|
case "presence-":
|
||||||
for _, c := range app.PresenceChannels() {
|
for _, c := range app.PresenceChannels() {
|
||||||
if requestedUserCount {
|
if requestedUserCount {
|
||||||
channels[c.ChannelID] = struct {
|
channels[c.ID] = struct {
|
||||||
UserCount int `json:"user_count"`
|
UserCount int `json:"user_count"`
|
||||||
}{
|
}{
|
||||||
c.TotalUsers(),
|
c.TotalUsers(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channels[c.ChannelID] = struct{}{}
|
channels[c.ID] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "public-":
|
case "public-":
|
||||||
for _, c := range app.PublicChannels() {
|
for _, c := range app.PublicChannels() {
|
||||||
channels[c.ChannelID] = struct{}{}
|
channels[c.ID] = struct{}{}
|
||||||
}
|
}
|
||||||
case "private-":
|
case "private-":
|
||||||
for _, c := range app.PrivateChannels() {
|
for _, c := range app.PrivateChannels() {
|
||||||
channels[c.ChannelID] = struct{}{}
|
channels[c.ID] = struct{}{}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
for _, c := range app.Channels {
|
for _, c := range app.Channels() {
|
||||||
channels[c.ChannelID] = struct{}{}
|
channels[c.ID] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
js := make(map[string]interface{}, 1)
|
js := make(map[string]interface{}, 1)
|
||||||
js["channels"] = channels
|
js["channels"] = channels
|
||||||
@@ -282,13 +292,15 @@ func (h *getChannelsHandler) ServeHTTPC(ctx context.Context, w http.ResponseWrit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGetChannelHandler(DB db) goji.HandlerFunc {
|
// GetChannel handle get channel
|
||||||
return commonHandlers(DB, &getChannelHandler{DB})
|
type GetChannel struct{ storage storage.Storage }
|
||||||
|
|
||||||
|
// NewGetChannel return a new GetChannel handler
|
||||||
|
func NewGetChannel(storage storage.Storage) *GetChannel {
|
||||||
|
return &GetChannel{storage: storage}
|
||||||
}
|
}
|
||||||
|
|
||||||
type getChannelHandler struct{ DB db }
|
// ServeHTTP Fetch info for one channel
|
||||||
|
|
||||||
// Fetch info for one channel
|
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
// {
|
// {
|
||||||
@@ -298,29 +310,28 @@ type getChannelHandler struct{ DB db }
|
|||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// GET /apps/{app_id}/channels/{channel_name}
|
// GET /apps/{app_id}/channels/{channel_name}
|
||||||
func (h *getChannelHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
func (h *GetChannel) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
var (
|
||||||
|
pathVars = mux.Vars(r)
|
||||||
|
queryVars = r.URL.Query()
|
||||||
|
appID = pathVars["app_id"]
|
||||||
|
channelName = pathVars["channel_name"]
|
||||||
|
info = queryVars.Get("info")
|
||||||
|
attributes = strings.Split(info, ",")
|
||||||
|
)
|
||||||
|
|
||||||
query := r.URL.Query()
|
app, err := h.storage.GetAppByAppID(appID)
|
||||||
|
|
||||||
appID := pat.Param(ctx, "app_id")
|
|
||||||
app, err := h.DB.GetAppByAppID(appID)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
channelName := pat.Param(ctx, "channel_name")
|
|
||||||
|
|
||||||
// Channel name could not be empty
|
// Channel name could not be empty
|
||||||
if strings.TrimSpace(channelName) == "" {
|
if strings.TrimSpace(channelName) == "" {
|
||||||
http.Error(w, "Empty channel name", http.StatusBadRequest)
|
http.Error(w, "Empty channel name", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
info := query.Get("info")
|
|
||||||
attributes := strings.Split(info, ",")
|
|
||||||
|
|
||||||
// Attributes requested
|
// Attributes requested
|
||||||
requestedUserCount := false
|
requestedUserCount := false
|
||||||
requestedSubscriptionCount := false
|
requestedSubscriptionCount := false
|
||||||
@@ -368,21 +379,22 @@ func (h *getChannelHandler) ServeHTTPC(ctx context.Context, w http.ResponseWrite
|
|||||||
dtoChannel.SubscriptionCount = channel.TotalSubscriptions()
|
dtoChannel.SubscriptionCount = channel.TotalSubscriptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(dtoChannel); err != nil {
|
if err := json.NewEncoder(w).Encode(dtoChannel); err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGetChannelUsersHandler(DB db) goji.HandlerFunc {
|
// GetChannelUsers handle get users from a channel
|
||||||
return commonHandlers(DB, &getChannelUsersHandler{DB})
|
type GetChannelUsers struct{ storage storage.Storage }
|
||||||
|
|
||||||
|
// NewGetChannelUsers return a new GetChannelUsers handler
|
||||||
|
func NewGetChannelUsers(storage storage.Storage) *GetChannelUsers {
|
||||||
|
return &GetChannelUsers{storage: storage}
|
||||||
}
|
}
|
||||||
|
|
||||||
type getChannelUsersHandler struct{ DB db }
|
// ServeHTTP Allowed only for presence-channels
|
||||||
|
|
||||||
// Allowed only for presence-channels
|
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
// {
|
// {
|
||||||
@@ -393,9 +405,12 @@ type getChannelUsersHandler struct{ DB db }
|
|||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// GET /apps/{app_id}/channels/{channel_name}/users
|
// GET /apps/{app_id}/channels/{channel_name}/users
|
||||||
func (h *getChannelUsersHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
func (h *GetChannelUsers) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
appID := pat.Param(ctx, "app_id")
|
var (
|
||||||
channelName := pat.Param(ctx, "channel_name")
|
pathVars = mux.Vars(r)
|
||||||
|
appID = pathVars["app_id"]
|
||||||
|
channelName = pathVars["channel_name"]
|
||||||
|
)
|
||||||
|
|
||||||
isPresence := utils.IsPresenceChannel(channelName)
|
isPresence := utils.IsPresenceChannel(channelName)
|
||||||
|
|
||||||
@@ -404,7 +419,7 @@ func (h *getChannelUsersHandler) ServeHTTPC(ctx context.Context, w http.Response
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app, err := h.DB.GetAppByAppID(appID)
|
app, err := h.storage.GetAppByAppID(appID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
||||||
@@ -423,7 +438,7 @@ func (h *getChannelUsersHandler) ServeHTTPC(ctx context.Context, w http.Response
|
|||||||
|
|
||||||
var users []interface{}
|
var users []interface{}
|
||||||
|
|
||||||
for _, s := range channel.Subscriptions {
|
for _, s := range channel.Subscriptions() {
|
||||||
users = append(users, struct {
|
users = append(users, struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
}{s.ID})
|
}{s.ID})
|
||||||
@@ -431,8 +446,7 @@ func (h *getChannelUsersHandler) ServeHTTPC(ctx context.Context, w http.Response
|
|||||||
|
|
||||||
result["users"] = users
|
result["users"] = users
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(result); err != nil {
|
if err := json.NewEncoder(w).Encode(result); err != nil {
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
@@ -1,61 +1,74 @@
|
|||||||
package ipe
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"goji.io/pattern"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
"ipe/app"
|
||||||
|
channel2 "ipe/channel"
|
||||||
|
"ipe/connection"
|
||||||
|
"ipe/mocks"
|
||||||
|
"ipe/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
testApp *app
|
testApp *app.Application
|
||||||
database db
|
database storage.Storage
|
||||||
|
id = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func newTestApp() *app.Application {
|
||||||
|
a := app.NewApplication("Test", strconv.Itoa(id), "123", "123", false, false, true, false, "")
|
||||||
|
id++
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
testApp = newTestApp()
|
testApp = newTestApp()
|
||||||
|
|
||||||
channel := newChannel("presence-c1")
|
channel := channel2.New("presence-c1")
|
||||||
testApp.AddChannel(channel)
|
testApp.AddChannel(channel)
|
||||||
testApp.AddChannel(newChannel("c2"))
|
testApp.AddChannel(channel2.New("c2"))
|
||||||
testApp.AddChannel(newChannel("private-c3"))
|
testApp.AddChannel(channel2.New("private-c3"))
|
||||||
|
|
||||||
conn := newConnection("123.456", mockSocket{})
|
conn := connection.New("123.456", mocks.MockSocket{})
|
||||||
testApp.Subscribe(channel, conn, "{}")
|
_ = testApp.Subscribe(channel, conn, "{}")
|
||||||
|
|
||||||
conn = newConnection("321.654", mockSocket{})
|
conn = connection.New("321.654", mocks.MockSocket{})
|
||||||
testApp.Subscribe(channel, conn, "{}")
|
_ = testApp.Subscribe(channel, conn, "{}")
|
||||||
|
|
||||||
db := newMemdb()
|
_storage := storage.NewInMemory()
|
||||||
db.AddApp(testApp)
|
_ = _storage.AddApp(testApp)
|
||||||
|
|
||||||
database = db
|
database = _storage
|
||||||
}
|
}
|
||||||
|
|
||||||
// All Channels
|
// All channels
|
||||||
func Test_getChannels_all(t *testing.T) {
|
func Test_getChannels_all(t *testing.T) {
|
||||||
appID := testApp.AppID
|
appID := testApp.AppID
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
|
||||||
|
|
||||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels", appID), nil)
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels", appID), nil)
|
||||||
|
r = mux.SetURLVars(r, map[string]string{
|
||||||
|
"app_id": appID,
|
||||||
|
})
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
handler := &getChannelsHandler{database}
|
handler := &GetChannels{database}
|
||||||
handler.ServeHTTPC(ctx, w, r)
|
handler.ServeHTTP(w, r)
|
||||||
|
|
||||||
if w.Code != http.StatusOK {
|
if w.Code != http.StatusOK {
|
||||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := make(map[string]interface{})
|
data := make(map[string]interface{})
|
||||||
json.Unmarshal(w.Body.Bytes(), &data)
|
_ = json.Unmarshal(w.Body.Bytes(), &data)
|
||||||
|
|
||||||
channels := data["channels"].(map[string]interface{})
|
channels := data["channels"].(map[string]interface{})
|
||||||
|
|
||||||
@@ -67,21 +80,21 @@ func Test_getChannels_all(t *testing.T) {
|
|||||||
func Test_getChannels_filter_by_presence_prefix(t *testing.T) {
|
func Test_getChannels_filter_by_presence_prefix(t *testing.T) {
|
||||||
appID := testApp.AppID
|
appID := testApp.AppID
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
|
||||||
|
|
||||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-", appID), nil)
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-", appID), nil)
|
||||||
|
r = mux.SetURLVars(r, map[string]string{
|
||||||
|
"app_id": appID,
|
||||||
|
})
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
handler := &getChannelsHandler{database}
|
handler := &GetChannels{database}
|
||||||
handler.ServeHTTPC(ctx, w, r)
|
handler.ServeHTTP(w, r)
|
||||||
|
|
||||||
if w.Code != http.StatusOK {
|
if w.Code != http.StatusOK {
|
||||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := make(map[string]interface{})
|
data := make(map[string]interface{})
|
||||||
json.Unmarshal(w.Body.Bytes(), &data)
|
_ = json.Unmarshal(w.Body.Bytes(), &data)
|
||||||
|
|
||||||
channels := data["channels"].(map[string]interface{})
|
channels := data["channels"].(map[string]interface{})
|
||||||
|
|
||||||
@@ -94,21 +107,21 @@ func Test_getChannels_filter_by_presence_prefix(t *testing.T) {
|
|||||||
func Test_getChannels_filter_by_presence_prefix_and_user_count(t *testing.T) {
|
func Test_getChannels_filter_by_presence_prefix_and_user_count(t *testing.T) {
|
||||||
appID := testApp.AppID
|
appID := testApp.AppID
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
|
||||||
|
|
||||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-&info=user_count", appID), nil)
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-&info=user_count", appID), nil)
|
||||||
|
r = mux.SetURLVars(r, map[string]string{
|
||||||
|
"app_id": appID,
|
||||||
|
})
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
handler := &getChannelsHandler{database}
|
handler := &GetChannels{database}
|
||||||
handler.ServeHTTPC(ctx, w, r)
|
handler.ServeHTTP(w, r)
|
||||||
|
|
||||||
if w.Code != http.StatusOK {
|
if w.Code != http.StatusOK {
|
||||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := make(map[string]interface{})
|
data := make(map[string]interface{})
|
||||||
json.Unmarshal(w.Body.Bytes(), &data)
|
_ = json.Unmarshal(w.Body.Bytes(), &data)
|
||||||
|
|
||||||
channels := data["channels"].(map[string]interface{})
|
channels := data["channels"].(map[string]interface{})
|
||||||
|
|
||||||
@@ -129,18 +142,18 @@ func Test_getChannels_filter_by_presence_prefix_and_user_count(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// User count only alowed in Presence channels
|
// User count only allowed in Presence channels
|
||||||
func Test_getChannels_filter_by_private_prefix_and_info_user_count(t *testing.T) {
|
func Test_getChannels_filter_by_private_prefix_and_info_user_count(t *testing.T) {
|
||||||
appID := testApp.AppID
|
appID := testApp.AppID
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
|
||||||
|
|
||||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-&info=user_count", appID), nil)
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-&info=user_count", appID), nil)
|
||||||
|
r = mux.SetURLVars(r, map[string]string{
|
||||||
|
"app_id": appID,
|
||||||
|
})
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
handler := &getChannelsHandler{database}
|
handler := &GetChannels{database}
|
||||||
handler.ServeHTTPC(ctx, w, r)
|
handler.ServeHTTP(w, r)
|
||||||
|
|
||||||
if w.Code != http.StatusBadRequest {
|
if w.Code != http.StatusBadRequest {
|
||||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusBadRequest)
|
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusBadRequest)
|
||||||
@@ -150,14 +163,14 @@ func Test_getChannels_filter_by_private_prefix_and_info_user_count(t *testing.T)
|
|||||||
func Test_getChannels_filter_by_public_prefix(t *testing.T) {
|
func Test_getChannels_filter_by_public_prefix(t *testing.T) {
|
||||||
appID := testApp.AppID
|
appID := testApp.AppID
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
|
||||||
|
|
||||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=public-", appID), nil)
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=public-", appID), nil)
|
||||||
|
r = mux.SetURLVars(r, map[string]string{
|
||||||
|
"app_id": appID,
|
||||||
|
})
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
handler := &getChannelsHandler{database}
|
handler := &GetChannels{database}
|
||||||
handler.ServeHTTPC(ctx, w, r)
|
handler.ServeHTTP(w, r)
|
||||||
|
|
||||||
if w.Code != http.StatusOK {
|
if w.Code != http.StatusOK {
|
||||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||||
@@ -165,7 +178,7 @@ func Test_getChannels_filter_by_public_prefix(t *testing.T) {
|
|||||||
|
|
||||||
data := make(map[string]interface{})
|
data := make(map[string]interface{})
|
||||||
|
|
||||||
json.Unmarshal(w.Body.Bytes(), &data)
|
_ = json.Unmarshal(w.Body.Bytes(), &data)
|
||||||
|
|
||||||
channels := data["channels"].(map[string]interface{})
|
channels := data["channels"].(map[string]interface{})
|
||||||
|
|
||||||
@@ -183,14 +196,14 @@ func Test_getChannels_filter_by_public_prefix(t *testing.T) {
|
|||||||
func Test_getChannels_filter_by_private_prefix(t *testing.T) {
|
func Test_getChannels_filter_by_private_prefix(t *testing.T) {
|
||||||
appID := testApp.AppID
|
appID := testApp.AppID
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
|
||||||
|
|
||||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-", appID), nil)
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-", appID), nil)
|
||||||
|
r = mux.SetURLVars(r, map[string]string{
|
||||||
|
"app_id": appID,
|
||||||
|
})
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
handler := &getChannelsHandler{database}
|
handler := &GetChannels{database}
|
||||||
handler.ServeHTTPC(ctx, w, r)
|
handler.ServeHTTP(w, r)
|
||||||
|
|
||||||
if w.Code != http.StatusOK {
|
if w.Code != http.StatusOK {
|
||||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||||
@@ -198,7 +211,7 @@ func Test_getChannels_filter_by_private_prefix(t *testing.T) {
|
|||||||
|
|
||||||
data := make(map[string]interface{})
|
data := make(map[string]interface{})
|
||||||
|
|
||||||
json.Unmarshal(w.Body.Bytes(), &data)
|
_ = json.Unmarshal(w.Body.Bytes(), &data)
|
||||||
|
|
||||||
channels := data["channels"].(map[string]interface{})
|
channels := data["channels"].(map[string]interface{})
|
||||||
|
|
||||||
+314
@@ -0,0 +1,314 @@
|
|||||||
|
// 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 app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"expvar"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
|
||||||
|
"ipe/channel"
|
||||||
|
"ipe/connection"
|
||||||
|
"ipe/events"
|
||||||
|
"ipe/subscription"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Application represents a Pusher application
|
||||||
|
type Application struct {
|
||||||
|
sync.RWMutex
|
||||||
|
|
||||||
|
Name string
|
||||||
|
AppID string
|
||||||
|
Key string
|
||||||
|
Secret string
|
||||||
|
OnlySSL bool
|
||||||
|
Enabled bool
|
||||||
|
UserEvents bool
|
||||||
|
WebHooks bool
|
||||||
|
URLWebHook string
|
||||||
|
|
||||||
|
channels map[string]*channel.Channel
|
||||||
|
connections map[string]*connection.Connection
|
||||||
|
|
||||||
|
Stats *expvar.Map `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApplication returns a new Application
|
||||||
|
func NewApplication(
|
||||||
|
name,
|
||||||
|
appID,
|
||||||
|
key,
|
||||||
|
secret string,
|
||||||
|
onlySSL,
|
||||||
|
enabled,
|
||||||
|
userEvents,
|
||||||
|
webHooks bool,
|
||||||
|
webHookURL string,
|
||||||
|
) *Application {
|
||||||
|
|
||||||
|
a := &Application{
|
||||||
|
Name: name,
|
||||||
|
AppID: appID,
|
||||||
|
Key: key,
|
||||||
|
Secret: secret,
|
||||||
|
OnlySSL: onlySSL,
|
||||||
|
Enabled: enabled,
|
||||||
|
UserEvents: userEvents,
|
||||||
|
WebHooks: webHooks,
|
||||||
|
URLWebHook: webHookURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
a.connections = make(map[string]*connection.Connection)
|
||||||
|
a.channels = make(map[string]*channel.Channel)
|
||||||
|
a.Stats = expvar.NewMap(fmt.Sprintf("%s (%s)", a.Name, a.AppID))
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channels returns the full list of channels
|
||||||
|
func (a *Application) Channels() []*channel.Channel {
|
||||||
|
a.RLock()
|
||||||
|
defer a.RUnlock()
|
||||||
|
|
||||||
|
var channels []*channel.Channel
|
||||||
|
|
||||||
|
for _, c := range a.channels {
|
||||||
|
channels = append(channels, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
// PresenceChannels Only Presence channels
|
||||||
|
func (a *Application) PresenceChannels() []*channel.Channel {
|
||||||
|
a.RLock()
|
||||||
|
defer a.RUnlock()
|
||||||
|
|
||||||
|
var channels []*channel.Channel
|
||||||
|
|
||||||
|
for _, c := range a.channels {
|
||||||
|
if c.IsPresence() {
|
||||||
|
channels = append(channels, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrivateChannels Only Private channels
|
||||||
|
func (a *Application) PrivateChannels() []*channel.Channel {
|
||||||
|
a.RLock()
|
||||||
|
defer a.RUnlock()
|
||||||
|
|
||||||
|
var channels []*channel.Channel
|
||||||
|
|
||||||
|
for _, c := range a.channels {
|
||||||
|
if c.IsPrivate() {
|
||||||
|
channels = append(channels, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicChannels Only Public channels
|
||||||
|
func (a *Application) PublicChannels() []*channel.Channel {
|
||||||
|
a.RLock()
|
||||||
|
defer a.RUnlock()
|
||||||
|
|
||||||
|
var channels []*channel.Channel
|
||||||
|
|
||||||
|
for _, c := range a.channels {
|
||||||
|
if c.IsPublic() {
|
||||||
|
channels = append(channels, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect Socket
|
||||||
|
func (a *Application) Disconnect(socketID string) {
|
||||||
|
log.Infof("disconnecting socket %+v", socketID)
|
||||||
|
|
||||||
|
conn, err := a.FindConnection(socketID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("socket not found, %+v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe from channels
|
||||||
|
for _, c := range a.channels {
|
||||||
|
if c.IsSubscribed(conn) {
|
||||||
|
if err := c.Unsubscribe(conn); err != nil {
|
||||||
|
log.Errorf("error while calling Channel.Unsubscribe, %+v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from Application
|
||||||
|
a.Lock()
|
||||||
|
_, exists := a.connections[conn.SocketID]
|
||||||
|
a.Unlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Lock()
|
||||||
|
delete(a.connections, conn.SocketID)
|
||||||
|
a.Unlock()
|
||||||
|
|
||||||
|
a.Stats.Add("TotalConnections", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect a new Subscriber
|
||||||
|
func (a *Application) Connect(conn *connection.Connection) {
|
||||||
|
log.Infof("adding a new Connection %s to Application %s", conn.SocketID, a.Name)
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
a.connections[conn.SocketID] = conn
|
||||||
|
|
||||||
|
a.Stats.Add("TotalConnections", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindConnection Find a Connection on this Application
|
||||||
|
func (a *Application) FindConnection(socketID string) (*connection.Connection, error) {
|
||||||
|
a.RLock()
|
||||||
|
defer a.RUnlock()
|
||||||
|
|
||||||
|
conn, exists := a.connections[socketID]
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("connection not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveChannel removes the Channel from Application
|
||||||
|
func (a *Application) RemoveChannel(c *channel.Channel) {
|
||||||
|
log.Infof("remove the Channel %s from Application %s", c.ID, a.Name)
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
delete(a.channels, c.ID)
|
||||||
|
|
||||||
|
if c.IsPresence() {
|
||||||
|
a.Stats.Add("TotalPresenceChannels", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsPrivate() {
|
||||||
|
a.Stats.Add("TotalPrivateChannels", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsPublic() {
|
||||||
|
a.Stats.Add("TotalPublicChannels", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Stats.Add("TotalChannels", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddChannel Add a new Channel to this APP
|
||||||
|
func (a *Application) AddChannel(c *channel.Channel) {
|
||||||
|
log.Infof("adding a new Channel %s to Application %s", c.ID, a.Name)
|
||||||
|
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
a.channels[c.ID] = c
|
||||||
|
|
||||||
|
if c.IsPresence() {
|
||||||
|
a.Stats.Add("TotalPresenceChannels", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsPrivate() {
|
||||||
|
a.Stats.Add("TotalPrivateChannels", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsPublic() {
|
||||||
|
a.Stats.Add("TotalPublicChannels", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Stats.Add("TotalChannels", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindOrCreateChannelByChannelID Returns a Channel from this Application
|
||||||
|
// If not found then the Channel is created and added to this Application
|
||||||
|
func (a *Application) FindOrCreateChannelByChannelID(n string) *channel.Channel {
|
||||||
|
c, err := a.FindChannelByChannelID(n)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c = channel.New(
|
||||||
|
n,
|
||||||
|
channel.WithChannelOccupiedListener(func(c *channel.Channel, s *subscription.Subscription) {
|
||||||
|
a.TriggerChannelOccupiedHook(c)
|
||||||
|
}),
|
||||||
|
channel.WithChannelVacatedListener(func(c *channel.Channel, s *subscription.Subscription) {
|
||||||
|
a.TriggerChannelVacatedHook(c)
|
||||||
|
}),
|
||||||
|
channel.WithMemberAddedListener(func(c *channel.Channel, s *subscription.Subscription) {
|
||||||
|
a.TriggerMemberAddedHook(c, s)
|
||||||
|
}),
|
||||||
|
channel.WithMemberRemovedListener(func(c *channel.Channel, s *subscription.Subscription) {
|
||||||
|
a.TriggerMemberRemovedHook(c, s)
|
||||||
|
}),
|
||||||
|
channel.WithClientEventListener(func(c *channel.Channel, s *subscription.Subscription, event string, data interface{}) {
|
||||||
|
a.TriggerClientEventHook(c, s, event, data)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
a.AddChannel(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindChannelByChannelID Find the Channel by Channel ID
|
||||||
|
func (a *Application) FindChannelByChannelID(n string) (*channel.Channel, error) {
|
||||||
|
a.RLock()
|
||||||
|
defer a.RUnlock()
|
||||||
|
|
||||||
|
c, exists := a.channels[n]
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("channel does not exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish an event into the channel
|
||||||
|
// skip the ignore connection
|
||||||
|
func (a *Application) Publish(c *channel.Channel, event events.Raw, ignore string) error {
|
||||||
|
a.Stats.Add("TotalUniqueMessages", 1)
|
||||||
|
|
||||||
|
return c.Publish(event, ignore)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe unsubscribe the given connection from the channel
|
||||||
|
// remove the channel from the application if it is empty
|
||||||
|
func (a *Application) Unsubscribe(c *channel.Channel, conn *connection.Connection) error {
|
||||||
|
err := c.Unsubscribe(conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.IsOccupied() {
|
||||||
|
a.RemoveChannel(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe the connection into the given channel
|
||||||
|
func (a *Application) Subscribe(c *channel.Channel, conn *connection.Connection, data string) error {
|
||||||
|
return c.Subscribe(conn, data)
|
||||||
|
}
|
||||||
+255
@@ -0,0 +1,255 @@
|
|||||||
|
// 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 app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
channel2 "ipe/channel"
|
||||||
|
"ipe/connection"
|
||||||
|
"ipe/mocks"
|
||||||
|
)
|
||||||
|
|
||||||
|
var id = 0
|
||||||
|
|
||||||
|
func newTestApp() *Application {
|
||||||
|
a := NewApplication("Test", strconv.Itoa(id), "123", "123", false, false, true, false, "")
|
||||||
|
id++
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnect(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
app.Connect(connection.New("socketID", mocks.MockSocket{}))
|
||||||
|
|
||||||
|
if len(app.connections) != 1 {
|
||||||
|
t.Errorf("len(Application.connections) == %d, wants %d", len(app.connections), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisconnect(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
app.Connect(connection.New("socketID", mocks.MockSocket{}))
|
||||||
|
app.Disconnect("socketID")
|
||||||
|
|
||||||
|
if len(app.connections) != 0 {
|
||||||
|
t.Errorf("len(Application.connections) == %d, wants %d", len(app.connections), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindConnection(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
app.Connect(connection.New("socketID", mocks.MockSocket{}))
|
||||||
|
|
||||||
|
if _, err := app.FindConnection("socketID"); err != nil {
|
||||||
|
t.Errorf("Application.FindConnection('socketID') == _, %q, wants %v", err, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := app.FindConnection("NotFound"); err == nil {
|
||||||
|
t.Errorf("Application.FindConnection('socketID') == _, %q, wants !nil", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindChannelByChannelID(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
channel := channel2.New("ID")
|
||||||
|
app.AddChannel(channel)
|
||||||
|
|
||||||
|
if _, err := app.FindChannelByChannelID("ID"); err != nil {
|
||||||
|
t.Errorf("Application.FindChannelByChannelID('ID') == _, %q, wants %v", err, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindOrCreateChannelByChannelID(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
if len(app.channels) != 0 {
|
||||||
|
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.FindOrCreateChannelByChannelID("ID")
|
||||||
|
|
||||||
|
if len(app.channels) != 1 {
|
||||||
|
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveChannel(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
if len(app.channels) != 0 {
|
||||||
|
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
channel := channel2.New("ID")
|
||||||
|
app.AddChannel(channel)
|
||||||
|
|
||||||
|
if len(app.channels) != 1 {
|
||||||
|
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.RemoveChannel(channel)
|
||||||
|
|
||||||
|
if len(app.channels) != 0 {
|
||||||
|
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_add_channels(t *testing.T) {
|
||||||
|
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
// Public
|
||||||
|
|
||||||
|
if len(app.PublicChannels()) != 0 {
|
||||||
|
t.Errorf("len(Application.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.AddChannel(channel2.New("ID"))
|
||||||
|
|
||||||
|
if len(app.PublicChannels()) != 1 {
|
||||||
|
t.Errorf("len(Application.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Presence
|
||||||
|
|
||||||
|
if len(app.PresenceChannels()) != 0 {
|
||||||
|
t.Errorf("len(Application.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.AddChannel(channel2.New("presence-test"))
|
||||||
|
|
||||||
|
if len(app.PresenceChannels()) != 1 {
|
||||||
|
t.Errorf("len(Application.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private
|
||||||
|
|
||||||
|
if len(app.PrivateChannels()) != 0 {
|
||||||
|
t.Errorf("len(Application.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.AddChannel(channel2.New("private-test"))
|
||||||
|
|
||||||
|
if len(app.PrivateChannels()) != 1 {
|
||||||
|
t.Errorf("len(Application.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AllChannels(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
app.AddChannel(channel2.New("private-test"))
|
||||||
|
app.AddChannel(channel2.New("presence-test"))
|
||||||
|
app.AddChannel(channel2.New("test"))
|
||||||
|
|
||||||
|
if len(app.channels) != 3 {
|
||||||
|
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_New_Subscriber(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
if len(app.connections) != 0 {
|
||||||
|
t.Errorf("len(Application.connections) == %d, wants %d", len(app.connections), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := connection.New("1", mocks.MockSocket{})
|
||||||
|
app.Connect(conn)
|
||||||
|
|
||||||
|
if len(app.connections) != 1 {
|
||||||
|
t.Errorf("len(Application.connections) == %d, wants %d", len(app.connections), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_find_subscriber(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
conn := connection.New("1", mocks.MockSocket{})
|
||||||
|
app.Connect(conn)
|
||||||
|
|
||||||
|
conn, err := app.FindConnection("1")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if conn.SocketID != "1" {
|
||||||
|
t.Errorf("conn.SocketID == %s, wants %s", conn.SocketID, "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a wrong subscriber
|
||||||
|
|
||||||
|
conn, err = app.FindConnection("DoesNotExists")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("err == %q, wants !nil", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if conn != nil {
|
||||||
|
t.Errorf("conn == %q, wants nil", conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_find_or_create_channels(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
// Public
|
||||||
|
if len(app.PublicChannels()) != 0 {
|
||||||
|
t.Errorf("len(Application.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := app.FindOrCreateChannelByChannelID("id")
|
||||||
|
|
||||||
|
if len(app.PublicChannels()) != 1 {
|
||||||
|
t.Errorf("len(Application.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ID != "id" {
|
||||||
|
t.Errorf("c.id == %s, wants %s", c.ID, "id")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Presence
|
||||||
|
if len(app.PresenceChannels()) != 0 {
|
||||||
|
t.Errorf("len(Application.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
c = app.FindOrCreateChannelByChannelID("presence-test")
|
||||||
|
|
||||||
|
if len(app.PresenceChannels()) != 1 {
|
||||||
|
t.Errorf("len(Application.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ID != "presence-test" {
|
||||||
|
t.Errorf("c.id == %s, wants %s", c.ID, "presence-test")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private
|
||||||
|
if len(app.PrivateChannels()) != 0 {
|
||||||
|
t.Errorf("len(Application.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
c = app.FindOrCreateChannelByChannelID("private-test")
|
||||||
|
|
||||||
|
if len(app.PrivateChannels()) != 1 {
|
||||||
|
t.Errorf("len(Application.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ID != "private-test" {
|
||||||
|
t.Errorf("c.id == %s, wants %s", c.ID, "private-test")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,19 +2,26 @@
|
|||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package ipe
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dimiro1/ipe/utils"
|
|
||||||
log "github.com/golang/glog"
|
log "github.com/golang/glog"
|
||||||
|
|
||||||
|
"ipe/channel"
|
||||||
|
"ipe/subscription"
|
||||||
|
"ipe/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A WebHook is sent as a HTTP POST request to the url which you specify.
|
const maxTimeout = 3 * time.Second
|
||||||
|
|
||||||
|
// A webHook is sent as a HTTP POST request to the url which you specify.
|
||||||
// The POST request payload (body) contains a JSON document, and follows the following format:
|
// The POST request payload (body) contains a JSON document, and follows the following format:
|
||||||
// {
|
// {
|
||||||
// "time_ms": 1327078148132
|
// "time_ms": 1327078148132
|
||||||
@@ -29,7 +36,7 @@ import (
|
|||||||
// You may use a HTTP or a HTTPS url for WebHooks. In most cases HTTP is sufficient, but HTTPS can be useful if your data is sensitive or if you wish to protect against replay attacks for example.
|
// You may use a HTTP or a HTTPS url for WebHooks. In most cases HTTP is sufficient, but HTTPS can be useful if your data is sensitive or if you wish to protect against replay attacks for example.
|
||||||
// Authentication
|
// Authentication
|
||||||
//
|
//
|
||||||
// Since anyone could in principle send WebHooks to your application, it’s important to verify that these WebHooks originated from Pusher. Valid WebHooks will therefore contain these headers which contain a HMAC signature of the WebHook payload (body):
|
// Since anyone could in principle send WebHooks to your application, it’s important to verify that these WebHooks originated from Pusher. Valid WebHooks will therefore contain these headers which contain a HMAC signature of the webHook payload (body):
|
||||||
//
|
//
|
||||||
// X-Pusher-Key: The App Key.
|
// X-Pusher-Key: The App Key.
|
||||||
// X-Pusher-Signature: A HMAC SHA256 hex digest formed by signing the POST payload (body) with the token’s secret.
|
// X-Pusher-Signature: A HMAC SHA256 hex digest formed by signing the POST payload (body) with the token’s secret.
|
||||||
@@ -47,40 +54,51 @@ type hookEvent struct {
|
|||||||
UserID string `json:"user_id,omitempty"`
|
UserID string `json:"user_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func newChannelOcuppiedHook(channel *channel) hookEvent {
|
func newChannelOcuppiedHook(channel *channel.Channel) hookEvent {
|
||||||
return hookEvent{Name: "channel_occupied", Channel: channel.ChannelID}
|
return hookEvent{Name: "channel_occupied", Channel: channel.ID}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newChannelVacatedHook(channel *channel) hookEvent {
|
func newChannelVacatedHook(channel *channel.Channel) hookEvent {
|
||||||
return hookEvent{Name: "channel_vacated", Channel: channel.ChannelID}
|
return hookEvent{Name: "channel_vacated", Channel: channel.ID}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMemberAddedHook(channel *channel, s *subscription) hookEvent {
|
func newMemberAddedHook(channel *channel.Channel, s *subscription.Subscription) hookEvent {
|
||||||
return hookEvent{Name: "member_added", Channel: channel.ChannelID, UserID: s.ID}
|
return hookEvent{Name: "member_added", Channel: channel.ID, UserID: s.ID}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMemberRemovedHook(channel *channel, s *subscription) hookEvent {
|
func newMemberRemovedHook(channel *channel.Channel, s *subscription.Subscription) hookEvent {
|
||||||
return hookEvent{Name: "member_removed", Channel: channel.ChannelID, UserID: s.ID}
|
return hookEvent{Name: "member_removed", Channel: channel.ID, UserID: s.ID}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newClientHook(channel *channel, s *subscription, event string, data interface{}) hookEvent {
|
func newClientHook(channel *channel.Channel, s *subscription.Subscription, event string, data interface{}) hookEvent {
|
||||||
return hookEvent{Name: "client_event", Channel: channel.ChannelID, Event: event, Data: data, SocketID: s.Connection.SocketID}
|
return hookEvent{Name: "client_event", Channel: channel.ID, Event: event, Data: data, SocketID: s.Connection.SocketID}
|
||||||
}
|
}
|
||||||
|
|
||||||
// channel_occupied
|
// TriggerChannelOccupiedHook channel_occupied
|
||||||
// { "name": "channel_occupied", "channel": "test_channel" }
|
// { "name": "channel_occupied", "channel": "test_channel" }
|
||||||
func (a *app) TriggerChannelOccupiedHook(c *channel) {
|
func (a *Application) TriggerChannelOccupiedHook(c *channel.Channel) {
|
||||||
event := newChannelOcuppiedHook(c)
|
event := newChannelOcuppiedHook(c)
|
||||||
triggerHook(event.Name, a, c, event)
|
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := triggerHook(ctx, a, event); err != nil {
|
||||||
|
log.Errorf("triggering webhook %+v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// channel_vacated
|
// TriggerChannelVacatedHook channel_vacated
|
||||||
// { "name": "channel_vacated", "channel": "test_channel" }
|
// { "name": "channel_vacated", "channel": "test_channel" }
|
||||||
func (a *app) TriggerChannelVacatedHook(c *channel) {
|
func (a *Application) TriggerChannelVacatedHook(c *channel.Channel) {
|
||||||
event := newChannelVacatedHook(c)
|
event := newChannelVacatedHook(c)
|
||||||
triggerHook(event.Name, a, c, event)
|
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := triggerHook(ctx, a, event); err != nil {
|
||||||
|
log.Errorf("triggering webhook %+v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TriggerClientEventHook client_events
|
||||||
// {
|
// {
|
||||||
// "name": "client_event",
|
// "name": "client_event",
|
||||||
// "channel": "name of the channel the event was published on",
|
// "channel": "name of the channel the event was published on",
|
||||||
@@ -89,44 +107,63 @@ func (a *app) TriggerChannelVacatedHook(c *channel) {
|
|||||||
// "socket_id": "socket_id of the sending socket",
|
// "socket_id": "socket_id of the sending socket",
|
||||||
// "user_id": "user_id associated with the sending socket" # Only for presence channels
|
// "user_id": "user_id associated with the sending socket" # Only for presence channels
|
||||||
// }
|
// }
|
||||||
func (a *app) TriggerClientEventHook(c *channel, s *subscription, clientEvent string, data interface{}) {
|
func (a *Application) TriggerClientEventHook(c *channel.Channel, s *subscription.Subscription, clientEvent string, data interface{}) {
|
||||||
event := newClientHook(c, s, clientEvent, data)
|
event := newClientHook(c, s, clientEvent, data)
|
||||||
|
|
||||||
if c.IsPresence() {
|
if c.IsPresence() {
|
||||||
event.UserID = s.ID
|
event.UserID = s.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerHook(event.Name, a, c, event)
|
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := triggerHook(ctx, a, event); err != nil {
|
||||||
|
log.Errorf("triggering webhook %+v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TriggerMemberAddedHook member_added
|
||||||
// {
|
// {
|
||||||
// "name": "member_added",
|
// "name": "member_added",
|
||||||
// "channel": "presence-your_channel_name",
|
// "channel": "presence-your_channel_name",
|
||||||
// "user_id": "a_user_id"
|
// "user_id": "a_user_id"
|
||||||
// }
|
// }
|
||||||
func (a *app) TriggerMemberAddedHook(c *channel, s *subscription) {
|
func (a *Application) TriggerMemberAddedHook(c *channel.Channel, s *subscription.Subscription) {
|
||||||
event := newMemberAddedHook(c, s)
|
event := newMemberAddedHook(c, s)
|
||||||
triggerHook(event.Name, a, c, event)
|
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := triggerHook(ctx, a, event); err != nil {
|
||||||
|
log.Errorf("triggering webhook %+v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TriggerMemberRemovedHook member_removed
|
||||||
// {
|
// {
|
||||||
// "name": "member_removed",
|
// "name": "member_removed",
|
||||||
// "channel": "presence-your_channel_name",
|
// "channel": "presence-your_channel_name",
|
||||||
// "user_id": "a_user_id"
|
// "user_id": "a_user_id"
|
||||||
// }
|
// }
|
||||||
func (a *app) TriggerMemberRemovedHook(c *channel, s *subscription) {
|
func (a *Application) TriggerMemberRemovedHook(c *channel.Channel, s *subscription.Subscription) {
|
||||||
event := newMemberRemovedHook(c, s)
|
event := newMemberRemovedHook(c, s)
|
||||||
triggerHook(event.Name, a, c, event)
|
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := triggerHook(ctx, a, event); err != nil {
|
||||||
|
log.Errorf("triggering webhook %+v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func triggerHook(name string, a *app, c *channel, event hookEvent) {
|
func triggerHook(ctx context.Context, a *Application, event hookEvent) error {
|
||||||
if !a.WebHooks {
|
if !a.WebHooks {
|
||||||
log.Infof("Webhooks are not enabled for app: %s", a.Name)
|
log.Infof("webhook are not enabled for app: %s", a.Name)
|
||||||
return
|
return fmt.Errorf("webhooks are not enabled for app: %s", a.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
done := make(chan bool)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
log.Infof("Triggering %s event", name)
|
log.Infof("Triggering %s event", event.Name)
|
||||||
|
|
||||||
hook := webHook{TimeMs: time.Now().Unix()}
|
hook := webHook{TimeMs: time.Now().Unix()}
|
||||||
|
|
||||||
@@ -145,11 +182,14 @@ func triggerHook(name string, a *app, c *channel, event hookEvent) {
|
|||||||
var req *http.Request
|
var req *http.Request
|
||||||
|
|
||||||
req, err = http.NewRequest("POST", a.URLWebHook, bytes.NewReader(js))
|
req, err = http.NewRequest("POST", a.URLWebHook, bytes.NewReader(js))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Error creating request: %+v", err)
|
log.Errorf("Error creating request: %+v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
req.Header.Set("User-Agent", "Ipe UA; (+https://github.com/dimiro1/ipe)")
|
req.Header.Set("User-Agent", "Ipe UA; (+https://github.com/dimiro1/ipe)")
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("X-Pusher-Key", a.Key)
|
req.Header.Set("X-Pusher-Key", a.Key)
|
||||||
@@ -162,11 +202,25 @@ func triggerHook(name string, a *app, c *channel, event hookEvent) {
|
|||||||
|
|
||||||
// See: http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#close_http_resp_body
|
// See: http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#close_http_resp_body
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
log.Errorf("error closing response body %+v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Error posting %s event: %+v", name, err)
|
log.Errorf("error posting %s event: %+v", event.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Successfully terminated
|
||||||
|
done <- true
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-done:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
// 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 channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
|
||||||
|
"ipe/connection"
|
||||||
|
"ipe/events"
|
||||||
|
"ipe/subscription"
|
||||||
|
"ipe/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option constructor function for Channel
|
||||||
|
type Option func(*Channel)
|
||||||
|
|
||||||
|
// ListenerFunc listener function
|
||||||
|
type ListenerFunc func(*Channel, *subscription.Subscription)
|
||||||
|
|
||||||
|
// ClientEventListenerFunc listener for client events
|
||||||
|
type ClientEventListenerFunc func(*Channel, *subscription.Subscription, string, interface{})
|
||||||
|
|
||||||
|
// Channel represents an application channel
|
||||||
|
type Channel struct {
|
||||||
|
sync.RWMutex
|
||||||
|
|
||||||
|
ID string
|
||||||
|
subscriptions map[string]*subscription.Subscription
|
||||||
|
|
||||||
|
createdAt time.Time
|
||||||
|
|
||||||
|
memberAddedListeners []ListenerFunc
|
||||||
|
memberRemovedListeners []ListenerFunc
|
||||||
|
channelOccupiedListeners []ListenerFunc
|
||||||
|
channelVacatedListeners []ListenerFunc
|
||||||
|
clientEventListeners []ClientEventListenerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Create a new Channel
|
||||||
|
func New(channelID string, options ...Option) *Channel {
|
||||||
|
log.Infof("Creating a new Channel: %s", channelID)
|
||||||
|
|
||||||
|
c := &Channel{ID: channelID, createdAt: time.Now(), subscriptions: make(map[string]*subscription.Subscription)}
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
option(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMemberAddedListener appends the given ListenerFunc into the memberAddedListeners list
|
||||||
|
func WithMemberAddedListener(f ListenerFunc) func(*Channel) {
|
||||||
|
return func(c *Channel) {
|
||||||
|
c.memberAddedListeners = append(c.memberAddedListeners, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMemberRemovedListener appends the given ListenerFunc into the memberRemovedListeners list
|
||||||
|
func WithMemberRemovedListener(f ListenerFunc) func(*Channel) {
|
||||||
|
return func(c *Channel) {
|
||||||
|
c.memberRemovedListeners = append(c.memberRemovedListeners, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithChannelOccupiedListener appends the given ListenerFunc into the channelOccupiedListeners list
|
||||||
|
func WithChannelOccupiedListener(f ListenerFunc) func(*Channel) {
|
||||||
|
return func(c *Channel) {
|
||||||
|
c.channelOccupiedListeners = append(c.channelOccupiedListeners, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithChannelVacatedListener appends the given ListenerFunc into the channelVacatedListeners list
|
||||||
|
func WithChannelVacatedListener(f ListenerFunc) func(*Channel) {
|
||||||
|
return func(c *Channel) {
|
||||||
|
c.channelVacatedListeners = append(c.channelVacatedListeners, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithClientEventListener appends the given ListenerFunc into the clientEventListeners list
|
||||||
|
func WithClientEventListener(f ClientEventListenerFunc) func(*Channel) {
|
||||||
|
return func(c *Channel) {
|
||||||
|
c.clientEventListeners = append(c.clientEventListeners, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscriptions returns a slice of subscriptions
|
||||||
|
func (c *Channel) Subscriptions() []*subscription.Subscription {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
var subscriptions []*subscription.Subscription
|
||||||
|
|
||||||
|
for _, sub := range c.subscriptions {
|
||||||
|
subscriptions = append(subscriptions, sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
return subscriptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsOccupied Return true if the Channel has at least one subscriber
|
||||||
|
func (c *Channel) IsOccupied() bool {
|
||||||
|
return c.TotalSubscriptions() > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPresenceOrPrivate Check if the type of the Channel is presence or is private
|
||||||
|
func (c *Channel) IsPresenceOrPrivate() bool {
|
||||||
|
return c.IsPresence() || c.IsPrivate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPublic Check if the type of the Channel is public
|
||||||
|
func (c *Channel) IsPublic() bool {
|
||||||
|
return !c.IsPresenceOrPrivate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPresence Check if the type of the Channel is presence
|
||||||
|
func (c *Channel) IsPresence() bool {
|
||||||
|
return utils.IsPresenceChannel(c.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPrivate Check if the type of the Channel is private
|
||||||
|
func (c *Channel) IsPrivate() bool {
|
||||||
|
return utils.IsPrivateChannel(c.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TotalSubscriptions Get the total of subscribers
|
||||||
|
func (c *Channel) TotalSubscriptions() int {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
return len(c.subscriptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TotalUsers Get the total of users.
|
||||||
|
func (c *Channel) TotalUsers() int {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
total := make(map[string]int)
|
||||||
|
|
||||||
|
for _, s := range c.subscriptions {
|
||||||
|
total[s.ID]++
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe Add a new subscriber to the Channel
|
||||||
|
func (c *Channel) Subscribe(conn *connection.Connection, channelData string) error {
|
||||||
|
log.Infof("Subscribing %s to Channel %s", conn.SocketID, c.ID)
|
||||||
|
|
||||||
|
_subscription := subscription.New(conn, channelData)
|
||||||
|
c.Lock()
|
||||||
|
c.subscriptions[conn.SocketID] = _subscription
|
||||||
|
c.Unlock()
|
||||||
|
|
||||||
|
if c.IsPresence() {
|
||||||
|
// User Info Data
|
||||||
|
var info struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
UserInfo json.RawMessage `json:"user_info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("%+v", channelData)
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(channelData), &info); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
js, err := info.UserInfo.MarshalJSON()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Lock()
|
||||||
|
// Update the Subscription
|
||||||
|
_subscription.ID = info.UserID
|
||||||
|
_subscription.Data = string(js)
|
||||||
|
c.Unlock()
|
||||||
|
|
||||||
|
// Publish pusher_internal:member_added
|
||||||
|
c.PublishMemberAddedEvent(channelData, _subscription)
|
||||||
|
|
||||||
|
for _, hook := range c.memberAddedListeners {
|
||||||
|
hook(c, _subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pusher_internal:subscription_succeeded
|
||||||
|
data := make(map[string]events.SubscriptionSucceededPresenceData)
|
||||||
|
data["presence"] = events.NewSubscriptionSucceedPresenceData(c.subscriptions)
|
||||||
|
|
||||||
|
js, err = json.Marshal(data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.Publish(events.NewSubscriptionSucceeded(c.ID, string(js)))
|
||||||
|
} else {
|
||||||
|
conn.Publish(events.NewSubscriptionSucceeded(c.ID, "{}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.TotalSubscriptions() == 1 {
|
||||||
|
for _, hook := range c.channelOccupiedListeners {
|
||||||
|
hook(c, _subscription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSubscribed check if the user is subscribed
|
||||||
|
func (c *Channel) IsSubscribed(conn *connection.Connection) bool {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
_, exists := c.subscriptions[conn.SocketID]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe Remove the subscriber from the Channel
|
||||||
|
// It destroy the Channel if the channels does not have any subscribers.
|
||||||
|
func (c *Channel) Unsubscribe(conn *connection.Connection) error {
|
||||||
|
log.Infof("unsubscribe %s from Channel %s", conn.SocketID, c.ID)
|
||||||
|
|
||||||
|
c.RLock()
|
||||||
|
_subscription, exists := c.subscriptions[conn.SocketID]
|
||||||
|
c.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return errors.New("_subscription not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Lock()
|
||||||
|
delete(c.subscriptions, conn.SocketID)
|
||||||
|
c.Unlock()
|
||||||
|
|
||||||
|
if c.IsPresence() {
|
||||||
|
// Publish pusher_internal:member_removed
|
||||||
|
c.PublishMemberRemovedEvent(_subscription)
|
||||||
|
|
||||||
|
for _, hook := range c.memberRemovedListeners {
|
||||||
|
hook(c, _subscription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.IsOccupied() {
|
||||||
|
for _, hook := range c.channelVacatedListeners {
|
||||||
|
hook(c, _subscription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishMemberAddedEvent Publish a MemberAddedEvent to all subscriptions
|
||||||
|
func (c *Channel) PublishMemberAddedEvent(data string, subscription *subscription.Subscription) {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
for _, subs := range c.subscriptions {
|
||||||
|
if subs != subscription {
|
||||||
|
subs.Connection.Publish(events.NewMemberAdded(c.ID, data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishMemberRemovedEvent Publish a MemberRemovedEvent to all subscriptions
|
||||||
|
func (c *Channel) PublishMemberRemovedEvent(subscription *subscription.Subscription) {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
for _, subs := range c.subscriptions {
|
||||||
|
if subs != subscription {
|
||||||
|
subs.Connection.Publish(events.NewMemberRemoved(c.ID, subscription.ID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish messages to all Subscribers
|
||||||
|
// skip the ignore connection
|
||||||
|
func (c *Channel) Publish(event events.Raw, ignore string) error {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
b, err := event.Data.MarshalJSON()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var v interface{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b, &v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Publishing message %+v to Channel %s", v, c.ID)
|
||||||
|
|
||||||
|
for _, subs := range c.subscriptions {
|
||||||
|
if subs.Connection.SocketID != ignore {
|
||||||
|
subs.Connection.Publish(events.NewResponse(event.Event, event.Channel, v))
|
||||||
|
} else {
|
||||||
|
if utils.IsClientEvent(event.Event) {
|
||||||
|
for _, hook := range c.clientEventListeners {
|
||||||
|
hook(c, subs, event.Event, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -2,18 +2,23 @@
|
|||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package ipe
|
package channel
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"ipe/connection"
|
||||||
|
"ipe/mocks"
|
||||||
|
"ipe/subscription"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestIsOccupied(t *testing.T) {
|
func TestIsOccupied(t *testing.T) {
|
||||||
c := newChannel("ID")
|
c := New("ID")
|
||||||
|
|
||||||
if c.IsOccupied() {
|
if c.IsOccupied() {
|
||||||
t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), false)
|
t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Subscriptions["ID"] = newSubscription(newConnection("ID", mockSocket{}), "")
|
c.subscriptions["ID"] = subscription.New(connection.New("ID", mocks.MockSocket{}), "")
|
||||||
|
|
||||||
if !c.IsOccupied() {
|
if !c.IsOccupied() {
|
||||||
t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), true)
|
t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), true)
|
||||||
@@ -21,7 +26,7 @@ func TestIsOccupied(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIsPrivate(t *testing.T) {
|
func TestIsPrivate(t *testing.T) {
|
||||||
c := newChannel("private-channel")
|
c := New("private-Channel")
|
||||||
|
|
||||||
if !c.IsPrivate() {
|
if !c.IsPrivate() {
|
||||||
t.Errorf("c.IsPrivate() == %t, wants %t", c.IsPrivate(), true)
|
t.Errorf("c.IsPrivate() == %t, wants %t", c.IsPrivate(), true)
|
||||||
@@ -29,7 +34,7 @@ func TestIsPrivate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIsPresence(t *testing.T) {
|
func TestIsPresence(t *testing.T) {
|
||||||
c := newChannel("presence-channel")
|
c := New("presence-Channel")
|
||||||
|
|
||||||
if !c.IsPresence() {
|
if !c.IsPresence() {
|
||||||
t.Errorf("c.IsPresence() == %t, wants %t", c.IsPresence(), true)
|
t.Errorf("c.IsPresence() == %t, wants %t", c.IsPresence(), true)
|
||||||
@@ -37,7 +42,7 @@ func TestIsPresence(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIsPublic(t *testing.T) {
|
func TestIsPublic(t *testing.T) {
|
||||||
c := newChannel("channel")
|
c := New("Channel")
|
||||||
|
|
||||||
if !c.IsPublic() {
|
if !c.IsPublic() {
|
||||||
t.Errorf("c.IsPublic() == %t, wants %t", c.IsPublic(), true)
|
t.Errorf("c.IsPublic() == %t, wants %t", c.IsPublic(), true)
|
||||||
@@ -45,13 +50,13 @@ func TestIsPublic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIsPrivateOrPresence(t *testing.T) {
|
func TestIsPrivateOrPresence(t *testing.T) {
|
||||||
c := newChannel("private-channel")
|
c := New("private-Channel")
|
||||||
|
|
||||||
if !c.IsPresenceOrPrivate() {
|
if !c.IsPresenceOrPrivate() {
|
||||||
t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
|
t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
c = newChannel("presence-channel")
|
c = New("presence-Channel")
|
||||||
|
|
||||||
if !c.IsPresenceOrPrivate() {
|
if !c.IsPresenceOrPrivate() {
|
||||||
t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
|
t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
|
||||||
@@ -59,21 +64,21 @@ func TestIsPrivateOrPresence(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTotalSubscriptions(t *testing.T) {
|
func TestTotalSubscriptions(t *testing.T) {
|
||||||
c := newChannel("ID")
|
c := New("ID")
|
||||||
|
|
||||||
if c.TotalSubscriptions() != len(c.Subscriptions) {
|
if c.TotalSubscriptions() != len(c.subscriptions) {
|
||||||
t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.Subscriptions))
|
t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.subscriptions))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTotalUsers(t *testing.T) {
|
func TestTotalUsers(t *testing.T) {
|
||||||
c := newChannel("ID")
|
c := New("ID")
|
||||||
|
|
||||||
c.Subscriptions["1"] = newSubscription(newConnection("ID", mockSocket{}), "")
|
c.subscriptions["1"] = subscription.New(connection.New("ID", mocks.MockSocket{}), "")
|
||||||
c.Subscriptions["2"] = newSubscription(newConnection("ID", mockSocket{}), "")
|
c.subscriptions["2"] = subscription.New(connection.New("ID", mocks.MockSocket{}), "")
|
||||||
|
|
||||||
if c.TotalSubscriptions() != len(c.Subscriptions) {
|
if c.TotalSubscriptions() != len(c.subscriptions) {
|
||||||
t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.Subscriptions))
|
t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.subscriptions))
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.TotalUsers() != 1 {
|
if c.TotalUsers() != 1 {
|
||||||
@@ -83,14 +88,14 @@ func TestTotalUsers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIsSubscribed(t *testing.T) {
|
func TestIsSubscribed(t *testing.T) {
|
||||||
c := newChannel("ID")
|
c := New("ID")
|
||||||
conn := newConnection("ID", mockSocket{})
|
conn := connection.New("ID", mocks.MockSocket{})
|
||||||
|
|
||||||
if c.IsSubscribed(conn) {
|
if c.IsSubscribed(conn) {
|
||||||
t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), false)
|
t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Subscriptions["ID"] = newSubscription(conn, "")
|
c.subscriptions["ID"] = subscription.New(conn, "")
|
||||||
|
|
||||||
if !c.IsSubscribed(conn) {
|
if !c.IsSubscribed(conn) {
|
||||||
t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), true)
|
t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), true)
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/dimiro1/ipe/ipe"
|
"ipe"
|
||||||
)
|
)
|
||||||
|
|
||||||
// These variables are generated by the linker
|
// These variables are generated by the linker
|
||||||
@@ -21,7 +21,7 @@ var (
|
|||||||
|
|
||||||
// Main function, initialize the system
|
// Main function, initialize the system
|
||||||
func main() {
|
func main() {
|
||||||
var filename = flag.String("config", "config.json", "Config file location")
|
var filename = flag.String("config", "config.yml", "Config file location")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
printBanner()
|
printBanner()
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright 2014, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
// File config file
|
||||||
|
type File struct {
|
||||||
|
Host string `yaml:"host"` // The host, eg: :8080 will start on 0.0.0.0:8080
|
||||||
|
SSL SSL `yaml:"ssl"`
|
||||||
|
Profiling bool `yaml:"profiling"`
|
||||||
|
Apps []Application `yaml:"apps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSL related configuration options
|
||||||
|
type SSL struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
KeyFile string `yaml:"key_file"`
|
||||||
|
CertFile string `yaml:"cert_file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application related configuration options
|
||||||
|
type Application struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
AppID string `yaml:"app_id"`
|
||||||
|
Key string `yaml:"key"`
|
||||||
|
Secret string `yaml:"secret"`
|
||||||
|
OnlySSL bool `yaml:"only_ssl"`
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
UserEvents bool `yaml:"user_events"`
|
||||||
|
WebHooks Webhooks `yaml:"webhooks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webhooks related configuration options
|
||||||
|
type Webhooks struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
URL string `yaml:"url"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// 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 connection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Socket interface to write to the client
|
||||||
|
type Socket interface {
|
||||||
|
WriteJSON(interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection An user connection
|
||||||
|
type Connection struct {
|
||||||
|
sync.Mutex
|
||||||
|
|
||||||
|
SocketID string
|
||||||
|
Socket Socket
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Create a new Subscriber
|
||||||
|
func New(socketID string, s Socket) *Connection {
|
||||||
|
log.Infof("Creating a new Subscriber %+v", socketID)
|
||||||
|
|
||||||
|
return &Connection{SocketID: socketID, Socket: s, CreatedAt: time.Now()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish the message to websocket attached to this client
|
||||||
|
func (conn *Connection) Publish(m interface{}) {
|
||||||
|
conn.Lock()
|
||||||
|
defer conn.Unlock()
|
||||||
|
|
||||||
|
if err := conn.Socket.WriteJSON(m); err != nil {
|
||||||
|
log.Errorf("error writing json into Socket, %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,23 +2,18 @@
|
|||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package ipe
|
package connection
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"ipe/mocks"
|
||||||
// mockSocket is a mock implementation of socket
|
"testing"
|
||||||
// used in the test suite
|
)
|
||||||
type mockSocket struct{}
|
|
||||||
|
|
||||||
func (s mockSocket) WriteJSON(i interface{}) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewConnection(t *testing.T) {
|
func TestNewConnection(t *testing.T) {
|
||||||
expectedSocketID := "socketID"
|
expectedSocketID := "socketID"
|
||||||
expectedSocket := mockSocket{}
|
expectedSocket := mocks.MockSocket{}
|
||||||
|
|
||||||
c := newConnection(expectedSocketID, expectedSocket)
|
c := New(expectedSocketID, expectedSocket)
|
||||||
|
|
||||||
if c.SocketID != expectedSocketID {
|
if c.SocketID != expectedSocketID {
|
||||||
t.Errorf("c.SocketID == %s, wants %s", c.SocketID, expectedSocketID)
|
t.Errorf("c.SocketID == %s, wants %s", c.SocketID, expectedSocketID)
|
||||||
@@ -29,6 +24,6 @@ func TestNewConnection(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.CreatedAt.IsZero() {
|
if c.CreatedAt.IsZero() {
|
||||||
t.Errorf("c.CreatedAt.IsZero() == %t, wants %t", c.CreatedAt.IsZero(), false)
|
t.Errorf("c.createdAt.IsZero() == %t, wants %t", c.CreatedAt.IsZero(), false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
if [ -z "$(ls -A /config)" ]; then
|
||||||
|
cp /app/config-example.yml /config/config.yml
|
||||||
|
fi
|
||||||
|
|
||||||
|
/app/ipe --config=/config/config.yml
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
// 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 events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
|
||||||
|
"ipe/subscription"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SubscribeData data for Subscribe event
|
||||||
|
type SubscribeData struct {
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Auth string `json:"auth,omitempty"`
|
||||||
|
ChannelData string `json:"channel_data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe event
|
||||||
|
// {
|
||||||
|
// "event": "pusher:subscribe",
|
||||||
|
// "data": {
|
||||||
|
// "channel": "the channel",
|
||||||
|
// "auth": "the auth",
|
||||||
|
// "channelData": "extra data"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
type Subscribe struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Data SubscribeData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSubscribe Create a new subscribe event with the specified channel and data
|
||||||
|
func NewSubscribe(channel, auth, channelData string) Subscribe {
|
||||||
|
data := SubscribeData{Channel: channel, Auth: auth, ChannelData: channelData}
|
||||||
|
return Subscribe{Event: "pusher:subscribe", Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsubscribeData event data
|
||||||
|
type UnsubscribeData struct {
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe event
|
||||||
|
// {
|
||||||
|
// "event": "pusher:unsubscribe",
|
||||||
|
// "data": {
|
||||||
|
// "channel": "The channel"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
type Unsubscribe struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Data UnsubscribeData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUnsubscribe Create a new unsubscribe event for the specified channel
|
||||||
|
func NewUnsubscribe(channel string) Unsubscribe {
|
||||||
|
data := UnsubscribeData{Channel: channel}
|
||||||
|
return Unsubscribe{Event: "pusher:unsubscribe", Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriptionSucceeded event
|
||||||
|
// {
|
||||||
|
// "event": "pusher_internal:subscription_succeeded",
|
||||||
|
// "channel": "the channel"
|
||||||
|
// }
|
||||||
|
type SubscriptionSucceeded struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSubscriptionSucceeded Create a new subscription succeed event for the specified channel
|
||||||
|
func NewSubscriptionSucceeded(channel, data string) SubscriptionSucceeded {
|
||||||
|
return SubscriptionSucceeded{Event: "pusher_internal:subscription_succeeded", Channel: channel, Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriptionSucceededPresenceData Data Subscription Succeed
|
||||||
|
// "{
|
||||||
|
// \"presence\": {
|
||||||
|
// \"ids\": [\"11814b369700141b222a3f3791cec2d9\",\"71dd6a29da2a4833336d2a964becf820\"],
|
||||||
|
// \"hash\": {
|
||||||
|
// \"11814b369700141b222a3f3791cec2d9\": {
|
||||||
|
// \"name\":\"Phil Leggetter\",
|
||||||
|
// \"twitter\": \"@leggetter\"
|
||||||
|
// },
|
||||||
|
// \"71dd6a29da2a4833336d2a964becf820\": {
|
||||||
|
// \"name\":\"Max Williams\",
|
||||||
|
// \"twitter\": \"@maxthelion\"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// \"count\": 2
|
||||||
|
// }
|
||||||
|
// }"
|
||||||
|
type SubscriptionSucceededPresenceData struct {
|
||||||
|
Ids []string `json:"ids"`
|
||||||
|
Hash map[string]interface{} `json:"hash"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSubscriptionSucceedPresenceData returns new SubscriptionSucceededPresenceData
|
||||||
|
func NewSubscriptionSucceedPresenceData(subscriptions map[string]*subscription.Subscription) SubscriptionSucceededPresenceData {
|
||||||
|
event := SubscriptionSucceededPresenceData{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ids []string
|
||||||
|
hash = make(map[string]interface{}, len(subscriptions))
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, s := range subscriptions {
|
||||||
|
// Do you have any other idea?
|
||||||
|
var js interface{}
|
||||||
|
if err := json.Unmarshal([]byte(s.Data), &js); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
hash[s.ID] = js
|
||||||
|
ids = append(ids, s.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
event.Ids = ids
|
||||||
|
event.Hash = hash
|
||||||
|
event.Count = len(subscriptions)
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pong event
|
||||||
|
// {
|
||||||
|
// "event": "pusher:pong",
|
||||||
|
// "data": {}
|
||||||
|
// }
|
||||||
|
type Pong struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPong Create a new pong event
|
||||||
|
func NewPong() Pong {
|
||||||
|
return Pong{Event: "pusher:pong", Data: "{}"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping event
|
||||||
|
// {
|
||||||
|
// "event": "pusher:ping",
|
||||||
|
// "data": {}
|
||||||
|
// }
|
||||||
|
type Ping struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPing Create a new ping event
|
||||||
|
func NewPing() Ping {
|
||||||
|
return Ping{Event: "pusher:ping", Data: "{}"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error event
|
||||||
|
// {
|
||||||
|
// "event": "pusher:error",
|
||||||
|
// "data": {
|
||||||
|
// "message": "A Message",
|
||||||
|
// "code": 4000
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
type Error struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewError Create a new error event
|
||||||
|
// Pusher protocol is very strange in some parts
|
||||||
|
// It send null in some errors.
|
||||||
|
func NewError(code int, message string) Error {
|
||||||
|
var data = struct {
|
||||||
|
Code *int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}{
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
if code == 0 {
|
||||||
|
data.Code = nil
|
||||||
|
} else {
|
||||||
|
data.Code = &code
|
||||||
|
}
|
||||||
|
|
||||||
|
return Error{Event: "pusher:error", Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectionEstablished event
|
||||||
|
// {
|
||||||
|
// "event" : "pusher:connection_established",
|
||||||
|
// "data" : {
|
||||||
|
// "socket_id" : "123456",
|
||||||
|
// "activity_timeout" : 120
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
type ConnectionEstablished struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConnectionEstablished Create a new connection established event using the specified socketId
|
||||||
|
func NewConnectionEstablished(socketID string) ConnectionEstablished {
|
||||||
|
b, err := json.Marshal(struct {
|
||||||
|
SocketID string `json:"socket_id"`
|
||||||
|
ActivityTimeout int `json:"activity_timeout"`
|
||||||
|
}{
|
||||||
|
SocketID: socketID, ActivityTimeout: 120,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic("events: Could not Marshal json ConnectionEstablishedEvent")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConnectionEstablished{Event: "pusher:connection_established", Data: string(b)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemberAdded event
|
||||||
|
// {
|
||||||
|
// "event": "pusher_internal:member_added",
|
||||||
|
// "channel": "presence-example-channel",
|
||||||
|
// "data": String
|
||||||
|
// }
|
||||||
|
type MemberAdded struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMemberAdded creates a new MemberAdded event
|
||||||
|
func NewMemberAdded(channel, data string) MemberAdded {
|
||||||
|
return MemberAdded{Event: "pusher_internal:member_added", Channel: channel, Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemberRemoved event
|
||||||
|
// {
|
||||||
|
// "event": "pusher_internal:member_removed",
|
||||||
|
// "channel": "presence-example-channel",
|
||||||
|
// "data": String
|
||||||
|
// }
|
||||||
|
type MemberRemoved struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMemberRemoved returns a new MemberRemoved event
|
||||||
|
func NewMemberRemoved(channel string, userID string) MemberRemoved {
|
||||||
|
data, err := json.Marshal(struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
}{
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MemberRemoved{Event: "pusher_internal:member_removed", Channel: channel, Data: string(data)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw event, usually used for client events
|
||||||
|
// {
|
||||||
|
// "event": "client-?",
|
||||||
|
// "channel": "The channel",
|
||||||
|
// "data": {}
|
||||||
|
// }
|
||||||
|
type Raw struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response event
|
||||||
|
type Response struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewResponse The response event that is broadcasted to the client sockets
|
||||||
|
func NewResponse(name, channel string, data interface{}) Response {
|
||||||
|
return Response{Event: name, Channel: channel, Data: data}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_newErrorEvent_with_invalid_code(t *testing.T) {
|
||||||
|
event := NewError(0, "The error message")
|
||||||
|
|
||||||
|
data, _ := json.Marshal(event)
|
||||||
|
expected := `{"event":"pusher:error","data":{"code":null,"message":"The error message"}}`
|
||||||
|
|
||||||
|
if bytes.Compare(data, []byte(expected)) != 0 {
|
||||||
|
t.Errorf("%s != %s", string(data), expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_newErrorEvent_with_valid_code(t *testing.T) {
|
||||||
|
event := NewError(4007, "Unsupported protocol version")
|
||||||
|
|
||||||
|
data, _ := json.Marshal(event)
|
||||||
|
expected := `{"event":"pusher:error","data":{"code":4007,"message":"Unsupported protocol version"}}`
|
||||||
|
|
||||||
|
if bytes.Compare(data, []byte(expected)) != 0 {
|
||||||
|
t.Errorf("%s != %s", string(data), expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
client: go run client.go
|
client: go run client.go
|
||||||
server: go run ../main.go -config ./functional-config.json -alsologtostderr
|
server: go run ../cmd/main.go -config ./functional-config.yml -alsologtostderr
|
||||||
+12
-13
@@ -1,24 +1,23 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Pusher Spec</title>
|
<title>Pusher Spec</title>
|
||||||
<link href="https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.css" rel="stylesheet" />
|
<link href="//cdnjs.cloudflare.com/ajax/libs/mocha/5.2.0/mocha.min.css" rel="stylesheet"/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="mocha"></div>
|
<div id="mocha"></div>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/mocha/5.2.0/mocha.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.3.4/mocha.min.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/chai/4.2.0/chai.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.4.1/chai.min.js"></script>
|
|
||||||
|
|
||||||
<script src="//js.pusher.com/3.2/pusher.min.js"></script>
|
<script src="//js.pusher.com/4.3.1/pusher.min.js"></script>
|
||||||
|
|
||||||
<script>mocha.setup('bdd')</script>
|
<script>mocha.setup('bdd')</script>
|
||||||
<script src="test.pusher.js"></script>
|
<script src="test.pusher.js"></script>
|
||||||
<script>
|
<script>
|
||||||
mocha.checkLeaks();
|
mocha.checkLeaks();
|
||||||
mocha.globals(['jQuery', 'Pusher']);
|
mocha.globals(['Pusher']);
|
||||||
mocha.run();
|
mocha.run();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+38
-6
@@ -1,10 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
|
||||||
"github.com/pusher/pusher-http-go"
|
"github.com/pusher/pusher-http-go"
|
||||||
)
|
)
|
||||||
@@ -34,7 +36,7 @@ func pusherPresenceAuth(res http.ResponseWriter, req *http.Request) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(res, string(response))
|
_, _ = fmt.Fprint(res, string(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
func pusherPrivateAuth(res http.ResponseWriter, req *http.Request) {
|
func pusherPrivateAuth(res http.ResponseWriter, req *http.Request) {
|
||||||
@@ -48,19 +50,49 @@ func pusherPrivateAuth(res http.ResponseWriter, req *http.Request) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(res, string(response))
|
_, _ = fmt.Fprint(res, string(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
func triggerMessage(res http.ResponseWriter, req *http.Request) {
|
func triggerMessage(res http.ResponseWriter, _ *http.Request) {
|
||||||
client.Trigger("private-messages", "messages", "The message from server")
|
_, err := client.Trigger("private-messages", "messages", "The message from server")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Fprintf(res, "OK")
|
_, _ = fmt.Fprint(res, "OK")
|
||||||
|
}
|
||||||
|
|
||||||
|
func hookcallback(res http.ResponseWriter, r *http.Request) {
|
||||||
|
bytes, err := httputil.DumpRequest(r, true)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(bytes))
|
||||||
|
|
||||||
|
event := struct {
|
||||||
|
Events []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"events"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&event)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.Trigger("private-webhook", event.Events[0].Name, "The Webhoook from server")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprint(res, "OK")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
http.HandleFunc("/pusher/presence/auth", pusherPresenceAuth)
|
http.HandleFunc("/pusher/presence/auth", pusherPresenceAuth)
|
||||||
http.HandleFunc("/pusher/private/auth", pusherPrivateAuth)
|
http.HandleFunc("/pusher/private/auth", pusherPrivateAuth)
|
||||||
http.HandleFunc("/trigger", triggerMessage)
|
http.HandleFunc("/trigger", triggerMessage)
|
||||||
|
http.HandleFunc("/hook", hookcallback)
|
||||||
http.Handle("/", http.FileServer(http.Dir("./")))
|
http.Handle("/", http.FileServer(http.Dir("./")))
|
||||||
http.ListenAndServe(":5000", nil)
|
_ = http.ListenAndServe(":5000", nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"Host": ":8080",
|
|
||||||
"Encrypted": false,
|
|
||||||
"SSLHost": ":8090",
|
|
||||||
"SSLKeyFile": "key.pem",
|
|
||||||
"SSLCertFile": "cert.pem",
|
|
||||||
"Apps": [
|
|
||||||
{
|
|
||||||
"ApplicationDisabled": false,
|
|
||||||
"OnlySSL": false,
|
|
||||||
"Secret": "7ad3753142a6693b25b9",
|
|
||||||
"Key": "278d525bdf162c739803",
|
|
||||||
"Name": "App for Functional Test",
|
|
||||||
"AppID": "1",
|
|
||||||
"UserEvents": true,
|
|
||||||
"WebHooks": false,
|
|
||||||
"URLWebHook": "http://127.0.0.1:4567/php/hook.php"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
host: ':8080'
|
||||||
|
profiling: false
|
||||||
|
ssl:
|
||||||
|
enabled: false
|
||||||
|
host: ':4343'
|
||||||
|
key_file: 'key.pem'
|
||||||
|
cert_file: 'cert.pem'
|
||||||
|
apps:
|
||||||
|
- name: 'Sample Application'
|
||||||
|
enabled: true
|
||||||
|
only_ssl: false
|
||||||
|
key: '278d525bdf162c739803'
|
||||||
|
secret: '7ad3753142a6693b25b9'
|
||||||
|
app_id: '1'
|
||||||
|
user_events: true
|
||||||
|
webhooks:
|
||||||
|
enabled: true # Default is false
|
||||||
|
url: 'http://127.0.0.1:5000/hook'
|
||||||
+127
-105
@@ -1,136 +1,158 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
var assert = chai.assert;
|
let assert = chai.assert;
|
||||||
|
|
||||||
var APP_KEY = "278d525bdf162c739803";
|
let APP_KEY = "278d525bdf162c739803";
|
||||||
var HOST = "localhost";
|
let HOST = "localhost";
|
||||||
var PORT = 8080;
|
let PORT = 8080;
|
||||||
var AUTH = "http://localhost:5000/pusher/private/auth"
|
let AUTH = "http://localhost:5000/pusher/private/auth";
|
||||||
var AUTH_PRESENCE = "http://localhost:5000/pusher/presence/auth"
|
let AUTH_PRESENCE = "http://localhost:5000/pusher/presence/auth";
|
||||||
|
|
||||||
Pusher.log = function(msg) {
|
Pusher.log = function (msg) {
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
function getPusher(auth) {
|
function getPusher(auth) {
|
||||||
return new Pusher(APP_KEY, {
|
return new Pusher(APP_KEY, {
|
||||||
wsHost: HOST,
|
wsHost: HOST,
|
||||||
wsPort: PORT,
|
wsPort: PORT,
|
||||||
authEndpoint: auth,
|
authEndpoint: auth,
|
||||||
enabledTransports: ["ws"],
|
enabledTransports: ["ws"],
|
||||||
disabledTransports: ["flash"],
|
disabledTransports: ["flash"],
|
||||||
cluster: "hello", // Should be ignored
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Pusher", function() {
|
describe("Pusher", function () {
|
||||||
|
|
||||||
describe("connection", function() {
|
describe("connection", function () {
|
||||||
it("should connect sucessfully with correct config", function(done) {
|
it("should connect sucessfully with correct config", function (done) {
|
||||||
var pusher = getPusher(AUTH);
|
let pusher = getPusher(AUTH);
|
||||||
|
|
||||||
pusher.connection.bind('connected', function() {
|
pusher.connection.bind('connected', function () {
|
||||||
assert.ok(true, "Connected");
|
assert.ok(true, "Connected");
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not connect without the correct config", function(done) {
|
it("should not connect without the correct config", function (done) {
|
||||||
var pusher = new Pusher("INVALID_APP_KEY", {
|
let pusher = new Pusher("INVALID_APP_KEY", {
|
||||||
wsHost: HOST,
|
wsHost: HOST,
|
||||||
wsPort: PORT,
|
wsPort: PORT,
|
||||||
enabledTransports: ["ws"],
|
enabledTransports: ["ws"],
|
||||||
disabledTransports: ["flash"]
|
disabledTransports: ["flash"]
|
||||||
});
|
});
|
||||||
|
|
||||||
pusher.connection.bind('disconnected', function() {
|
pusher.connection.bind('disconnected', function () {
|
||||||
assert.ok(true, "Not Connected");
|
assert.ok(true, "Not Connected");
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}); // connection
|
}); // connection
|
||||||
|
|
||||||
describe("subscription", function() {
|
describe("subscription", function () {
|
||||||
it("should subscribe to a public channel", function(done) {
|
it("should subscribe to a public channel", function (done) {
|
||||||
var pusher = getPusher(AUTH);
|
let pusher = getPusher(AUTH);
|
||||||
|
|
||||||
var channel = pusher.subscribe('public-channel');
|
let channel = pusher.subscribe('public-channel');
|
||||||
channel.bind("pusher:subscription_succeeded", function(data) {
|
channel.bind("pusher:subscription_succeeded", function () {
|
||||||
assert.ok(true, "Connected to the channel");
|
assert.ok(true, "Connected to the channel");
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should subscribe to a private channel", function(done) {
|
it("should subscribe to a private channel", function (done) {
|
||||||
var pusher = getPusher(AUTH);
|
let pusher = getPusher(AUTH);
|
||||||
|
|
||||||
var channel = pusher.subscribe('private-channel');
|
let channel = pusher.subscribe('private-channel');
|
||||||
channel.bind("pusher:subscription_succeeded", function(data) {
|
channel.bind("pusher:subscription_succeeded", function () {
|
||||||
assert.ok(true, "Connected to the channel");
|
assert.ok(true, "Connected to the channel");
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should subscribe to a presence channel", function(done) {
|
it("should subscribe to a presence channel", function (done) {
|
||||||
var pusher = getPusher(AUTH_PRESENCE);
|
let pusher = getPusher(AUTH_PRESENCE);
|
||||||
|
|
||||||
var channel = pusher.subscribe('presence-channel');
|
let channel = pusher.subscribe('presence-channel');
|
||||||
channel.bind("pusher:subscription_succeeded", function(data) {
|
channel.bind("pusher:subscription_succeeded", function () {
|
||||||
assert.ok(true, "Connected to the channel");
|
assert.ok(true, "Connected to the channel");
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}); // subscription
|
}); // subscription
|
||||||
|
|
||||||
describe("events", function() {
|
describe("hooks", function () {
|
||||||
it('should not allowed client events on public channels', function(done) {
|
it('should receive hook', function (done) {
|
||||||
var pusher = getPusher(AUTH);
|
let pusher = getPusher(AUTH);
|
||||||
var channel = pusher.subscribe('public-channel');
|
let channel = pusher.subscribe('private-webhook');
|
||||||
|
|
||||||
channel.bind("pusher:subscription_succeeded", function(data) {
|
channel.bind("pusher:subscription_succeeded", function () {
|
||||||
channel.trigger("client-message", "The message");
|
console.log("subscribed");
|
||||||
});
|
});
|
||||||
|
|
||||||
pusher.bind("pusher:error", function(data) {
|
channel.bind("channel_occupied", function (data) {
|
||||||
assert.ok(true, "Expected error");
|
assert.equal(data, "The Webhoook from server");
|
||||||
done();
|
pusher.unsubscribe('private-webhook');
|
||||||
});
|
done();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}); // hooks
|
||||||
|
|
||||||
it('should allow client events on private channels', function(done) {
|
describe("events", function () {
|
||||||
var pusher_a = getPusher(AUTH);
|
it('should not allowed client events on public channels', function (done) {
|
||||||
var pusher_b = getPusher(AUTH);
|
let pusher = getPusher(AUTH);
|
||||||
|
let channel = pusher.subscribe('public-channel');
|
||||||
|
|
||||||
var channel_a = pusher_a.subscribe('private-channel');
|
channel.bind("pusher:subscription_succeeded", function () {
|
||||||
var channel_b = pusher_b.subscribe('private-channel');
|
channel.trigger("client-message", "The message");
|
||||||
|
});
|
||||||
|
|
||||||
channel_a.bind("pusher:subscription_succeeded", function() {
|
pusher.bind("pusher:error", function () {
|
||||||
channel_a.trigger("client-message", "The message");
|
assert.ok(true, "Expected error");
|
||||||
});
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
channel_b.bind("client-message", function(data) {
|
it('should allow client events on private channels', function (done) {
|
||||||
assert.equal(data, "The message");
|
let pusher_a = getPusher(AUTH);
|
||||||
done();
|
let pusher_b = getPusher(AUTH);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should publish event on private channel', function(done) {
|
let channel_a = pusher_a.subscribe('private-channel');
|
||||||
var pusher_a = getPusher(AUTH);
|
let channel_b = pusher_b.subscribe('private-channel');
|
||||||
var pusher_b = getPusher(AUTH);
|
|
||||||
|
|
||||||
var channel_a = pusher_a.subscribe('private-messages');
|
channel_a.bind("pusher:subscription_succeeded", function () {
|
||||||
var channel_b = pusher_b.subscribe('private-messages');
|
channel_a.trigger("client-message", "The message");
|
||||||
|
});
|
||||||
|
|
||||||
channel_a.bind("pusher:subscription_succeeded", function() {
|
channel_b.bind("client-message", function (data) {
|
||||||
var xhttp = new XMLHttpRequest();
|
assert.equal(data, "The message");
|
||||||
xhttp.open("GET", "/trigger", true);
|
done();
|
||||||
xhttp.send();
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
channel_b.bind("messages", function(data) {
|
it('should publish event on private channel', function (done) {
|
||||||
assert.equal(data, "The message from server");
|
let pusher_a = getPusher(AUTH);
|
||||||
done();
|
let pusher_b = getPusher(AUTH);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
}); // events
|
let channel_a = pusher_a.subscribe('private-messages');
|
||||||
|
let channel_b = pusher_b.subscribe('private-messages');
|
||||||
|
|
||||||
|
channel_a.bind("pusher:subscription_succeeded", function () {
|
||||||
|
console.log("channel_a connected");
|
||||||
|
let xhttp = new XMLHttpRequest();
|
||||||
|
xhttp.open("GET", "/trigger", true);
|
||||||
|
xhttp.send();
|
||||||
|
});
|
||||||
|
|
||||||
|
channel_b.bind("pusher:subscription_succeeded", function () {
|
||||||
|
console.log("channel_b connected");
|
||||||
|
});
|
||||||
|
|
||||||
|
channel_b.bind("messages", function (data) {
|
||||||
|
assert.equal(data, "The message from server");
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}); // events
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
-20
@@ -1,20 +0,0 @@
|
|||||||
hash: 0a451841a1f9493d419d658fab8da6863ef5df0645ddea506a225865078c9dcb
|
|
||||||
updated: 2016-08-10T22:23:10.989237578-03:00
|
|
||||||
imports:
|
|
||||||
- name: github.com/golang/glog
|
|
||||||
version: 23def4e6c14b4da8ac2ed8007337bc5eb5007998
|
|
||||||
- name: github.com/gorilla/websocket
|
|
||||||
version: a69d25be2fe2923a97c2af6849b2f52426f68fc0
|
|
||||||
- name: github.com/pusher/pusher-http-go
|
|
||||||
version: 2bba5f217f6f0f4f0c0a9bb11b945b206b32bec5
|
|
||||||
- name: goji.io
|
|
||||||
version: e355964ac565b94cf0fc7f218346626529125086
|
|
||||||
subpackages:
|
|
||||||
- pat
|
|
||||||
- pattern
|
|
||||||
- internal
|
|
||||||
- name: golang.org/x/net
|
|
||||||
version: 075e191f18186a8ff2becaf64478e30f4545cdad
|
|
||||||
subpackages:
|
|
||||||
- context
|
|
||||||
testImports: []
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
package: github.com/dimiro1/ipe
|
|
||||||
import:
|
|
||||||
- package: github.com/golang/glog
|
|
||||||
- package: github.com/gorilla/websocket
|
|
||||||
- package: github.com/pusher/pusher-http-go
|
|
||||||
- package: goji.io
|
|
||||||
subpackages:
|
|
||||||
- pat
|
|
||||||
- package: golang.org/x/net
|
|
||||||
subpackages:
|
|
||||||
- context
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
module ipe
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
|
||||||
|
github.com/gorilla/context v1.1.1 // indirect
|
||||||
|
github.com/gorilla/handlers v1.4.0
|
||||||
|
github.com/gorilla/mux v1.6.2
|
||||||
|
github.com/gorilla/websocket v1.4.0
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/pusher/pusher-http-go v1.3.0
|
||||||
|
github.com/stretchr/testify v1.2.2 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.2.1
|
||||||
|
)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
|
github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA=
|
||||||
|
github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||||
|
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
|
||||||
|
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pusher/pusher-http-go v1.3.0 h1:dWrjIsNheCUEo6YE9qlS8pIl3XzJa5yM8VlBu/LUl3M=
|
||||||
|
github.com/pusher/pusher-http-go v1.3.0/go.mod h1:XAv1fxRmVTI++2xsfofDhg7whapsLRG/gH/DXbF3a18=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 h1:kkXA53yGe04D0adEYJwEVQjeBppL01Exg+fnMjfUraU=
|
||||||
|
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
// Copyright 2015 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package ipe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
"github.com/gorilla/handlers"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"ipe/api"
|
||||||
|
"ipe/app"
|
||||||
|
"ipe/config"
|
||||||
|
"ipe/storage"
|
||||||
|
"ipe/websockets"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start Parse the configuration file and starts the ipe server
|
||||||
|
// It Panic if could not start the HTTP or HTTPS server
|
||||||
|
func Start(filename string) {
|
||||||
|
var conf config.File
|
||||||
|
|
||||||
|
rand.Seed(time.Now().Unix())
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand env vars
|
||||||
|
data = []byte(os.ExpandEnv(string(data)))
|
||||||
|
|
||||||
|
// Decoding config
|
||||||
|
if err := yaml.UnmarshalStrict(data, &conf); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using a in memory database
|
||||||
|
inMemoryStorage := storage.NewInMemory()
|
||||||
|
|
||||||
|
// Adding applications
|
||||||
|
for _, a := range conf.Apps {
|
||||||
|
application := app.NewApplication(
|
||||||
|
a.Name,
|
||||||
|
a.AppID,
|
||||||
|
a.Key,
|
||||||
|
a.Secret,
|
||||||
|
a.OnlySSL,
|
||||||
|
a.Enabled,
|
||||||
|
a.UserEvents,
|
||||||
|
a.WebHooks.Enabled,
|
||||||
|
a.WebHooks.URL,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := inMemoryStorage.AddApp(application); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router := mux.NewRouter()
|
||||||
|
router.Use(handlers.RecoveryHandler())
|
||||||
|
|
||||||
|
router.Path("/app/{key}").Methods("GET").Handler(
|
||||||
|
websockets.NewWebsocket(inMemoryStorage),
|
||||||
|
)
|
||||||
|
|
||||||
|
appsRouter := router.PathPrefix("/apps/{app_id}").Subrouter()
|
||||||
|
appsRouter.Use(
|
||||||
|
api.CheckAppDisabled(inMemoryStorage),
|
||||||
|
api.Authentication(inMemoryStorage),
|
||||||
|
)
|
||||||
|
|
||||||
|
appsRouter.Path("/events").Methods("POST").Handler(
|
||||||
|
api.NewPostEvents(inMemoryStorage),
|
||||||
|
)
|
||||||
|
appsRouter.Path("/channels").Methods("GET").Handler(
|
||||||
|
api.NewGetChannels(inMemoryStorage),
|
||||||
|
)
|
||||||
|
appsRouter.Path("/channels/{channel_name}").Methods("GET").Handler(
|
||||||
|
api.NewGetChannel(inMemoryStorage),
|
||||||
|
)
|
||||||
|
appsRouter.Path("/channels/{channel_name}/users").Methods("GET").Handler(
|
||||||
|
api.NewGetChannelUsers(inMemoryStorage),
|
||||||
|
)
|
||||||
|
|
||||||
|
if conf.SSL.Enabled {
|
||||||
|
go func() {
|
||||||
|
log.Infof("Starting HTTPS service on %s ...", conf.SSL.Host)
|
||||||
|
log.Fatal(http.ListenAndServeTLS(conf.SSL.Host, conf.SSL.CertFile, conf.SSL.KeyFile, router))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Starting HTTP service on %s ...", conf.Host)
|
||||||
|
log.Fatal(http.ListenAndServe(conf.Host, router))
|
||||||
|
}
|
||||||
-234
@@ -1,234 +0,0 @@
|
|||||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package ipe
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"expvar"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
log "github.com/golang/glog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// An App
|
|
||||||
type app struct {
|
|
||||||
sync.Mutex
|
|
||||||
|
|
||||||
Name string
|
|
||||||
AppID string
|
|
||||||
Key string
|
|
||||||
Secret string
|
|
||||||
OnlySSL bool
|
|
||||||
ApplicationDisabled bool
|
|
||||||
UserEvents bool
|
|
||||||
WebHooks bool
|
|
||||||
URLWebHook string
|
|
||||||
|
|
||||||
Channels map[string]*channel
|
|
||||||
Connections map[string]*connection
|
|
||||||
|
|
||||||
Stats *expvar.Map
|
|
||||||
}
|
|
||||||
|
|
||||||
func newApp(name, appID, key, secret string, onlySSL, disabled, userEvents, webHooks bool, webHookURL string) *app {
|
|
||||||
|
|
||||||
a := &app{
|
|
||||||
Name: name,
|
|
||||||
AppID: appID,
|
|
||||||
Key: key,
|
|
||||||
Secret: secret,
|
|
||||||
OnlySSL: onlySSL,
|
|
||||||
ApplicationDisabled: disabled,
|
|
||||||
UserEvents: userEvents,
|
|
||||||
WebHooks: webHooks,
|
|
||||||
URLWebHook: webHookURL,
|
|
||||||
}
|
|
||||||
|
|
||||||
a.Connections = make(map[string]*connection)
|
|
||||||
a.Channels = make(map[string]*channel)
|
|
||||||
a.Stats = expvar.NewMap(fmt.Sprintf("%s (%s)", a.Name, a.AppID))
|
|
||||||
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only Presence channels
|
|
||||||
func (a *app) PresenceChannels() []*channel {
|
|
||||||
var channels []*channel
|
|
||||||
|
|
||||||
for _, c := range a.Channels {
|
|
||||||
if c.IsPresence() {
|
|
||||||
channels = append(channels, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return channels
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only Private channels
|
|
||||||
func (a *app) PrivateChannels() []*channel {
|
|
||||||
var channels []*channel
|
|
||||||
|
|
||||||
for _, c := range a.Channels {
|
|
||||||
if c.IsPrivate() {
|
|
||||||
channels = append(channels, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return channels
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only Public channels
|
|
||||||
func (a *app) PublicChannels() []*channel {
|
|
||||||
var channels []*channel
|
|
||||||
|
|
||||||
for _, c := range a.Channels {
|
|
||||||
if c.IsPublic() {
|
|
||||||
channels = append(channels, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return channels
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disconnect Socket
|
|
||||||
func (a *app) Disconnect(socketID string) {
|
|
||||||
log.Infof("Disconnecting socket %+v", socketID)
|
|
||||||
|
|
||||||
conn, err := a.FindConnection(socketID)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Infof("Socket not found, %+v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unsubscribe from channels
|
|
||||||
for _, c := range a.Channels {
|
|
||||||
if c.IsSubscribed(conn) {
|
|
||||||
c.Unsubscribe(a, conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from app
|
|
||||||
a.Lock()
|
|
||||||
defer a.Unlock()
|
|
||||||
|
|
||||||
_, exists := a.Connections[conn.SocketID]
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(a.Connections, conn.SocketID)
|
|
||||||
|
|
||||||
a.Stats.Add("TotalConnections", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect a new Subscriber
|
|
||||||
func (a *app) Connect(conn *connection) {
|
|
||||||
log.Infof("Adding a new Connection %s to app %s", conn.SocketID, a.Name)
|
|
||||||
a.Lock()
|
|
||||||
defer a.Unlock()
|
|
||||||
|
|
||||||
a.Connections[conn.SocketID] = conn
|
|
||||||
|
|
||||||
a.Stats.Add("TotalConnections", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a Connection on this app
|
|
||||||
func (a *app) FindConnection(socketID string) (*connection, error) {
|
|
||||||
conn, exists := a.Connections[socketID]
|
|
||||||
|
|
||||||
if exists {
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.New("Connection not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteChannel removes the channel from app
|
|
||||||
func (a *app) RemoveChannel(c *channel) {
|
|
||||||
log.Infof("Remove the channel %s from app %s", c.ChannelID, a.Name)
|
|
||||||
a.Lock()
|
|
||||||
defer a.Unlock()
|
|
||||||
|
|
||||||
delete(a.Channels, c.ChannelID)
|
|
||||||
|
|
||||||
if c.IsPresence() {
|
|
||||||
a.Stats.Add("TotalPresenceChannels", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.IsPrivate() {
|
|
||||||
a.Stats.Add("TotalPrivateChannels", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.IsPublic() {
|
|
||||||
a.Stats.Add("TotalPublicChannels", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
a.Stats.Add("TotalChannels", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a new Channel to this APP
|
|
||||||
func (a *app) AddChannel(c *channel) {
|
|
||||||
log.Infof("Adding a new channel %s to app %s", c.ChannelID, a.Name)
|
|
||||||
|
|
||||||
a.Lock()
|
|
||||||
defer a.Unlock()
|
|
||||||
|
|
||||||
a.Channels[c.ChannelID] = c
|
|
||||||
|
|
||||||
if c.IsPresence() {
|
|
||||||
a.Stats.Add("TotalPresenceChannels", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.IsPrivate() {
|
|
||||||
a.Stats.Add("TotalPrivateChannels", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.IsPublic() {
|
|
||||||
a.Stats.Add("TotalPublicChannels", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
a.Stats.Add("TotalChannels", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a Channel from this app
|
|
||||||
// If not found then the channel is created and added to this app
|
|
||||||
func (a *app) FindOrCreateChannelByChannelID(n string) *channel {
|
|
||||||
c, err := a.FindChannelByChannelID(n)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
c = newChannel(n)
|
|
||||||
a.AddChannel(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the channel by channel ID
|
|
||||||
func (a *app) FindChannelByChannelID(n string) (*channel, error) {
|
|
||||||
c, exists := a.Channels[n]
|
|
||||||
|
|
||||||
if exists {
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.New("Channel does not exists")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *app) Publish(c *channel, event rawEvent, ignore string) error {
|
|
||||||
a.Stats.Add("TotalUniqueMessages", 1)
|
|
||||||
|
|
||||||
return c.Publish(a, event, ignore)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *app) Unsubscribe(c *channel, conn *connection) error {
|
|
||||||
return c.Unsubscribe(a, conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *app) Subscribe(c *channel, conn *connection, data string) error {
|
|
||||||
return c.Subscribe(a, conn, data)
|
|
||||||
}
|
|
||||||
-252
@@ -1,252 +0,0 @@
|
|||||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package ipe
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var id = 0
|
|
||||||
|
|
||||||
func newTestApp() *app {
|
|
||||||
|
|
||||||
a := newApp("Test", strconv.Itoa(id), "123", "123", false, false, true, false, "")
|
|
||||||
id++
|
|
||||||
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConnect(t *testing.T) {
|
|
||||||
app := newTestApp()
|
|
||||||
|
|
||||||
app.Connect(newConnection("socketID", mockSocket{}))
|
|
||||||
|
|
||||||
if len(app.Connections) != 1 {
|
|
||||||
t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDisconnect(t *testing.T) {
|
|
||||||
app := newTestApp()
|
|
||||||
|
|
||||||
app.Connect(newConnection("socketID", mockSocket{}))
|
|
||||||
app.Disconnect("socketID")
|
|
||||||
|
|
||||||
if len(app.Connections) != 0 {
|
|
||||||
t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFindConnection(t *testing.T) {
|
|
||||||
app := newTestApp()
|
|
||||||
|
|
||||||
app.Connect(newConnection("socketID", mockSocket{}))
|
|
||||||
|
|
||||||
if _, err := app.FindConnection("socketID"); err != nil {
|
|
||||||
t.Errorf("app.FindConnection('socketID') == _, %q, wants %v", err, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := app.FindConnection("NotFound"); err == nil {
|
|
||||||
t.Errorf("app.FindConnection('socketID') == _, %q, wants !nil", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFindChannelByChannelID(t *testing.T) {
|
|
||||||
app := newTestApp()
|
|
||||||
|
|
||||||
channel := newChannel("ID")
|
|
||||||
app.AddChannel(channel)
|
|
||||||
|
|
||||||
if _, err := app.FindChannelByChannelID("ID"); err != nil {
|
|
||||||
t.Errorf("app.FindChannelByChannelID('ID') == _, %q, wants %v", err, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFindOrCreateChannelByChannelID(t *testing.T) {
|
|
||||||
app := newTestApp()
|
|
||||||
|
|
||||||
if len(app.Channels) != 0 {
|
|
||||||
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.FindOrCreateChannelByChannelID("ID")
|
|
||||||
|
|
||||||
if len(app.Channels) != 1 {
|
|
||||||
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemoveChannel(t *testing.T) {
|
|
||||||
app := newTestApp()
|
|
||||||
|
|
||||||
if len(app.Channels) != 0 {
|
|
||||||
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
channel := newChannel("ID")
|
|
||||||
app.AddChannel(channel)
|
|
||||||
|
|
||||||
if len(app.Channels) != 1 {
|
|
||||||
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.RemoveChannel(channel)
|
|
||||||
|
|
||||||
if len(app.Channels) != 0 {
|
|
||||||
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_add_channels(t *testing.T) {
|
|
||||||
|
|
||||||
app := newTestApp()
|
|
||||||
|
|
||||||
// Public
|
|
||||||
|
|
||||||
if len(app.PublicChannels()) != 0 {
|
|
||||||
t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.AddChannel(newChannel("ID"))
|
|
||||||
|
|
||||||
if len(app.PublicChannels()) != 1 {
|
|
||||||
t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Presence
|
|
||||||
|
|
||||||
if len(app.PresenceChannels()) != 0 {
|
|
||||||
t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.AddChannel(newChannel("presence-test"))
|
|
||||||
|
|
||||||
if len(app.PresenceChannels()) != 1 {
|
|
||||||
t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private
|
|
||||||
|
|
||||||
if len(app.PrivateChannels()) != 0 {
|
|
||||||
t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.AddChannel(newChannel("private-test"))
|
|
||||||
|
|
||||||
if len(app.PrivateChannels()) != 1 {
|
|
||||||
t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_AllChannels(t *testing.T) {
|
|
||||||
app := newTestApp()
|
|
||||||
app.AddChannel(newChannel("private-test"))
|
|
||||||
app.AddChannel(newChannel("presence-test"))
|
|
||||||
app.AddChannel(newChannel("test"))
|
|
||||||
|
|
||||||
if len(app.Channels) != 3 {
|
|
||||||
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_New_Subscriber(t *testing.T) {
|
|
||||||
app := newTestApp()
|
|
||||||
|
|
||||||
if len(app.Connections) != 0 {
|
|
||||||
t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn := newConnection("1", mockSocket{})
|
|
||||||
app.Connect(conn)
|
|
||||||
|
|
||||||
if len(app.Connections) != 1 {
|
|
||||||
t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_find_subscriber(t *testing.T) {
|
|
||||||
app := newTestApp()
|
|
||||||
conn := newConnection("1", mockSocket{})
|
|
||||||
app.Connect(conn)
|
|
||||||
|
|
||||||
conn, err := app.FindConnection("1")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conn.SocketID != "1" {
|
|
||||||
t.Errorf("conn.SocketID == %s, wants %s", conn.SocketID, "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a wrong subscriber
|
|
||||||
|
|
||||||
conn, err = app.FindConnection("DoesNotExists")
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("err == %q, wants !nil", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conn != nil {
|
|
||||||
t.Errorf("conn == %q, wants nil", conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_find_or_create_channels(t *testing.T) {
|
|
||||||
app := newTestApp()
|
|
||||||
|
|
||||||
// Public
|
|
||||||
if len(app.PublicChannels()) != 0 {
|
|
||||||
t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := app.FindOrCreateChannelByChannelID("id")
|
|
||||||
|
|
||||||
if len(app.PublicChannels()) != 1 {
|
|
||||||
t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.ChannelID != "id" {
|
|
||||||
t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "id")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Presence
|
|
||||||
if len(app.PresenceChannels()) != 0 {
|
|
||||||
t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
c = app.FindOrCreateChannelByChannelID("presence-test")
|
|
||||||
|
|
||||||
if len(app.PresenceChannels()) != 1 {
|
|
||||||
t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.ChannelID != "presence-test" {
|
|
||||||
t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "presence-test")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private
|
|
||||||
if len(app.PrivateChannels()) != 0 {
|
|
||||||
t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
c = app.FindOrCreateChannelByChannelID("private-test")
|
|
||||||
|
|
||||||
if len(app.PrivateChannels()) != 1 {
|
|
||||||
t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.ChannelID != "private-test" {
|
|
||||||
t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "private-test")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
-224
@@ -1,224 +0,0 @@
|
|||||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package ipe
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/dimiro1/ipe/utils"
|
|
||||||
log "github.com/golang/glog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A Channel
|
|
||||||
type channel struct {
|
|
||||||
sync.Mutex
|
|
||||||
|
|
||||||
CreatedAt time.Time
|
|
||||||
ChannelID string
|
|
||||||
Subscriptions map[string]*subscription
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return true if the channel has at least one subscriber
|
|
||||||
func (c *channel) IsOccupied() bool {
|
|
||||||
return c.TotalSubscriptions() > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the type of the channel is presence or is private
|
|
||||||
func (c *channel) IsPresenceOrPrivate() bool {
|
|
||||||
return c.IsPresence() || c.IsPrivate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the type of the channel is public
|
|
||||||
func (c *channel) IsPublic() bool {
|
|
||||||
return !c.IsPresenceOrPrivate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the type of the channel is presence
|
|
||||||
func (c *channel) IsPresence() bool {
|
|
||||||
return utils.IsPresenceChannel(c.ChannelID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the type of the channel is private
|
|
||||||
func (c *channel) IsPrivate() bool {
|
|
||||||
return utils.IsPrivateChannel(c.ChannelID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the total of subscribers
|
|
||||||
func (c *channel) TotalSubscriptions() int {
|
|
||||||
return len(c.Subscriptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the total of users.
|
|
||||||
func (c *channel) TotalUsers() int {
|
|
||||||
total := make(map[string]int)
|
|
||||||
|
|
||||||
for _, s := range c.Subscriptions {
|
|
||||||
total[s.ID]++
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(total)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a new subscriber to the channel
|
|
||||||
func (c *channel) Subscribe(a *app, conn *connection, channelData string) error {
|
|
||||||
log.Infof("Subscribing %s to channel %s", conn.SocketID, c.ChannelID)
|
|
||||||
|
|
||||||
c.Lock()
|
|
||||||
defer c.Unlock()
|
|
||||||
|
|
||||||
subscription := newSubscription(conn, channelData)
|
|
||||||
c.Subscriptions[conn.SocketID] = subscription
|
|
||||||
|
|
||||||
if c.IsPresence() {
|
|
||||||
// User Info Data
|
|
||||||
var info struct {
|
|
||||||
UserID string `json:"user_id"`
|
|
||||||
UserInfo json.RawMessage `json:"user_info"`
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("%+v", channelData)
|
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(channelData), &info); err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
js, err := info.UserInfo.MarshalJSON()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the Subscription
|
|
||||||
subscription.ID = info.UserID
|
|
||||||
subscription.Data = string(js)
|
|
||||||
|
|
||||||
// Publish pusher_internal:member_added
|
|
||||||
c.PublishMemberAddedEvent(a, channelData, subscription)
|
|
||||||
// WebHook
|
|
||||||
a.TriggerMemberAddedHook(c, subscription)
|
|
||||||
|
|
||||||
// pusher_internal:subscription_succeeded
|
|
||||||
data := make(map[string]subscriptionSucceeedEventPresenceData)
|
|
||||||
data["presence"] = newSubscriptionSucceedEventPresenceData(c)
|
|
||||||
|
|
||||||
js, err = json.Marshal(data)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.Publish(newSubscriptionSucceededEvent(c.ChannelID, string(js)))
|
|
||||||
} else {
|
|
||||||
conn.Publish(newSubscriptionSucceededEvent(c.ChannelID, "{}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebHook
|
|
||||||
if c.TotalSubscriptions() == 1 {
|
|
||||||
a.TriggerChannelOccupiedHook(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsSubscribed check if the user is subscribed
|
|
||||||
func (c *channel) IsSubscribed(conn *connection) bool {
|
|
||||||
_, exists := c.Subscriptions[conn.SocketID]
|
|
||||||
return exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the subscriber from the channel
|
|
||||||
// It destroy the channel if the channels does not have any subscribers.
|
|
||||||
func (c *channel) Unsubscribe(a *app, conn *connection) error {
|
|
||||||
log.Infof("Unsubscribing %s from channel %s", conn.SocketID, c.ChannelID)
|
|
||||||
|
|
||||||
c.Lock()
|
|
||||||
defer c.Unlock()
|
|
||||||
|
|
||||||
subscription, exists := c.Subscriptions[conn.SocketID]
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return errors.New("Subscription not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(c.Subscriptions, conn.SocketID)
|
|
||||||
|
|
||||||
if c.IsPresence() {
|
|
||||||
// Publish pusher_internal:member_removed
|
|
||||||
c.PublishMemberRemovedEvent(a, subscription)
|
|
||||||
// Webhook
|
|
||||||
a.TriggerMemberRemovedHook(c, subscription)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !c.IsOccupied() {
|
|
||||||
// WebHook
|
|
||||||
a.TriggerChannelVacatedHook(c)
|
|
||||||
|
|
||||||
// Remove the empty Channel
|
|
||||||
a.RemoveChannel(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new Channel
|
|
||||||
func newChannel(channelID string) *channel {
|
|
||||||
log.Infof("Creating a new channel: %s", channelID)
|
|
||||||
|
|
||||||
return &channel{ChannelID: channelID, CreatedAt: time.Now(), Subscriptions: make(map[string]*subscription)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish a MemberAddedEvent to all subscriptions
|
|
||||||
func (c *channel) PublishMemberAddedEvent(a *app, data string, subscription *subscription) {
|
|
||||||
for _, subs := range c.Subscriptions {
|
|
||||||
if subs != subscription {
|
|
||||||
subs.Connection.Publish(newMemberAddedEvent(c.ChannelID, data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish a MemberRemovedEvent to all subscriptions
|
|
||||||
func (c *channel) PublishMemberRemovedEvent(a *app, subscription *subscription) {
|
|
||||||
for _, subs := range c.Subscriptions {
|
|
||||||
if subs != subscription {
|
|
||||||
subs.Connection.Publish(newMemberRemovedEvent(c.ChannelID, subscription))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish messages to all Subscribers
|
|
||||||
func (c *channel) Publish(a *app, event rawEvent, ignore string) error {
|
|
||||||
b, err := event.Data.MarshalJSON()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var v interface{}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(b, &v); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("Publishing message %+v to channel %s", v, c.ChannelID)
|
|
||||||
|
|
||||||
for _, subs := range c.Subscriptions {
|
|
||||||
if subs.Connection.SocketID != ignore {
|
|
||||||
subs.Connection.Publish(newResponseEvent(event.Event, event.Channel, v))
|
|
||||||
} else {
|
|
||||||
// Webhook
|
|
||||||
if utils.IsClientEvent(event.Event) {
|
|
||||||
a.TriggerClientEventHook(c, subs, event.Event, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"Host": ":8080",
|
|
||||||
"SSL": false,
|
|
||||||
"SSLHost": ":4433",
|
|
||||||
"SSLKeyFile": "A key.pem file",
|
|
||||||
"SSLCertFile": "A cert.pem file",
|
|
||||||
"Apps": [
|
|
||||||
{
|
|
||||||
"ApplicationDisabled": false,
|
|
||||||
"Secret": "A really secret random string",
|
|
||||||
"Key": "A random Key string",
|
|
||||||
"OnlySSL": false,
|
|
||||||
"Name": "The app name",
|
|
||||||
"AppID": "The app ID",
|
|
||||||
"UserEvents": true,
|
|
||||||
"WebHooks": true,
|
|
||||||
"URLWebHook": "Some URL to send webhooks"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
// Copyright 2014, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package ipe
|
|
||||||
|
|
||||||
// The config file
|
|
||||||
type configFile struct {
|
|
||||||
Host string // The host, eg: :8080 will start on 0.0.0.0:8080
|
|
||||||
User string
|
|
||||||
SSL bool
|
|
||||||
SSLHost string
|
|
||||||
SSLKeyFile string
|
|
||||||
SSLCertFile string
|
|
||||||
|
|
||||||
Apps []configApp
|
|
||||||
}
|
|
||||||
|
|
||||||
type configApp struct {
|
|
||||||
Name string
|
|
||||||
AppID string
|
|
||||||
Key string
|
|
||||||
Secret string
|
|
||||||
OnlySSL bool
|
|
||||||
ApplicationDisabled bool
|
|
||||||
UserEvents bool
|
|
||||||
WebHooks bool
|
|
||||||
URLWebHook string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAppFromConfig(a configApp) *app {
|
|
||||||
return newApp(
|
|
||||||
a.Name,
|
|
||||||
a.AppID,
|
|
||||||
a.Key,
|
|
||||||
a.Secret,
|
|
||||||
a.OnlySSL,
|
|
||||||
a.ApplicationDisabled,
|
|
||||||
a.UserEvents,
|
|
||||||
a.WebHooks,
|
|
||||||
a.URLWebHook,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package ipe
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/golang/glog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// socket interface to write to the client
|
|
||||||
type socket interface {
|
|
||||||
WriteJSON(interface{}) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// An User Connection
|
|
||||||
type connection struct {
|
|
||||||
SocketID string
|
|
||||||
Socket socket
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new Subscriber
|
|
||||||
func newConnection(socketID string, s socket) *connection {
|
|
||||||
log.Infof("Creating a new Subscriber %+v", socketID)
|
|
||||||
|
|
||||||
return &connection{SocketID: socketID, Socket: s, CreatedAt: time.Now()}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish the message to websocket atached to this client
|
|
||||||
func (conn *connection) Publish(m interface{}) {
|
|
||||||
conn.Socket.WriteJSON(m)
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package ipe
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
applicationOnlyAcceptsSSL = 4000
|
|
||||||
applicationDoesNotExists = 4001
|
|
||||||
applicationDisabled = 4003
|
|
||||||
applicationIsOverConnectionQuota = 4004 // Not Implemented
|
|
||||||
pathNotFound = 4005 // Not Implemented
|
|
||||||
invalidVersionStringFormat = 4006
|
|
||||||
unsupportedProtocolVersion = 4007
|
|
||||||
noProtocolVersionSupplied = 4008
|
|
||||||
|
|
||||||
// 4100 - 4199
|
|
||||||
// Indicates an error resulting in the connection being closed by Pusher,
|
|
||||||
// and the client may reconnect after 1s or more
|
|
||||||
overCapacity = 4100 // Not Implemented
|
|
||||||
|
|
||||||
// 4200 - 4299
|
|
||||||
// Indicate an error resulting in the connection being closed by Pusher,
|
|
||||||
// and the client my reconnect immediately
|
|
||||||
genericReconnectImmediately = 4200
|
|
||||||
pongReplyNotReceived = 4201 // Ping was sent to the client, but no reply was received; Not Implemented
|
|
||||||
closedAfterInactivity = 4202 // Client has been inactive for a long time (24 hours) and client does not suppot ping.; Not Implemented
|
|
||||||
|
|
||||||
// 4300 - 4399
|
|
||||||
// Any other type of error
|
|
||||||
clientRejectedDueToRateLimit = 4301 // Not Implemented
|
|
||||||
|
|
||||||
// Pusher send null, This app use this error code to send the null value
|
|
||||||
// see ErrorEvent
|
|
||||||
otherError = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
// Only this version is supported
|
|
||||||
const supportedProtocolVersion = 7
|
|
||||||
|
|
||||||
// // Maximun event size permitted 10 kB
|
|
||||||
// See: http://blogs.gnome.org/cneumair/2008/09/30/1-kb-1024-bytes-no-1-kb-1000-bytes/
|
|
||||||
const maxDataEventSize = 10 * 1000
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package ipe
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// db represents a app database
|
|
||||||
// For now it there is only one memory database implementation
|
|
||||||
// but in the future I can write a sql implementation
|
|
||||||
type db interface {
|
|
||||||
GetAppByAppID(appID string) (*app, error)
|
|
||||||
GetAppByKey(key string) (*app, error)
|
|
||||||
AddApp(*app) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// memdb is an in memory implementation of db interface
|
|
||||||
type memdb struct {
|
|
||||||
IDMutex sync.Mutex
|
|
||||||
KeyMutex sync.Mutex
|
|
||||||
AppsByAppID map[string]*app
|
|
||||||
AppsByKey map[string]*app
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMemdb() *memdb {
|
|
||||||
return &memdb{
|
|
||||||
AppsByAppID: make(map[string]*app),
|
|
||||||
AppsByKey: make(map[string]*app),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *memdb) AddApp(a *app) error {
|
|
||||||
db.IDMutex.Lock()
|
|
||||||
db.AppsByAppID[a.AppID] = a
|
|
||||||
db.IDMutex.Unlock()
|
|
||||||
|
|
||||||
db.KeyMutex.Lock()
|
|
||||||
db.AppsByKey[a.Key] = a
|
|
||||||
db.KeyMutex.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAppByAppID returns an App with by appID
|
|
||||||
func (db *memdb) GetAppByAppID(appID string) (*app, error) {
|
|
||||||
db.IDMutex.Lock()
|
|
||||||
a, ok := db.AppsByAppID[appID]
|
|
||||||
db.IDMutex.Unlock()
|
|
||||||
if ok {
|
|
||||||
return a, nil
|
|
||||||
}
|
|
||||||
return nil, errors.New("App not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAppByKey returns an App with by key
|
|
||||||
func (db *memdb) GetAppByKey(key string) (*app, error) {
|
|
||||||
db.KeyMutex.Lock()
|
|
||||||
a, ok := db.AppsByKey[key]
|
|
||||||
db.KeyMutex.Unlock()
|
|
||||||
if ok {
|
|
||||||
return a, nil
|
|
||||||
}
|
|
||||||
return nil, errors.New("App not found")
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package ipe
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func Test_db_GetAppByAppID(t *testing.T) {
|
|
||||||
app := &app{AppID: "123456", Name: "Example"}
|
|
||||||
|
|
||||||
db := newMemdb()
|
|
||||||
db.AddApp(app)
|
|
||||||
|
|
||||||
a, err := db.GetAppByAppID("123456")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("GetAppByAppID(%q) == %q, want %q", "123456", a, app)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_db_GetAppByAppID__error(t *testing.T) {
|
|
||||||
app := &app{AppID: "123456", Name: "Example"}
|
|
||||||
|
|
||||||
db := newMemdb()
|
|
||||||
db.AddApp(app)
|
|
||||||
|
|
||||||
a, err := db.GetAppByAppID("not-found")
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("GetAppByAppID(%q) == %q, want %q", "123456", a, app)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_db_GetAppByKey(t *testing.T) {
|
|
||||||
app := &app{AppID: "123456", Name: "Example", Key: "654321"}
|
|
||||||
|
|
||||||
db := newMemdb()
|
|
||||||
db.AddApp(app)
|
|
||||||
|
|
||||||
a, err := db.GetAppByKey("654321")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("GetAppByKey(%q) == %q, want %q", "654321", a, app)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_db_GetAppByKey__error(t *testing.T) {
|
|
||||||
app := &app{AppID: "123456", Name: "Example", Key: "654321"}
|
|
||||||
|
|
||||||
db := newMemdb()
|
|
||||||
db.AddApp(app)
|
|
||||||
|
|
||||||
a, err := db.GetAppByKey("not-found")
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("GetAppByKey(%q) == %q, want %v", "not-found", a, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-118
@@ -1,118 +0,0 @@
|
|||||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package ipe
|
|
||||||
|
|
||||||
// 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: unsupportedProtocolVersion, Msg: "Unsupported protocol version"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The application does not exists
|
|
||||||
// See the configuration file
|
|
||||||
type applicationDoesNotExistsError struct {
|
|
||||||
baseWebsocketError
|
|
||||||
}
|
|
||||||
|
|
||||||
func newApplicationDoesNotExistsError() applicationDoesNotExistsError {
|
|
||||||
return applicationDoesNotExistsError{
|
|
||||||
baseWebsocketError{Code: applicationDoesNotExists, 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: noProtocolVersionSupplied, 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: applicationDisabled, Msg: "Application disabled"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the application only accepts SSL connections
|
|
||||||
type applicationOnlyAccepsSSLError struct {
|
|
||||||
baseWebsocketError
|
|
||||||
}
|
|
||||||
|
|
||||||
func newApplicationOnlyAccepsSSLError() applicationOnlyAccepsSSLError {
|
|
||||||
return applicationOnlyAccepsSSLError{
|
|
||||||
baseWebsocketError{Code: applicationOnlyAcceptsSSL, 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: invalidVersionStringFormat, 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: genericReconnectImmediately, 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: otherError, Msg: msg},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-278
@@ -1,278 +0,0 @@
|
|||||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package ipe
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
log "github.com/golang/glog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// {
|
|
||||||
// "event": "pusher:subscribe",
|
|
||||||
// "data": {
|
|
||||||
// "channel": "the channel",
|
|
||||||
// "auth": "the auth",
|
|
||||||
// "channelData": "extra data"
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
type subscribeEventData struct {
|
|
||||||
Channel string `json:"channel"`
|
|
||||||
Auth string `json:"auth,omitempty"`
|
|
||||||
ChannelData string `json:"channel_data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type subscribeEvent struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Data subscribeEventData `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new subscribe event with the specified channel and data
|
|
||||||
func newSubscribeEvent(channel, auth, channelData string) subscribeEvent {
|
|
||||||
data := subscribeEventData{Channel: channel, Auth: auth, ChannelData: channelData}
|
|
||||||
return subscribeEvent{Event: "pusher:subscribe", Data: data}
|
|
||||||
}
|
|
||||||
|
|
||||||
type unsubscribeEventData struct {
|
|
||||||
Channel string `json:"channel"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// {
|
|
||||||
// "event": "pusher:unsubscribe",
|
|
||||||
// "data": {
|
|
||||||
// "channel": "The channel"
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
type unsubscribeEvent struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Data unsubscribeEventData `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new unsubscribe event for the specified channel
|
|
||||||
func newUnsubscribeEvent(channel string) unsubscribeEvent {
|
|
||||||
data := unsubscribeEventData{Channel: channel}
|
|
||||||
return unsubscribeEvent{Event: "pusher:unsubscribe", Data: data}
|
|
||||||
}
|
|
||||||
|
|
||||||
// {
|
|
||||||
// "event": "pusher_internal:subscription_succeeded",
|
|
||||||
// "channel": "the channel"
|
|
||||||
// }
|
|
||||||
type subscriptionSucceededEvent struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Channel string `json:"channel"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new subscription succeed event for the specified channel
|
|
||||||
func newSubscriptionSucceededEvent(channel, data string) subscriptionSucceededEvent {
|
|
||||||
return subscriptionSucceededEvent{Event: "pusher_internal:subscription_succeeded", Channel: channel, Data: data}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data Subscription Succeed
|
|
||||||
|
|
||||||
// "{
|
|
||||||
// \"presence\": {
|
|
||||||
// \"ids\": [\"11814b369700141b222a3f3791cec2d9\",\"71dd6a29da2a4833336d2a964becf820\"],
|
|
||||||
// \"hash\": {
|
|
||||||
// \"11814b369700141b222a3f3791cec2d9\": {
|
|
||||||
// \"name\":\"Phil Leggetter\",
|
|
||||||
// \"twitter\": \"@leggetter\"
|
|
||||||
// },
|
|
||||||
// \"71dd6a29da2a4833336d2a964becf820\": {
|
|
||||||
// \"name\":\"Max Williams\",
|
|
||||||
// \"twitter\": \"@maxthelion\"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// \"count\": 2
|
|
||||||
// }
|
|
||||||
// }"
|
|
||||||
type subscriptionSucceeedEventPresenceData struct {
|
|
||||||
Ids []string `json:"ids"`
|
|
||||||
Hash map[string]interface{} `json:"hash"`
|
|
||||||
Count int `json:"count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSubscriptionSucceedEventPresenceData(c *channel) subscriptionSucceeedEventPresenceData {
|
|
||||||
event := subscriptionSucceeedEventPresenceData{}
|
|
||||||
|
|
||||||
var ids []string
|
|
||||||
hash := make(map[string]interface{}, c.TotalSubscriptions())
|
|
||||||
|
|
||||||
for _, s := range c.Subscriptions {
|
|
||||||
// Do you have any other idea?
|
|
||||||
var js interface{}
|
|
||||||
json.Unmarshal([]byte(s.Data), &js)
|
|
||||||
|
|
||||||
hash[s.ID] = js
|
|
||||||
ids = append(ids, s.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
event.Ids = ids
|
|
||||||
event.Hash = hash
|
|
||||||
event.Count = c.TotalSubscriptions()
|
|
||||||
|
|
||||||
return event
|
|
||||||
}
|
|
||||||
|
|
||||||
// {
|
|
||||||
// "event": "pusher:pong",
|
|
||||||
// "data": {}
|
|
||||||
// }
|
|
||||||
type pongEvent struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new pong event
|
|
||||||
func newPongEvent() pongEvent {
|
|
||||||
return pongEvent{Event: "pusher:pong", Data: "{}"}
|
|
||||||
}
|
|
||||||
|
|
||||||
// {
|
|
||||||
// "event": "pusher:ping",
|
|
||||||
// "data": {}
|
|
||||||
// }
|
|
||||||
type pingEvent struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new ping event
|
|
||||||
func newPingEvent() pingEvent {
|
|
||||||
return pingEvent{Event: "pusher:ping", Data: "{}"}
|
|
||||||
}
|
|
||||||
|
|
||||||
// {
|
|
||||||
// "event": "pusher:error",
|
|
||||||
// "data": {
|
|
||||||
// "message": "A Message",
|
|
||||||
// "code": 4000
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
type errorEvent struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Data interface{} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new error event
|
|
||||||
// Pusher protocol is very strange in some parts
|
|
||||||
// It send null in some errors.
|
|
||||||
// So I created this GENERIC_ERROR thing, just to verify if the json must have null on the error code
|
|
||||||
func newErrorEvent(code int, message string) errorEvent {
|
|
||||||
var data interface{}
|
|
||||||
|
|
||||||
if code == otherError {
|
|
||||||
data = struct {
|
|
||||||
Code *int `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}{
|
|
||||||
nil,
|
|
||||||
message,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
data = struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}{
|
|
||||||
code,
|
|
||||||
message,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errorEvent{Event: "pusher:error", Data: data}
|
|
||||||
}
|
|
||||||
|
|
||||||
// {
|
|
||||||
// "event" : "pusher:connection_established",
|
|
||||||
// "data" : {
|
|
||||||
// "socket_id" : "123456",
|
|
||||||
// "activity_timeout" : 120
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
type connectionEstablishedEventData struct {
|
|
||||||
SocketID string `json:"socket_id"`
|
|
||||||
ActivityTimeout int `json:"activity_timeout"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type connectionEstablishedEvent struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new connection established event using the specified socketId
|
|
||||||
func newConnectionEstablishedEvent(socketID string) connectionEstablishedEvent {
|
|
||||||
data := connectionEstablishedEventData{SocketID: socketID, ActivityTimeout: 120}
|
|
||||||
|
|
||||||
b, err := json.Marshal(data)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic("events: Could not Marshal json ConnectionEstablishedEvent")
|
|
||||||
}
|
|
||||||
|
|
||||||
return connectionEstablishedEvent{Event: "pusher:connection_established", Data: string(b)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// {
|
|
||||||
// "event": "pusher_internal:member_added",
|
|
||||||
// "channel": "presence-example-channel",
|
|
||||||
// "data": String
|
|
||||||
// }
|
|
||||||
type memberAddedEvent struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Channel string `json:"channel"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMemberAddedEvent(channel, data string) memberAddedEvent {
|
|
||||||
return memberAddedEvent{Event: "pusher_internal:member_added", Channel: channel, Data: data}
|
|
||||||
}
|
|
||||||
|
|
||||||
// {
|
|
||||||
// "event": "pusher_internal:member_removed",
|
|
||||||
// "channel": "presence-example-channel",
|
|
||||||
// "data": String
|
|
||||||
// }
|
|
||||||
type memberRemovedEvent struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Channel string `json:"channel"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMemberRemovedEvent(channel string, s *subscription) memberRemovedEvent {
|
|
||||||
data, err := json.Marshal(struct {
|
|
||||||
UserID string `json:"user_id"`
|
|
||||||
}{
|
|
||||||
UserID: s.ID,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return memberRemovedEvent{Event: "pusher_internal:member_removed", Channel: channel, Data: string(data)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// {
|
|
||||||
// "event": "client-?",
|
|
||||||
// "channel": "The channel",
|
|
||||||
// "data": {}
|
|
||||||
// }
|
|
||||||
type rawEvent struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Channel string `json:"channel"`
|
|
||||||
Data json.RawMessage `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type responseEvent struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Channel string `json:"channel"`
|
|
||||||
Data interface{} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// The response event that is broadcasted to the client sockets
|
|
||||||
func newResponseEvent(name, channel string, data interface{}) responseEvent {
|
|
||||||
return responseEvent{Event: name, Channel: channel, Data: data}
|
|
||||||
}
|
|
||||||
-64
@@ -1,64 +0,0 @@
|
|||||||
// Copyright 2015 Claudemiro Alves Feitosa Neto. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package ipe
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"goji.io/pat"
|
|
||||||
|
|
||||||
goji "goji.io"
|
|
||||||
|
|
||||||
log "github.com/golang/glog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Start Parse the configuration file and starts the ipe server
|
|
||||||
// It Panic if could not start the HTTP or HTTPS server
|
|
||||||
func Start(filename string) {
|
|
||||||
var conf configFile
|
|
||||||
|
|
||||||
rand.Seed(time.Now().Unix())
|
|
||||||
file, err := os.Open(filename)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
// Reading config
|
|
||||||
if err := json.NewDecoder(file).Decode(&conf); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using a in memory database
|
|
||||||
db := newMemdb()
|
|
||||||
|
|
||||||
// Adding applications
|
|
||||||
for _, a := range conf.Apps {
|
|
||||||
db.AddApp(newAppFromConfig(a))
|
|
||||||
}
|
|
||||||
|
|
||||||
router := goji.NewMux()
|
|
||||||
router.HandleFuncC(pat.Post("/apps/:app_id/events"), newPostEventsHandler(db))
|
|
||||||
router.HandleFuncC(pat.Get("/apps/:app_id/channels"), newGetChannelsHandler(db))
|
|
||||||
router.HandleFuncC(pat.Get("/apps/:app_id/channels/:channel_name"), newGetChannelHandler(db))
|
|
||||||
router.HandleFuncC(pat.Get("/apps/:app_id/channels/:channel_name/users"), newGetChannelUsersHandler(db))
|
|
||||||
router.HandleC(pat.Get("/app/:key"), newWebsocketHandler(db))
|
|
||||||
|
|
||||||
if conf.SSL {
|
|
||||||
go func() {
|
|
||||||
log.Infof("Starting HTTPS service on %s ...", conf.SSLHost)
|
|
||||||
log.Fatal(http.ListenAndServeTLS(conf.SSLHost, conf.SSLCertFile, conf.SSLKeyFile, router))
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("Starting HTTP service on %s ...", conf.Host)
|
|
||||||
log.Fatal(http.ListenAndServe(conf.Host, router))
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package ipe
|
|
||||||
|
|
||||||
// A Channel Subscription
|
|
||||||
type subscription struct {
|
|
||||||
Connection *connection
|
|
||||||
ID string
|
|
||||||
Data string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new Subscription
|
|
||||||
func newSubscription(conn *connection, data string) *subscription {
|
|
||||||
return &subscription{Connection: conn, Data: data}
|
|
||||||
}
|
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package ipe
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
goji "goji.io"
|
|
||||||
|
|
||||||
"goji.io/pat"
|
|
||||||
|
|
||||||
log "github.com/golang/glog"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
|
|
||||||
"github.com/dimiro1/ipe/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{
|
|
||||||
ReadBufferSize: 1024,
|
|
||||||
WriteBufferSize: 1024,
|
|
||||||
CheckOrigin: func(r *http.Request) bool { return true },
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle open Subscriber.
|
|
||||||
func onOpen(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, sessionID string, app *app) websocketError {
|
|
||||||
params := r.URL.Query()
|
|
||||||
p := params.Get("protocol")
|
|
||||||
|
|
||||||
protocol, err := strconv.Atoi(p)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return newInvalidVersionStringFormatError()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case strings.TrimSpace(p) == "":
|
|
||||||
return newNoProtocolVersionSuppliedError()
|
|
||||||
case protocol != supportedProtocolVersion:
|
|
||||||
return newUnsupportedProtocolVersionError()
|
|
||||||
case app.ApplicationDisabled:
|
|
||||||
return newApplicationDisabledError()
|
|
||||||
case app.OnlySSL:
|
|
||||||
if r.TLS == nil {
|
|
||||||
return newApplicationOnlyAccepsSSLError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the new Subscriber
|
|
||||||
connection := newConnection(sessionID, conn)
|
|
||||||
app.Connect(connection)
|
|
||||||
|
|
||||||
// Everything went fine. Huhu.
|
|
||||||
if err := conn.WriteJSON(newConnectionEstablishedEvent(connection.SocketID)); err != nil {
|
|
||||||
return newGenericReconnectImmediatelyError()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the close event
|
|
||||||
func onClose(sessionID string, app *app) {
|
|
||||||
app.Disconnect(sessionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle messages
|
|
||||||
//
|
|
||||||
// If there is an unrecoverable error then break the loop,
|
|
||||||
// otherwise just keep going.
|
|
||||||
func onMessage(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, sessionID string, app *app) {
|
|
||||||
var event struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
_, message, err := conn.ReadMessage()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%+v", err)
|
|
||||||
if err == io.EOF {
|
|
||||||
onClose(sessionID, app)
|
|
||||||
} else if _, ok := err.(*websocket.CloseError); ok {
|
|
||||||
onClose(sessionID, app)
|
|
||||||
} else {
|
|
||||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(message, &event); err != nil {
|
|
||||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("websockets: Handling %s event", event.Event)
|
|
||||||
|
|
||||||
switch event.Event {
|
|
||||||
case "pusher:ping":
|
|
||||||
if err := conn.WriteJSON(newPongEvent()); err != nil {
|
|
||||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
|
||||||
}
|
|
||||||
case "pusher:subscribe":
|
|
||||||
subscribeEvent := subscribeEvent{}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(message, &subscribeEvent); err != nil {
|
|
||||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
connection, err := app.FindConnection(sessionID)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
channelName := strings.TrimSpace(subscribeEvent.Data.Channel)
|
|
||||||
|
|
||||||
if !utils.IsChannelNameValid(channelName) {
|
|
||||||
emitWSError(newGenericError(fmt.Sprintf("This channel name is not valid")), conn)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
isPresence := utils.IsPresenceChannel(channelName)
|
|
||||||
isPrivate := utils.IsPrivateChannel(channelName)
|
|
||||||
|
|
||||||
if isPresence || isPrivate {
|
|
||||||
toSign := []string{connection.SocketID, channelName}
|
|
||||||
|
|
||||||
if isPresence || len(subscribeEvent.Data.ChannelData) > 0 {
|
|
||||||
toSign = append(toSign, subscribeEvent.Data.ChannelData)
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedAuthKey := fmt.Sprintf("%s:%s", app.Key, utils.HashMAC([]byte(strings.Join(toSign, ":")), []byte(app.Secret)))
|
|
||||||
if subscribeEvent.Data.Auth != expectedAuthKey {
|
|
||||||
emitWSError(newGenericError(fmt.Sprintf("Auth value for subscription to %s is invalid", channelName)), conn)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
channel := app.FindOrCreateChannelByChannelID(channelName)
|
|
||||||
log.Info(subscribeEvent.Data.ChannelData)
|
|
||||||
|
|
||||||
if err := app.Subscribe(channel, connection, subscribeEvent.Data.ChannelData); err != nil {
|
|
||||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
|
||||||
}
|
|
||||||
case "pusher:unsubscribe":
|
|
||||||
unsubscribeEvent := unsubscribeEvent{}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(message, &unsubscribeEvent); err != nil {
|
|
||||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
connection, err := app.FindConnection(sessionID)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
emitWSError(newGenericError(fmt.Sprintf("Could not find a connection with the id %s", sessionID)), conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
channel, err := app.FindChannelByChannelID(unsubscribeEvent.Data.Channel)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
emitWSError(newGenericError(fmt.Sprintf("Could not find a channel with the id %s", unsubscribeEvent.Data.Channel)), conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := app.Unsubscribe(channel, connection); err != nil {
|
|
||||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default: // CLient Events ??
|
|
||||||
// see http://pusher.com/docs/client_api_guide/client_events#trigger-events
|
|
||||||
if utils.IsClientEvent(event.Event) {
|
|
||||||
if !app.UserEvents {
|
|
||||||
emitWSError(newGenericError("To send client events, you must enable this feature in the Settings."), conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
clientEvent := rawEvent{}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(message, &clientEvent); err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
channel, err := app.FindChannelByChannelID(clientEvent.Channel)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
emitWSError(newGenericError(fmt.Sprintf("Could not find a channel with the id %s", clientEvent.Channel)), conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !channel.IsPresenceOrPrivate() {
|
|
||||||
emitWSError(newGenericError("Client event rejected - only supported on private and presence channels"), conn)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := app.Publish(channel, clientEvent, sessionID); err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // switch
|
|
||||||
} // For
|
|
||||||
}
|
|
||||||
|
|
||||||
func newWebsocketHandler(DB db) goji.Handler {
|
|
||||||
return &websocketHandler{DB}
|
|
||||||
}
|
|
||||||
|
|
||||||
type websocketHandler struct{ DB db }
|
|
||||||
|
|
||||||
// Websocket GET /app/{key}
|
|
||||||
func (h *websocketHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
|
||||||
defer func() {
|
|
||||||
if conn != nil {
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
appKey := pat.Param(ctx, "key")
|
|
||||||
|
|
||||||
app, err := h.DB.GetAppByKey(appKey)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
emitWSError(newApplicationDoesNotExistsError(), conn)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionID := utils.GenerateSessionID()
|
|
||||||
|
|
||||||
if err := onOpen(conn, w, r, sessionID, app); err != nil {
|
|
||||||
emitWSError(err, conn)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
onMessage(conn, w, r, sessionID, app)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit an Websocket ErrorEvent
|
|
||||||
func emitWSError(err websocketError, conn *websocket.Conn) {
|
|
||||||
|
|
||||||
event := newErrorEvent(err.GetCode(), err.GetMsg())
|
|
||||||
|
|
||||||
if err := conn.WriteJSON(event); err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package mocks
|
||||||
|
|
||||||
|
// MockSocket is a mock implementation of Socket
|
||||||
|
// used in the test suite
|
||||||
|
type MockSocket struct{}
|
||||||
|
|
||||||
|
// WriteJSON always returns nil
|
||||||
|
// used in the test suite
|
||||||
|
func (s MockSocket) WriteJSON(i interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"ipe/app"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Storage represents a app database
|
||||||
|
// For now it there is only one memory database implementation
|
||||||
|
// but in the future I can write a sql implementation
|
||||||
|
type Storage interface {
|
||||||
|
GetAppByAppID(appID string) (*app.Application, error)
|
||||||
|
GetAppByKey(key string) (*app.Application, error)
|
||||||
|
AddApp(application *app.Application) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// InMemory in memory implementation of Storage
|
||||||
|
type InMemory struct {
|
||||||
|
sync.RWMutex
|
||||||
|
Apps []*app.Application
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInMemory returns an InMemory storage
|
||||||
|
func NewInMemory() Storage {
|
||||||
|
return &InMemory{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddApp adds app into memory
|
||||||
|
func (db *InMemory) AddApp(application *app.Application) error {
|
||||||
|
db.Lock()
|
||||||
|
defer db.Unlock()
|
||||||
|
|
||||||
|
db.Apps = append(db.Apps, application)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppByAppID returns an App with by appID
|
||||||
|
func (db *InMemory) GetAppByAppID(appID string) (*app.Application, error) {
|
||||||
|
db.RLock()
|
||||||
|
defer db.RUnlock()
|
||||||
|
|
||||||
|
for _, a := range db.Apps {
|
||||||
|
if a.AppID == appID {
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("app not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppByKey returns an App with by key
|
||||||
|
func (db *InMemory) GetAppByKey(key string) (*app.Application, error) {
|
||||||
|
db.RLock()
|
||||||
|
defer db.RUnlock()
|
||||||
|
|
||||||
|
for _, a := range db.Apps {
|
||||||
|
if a.Key == key {
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("app not found")
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ipe/app"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Benchmark_memdb_GetAppByAppID(b *testing.B) {
|
||||||
|
storage := NewInMemory()
|
||||||
|
_ = storage.AddApp(&app.Application{AppID: "123456", Name: "Example"})
|
||||||
|
_ = storage.AddApp(&app.Application{AppID: "654321", Name: "Example2"})
|
||||||
|
_ = storage.AddApp(&app.Application{AppID: "678901", Name: "Example3"})
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = storage.GetAppByAppID("123456")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_db_GetAppByAppID(t *testing.T) {
|
||||||
|
_app := &app.Application{AppID: "123456", Name: "Example"}
|
||||||
|
|
||||||
|
storage := NewInMemory()
|
||||||
|
_ = storage.AddApp(_app)
|
||||||
|
|
||||||
|
a, err := storage.GetAppByAppID("123456")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetAppByAppID(%q) == %+v, want %+v", "123456", a, _app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_db_GetAppByAppID__error(t *testing.T) {
|
||||||
|
_app := &app.Application{AppID: "123456", Name: "Example"}
|
||||||
|
|
||||||
|
storage := NewInMemory()
|
||||||
|
_ = storage.AddApp(_app)
|
||||||
|
|
||||||
|
a, err := storage.GetAppByAppID("not-found")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("GetAppByAppID(%q) == %+v, want %+v", "123456", a, _app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_db_GetAppByKey(t *testing.T) {
|
||||||
|
_app := &app.Application{AppID: "123456", Name: "Example", Key: "654321"}
|
||||||
|
|
||||||
|
storage := NewInMemory()
|
||||||
|
_ = storage.AddApp(_app)
|
||||||
|
|
||||||
|
a, err := storage.GetAppByKey("654321")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetAppByKey(%q) == %+v, want %+v", "654321", a, _app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_db_GetAppByKey__error(t *testing.T) {
|
||||||
|
_app := &app.Application{AppID: "123456", Name: "Example", Key: "654321"}
|
||||||
|
|
||||||
|
storage := NewInMemory()
|
||||||
|
_ = storage.AddApp(_app)
|
||||||
|
|
||||||
|
a, err := storage.GetAppByKey("not-found")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("GetAppByKey(%q) == %+v, want %+v", "not-found", a, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// 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 subscription
|
||||||
|
|
||||||
|
import "ipe/connection"
|
||||||
|
|
||||||
|
// Subscription A Channel Subscription
|
||||||
|
type Subscription struct {
|
||||||
|
Connection *connection.Connection
|
||||||
|
ID string
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Create a new Subscription
|
||||||
|
func New(conn *connection.Connection, data string) *Subscription {
|
||||||
|
return &Subscription{Connection: conn, Data: data}
|
||||||
|
}
|
||||||
+1
-1
@@ -28,7 +28,7 @@ func HashMAC(message, key []byte) string {
|
|||||||
|
|
||||||
// GenerateSessionID Generate a new random Hash
|
// GenerateSessionID Generate a new random Hash
|
||||||
func GenerateSessionID() string {
|
func GenerateSessionID() string {
|
||||||
return fmt.Sprintf("%d.%d", rand.Intn(math.MaxInt64), rand.Intn(math.MaxInt64))
|
return fmt.Sprintf("%d.%d", rand.Intn(math.MaxInt32), rand.Intn(math.MaxInt32))
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsChannelNameValid Verify if the channel name is valid
|
// IsChannelNameValid Verify if the channel name is valid
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// 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 websockets
|
||||||
|
|
||||||
|
type websocketError struct {
|
||||||
|
Code int
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
applicationOnlyAcceptsSSL = &websocketError{Code: 4000, Msg: "Application only accepts SSL connections, reconnect using wss://"}
|
||||||
|
applicationDoesNotExists = &websocketError{Code: 4001, Msg: "Could not found an app with the given key"}
|
||||||
|
applicationDisabled = &websocketError{Code: 4003, Msg: "Application disabled"}
|
||||||
|
invalidVersionStringFormat = &websocketError{Code: 4006, Msg: "Invalid version string format"}
|
||||||
|
unsupportedProtocolVersion = &websocketError{Code: 4007, Msg: "Unsupported protocol version"}
|
||||||
|
noProtocolVersionSupplied = &websocketError{Code: 4008, Msg: "No protocol version supplied"}
|
||||||
|
reconnectImmediately = &websocketError{Code: 4200, Msg: "Generic reconnect immediately"}
|
||||||
|
)
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
// 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 websockets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
|
||||||
|
"ipe/app"
|
||||||
|
"ipe/connection"
|
||||||
|
"ipe/events"
|
||||||
|
"ipe/storage"
|
||||||
|
"ipe/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Only this version is supported
|
||||||
|
const supportedProtocolVersion = 7
|
||||||
|
|
||||||
|
var upgrader = websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
CheckOrigin: func(_ *http.Request) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Websocket handler for real time websocket messages
|
||||||
|
type Websocket struct {
|
||||||
|
storage storage.Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWebsocket returns a new Websocket handler
|
||||||
|
func NewWebsocket(storage storage.Storage) *Websocket {
|
||||||
|
return &Websocket{storage: storage}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP Websocket GET /app/{key}
|
||||||
|
func (h *Websocket) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
defer func() {
|
||||||
|
if conn != nil {
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
log.Errorf("closing the websocket connection %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
pathVars = mux.Vars(r)
|
||||||
|
appKey = pathVars["key"]
|
||||||
|
)
|
||||||
|
|
||||||
|
_app, err := h.storage.GetAppByKey(appKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
emitError(applicationDoesNotExists, conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionID := utils.GenerateSessionID()
|
||||||
|
|
||||||
|
if err := onOpen(conn, r, sessionID, _app); err != nil {
|
||||||
|
emitError(err, conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessages(conn, sessionID, _app)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessages(conn *websocket.Conn, sessionID string, app *app.Application) {
|
||||||
|
var event struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, message, err := conn.ReadMessage()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
handleError(conn, sessionID, app, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(message, &event); err != nil {
|
||||||
|
emitError(reconnectImmediately, conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("websocket: Handling %s event", event.Event)
|
||||||
|
|
||||||
|
switch event.Event {
|
||||||
|
case "pusher:ping":
|
||||||
|
handlePing(conn)
|
||||||
|
case "pusher:subscribe":
|
||||||
|
handleSubscribe(conn, sessionID, app, message)
|
||||||
|
case "pusher:unsubscribe":
|
||||||
|
handleUnsubscribe(conn, sessionID, app, message)
|
||||||
|
default:
|
||||||
|
if utils.IsClientEvent(event.Event) {
|
||||||
|
handleClientEvent(conn, sessionID, app, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit an Websocket ErrorEvent
|
||||||
|
func emitError(err *websocketError, conn *websocket.Conn) {
|
||||||
|
event := events.NewError(err.Code, err.Msg)
|
||||||
|
|
||||||
|
if err := conn.WriteJSON(event); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError(conn *websocket.Conn, sessionID string, app *app.Application, err error) {
|
||||||
|
log.Errorf("%+v", err)
|
||||||
|
if err == io.EOF {
|
||||||
|
onClose(sessionID, app)
|
||||||
|
} else if _, ok := err.(*websocket.CloseError); ok {
|
||||||
|
onClose(sessionID, app)
|
||||||
|
} else {
|
||||||
|
emitError(reconnectImmediately, conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func onOpen(conn *websocket.Conn, r *http.Request, sessionID string, app *app.Application) *websocketError {
|
||||||
|
var (
|
||||||
|
queryVars = r.URL.Query()
|
||||||
|
strProtocol = queryVars.Get("protocol")
|
||||||
|
)
|
||||||
|
|
||||||
|
protocol, err := strconv.Atoi(strProtocol)
|
||||||
|
if err != nil {
|
||||||
|
return invalidVersionStringFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.TrimSpace(strProtocol) == "":
|
||||||
|
return noProtocolVersionSupplied
|
||||||
|
case protocol != supportedProtocolVersion:
|
||||||
|
return unsupportedProtocolVersion
|
||||||
|
case !app.Enabled:
|
||||||
|
return applicationDisabled
|
||||||
|
case app.OnlySSL:
|
||||||
|
if r.TLS == nil {
|
||||||
|
return applicationOnlyAcceptsSSL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the new Subscriber
|
||||||
|
_connection := connection.New(sessionID, conn)
|
||||||
|
app.Connect(_connection)
|
||||||
|
|
||||||
|
// Everything went fine.
|
||||||
|
if err := conn.WriteJSON(events.NewConnectionEstablished(_connection.SocketID)); err != nil {
|
||||||
|
return reconnectImmediately
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func onClose(sessionID string, app *app.Application) {
|
||||||
|
app.Disconnect(sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePing(conn *websocket.Conn) {
|
||||||
|
if err := conn.WriteJSON(events.NewPong()); err != nil {
|
||||||
|
emitError(reconnectImmediately, conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleClientEvent(conn *websocket.Conn, sessionID string, app *app.Application, message []byte) {
|
||||||
|
if !app.UserEvents {
|
||||||
|
emitError(&websocketError{Code: 0, Msg: "To send client events, you must enable this feature in the Settings."}, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientEvent := events.Raw{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(message, &clientEvent); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
emitError(reconnectImmediately, conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channel, err := app.FindChannelByChannelID(clientEvent.Channel)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
emitError(&websocketError{Code: 0, Msg: fmt.Sprintf("Could not find a channel with the id %s", clientEvent.Channel)}, conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !channel.IsPresenceOrPrivate() {
|
||||||
|
emitError(&websocketError{Code: 0, Msg: "Client event rejected - only supported on private and presence channels"}, conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Publish(channel, clientEvent, sessionID); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
emitError(reconnectImmediately, conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUnsubscribe(conn *websocket.Conn, sessionID string, app *app.Application, message []byte) {
|
||||||
|
unsubscribeEvent := events.Unsubscribe{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(message, &unsubscribeEvent); err != nil {
|
||||||
|
emitError(reconnectImmediately, conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_connection, err := app.FindConnection(sessionID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
emitError(&websocketError{Code: 0, Msg: fmt.Sprintf("Could not find a connection with the id %s", sessionID)}, conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channel, err := app.FindChannelByChannelID(unsubscribeEvent.Data.Channel)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
emitError(&websocketError{Code: 0, Msg: fmt.Sprintf("Could not find a channel with the id %s", unsubscribeEvent.Data.Channel)}, conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Unsubscribe(channel, _connection); err != nil {
|
||||||
|
emitError(reconnectImmediately, conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSubscribe(conn *websocket.Conn, sessionID string, app *app.Application, message []byte) {
|
||||||
|
subscribeEvent := events.Subscribe{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(message, &subscribeEvent); err != nil {
|
||||||
|
emitError(reconnectImmediately, conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_connection, err := app.FindConnection(sessionID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
emitError(reconnectImmediately, conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channelName := strings.TrimSpace(subscribeEvent.Data.Channel)
|
||||||
|
|
||||||
|
if !utils.IsChannelNameValid(channelName) {
|
||||||
|
emitError(&websocketError{Code: 0, Msg: "This channel name is not valid"}, conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isPresence := utils.IsPresenceChannel(channelName)
|
||||||
|
isPrivate := utils.IsPrivateChannel(channelName)
|
||||||
|
|
||||||
|
if isPresence || isPrivate {
|
||||||
|
toSign := []string{_connection.SocketID, channelName}
|
||||||
|
|
||||||
|
if isPresence || len(subscribeEvent.Data.ChannelData) > 0 {
|
||||||
|
toSign = append(toSign, subscribeEvent.Data.ChannelData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validateAuthKey(subscribeEvent.Data.Auth, toSign, app) {
|
||||||
|
emitError(&websocketError{Code: 0, Msg: fmt.Sprintf("Auth value for subscription to %s is invalid", channelName)}, conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channel := app.FindOrCreateChannelByChannelID(channelName)
|
||||||
|
log.Info(subscribeEvent.Data.ChannelData)
|
||||||
|
|
||||||
|
if err := app.Subscribe(channel, _connection, subscribeEvent.Data.ChannelData); err != nil {
|
||||||
|
emitError(reconnectImmediately, conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAuthKey(givenAuthKey string, toSign []string, app *app.Application) bool {
|
||||||
|
expectedAuthKey := fmt.Sprintf("%s:%s", app.Key, utils.HashMAC([]byte(strings.Join(toSign, ":")), []byte(app.Secret)))
|
||||||
|
return givenAuthKey == expectedAuthKey
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user