30 Commits

Author SHA1 Message Date
Arjun Patel d7deb3edc3 making it work and build 2022-08-12 21:01:08 -07:00
Claudemiro ea6e116836 Merge pull request #48 from izimobil/patch-1
Added instructions on how to build ipe
2018-12-19 13:57:28 +01:00
David Jean Louis 3cb9eb27e1 Added instructions on how to build ipe
Instructions on how to build ipe from a local checkout (closes #47).
2018-12-19 12:42:15 +01:00
Claudemiro 0ff81caae4 Merge pull request #46 from craigjmidwinter/docker
Add dockerfile and entrypoint script
2018-12-08 13:22:34 +01:00
Craig J. Midwinter 0de46888eb Add dockerfile and entrypoint script 2018-12-07 15:12:15 -06:00
Claudemiro 05c088f158 Fix golint issues 2018-11-26 22:57:50 +01:00
Claudemiro 3c51d0b614 Fix golint issues 2018-11-26 22:55:00 +01:00
Claudemiro 1a9fb1a706 Fix possible concurrency issues 2018-11-26 09:15:08 +01:00
Claudemiro d412759c16 Update README
- Removed maintainer needed banner
- Updated code examples
- Updated configuration example

Resolve: https://github.com/dimiro1/ipe/issues/42
2018-11-26 08:57:42 +01:00
Claudemiro f4781ee483 Change config from json to yaml
- Added possibility to override config with env vars
- Reorganized config structures
2018-11-25 23:39:12 +01:00
Claudemiro 964df8a5dd Update functional test dependencies 2018-11-25 21:24:05 +01:00
Claudemiro 8da763ec2f Refactor to make the maintanance simpler
- Fixed issue with webhooks
2018-11-25 17:40:43 +01:00
Claudemiro 4523549f71 Fix https://github.com/dimiro1/ipe/issues/37 2018-06-17 23:09:55 +02:00
Claudemiro fe1eb8c5eb Maintainer Needed 2018-02-14 22:23:06 +01:00
claudemiro 481311061f Merge remote-tracking branch 'origin/master' 2016-12-26 22:08:56 -02:00
claudemiro b117386591 Typos 2016-12-26 22:08:29 -02:00
claudemiro 746b978e52 This change allow 32bit compilation 2016-12-26 22:06:26 -02:00
Claudemiro da48528e51 Added Sourcegraph link 2016-11-08 13:11:45 -02:00
claudemiro 065d6ab286 Removing unused paremeters 2016-09-29 23:30:28 -03:00
somebody 1d1ffa00b5 correcting typo in README.md: instalation ->installation (#33) 2016-09-27 21:00:29 -03:00
claudemiro 983f490737 Params not necessary 2016-09-01 23:43:09 -03:00
claudemiro 5c9528bce4 Reverting to slicebased memdb after checking benchmark. 2016-09-01 23:42:55 -03:00
claudemiro 8fb96f3cbf Simpler logic 2016-09-01 23:42:18 -03:00
claudemiro 9d732fb01a Added option to enable pprof. 2016-09-01 21:03:29 -03:00
claudemiro 0bd4fa02e9 Using method references for Mux 2016-08-22 23:20:30 -03:00
claudemiro 4a305b32a6 Removed GoJi in favor of Pressly Chi 2016-08-22 23:06:15 -03:00
Claudemiro 4c5d5302ec Basic context implementation in webhooks. 2016-08-21 10:22:35 -03:00
Claudemiro 994e8e00f1 Show only a log message when could not read conf 2016-08-14 21:00:47 -03:00
Claudemiro 9ad14daeeb Simplified the websockets handler code (#30)
* Simplified the websockets handler code

* created function validateAuthKey

* Using go default error interface.
2016-08-13 12:25:38 -03:00
claudemiro 0ac6c7a55c Started working on v1.3.0 2016-08-11 22:00:19 -03:00
52 changed files with 2537 additions and 2274 deletions
+1
View File
@@ -155,6 +155,7 @@ flymake*
ignore_http/* ignore_http/*
config.json config.json
config.yml
*.pem *.pem
build build
.vscode/* .vscode/*
+20
View File
@@ -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
+57 -29
View File
@@ -1,14 +1,17 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/dimiro1/ipe)](https://goreportcard.com/report/github.com/dimiro1/ipe) [![Go Report Card](https://goreportcard.com/badge/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.
-58
View 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
-41
View File
@@ -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
View File
@@ -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)
+65 -52
View File
@@ -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
View File
@@ -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
View File
@@ -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")
}
}
+86 -32
View File
@@ -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, its important to verify that these WebHooks originated from Pusher. Valid WebHooks will therefore contain these headers which contain a HMAC signature of the WebHook payload (body): // Since anyone could in principle send WebHooks to your application, its important to verify that these WebHooks originated from Pusher. Valid WebHooks will therefore contain these headers which contain a HMAC signature of the webHook payload (body):
// //
// X-Pusher-Key: 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 tokens secret. // X-Pusher-Signature: A HMAC SHA256 hex digest formed by signing the POST payload (body) with the tokens 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
}
} }
+324
View File
@@ -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
}
+25 -20
View File
@@ -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)
Executable
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -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()
+39
View File
@@ -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"`
}
+43
View File
@@ -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)
} }
} }
+6
View File
@@ -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
+289
View File
@@ -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}
}
+29
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
} }
-20
View File
@@ -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"
}
]
}
+19
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+15
View File
@@ -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
)
+23
View File
@@ -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=
+106
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
-20
View File
@@ -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"
}
]
}
-43
View File
@@ -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,
)
}
-35
View File
@@ -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)
}
-47
View File
@@ -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
-67
View File
@@ -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")
}
-59
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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))
}
-17
View File
@@ -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}
}
-262
View File
@@ -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)
}
}
+11
View File
@@ -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
}
+67
View File
@@ -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")
}
+75
View File
@@ -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)
}
}
+19
View File
@@ -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
View File
@@ -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
+20
View File
@@ -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"}
)
+296
View File
@@ -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
}