Compare commits
13 Commits
v1.2.0
...
better-errors
| Author | SHA1 | Date | |
|---|---|---|---|
| 722e0ad3e3 | |||
| 065d6ab286 | |||
| 1d1ffa00b5 | |||
| 983f490737 | |||
| 5c9528bce4 | |||
| 8fb96f3cbf | |||
| 9d732fb01a | |||
| 0bd4fa02e9 | |||
| 4a305b32a6 | |||
| 4c5d5302ec | |||
| 994e8e00f1 | |||
| 9ad14daeeb | |||
| 0ac6c7a55c |
@@ -18,7 +18,7 @@ This software is written in Go - the WYSIWYG lang
|
||||
* Web Hooks;
|
||||
* Client events;
|
||||
* Complete REST API;
|
||||
* Easy instalation;
|
||||
* Easy installation;
|
||||
* A single binary without dependencies;
|
||||
* Easy configuration;
|
||||
* Protocol version 7;
|
||||
@@ -50,7 +50,8 @@ $ go install github.com/dimiro1/ipe
|
||||
```javascript
|
||||
{
|
||||
"Host": ":8080", // Required
|
||||
"SSL": false, // Required but can be false
|
||||
"SSL": false, // Not Required, default is false
|
||||
"Profiling": false, // Mount pprof at /debug. Not Required, default is false
|
||||
"SSLHost": ":4433", // Required if SSL is true
|
||||
"SSLKeyFile": "A key.pem file", // Required if SSL is true
|
||||
"SSLCertFile": "A cert.pem file", // Required if SSL is true
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
require 'rake/clean'
|
||||
|
||||
VERSION = 'v1.2.0'
|
||||
VERSION = 'v1.3.0-SNAPSHOT'
|
||||
GITHASH = `git rev-parse --short HEAD`
|
||||
DATE = Time.now.strftime '%Y%m%d%H%M%S'
|
||||
|
||||
|
||||
@@ -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
|
||||
+12
-12
@@ -1,24 +1,24 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Pusher Spec</title>
|
||||
<link href="https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.css" rel="stylesheet" />
|
||||
<meta charset="utf-8">
|
||||
<title>Pusher Spec</title>
|
||||
<link href="https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.css" rel="stylesheet"/>
|
||||
</head>
|
||||
<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="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.3.4/mocha.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.4.1/chai.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.3.4/mocha.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/3.2/pusher.min.js"></script>
|
||||
|
||||
<script>mocha.setup('bdd')</script>
|
||||
<script src="test.pusher.js"></script>
|
||||
<script>
|
||||
<script>mocha.setup('bdd')</script>
|
||||
<script src="test.pusher.js"></script>
|
||||
<script>
|
||||
mocha.checkLeaks();
|
||||
mocha.globals(['jQuery', 'Pusher']);
|
||||
mocha.run();
|
||||
</script>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -34,7 +34,7 @@ func pusherPresenceAuth(res http.ResponseWriter, req *http.Request) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(res, string(response))
|
||||
fmt.Fprint(res, string(response))
|
||||
}
|
||||
|
||||
func pusherPrivateAuth(res http.ResponseWriter, req *http.Request) {
|
||||
@@ -48,13 +48,13 @@ func pusherPrivateAuth(res http.ResponseWriter, req *http.Request) {
|
||||
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")
|
||||
|
||||
fmt.Fprintf(res, "OK")
|
||||
fmt.Fprint(res, "OK")
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
"Host": ":8080",
|
||||
"SSL": false,
|
||||
"Profiling": true,
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Generated
+5
-13
@@ -1,20 +1,12 @@
|
||||
hash: 0a451841a1f9493d419d658fab8da6863ef5df0645ddea506a225865078c9dcb
|
||||
updated: 2016-08-10T22:23:10.989237578-03:00
|
||||
hash: ee2de935e70f2a39cbfd2dd16add0736c698b32f5c02409d0c25844d883c91a1
|
||||
updated: 2016-08-22T22:55:07.279113421-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
|
||||
- name: github.com/pressly/chi
|
||||
version: 12aad88c7d86de2affe686f855b6ed94a07cba9c
|
||||
subpackages:
|
||||
- pat
|
||||
- pattern
|
||||
- internal
|
||||
- name: golang.org/x/net
|
||||
version: 075e191f18186a8ff2becaf64478e30f4545cdad
|
||||
subpackages:
|
||||
- context
|
||||
- middleware
|
||||
testImports: []
|
||||
|
||||
+3
-7
@@ -2,10 +2,6 @@ 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
|
||||
- package: github.com/pressly/chi
|
||||
excludeDirs:
|
||||
- functional
|
||||
+43
-43
@@ -74,51 +74,51 @@ func (c *channel) Subscribe(a *app, conn *connection, channelData string) error
|
||||
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 {
|
||||
if !c.IsPresence() {
|
||||
conn.Publish(newSubscriptionSucceededEvent(c.ChannelID, "{}"))
|
||||
return nil
|
||||
}
|
||||
// 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)))
|
||||
|
||||
// WebHook
|
||||
if c.TotalSubscriptions() == 1 {
|
||||
|
||||
+19
-18
@@ -1,20 +1,21 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
"Host": ":8080",
|
||||
"SSL": false,
|
||||
"Profiling": 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ type configFile struct {
|
||||
Host string // The host, eg: :8080 will start on 0.0.0.0:8080
|
||||
User string
|
||||
SSL bool
|
||||
Profiling bool
|
||||
SSLHost string
|
||||
SSLKeyFile string
|
||||
SSLCertFile string
|
||||
|
||||
@@ -4,41 +4,6 @@
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -18,50 +18,38 @@ type db interface {
|
||||
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
|
||||
sync.Mutex
|
||||
Apps []*app
|
||||
}
|
||||
|
||||
func newMemdb() *memdb {
|
||||
return &memdb{
|
||||
AppsByAppID: make(map[string]*app),
|
||||
AppsByKey: make(map[string]*app),
|
||||
}
|
||||
func newMemdb() db {
|
||||
return &memdb{}
|
||||
}
|
||||
|
||||
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()
|
||||
db.Lock()
|
||||
db.Apps = append(db.Apps, a)
|
||||
db.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
|
||||
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 *memdb) GetAppByKey(key string) (*app, error) {
|
||||
db.KeyMutex.Lock()
|
||||
a, ok := db.AppsByKey[key]
|
||||
db.KeyMutex.Unlock()
|
||||
if ok {
|
||||
return a, nil
|
||||
for _, a := range db.Apps {
|
||||
if a.Key == key {
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("App not found")
|
||||
}
|
||||
|
||||
@@ -6,6 +6,19 @@ package ipe
|
||||
|
||||
import "testing"
|
||||
|
||||
func Benchmark_memdb_GetAppByAppID(b *testing.B) {
|
||||
db := newMemdb()
|
||||
db.AddApp(&app{AppID: "123456", Name: "Example"})
|
||||
db.AddApp(&app{AppID: "654321", Name: "Example2"})
|
||||
db.AddApp(&app{AppID: "678901", Name: "Example3"})
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
db.GetAppByAppID("123456")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_db_GetAppByAppID(t *testing.T) {
|
||||
app := &app{AppID: "123456", Name: "Example"}
|
||||
|
||||
|
||||
+34
-88
@@ -4,115 +4,61 @@
|
||||
|
||||
package ipe
|
||||
|
||||
// Base interface
|
||||
type websocketError interface {
|
||||
GetCode() int
|
||||
GetMsg() string
|
||||
}
|
||||
import "fmt"
|
||||
|
||||
// Base struct
|
||||
type baseWebsocketError struct {
|
||||
Code int
|
||||
type websocketError struct {
|
||||
Code *int
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e baseWebsocketError) GetCode() int {
|
||||
func (e websocketError) GetCode() *int {
|
||||
return e.Code
|
||||
}
|
||||
|
||||
func (e baseWebsocketError) GetMsg() string {
|
||||
func (e websocketError) GetMsg() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
// Unsupprted protocol version
|
||||
type unsupportedProtocolVersionError struct {
|
||||
baseWebsocketError
|
||||
func (e websocketError) Error() string {
|
||||
return fmt.Sprintf("%d: %s", e.Code, e.Msg)
|
||||
}
|
||||
|
||||
func newUnsupportedProtocolVersionError() unsupportedProtocolVersionError {
|
||||
return unsupportedProtocolVersionError{
|
||||
baseWebsocketError{Code: unsupportedProtocolVersion, Msg: "Unsupported protocol version"},
|
||||
}
|
||||
func newWebsocketError(code int, msg string) websocketError {
|
||||
return websocketError{Code: &code, Msg: msg}
|
||||
}
|
||||
|
||||
// The application does not exists
|
||||
// See the configuration file
|
||||
type applicationDoesNotExistsError struct {
|
||||
baseWebsocketError
|
||||
}
|
||||
var (
|
||||
// Unsupprted protocol version
|
||||
unsupportedProtocolVersionError = newWebsocketError(4007, "Unsupported protocol version")
|
||||
|
||||
func newApplicationDoesNotExistsError() applicationDoesNotExistsError {
|
||||
return applicationDoesNotExistsError{
|
||||
baseWebsocketError{Code: applicationDoesNotExists, Msg: "Could not found an app with the given key"},
|
||||
}
|
||||
}
|
||||
// The application does not exists
|
||||
// See the configuration file
|
||||
applicationDoesNotExistsError = newWebsocketError(4001, "Could not found an app with the given key")
|
||||
|
||||
// The user did not send the protocol version
|
||||
type noProtocolVersionSuppliedError struct {
|
||||
baseWebsocketError
|
||||
}
|
||||
// The user did not send the protocol version
|
||||
noProtocolVersionSuppliedError = newWebsocketError(4008, "No protocol version supplied")
|
||||
|
||||
func newNoProtocolVersionSuppliedError() noProtocolVersionSuppliedError {
|
||||
return noProtocolVersionSuppliedError{
|
||||
baseWebsocketError{Code: noProtocolVersionSupplied, Msg: "No protocol version supplied"},
|
||||
}
|
||||
}
|
||||
// When the application is disabled.
|
||||
// See the configuration file
|
||||
applicationDisabledError = newWebsocketError(4003, "Application disabled")
|
||||
|
||||
// When the application is disabled.
|
||||
// See the configuration file
|
||||
type applicationDisabledError struct {
|
||||
baseWebsocketError
|
||||
}
|
||||
// When the application only accepts SSL connections
|
||||
applicationOnlyAccepsSSLError = newWebsocketError(4000, "Application only accepts SSL connections, reconnect using wss://")
|
||||
|
||||
func newApplicationDisabledError() noProtocolVersionSuppliedError {
|
||||
return noProtocolVersionSuppliedError{
|
||||
baseWebsocketError{Code: applicationDisabled, Msg: "Application disabled"},
|
||||
}
|
||||
}
|
||||
// When the user send an invalid version
|
||||
invalidVersionStringFormatError = newWebsocketError(4006, "Invalid version string format")
|
||||
|
||||
// When the application only accepts SSL connections
|
||||
type applicationOnlyAccepsSSLError struct {
|
||||
baseWebsocketError
|
||||
}
|
||||
// Used when the error was internal
|
||||
// * Decoding json
|
||||
// * Writing to output
|
||||
genericReconnectImmediatelyError = newWebsocketError(4200, "Generic reconnect immediately")
|
||||
|
||||
func newApplicationOnlyAccepsSSLError() applicationOnlyAccepsSSLError {
|
||||
return applicationOnlyAccepsSSLError{
|
||||
baseWebsocketError{Code: applicationOnlyAcceptsSSL, Msg: "Application only accepts SSL connections, reconnect using wss://"},
|
||||
}
|
||||
}
|
||||
// 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
|
||||
genericError = newWebsocketError(0, "Generic Error")
|
||||
|
||||
// When the user send an invalid version
|
||||
type invalidVersionStringFormatError struct {
|
||||
baseWebsocketError
|
||||
}
|
||||
disabledClientEventsError = websocketError{Msg: "To send client events, you must enable this feature in the Settings."}
|
||||
|
||||
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},
|
||||
}
|
||||
}
|
||||
couldNotFoundChannelError = websocketError{Msg: "Could not find a channel with the given id"}
|
||||
)
|
||||
|
||||
+8
-20
@@ -160,29 +160,17 @@ type errorEvent struct {
|
||||
// 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{}
|
||||
func newErrorEvent(code *int, message string) errorEvent {
|
||||
|
||||
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,
|
||||
}
|
||||
type dataErrorEvent struct {
|
||||
Code *int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
return errorEvent{Event: "pusher:error", Data: data}
|
||||
return errorEvent{Event: "pusher:error", Data: dataErrorEvent{
|
||||
Code: code,
|
||||
Message: message,
|
||||
}}
|
||||
}
|
||||
|
||||
// {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_newErrorEvent_with_invalid_code(t *testing.T) {
|
||||
event := newErrorEvent(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 := newErrorEvent(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)
|
||||
}
|
||||
}
|
||||
+54
-85
@@ -12,12 +12,8 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
goji "goji.io"
|
||||
|
||||
"goji.io/pat"
|
||||
|
||||
log "github.com/golang/glog"
|
||||
"golang.org/x/net/context"
|
||||
"github.com/pressly/chi"
|
||||
|
||||
"github.com/dimiro1/ipe/utils"
|
||||
)
|
||||
@@ -51,79 +47,64 @@ func prepareQueryString(params url.Values) string {
|
||||
// * The request path (e.g. /some/resource)
|
||||
// * The query parameters sorted by key, with keys converted to lowercase, then joined as in the query string.
|
||||
// Note that the string must not be url escaped (e.g. given the keys auth_key: foo, Name: Something else, you get auth_key=foo&name=Something else)
|
||||
func restAuthenticationHandler(DB db, next goji.Handler) goji.HandlerFunc {
|
||||
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
appID := pat.Param(ctx, "app_id")
|
||||
func authenticationHandler(DB db) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "app_id")
|
||||
|
||||
app, err := DB.GetAppByAppID(appID)
|
||||
app, err := DB.GetAppByAppID(appID)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||
return
|
||||
if err != nil {
|
||||
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.ServeHTTP(w, r)
|
||||
} else {
|
||||
log.Error("Not authorized")
|
||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
func checkAppDisabled(DB db) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "app_id")
|
||||
|
||||
currentApp, err := DB.GetAppByAppID(appID)
|
||||
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)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}()
|
||||
next.ServeHTTPC(ctx, w, r)
|
||||
|
||||
if currentApp.ApplicationDisabled {
|
||||
http.Error(w, "Application disabled", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// commonHandlers combine restCheckAppDisabledHandler and restAuthenticationHandler handlers
|
||||
func commonHandlers(DB db, next goji.Handler) goji.HandlerFunc {
|
||||
return recoverHandler(restCheckAppDisabledHandler(DB, restAuthenticationHandler(DB, next)))
|
||||
}
|
||||
|
||||
func newPostEventsHandler(DB db) goji.HandlerFunc {
|
||||
return commonHandlers(DB, &postEventsHandler{DB})
|
||||
}
|
||||
|
||||
type postEventsHandler struct{ DB db }
|
||||
|
||||
// ServeHTTPC An event consists of a name and data (typically JSON) which may be sent to all subscribers to a particular channel or channels.
|
||||
@@ -141,8 +122,8 @@ type postEventsHandler struct{ DB db }
|
||||
// Response is an empty JSON hash.
|
||||
//
|
||||
// POST /apps/{app_id}/events
|
||||
func (h *postEventsHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
appID := pat.Param(ctx, "app_id")
|
||||
func (h *postEventsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "app_id")
|
||||
|
||||
app, err := h.DB.GetAppByAppID(appID)
|
||||
|
||||
@@ -187,10 +168,6 @@ func (h *postEventsHandler) ServeHTTPC(ctx context.Context, w http.ResponseWrite
|
||||
w.Write([]byte("{}"))
|
||||
}
|
||||
|
||||
func newGetChannelsHandler(DB db) goji.HandlerFunc {
|
||||
return commonHandlers(DB, &getChannelsHandler{DB})
|
||||
}
|
||||
|
||||
type getChannelsHandler struct{ DB db }
|
||||
|
||||
// Allows fetching a hash of occupied channels (optionally filtered by prefix),
|
||||
@@ -212,10 +189,10 @@ type getChannelsHandler struct{ DB db }
|
||||
// }
|
||||
//
|
||||
// GET /apps/{app_id}/channels
|
||||
func (h *getChannelsHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
func (h *getChannelsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
|
||||
appID := pat.Param(ctx, "app_id")
|
||||
appID := chi.URLParam(r, "app_id")
|
||||
filter := query.Get("filter_by_prefix")
|
||||
info := query.Get("info")
|
||||
|
||||
@@ -282,10 +259,6 @@ func (h *getChannelsHandler) ServeHTTPC(ctx context.Context, w http.ResponseWrit
|
||||
}
|
||||
}
|
||||
|
||||
func newGetChannelHandler(DB db) goji.HandlerFunc {
|
||||
return commonHandlers(DB, &getChannelHandler{DB})
|
||||
}
|
||||
|
||||
type getChannelHandler struct{ DB db }
|
||||
|
||||
// Fetch info for one channel
|
||||
@@ -298,19 +271,19 @@ type getChannelHandler struct{ DB db }
|
||||
// }
|
||||
//
|
||||
// GET /apps/{app_id}/channels/{channel_name}
|
||||
func (h *getChannelHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
func (h *getChannelHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
query := r.URL.Query()
|
||||
|
||||
appID := pat.Param(ctx, "app_id")
|
||||
appID := chi.URLParam(r, "app_id")
|
||||
app, err := h.DB.GetAppByAppID(appID)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
channelName := pat.Param(ctx, "channel_name")
|
||||
channelName := chi.URLParam(r, "channel_name")
|
||||
|
||||
// Channel name could not be empty
|
||||
if strings.TrimSpace(channelName) == "" {
|
||||
@@ -376,10 +349,6 @@ func (h *getChannelHandler) ServeHTTPC(ctx context.Context, w http.ResponseWrite
|
||||
}
|
||||
}
|
||||
|
||||
func newGetChannelUsersHandler(DB db) goji.HandlerFunc {
|
||||
return commonHandlers(DB, &getChannelUsersHandler{DB})
|
||||
}
|
||||
|
||||
type getChannelUsersHandler struct{ DB db }
|
||||
|
||||
// Allowed only for presence-channels
|
||||
@@ -393,9 +362,9 @@ type getChannelUsersHandler struct{ DB db }
|
||||
// }
|
||||
//
|
||||
// GET /apps/{app_id}/channels/{channel_name}/users
|
||||
func (h *getChannelUsersHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
appID := pat.Param(ctx, "app_id")
|
||||
channelName := pat.Param(ctx, "channel_name")
|
||||
func (h *getChannelUsersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "app_id")
|
||||
channelName := chi.URLParam(r, "channel_name")
|
||||
|
||||
isPresence := utils.IsPresenceChannel(channelName)
|
||||
|
||||
|
||||
+26
-21
@@ -1,15 +1,14 @@
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"goji.io/pattern"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"github.com/pressly/chi"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -41,14 +40,15 @@ func init() {
|
||||
func Test_getChannels_all(t *testing.T) {
|
||||
appID := testApp.AppID
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("app_id", appID)
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels", appID), nil)
|
||||
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
handler.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||
@@ -67,14 +67,15 @@ func Test_getChannels_all(t *testing.T) {
|
||||
func Test_getChannels_filter_by_presence_prefix(t *testing.T) {
|
||||
appID := testApp.AppID
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("app_id", appID)
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-", appID), nil)
|
||||
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
handler.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||
@@ -94,14 +95,15 @@ func Test_getChannels_filter_by_presence_prefix(t *testing.T) {
|
||||
func Test_getChannels_filter_by_presence_prefix_and_user_count(t *testing.T) {
|
||||
appID := testApp.AppID
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("app_id", appID)
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-&info=user_count", appID), nil)
|
||||
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
handler.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||
@@ -133,14 +135,15 @@ func Test_getChannels_filter_by_presence_prefix_and_user_count(t *testing.T) {
|
||||
func Test_getChannels_filter_by_private_prefix_and_info_user_count(t *testing.T) {
|
||||
appID := testApp.AppID
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("app_id", appID)
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-&info=user_count", appID), nil)
|
||||
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
handler.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusBadRequest)
|
||||
@@ -150,14 +153,15 @@ func Test_getChannels_filter_by_private_prefix_and_info_user_count(t *testing.T)
|
||||
func Test_getChannels_filter_by_public_prefix(t *testing.T) {
|
||||
appID := testApp.AppID
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("app_id", appID)
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=public-", appID), nil)
|
||||
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
handler.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||
@@ -183,14 +187,15 @@ func Test_getChannels_filter_by_public_prefix(t *testing.T) {
|
||||
func Test_getChannels_filter_by_private_prefix(t *testing.T) {
|
||||
appID := testApp.AppID
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("app_id", appID)
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-", appID), nil)
|
||||
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
handler.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||
|
||||
+25
-14
@@ -11,11 +11,9 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"goji.io/pat"
|
||||
|
||||
goji "goji.io"
|
||||
|
||||
log "github.com/golang/glog"
|
||||
"github.com/pressly/chi"
|
||||
"github.com/pressly/chi/middleware"
|
||||
)
|
||||
|
||||
// Start Parse the configuration file and starts the ipe server
|
||||
@@ -27,14 +25,16 @@ func Start(filename string) {
|
||||
file, err := os.Open(filename)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
// Reading config
|
||||
if err := json.NewDecoder(file).Decode(&conf); err != nil {
|
||||
log.Fatal(err)
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Using a in memory database
|
||||
@@ -45,20 +45,31 @@ func Start(filename string) {
|
||||
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))
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
r.Get("/app/:key", (&websocketHandler{db}).ServeHTTP)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(checkAppDisabled(db))
|
||||
r.Use(authenticationHandler(db))
|
||||
|
||||
r.Post("/apps/:app_id/events", (&postEventsHandler{db}).ServeHTTP)
|
||||
r.Get("/apps/:app_id/channels", (&getChannelsHandler{db}).ServeHTTP)
|
||||
r.Get("/apps/:app_id/channels/:channel_name", (&getChannelHandler{db}).ServeHTTP)
|
||||
r.Get("/apps/:app_id/channels/:channel_name/users", (&getChannelUsersHandler{db}).ServeHTTP)
|
||||
})
|
||||
|
||||
if conf.Profiling {
|
||||
r.Mount("/debug", middleware.Profiler())
|
||||
}
|
||||
|
||||
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.Fatal(http.ListenAndServeTLS(conf.SSLHost, conf.SSLCertFile, conf.SSLKeyFile, r))
|
||||
}()
|
||||
}
|
||||
|
||||
log.Infof("Starting HTTP service on %s ...", conf.Host)
|
||||
log.Fatal(http.ListenAndServe(conf.Host, router))
|
||||
log.Fatal(http.ListenAndServe(conf.Host, r))
|
||||
}
|
||||
|
||||
+41
-9
@@ -10,10 +10,15 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/dimiro1/ipe/utils"
|
||||
log "github.com/golang/glog"
|
||||
)
|
||||
|
||||
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:
|
||||
// {
|
||||
@@ -71,14 +76,19 @@ func newClientHook(channel *channel, s *subscription, event string, data interfa
|
||||
// { "name": "channel_occupied", "channel": "test_channel" }
|
||||
func (a *app) TriggerChannelOccupiedHook(c *channel) {
|
||||
event := newChannelOcuppiedHook(c)
|
||||
triggerHook(event.Name, a, c, event)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||
defer cancel()
|
||||
|
||||
triggerHook(ctx, a, event)
|
||||
}
|
||||
|
||||
// channel_vacated
|
||||
// { "name": "channel_vacated", "channel": "test_channel" }
|
||||
func (a *app) TriggerChannelVacatedHook(c *channel) {
|
||||
event := newChannelVacatedHook(c)
|
||||
triggerHook(event.Name, a, c, event)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||
defer cancel()
|
||||
triggerHook(ctx, a, event)
|
||||
}
|
||||
|
||||
// {
|
||||
@@ -96,7 +106,9 @@ func (a *app) TriggerClientEventHook(c *channel, s *subscription, clientEvent st
|
||||
event.UserID = s.ID
|
||||
}
|
||||
|
||||
triggerHook(event.Name, a, c, event)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||
defer cancel()
|
||||
triggerHook(ctx, a, event)
|
||||
}
|
||||
|
||||
// {
|
||||
@@ -106,7 +118,9 @@ func (a *app) TriggerClientEventHook(c *channel, s *subscription, clientEvent st
|
||||
// }
|
||||
func (a *app) TriggerMemberAddedHook(c *channel, s *subscription) {
|
||||
event := newMemberAddedHook(c, s)
|
||||
triggerHook(event.Name, a, c, event)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||
defer cancel()
|
||||
triggerHook(ctx, a, event)
|
||||
}
|
||||
|
||||
// {
|
||||
@@ -116,17 +130,22 @@ func (a *app) TriggerMemberAddedHook(c *channel, s *subscription) {
|
||||
// }
|
||||
func (a *app) TriggerMemberRemovedHook(c *channel, s *subscription) {
|
||||
event := newMemberRemovedHook(c, s)
|
||||
triggerHook(event.Name, a, c, event)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||
defer cancel()
|
||||
triggerHook(ctx, a, event)
|
||||
}
|
||||
|
||||
func triggerHook(name string, a *app, c *channel, event hookEvent) {
|
||||
func triggerHook(ctx context.Context, a *app, event hookEvent) error {
|
||||
if !a.WebHooks {
|
||||
log.Infof("Webhooks are not enabled for app: %s", a.Name)
|
||||
return
|
||||
return fmt.Errorf("Webhooks are not enabled for app: %s", a.Name)
|
||||
}
|
||||
|
||||
var done chan bool
|
||||
defer close(done)
|
||||
|
||||
go func() {
|
||||
log.Infof("Triggering %s event", name)
|
||||
log.Infof("Triggering %s event", event.Name)
|
||||
|
||||
hook := webHook{TimeMs: time.Now().Unix()}
|
||||
|
||||
@@ -145,11 +164,14 @@ func triggerHook(name string, a *app, c *channel, event hookEvent) {
|
||||
var req *http.Request
|
||||
|
||||
req, err = http.NewRequest("POST", a.URLWebHook, bytes.NewReader(js))
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Error creating request: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.WithContext(ctx)
|
||||
|
||||
req.Header.Set("User-Agent", "Ipe UA; (+https://github.com/dimiro1/ipe)")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Pusher-Key", a.Key)
|
||||
@@ -166,7 +188,17 @@ func triggerHook(name string, a *app, c *channel, event hookEvent) {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
+177
-159
@@ -12,13 +12,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
goji "goji.io"
|
||||
|
||||
"goji.io/pat"
|
||||
|
||||
log "github.com/golang/glog"
|
||||
"github.com/gorilla/websocket"
|
||||
"golang.org/x/net/context"
|
||||
"github.com/pressly/chi"
|
||||
|
||||
"github.com/dimiro1/ipe/utils"
|
||||
)
|
||||
@@ -26,11 +22,58 @@ import (
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
CheckOrigin: func(_ *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// Handle open Subscriber.
|
||||
func onOpen(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, sessionID string, app *app) websocketError {
|
||||
func handleMessages(conn *websocket.Conn, sessionID string, app *app) {
|
||||
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 {
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("websockets: Handling %s event", event.Event)
|
||||
|
||||
switch event.Event {
|
||||
case "pusher:ping":
|
||||
onPing(conn)
|
||||
case "pusher:subscribe":
|
||||
onSubscribe(conn, sessionID, app, message)
|
||||
case "pusher:unsubscribe":
|
||||
onUnsubscribe(conn, sessionID, app, message)
|
||||
default:
|
||||
if utils.IsClientEvent(event.Event) {
|
||||
onClientEvent(conn, sessionID, app, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleError(conn *websocket.Conn, sessionID string, app *app, err error) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func onOpen(conn *websocket.Conn, r *http.Request, sessionID string, app *app) error {
|
||||
params := r.URL.Query()
|
||||
p := params.Get("protocol")
|
||||
|
||||
@@ -44,7 +87,7 @@ func onOpen(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, sessio
|
||||
case strings.TrimSpace(p) == "":
|
||||
return newNoProtocolVersionSuppliedError()
|
||||
case protocol != supportedProtocolVersion:
|
||||
return newUnsupportedProtocolVersionError()
|
||||
return unsupportedProtocolVersionError
|
||||
case app.ApplicationDisabled:
|
||||
return newApplicationDisabledError()
|
||||
case app.OnlySSL:
|
||||
@@ -65,160 +108,145 @@ func onOpen(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, sessio
|
||||
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"`
|
||||
func onPing(conn *websocket.Conn) {
|
||||
if err := conn.WriteJSON(newPongEvent()); err != nil {
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
}
|
||||
}
|
||||
|
||||
func onClientEvent(conn *websocket.Conn, sessionID string, app *app, message []byte) {
|
||||
if !app.UserEvents {
|
||||
emitWSError(newGenericError("To send client events, you must enable this feature in the Settings."), conn)
|
||||
}
|
||||
|
||||
for {
|
||||
_, message, err := conn.ReadMessage()
|
||||
clientEvent := rawEvent{}
|
||||
|
||||
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, &clientEvent); err != nil {
|
||||
log.Error(err)
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(message, &event); err != nil {
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
break
|
||||
}
|
||||
channel, err := app.FindChannelByChannelID(clientEvent.Channel)
|
||||
|
||||
log.Infof("websockets: Handling %s event", event.Event)
|
||||
if err != nil {
|
||||
emitWSError(newGenericError(fmt.Sprintf("Could not find a channel with the id %s", clientEvent.Channel)), conn)
|
||||
}
|
||||
|
||||
switch event.Event {
|
||||
case "pusher:ping":
|
||||
if err := conn.WriteJSON(newPongEvent()); err != nil {
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
}
|
||||
case "pusher:subscribe":
|
||||
subscribeEvent := subscribeEvent{}
|
||||
if !channel.IsPresenceOrPrivate() {
|
||||
emitWSError(newGenericError("Client event rejected - only supported on private and presence channels"), conn)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
if err := app.Publish(channel, clientEvent, sessionID); err != nil {
|
||||
log.Error(err)
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func newWebsocketHandler(DB db) goji.Handler {
|
||||
return &websocketHandler{DB}
|
||||
func onUnsubscribe(conn *websocket.Conn, sessionID string, app *app, message []byte) {
|
||||
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)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type websocketHandler struct{ DB db }
|
||||
func onSubscribe(conn *websocket.Conn, sessionID string, app *app, message []byte) {
|
||||
subscribeEvent := subscribeEvent{}
|
||||
|
||||
if err := json.Unmarshal(message, &subscribeEvent); err != nil {
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
return
|
||||
}
|
||||
|
||||
connection, err := app.FindConnection(sessionID)
|
||||
|
||||
if err != nil {
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
return
|
||||
}
|
||||
|
||||
channelName := strings.TrimSpace(subscribeEvent.Data.Channel)
|
||||
|
||||
if !utils.IsChannelNameValid(channelName) {
|
||||
emitWSError(newGenericError("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) {
|
||||
emitWSError(newGenericError(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 {
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
}
|
||||
}
|
||||
|
||||
func validateAuthKey(givenAuthKey string, toSign []string, app *app) bool {
|
||||
expectedAuthKey := fmt.Sprintf("%s:%s", app.Key, utils.HashMAC([]byte(strings.Join(toSign, ":")), []byte(app.Secret)))
|
||||
return givenAuthKey == expectedAuthKey
|
||||
}
|
||||
|
||||
// Emit an Websocket ErrorEvent
|
||||
func emitWSError(err error, conn *websocket.Conn) {
|
||||
e, ok := err.(websocketError)
|
||||
|
||||
if !ok {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
event := newErrorEvent(e.GetCode(), e.GetMsg())
|
||||
|
||||
if err := conn.WriteJSON(event); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
type websocketHandler struct {
|
||||
DB db
|
||||
}
|
||||
|
||||
// Websocket GET /app/{key}
|
||||
func (h *websocketHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
func (h *websocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
defer func() {
|
||||
if conn != nil {
|
||||
@@ -231,7 +259,7 @@ func (h *websocketHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter
|
||||
return
|
||||
}
|
||||
|
||||
appKey := pat.Param(ctx, "key")
|
||||
appKey := chi.URLParam(r, "key")
|
||||
|
||||
app, err := h.DB.GetAppByKey(appKey)
|
||||
|
||||
@@ -243,20 +271,10 @@ func (h *websocketHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter
|
||||
|
||||
sessionID := utils.GenerateSessionID()
|
||||
|
||||
if err := onOpen(conn, w, r, sessionID, app); err != nil {
|
||||
if err := onOpen(conn, 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)
|
||||
}
|
||||
handleMessages(conn, sessionID, app)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user