Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
015cbdf07a | ||
|
|
431a19fe7b | ||
|
|
6225d8006f | ||
|
|
7a4568e92d | ||
|
|
6e46bcb2aa | ||
|
|
0c599ea5fb | ||
|
|
46ebd1b62b | ||
|
|
06449fdb58 | ||
|
|
6092cd85ca | ||
|
|
9c49ae16d3 | ||
|
|
7a794c97d7 | ||
|
|
f0b3aadf74 | ||
|
|
2eac54a0d3 | ||
|
|
a6f44d2175 | ||
|
|
6c479ced3c | ||
|
|
178bbc9b8e | ||
|
|
9f47b4fe2b | ||
|
|
e9db8ea35e | ||
|
|
24425a2a9a | ||
|
|
952983a0f8 | ||
|
|
de869b3e63 | ||
|
|
abf9a980eb | ||
|
|
e15a8a84e2 | ||
|
|
8c54491e27 | ||
|
|
03483592fc | ||
|
|
edabc10008 | ||
|
|
bfa96ebbfb | ||
|
|
5ce17c8856 | ||
|
|
e71294de18 | ||
|
|
a7c5813501 | ||
|
|
383c02de1c | ||
|
|
1bae7f13ad | ||
|
|
f07549fb6a | ||
|
|
c272361861 | ||
|
|
9dfc8c9cc0 | ||
|
|
6c55af2b5c | ||
|
|
679ff1e589 | ||
|
|
cbdd4e428a | ||
|
|
805e9e3957 | ||
|
|
44bbc1b1ea | ||
|
|
e02773bbeb | ||
|
|
5fa621d743 | ||
|
|
880d7bda2a | ||
|
|
7dad2de275 | ||
|
|
081611fd87 | ||
|
|
8810e523f0 | ||
|
|
baf551ecdf | ||
|
|
f3dba811b7 | ||
|
|
55a7a0b96c | ||
|
|
cadee4e26d | ||
|
|
03e94887a7 | ||
|
|
e93145efe7 | ||
|
|
051da185e5 | ||
|
|
51eacdcf51 | ||
|
|
412d8be451 | ||
|
|
68f2e15320 | ||
|
|
a77fb791f7 | ||
|
|
8bf0786cb5 | ||
|
|
ba3b699517 | ||
|
|
517462a7af | ||
|
|
56da1dea01 | ||
|
|
1971da652f | ||
|
|
f81a365e0c | ||
|
|
1de947cedc | ||
|
|
45cffc17a8 | ||
|
|
59abf6ac5b | ||
|
|
cbb34a928f | ||
|
|
403138e28f | ||
|
|
8dea85e71c | ||
|
|
3fc072373a | ||
|
|
cad813e729 | ||
|
|
1054411b60 | ||
|
|
b972058622 | ||
|
|
f28e9da982 | ||
|
|
341909e600 | ||
|
|
0bf660183b | ||
|
|
c697311be3 | ||
|
|
f910433853 | ||
|
|
883376b0c8 | ||
|
|
8dbd3ccbea | ||
|
|
228ed284d9 | ||
|
|
10e6048d11 | ||
|
|
e2d07e9760 | ||
|
|
485bc01abe | ||
|
|
7b578c4fb4 | ||
|
|
03607062aa | ||
|
|
846231f5f3 | ||
|
|
201cba86a9 | ||
|
|
d74426127c | ||
|
|
cd2e58d0c5 | ||
|
|
eca114dcf1 | ||
|
|
a3465f332e | ||
|
|
e9bdc0859e | ||
|
|
b426bc2534 | ||
|
|
cd1fd8995d | ||
|
|
c7f757e517 | ||
|
|
8d671751b3 | ||
|
|
018391ad66 | ||
|
|
8586b64bda | ||
|
|
ef756d789a | ||
|
|
67676742a9 | ||
|
|
5218fb2ca4 | ||
|
|
d1473197b8 | ||
|
|
b93da8ccc8 | ||
|
|
8def052b9d | ||
|
|
0f5d238065 | ||
|
|
7367f06ccb | ||
|
|
a4e08d1e85 | ||
|
|
f6e1993eb6 | ||
|
|
1dc3105155 | ||
|
|
a51629f7b5 | ||
|
|
0d4e9742b9 | ||
|
|
7032e69ffb | ||
|
|
edf4b421bf | ||
|
|
874c07267f | ||
|
|
414c1d98ee | ||
|
|
5043b05c7b | ||
|
|
5695bb445d | ||
|
|
dd78126371 | ||
|
|
431812f695 | ||
|
|
3999ad5aa0 | ||
|
|
2394f167a5 | ||
|
|
0e3440bc35 | ||
|
|
59457cc734 | ||
|
|
1f5ec97f57 | ||
|
|
4b08b0c658 | ||
|
|
7e91fbe35d | ||
|
|
2cb8b13dc6 | ||
|
|
0aec168ab3 | ||
|
|
be66978479 | ||
|
|
c125bc7206 | ||
|
|
dc28fa90ff | ||
|
|
3a9b786a46 | ||
|
|
ef1070887b | ||
|
|
cadc72d482 | ||
|
|
71d5099ac3 | ||
|
|
387a2ae28d | ||
|
|
e2412827cf | ||
|
|
ff2fcf3c54 | ||
|
|
e1b7bd8252 |
+163
@@ -0,0 +1,163 @@
|
||||
# Created by https://www.gitignore.io
|
||||
|
||||
### Go ###
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
|
||||
### SublimeText ###
|
||||
# cache files for sublime text
|
||||
*.tmlanguage.cache
|
||||
*.tmPreferences.cache
|
||||
*.stTheme.cache
|
||||
|
||||
# workspace files are user-specific
|
||||
*.sublime-workspace
|
||||
|
||||
# project files should be checked into the repository, unless a significant
|
||||
# proportion of contributors will probably not be using SublimeText
|
||||
*.sublime-project
|
||||
|
||||
# sftp configuration file
|
||||
sftp-config.json
|
||||
|
||||
|
||||
### OSX ###
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear on external disk
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
|
||||
### Intellij ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
|
||||
|
||||
*.iml
|
||||
|
||||
## Directory-based project format:
|
||||
.idea/
|
||||
# if you remove the above rule, at least ignore the following:
|
||||
|
||||
# User-specific stuff:
|
||||
# .idea/workspace.xml
|
||||
# .idea/tasks.xml
|
||||
# .idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
# .idea/dataSources.ids
|
||||
# .idea/dataSources.xml
|
||||
# .idea/sqlDataSources.xml
|
||||
# .idea/dynamic.xml
|
||||
# .idea/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
# .idea/gradle.xml
|
||||
# .idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
# .idea/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
|
||||
### Emacs ###
|
||||
# -*- mode: gitignore; -*-
|
||||
*~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
*.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
flymake*
|
||||
|
||||
# Org-mode
|
||||
.org-id-locations
|
||||
*_archive
|
||||
|
||||
# flymake-mode
|
||||
*_flymake.*
|
||||
|
||||
# eshell files
|
||||
/eshell/history
|
||||
/eshell/lastdir
|
||||
|
||||
# elpa packages
|
||||
/elpa/
|
||||
|
||||
# reftex files
|
||||
*.rel
|
||||
|
||||
# AUCTeX auto folder
|
||||
/auto/
|
||||
|
||||
# cask packages
|
||||
.cask/
|
||||
|
||||
# Project Specific
|
||||
|
||||
ignore_http/*
|
||||
config.json
|
||||
*.pem
|
||||
build
|
||||
.vscode/*
|
||||
debug
|
||||
|
||||
vendor/*
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2015 Claudemiro Alves Feitosa Neto <[email protected]>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,157 @@
|
||||
[](https://goreportcard.com/report/github.com/dimiro1/ipe)
|
||||
|
||||
# IPÊ
|
||||
|
||||
This software is written in Go - the WYSIWYG lang
|
||||
|
||||
# Why I wrote this software?
|
||||
|
||||
1. I wanted to learn Go and I needed a non trivial application;
|
||||
2. I use Pusher in some projects;
|
||||
3. I really like Pusher;
|
||||
|
||||
# Features
|
||||
|
||||
* Public Channels;
|
||||
* Private Channels;
|
||||
* Presence Channels;
|
||||
* Web Hooks;
|
||||
* Client events;
|
||||
* Complete REST API;
|
||||
* Easy instalation;
|
||||
* A single binary without dependencies;
|
||||
* Easy configuration;
|
||||
* Protocol version 7;
|
||||
* Multiple apps in the same instance;
|
||||
* Drop in replacement for pusher server;
|
||||
|
||||
# Download pre built binaries
|
||||
|
||||
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
|
||||
|
||||
```console
|
||||
$ go get github.com/dimiro1/ipe
|
||||
```
|
||||
|
||||
or simply
|
||||
|
||||
```console
|
||||
$ go install github.com/dimiro1/ipe
|
||||
```
|
||||
|
||||
# How to configure?
|
||||
|
||||
## The server
|
||||
|
||||
```javascript
|
||||
{
|
||||
"Host": ":8080", // Required
|
||||
"SSL": false, // Required but can be 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
|
||||
"Apps": [ // Required, A Json arrays with multiple apps
|
||||
{
|
||||
"ApplicationDisabled": false, // Required but can be false
|
||||
"Secret": "A really secret random string", // Required
|
||||
"Key": "A random Key string", // Required
|
||||
"OnlySSL": false, // Required but can be false
|
||||
"Name": "The app name", // Required
|
||||
"AppID": "The app ID", // Required
|
||||
"UserEvents": true, // Required but can be false
|
||||
"WebHooks": true, // Required but can be false
|
||||
"URLWebHook": "Some URL to send webhooks" // Required if WebHooks is true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Libraries
|
||||
|
||||
### Client javascript library
|
||||
|
||||
```javascript
|
||||
var pusher = new Pusher(APP_KEY, {
|
||||
wsHost: 'localhost',
|
||||
wsPort: 8080,
|
||||
wssPort: 4433, // Required if encrypted is true
|
||||
encrypted: false, // Optional. the application must use only SSL connections
|
||||
enabledTransports: ["ws", "flash"],
|
||||
disabledTransports: ["flash"]
|
||||
});
|
||||
```
|
||||
|
||||
### Client server libraries
|
||||
|
||||
Ruby
|
||||
|
||||
```ruby
|
||||
Pusher.host = 'localhost'
|
||||
Pusher.port = 8080
|
||||
```
|
||||
|
||||
PHP
|
||||
|
||||
```php
|
||||
$pusher = new Pusher(APP_KEY, APP_SECRET, APP_ID, DEBUG, "http://localhost", "8080");
|
||||
```
|
||||
|
||||
NodeJS
|
||||
|
||||
```javascript
|
||||
var pusher = new Pusher({
|
||||
appId: APP_ID,
|
||||
key: APP_KEY,
|
||||
secret: APP_SECRET
|
||||
domain: 'localhost',
|
||||
port: 80
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
# Logging
|
||||
|
||||
This software uses the [glog](https://github.com/golang/glog) library
|
||||
|
||||
for more information about logging type the following in console.
|
||||
|
||||
```console
|
||||
$ ipe -h
|
||||
```
|
||||
|
||||
# When use this software?
|
||||
|
||||
* When you are offline;
|
||||
* When you want to control your infrastructure;
|
||||
* When you do not want to have external dependencies;
|
||||
* When you want extend the protocol;
|
||||
|
||||
# Contributing.
|
||||
|
||||
Feel free to fork this repo.
|
||||
|
||||
# Pusher
|
||||
|
||||
Pusher is an excelent service, their service is very reliable. I recomend for everyone.
|
||||
|
||||
# Where this name came from?
|
||||
|
||||
Here in Brazil we have this beautiful tree called [Ipê](http://en.wikipedia.org/wiki/Tabebuia_aurea), it comes in differente colors: yellow, pink, white, purple.
|
||||
|
||||
[I want to see pictures](https://www.flickr.com/search/?q=ipe)
|
||||
|
||||
# Author
|
||||
|
||||
Claudemiro Alves Feitosa Neto
|
||||
|
||||
# LICENSE
|
||||
|
||||
Copyright 2014, 2015, 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.
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# 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
|
||||
@@ -0,0 +1,41 @@
|
||||
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
|
||||
@@ -0,0 +1,2 @@
|
||||
client: go run client.go
|
||||
server: go run ../main.go -config ./functional-config.json -alsologtostderr
|
||||
@@ -0,0 +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" />
|
||||
</head>
|
||||
<body>
|
||||
<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="//js.pusher.com/3.2/pusher.min.js"></script>
|
||||
|
||||
<script>mocha.setup('bdd')</script>
|
||||
<script src="test.pusher.js"></script>
|
||||
<script>
|
||||
mocha.checkLeaks();
|
||||
mocha.globals(['jQuery', 'Pusher']);
|
||||
mocha.run();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,66 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/pusher/pusher-http-go"
|
||||
)
|
||||
|
||||
var client pusher.Client
|
||||
|
||||
func init() {
|
||||
client = pusher.Client{
|
||||
AppId: "1",
|
||||
Key: "278d525bdf162c739803",
|
||||
Secret: "7ad3753142a6693b25b9",
|
||||
Host: ":8080",
|
||||
}
|
||||
}
|
||||
|
||||
func pusherPresenceAuth(res http.ResponseWriter, req *http.Request) {
|
||||
log.Println("Presence Request")
|
||||
presenceData := pusher.MemberData{
|
||||
UserId: "1",
|
||||
UserInfo: map[string]string{},
|
||||
}
|
||||
|
||||
params, _ := ioutil.ReadAll(req.Body)
|
||||
response, err := client.AuthenticatePresenceChannel(params, presenceData)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(res, string(response))
|
||||
}
|
||||
|
||||
func pusherPrivateAuth(res http.ResponseWriter, req *http.Request) {
|
||||
params, _ := ioutil.ReadAll(req.Body)
|
||||
response, err := client.AuthenticatePrivateChannel(params)
|
||||
|
||||
log.Printf("Private Request %s", params)
|
||||
log.Printf("Auth %s", response)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(res, string(response))
|
||||
}
|
||||
|
||||
func triggerMessage(res http.ResponseWriter, req *http.Request) {
|
||||
client.Trigger("private-messages", "messages", "The message from server")
|
||||
|
||||
fmt.Fprintf(res, "OK")
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/pusher/presence/auth", pusherPresenceAuth)
|
||||
http.HandleFunc("/pusher/private/auth", pusherPrivateAuth)
|
||||
http.HandleFunc("/trigger", triggerMessage)
|
||||
http.Handle("/", http.FileServer(http.Dir("./")))
|
||||
http.ListenAndServe(":5000", nil)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"Host": ":8080",
|
||||
"Encrypted": false,
|
||||
"SSLHost": ":8090",
|
||||
"SSLKeyFile": "key.pem",
|
||||
"SSLCertFile": "cert.pem",
|
||||
"Apps": [
|
||||
{
|
||||
"ApplicationDisabled": false,
|
||||
"OnlySSL": false,
|
||||
"Secret": "7ad3753142a6693b25b9",
|
||||
"Key": "278d525bdf162c739803",
|
||||
"Name": "App for Functional Test",
|
||||
"AppID": "1",
|
||||
"UserEvents": true,
|
||||
"WebHooks": false,
|
||||
"URLWebHook": "http://127.0.0.1:4567/php/hook.php"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
|
||||
var assert = chai.assert;
|
||||
|
||||
var APP_KEY = "278d525bdf162c739803";
|
||||
var HOST = "localhost";
|
||||
var PORT = 8080;
|
||||
var AUTH = "http://localhost:5000/pusher/private/auth"
|
||||
var AUTH_PRESENCE = "http://localhost:5000/pusher/presence/auth"
|
||||
|
||||
Pusher.log = function(msg) {
|
||||
console.log(msg);
|
||||
};
|
||||
|
||||
function getPusher(auth) {
|
||||
return new Pusher(APP_KEY, {
|
||||
wsHost: HOST,
|
||||
wsPort: PORT,
|
||||
authEndpoint: auth,
|
||||
enabledTransports: ["ws"],
|
||||
disabledTransports: ["flash"],
|
||||
cluster: "hello", // Should be ignored
|
||||
});
|
||||
}
|
||||
|
||||
describe("Pusher", function() {
|
||||
|
||||
describe("connection", function() {
|
||||
it("should connect sucessfully with correct config", function(done) {
|
||||
var pusher = getPusher(AUTH);
|
||||
|
||||
pusher.connection.bind('connected', function() {
|
||||
assert.ok(true, "Connected");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not connect without the correct config", function(done) {
|
||||
var pusher = new Pusher("INVALID_APP_KEY", {
|
||||
wsHost: HOST,
|
||||
wsPort: PORT,
|
||||
enabledTransports: ["ws"],
|
||||
disabledTransports: ["flash"]
|
||||
});
|
||||
|
||||
pusher.connection.bind('disconnected', function() {
|
||||
assert.ok(true, "Not Connected");
|
||||
done();
|
||||
});
|
||||
});
|
||||
}); // connection
|
||||
|
||||
describe("subscription", function() {
|
||||
it("should subscribe to a public channel", function(done) {
|
||||
var pusher = getPusher(AUTH);
|
||||
|
||||
var channel = pusher.subscribe('public-channel');
|
||||
channel.bind("pusher:subscription_succeeded", function(data) {
|
||||
assert.ok(true, "Connected to the channel");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should subscribe to a private channel", function(done) {
|
||||
var pusher = getPusher(AUTH);
|
||||
|
||||
var channel = pusher.subscribe('private-channel');
|
||||
channel.bind("pusher:subscription_succeeded", function(data) {
|
||||
assert.ok(true, "Connected to the channel");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should subscribe to a presence channel", function(done) {
|
||||
var pusher = getPusher(AUTH_PRESENCE);
|
||||
|
||||
var channel = pusher.subscribe('presence-channel');
|
||||
channel.bind("pusher:subscription_succeeded", function(data) {
|
||||
assert.ok(true, "Connected to the channel");
|
||||
done();
|
||||
});
|
||||
});
|
||||
}); // subscription
|
||||
|
||||
describe("events", function() {
|
||||
it('should not allowed client events on public channels', function(done) {
|
||||
var pusher = getPusher(AUTH);
|
||||
var channel = pusher.subscribe('public-channel');
|
||||
|
||||
channel.bind("pusher:subscription_succeeded", function(data) {
|
||||
channel.trigger("client-message", "The message");
|
||||
});
|
||||
|
||||
pusher.bind("pusher:error", function(data) {
|
||||
assert.ok(true, "Expected error");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow client events on private channels', function(done) {
|
||||
var pusher_a = getPusher(AUTH);
|
||||
var pusher_b = getPusher(AUTH);
|
||||
|
||||
var channel_a = pusher_a.subscribe('private-channel');
|
||||
var channel_b = pusher_b.subscribe('private-channel');
|
||||
|
||||
channel_a.bind("pusher:subscription_succeeded", function() {
|
||||
channel_a.trigger("client-message", "The message");
|
||||
});
|
||||
|
||||
channel_b.bind("client-message", function(data) {
|
||||
assert.equal(data, "The message");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should publish event on private channel', function(done) {
|
||||
var pusher_a = getPusher(AUTH);
|
||||
var pusher_b = getPusher(AUTH);
|
||||
|
||||
var channel_a = pusher_a.subscribe('private-messages');
|
||||
var channel_b = pusher_b.subscribe('private-messages');
|
||||
|
||||
channel_a.bind("pusher:subscription_succeeded", function() {
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.open("GET", "/trigger", true);
|
||||
xhttp.send();
|
||||
});
|
||||
|
||||
channel_b.bind("messages", function(data) {
|
||||
assert.equal(data, "The message from server");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
}); // events
|
||||
});
|
||||
Generated
+20
@@ -0,0 +1,20 @@
|
||||
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
@@ -0,0 +1,11 @@
|
||||
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
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 268 B |
-194
@@ -1,194 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Ipe by dimiro1</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="stylesheets/normalize.css" media="screen">
|
||||
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'>
|
||||
<link rel="stylesheet" type="text/css" href="stylesheets/stylesheet.css" media="screen">
|
||||
<link rel="stylesheet" type="text/css" href="stylesheets/github-light.css" media="screen">
|
||||
</head>
|
||||
<body>
|
||||
<section class="page-header">
|
||||
<h1 class="project-name">Ipe</h1>
|
||||
<h2 class="project-tagline">A Pusher server implementation compatible with Pusher client libraries written in GO</h2>
|
||||
<a href="https://github.com/dimiro1/ipe" class="btn">View on GitHub</a>
|
||||
<a href="https://github.com/dimiro1/ipe/zipball/master" class="btn">Download .zip</a>
|
||||
<a href="https://github.com/dimiro1/ipe/tarball/master" class="btn">Download .tar.gz</a>
|
||||
</section>
|
||||
|
||||
<section class="main-content">
|
||||
<p><a href="http://goreportcard.com/report/dimiro1/ipe"><img src="http://goreportcard.com/badge/dimiro1/ipe" alt="Quality code report"></a></p>
|
||||
|
||||
<h1>
|
||||
<a id="ipÊ" class="anchor" href="#ip%C3%8A" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>IPÊ</h1>
|
||||
|
||||
<p>This software is written in Go - the WYSIWYG lang</p>
|
||||
|
||||
<h1>
|
||||
<a id="why-i-wrote-this-software" class="anchor" href="#why-i-wrote-this-software" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Why I wrote this software?</h1>
|
||||
|
||||
<ol>
|
||||
<li>I wanted to learn Go and I needed a non trivial application;</li>
|
||||
<li>I use Pusher in some projects;</li>
|
||||
<li>I really like Pusher;</li>
|
||||
</ol>
|
||||
|
||||
<h1>
|
||||
<a id="features" class="anchor" href="#features" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Features</h1>
|
||||
|
||||
<ul>
|
||||
<li>Public Channels;</li>
|
||||
<li>Private Channels;</li>
|
||||
<li>Presence Channels;</li>
|
||||
<li>Web Hooks;</li>
|
||||
<li>Client events;</li>
|
||||
<li>Complete REST API;</li>
|
||||
<li>Easy instalation;</li>
|
||||
<li>A single binary without dependencies;</li>
|
||||
<li>Easy configuration;</li>
|
||||
<li>Protocol version 7;</li>
|
||||
<li>Multiple apps in the same instance;</li>
|
||||
<li>Drop in replacement for pusher server;</li>
|
||||
</ul>
|
||||
|
||||
<h1>
|
||||
<a id="download-pre-built-binaries" class="anchor" href="#download-pre-built-binaries" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Download pre built binaries</h1>
|
||||
|
||||
<p>You can download pre built binaries from the <a href="https://github.com/dimiro1/ipe/releases">releases tab</a>.</p>
|
||||
|
||||
<p>I do not have a Windows machine, so I can only distribute binaries for amd64 linux and amd64 darwin.</p>
|
||||
|
||||
<h1>
|
||||
<a id="building" class="anchor" href="#building" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Building</h1>
|
||||
|
||||
<div class="highlight highlight-text-shell-session"><pre>$ <span class="pl-s1">go get github.com/dimiro1/ipe</span></pre></div>
|
||||
|
||||
<p>or simply</p>
|
||||
|
||||
<div class="highlight highlight-text-shell-session"><pre>$ <span class="pl-s1">go install github.com/dimiro1/ipe</span></pre></div>
|
||||
|
||||
<h1>
|
||||
<a id="how-to-configure" class="anchor" href="#how-to-configure" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>How to configure?</h1>
|
||||
|
||||
<h2>
|
||||
<a id="the-server" class="anchor" href="#the-server" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>The server</h2>
|
||||
|
||||
<div class="highlight highlight-source-json"><pre>{
|
||||
<span class="pl-s"><span class="pl-pds">"</span>Host<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>:8080<span class="pl-pds">"</span></span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>SSL<span class="pl-pds">"</span></span>: <span class="pl-c1">false</span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>SSLHost<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>:4433<span class="pl-pds">"</span></span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>SSLKeyFile<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>A key.pem file<span class="pl-pds">"</span></span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>SSLCertFile<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>A cert.pem file<span class="pl-pds">"</span></span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>Apps<span class="pl-pds">"</span></span>: [
|
||||
{
|
||||
<span class="pl-s"><span class="pl-pds">"</span>ApplicationDisabled<span class="pl-pds">"</span></span>: <span class="pl-c1">false</span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>Secret<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>A really secret random string<span class="pl-pds">"</span></span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>Key<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>A random Key string<span class="pl-pds">"</span></span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>OnlySSL<span class="pl-pds">"</span></span>: <span class="pl-c1">false</span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>Name<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>The app name<span class="pl-pds">"</span></span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>AppID<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>The app ID<span class="pl-pds">"</span></span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>UserEvents<span class="pl-pds">"</span></span>: <span class="pl-c1">true</span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>WebHooks<span class="pl-pds">"</span></span>: <span class="pl-c1">true</span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>URLWebHook<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>Some URL to send webhooks<span class="pl-pds">"</span></span>
|
||||
}
|
||||
]
|
||||
}
|
||||
</pre></div>
|
||||
|
||||
<h2>
|
||||
<a id="libraries" class="anchor" href="#libraries" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Libraries</h2>
|
||||
|
||||
<h3>
|
||||
<a id="client-javascript-library" class="anchor" href="#client-javascript-library" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Client javascript library</h3>
|
||||
|
||||
<div class="highlight highlight-source-js"><pre><span class="pl-k">var</span> pusher <span class="pl-k">=</span> <span class="pl-k">new</span> <span class="pl-en">Pusher</span>(<span class="pl-c1">APP_KEY</span>, {
|
||||
wsHost<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">'</span>localhost<span class="pl-pds">'</span></span>,
|
||||
wsPort<span class="pl-k">:</span> <span class="pl-c1">8080</span>,
|
||||
enabledTransports<span class="pl-k">:</span> [<span class="pl-s"><span class="pl-pds">"</span>ws<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>flash<span class="pl-pds">"</span></span>],
|
||||
disabledTransports<span class="pl-k">:</span> [<span class="pl-s"><span class="pl-pds">"</span>flash<span class="pl-pds">"</span></span>]
|
||||
});</pre></div>
|
||||
|
||||
<h3>
|
||||
<a id="client-server-libraries" class="anchor" href="#client-server-libraries" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Client server libraries</h3>
|
||||
|
||||
<p>Ruby</p>
|
||||
|
||||
<div class="highlight highlight-source-ruby"><pre><span class="pl-c1">Pusher</span>.host <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">'</span>localhost<span class="pl-pds">'</span></span>
|
||||
<span class="pl-c1">Pusher</span>.port <span class="pl-k">=</span> <span class="pl-c1">8080</span></pre></div>
|
||||
|
||||
<p>PHP</p>
|
||||
|
||||
<div class="highlight highlight-text-html-php"><pre><span class="pl-s1"><span class="pl-smi">$pusher</span> <span class="pl-k">=</span> <span class="pl-k">new</span> <span class="pl-c1">Pusher</span>(<span class="pl-c1">APP_KEY</span>, <span class="pl-c1">APP_SECRET</span>, <span class="pl-c1">APP_ID</span>, <span class="pl-c1">DEBUG</span>, <span class="pl-s"><span class="pl-pds">"</span>http://localhost<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>8080<span class="pl-pds">"</span></span>);</span></pre></div>
|
||||
|
||||
<p>NodeJS</p>
|
||||
|
||||
<div class="highlight highlight-source-js"><pre><span class="pl-k">var</span> pusher <span class="pl-k">=</span> <span class="pl-k">new</span> <span class="pl-en">Pusher</span>({
|
||||
appId<span class="pl-k">:</span> <span class="pl-c1">APP_ID</span>,
|
||||
key<span class="pl-k">:</span> <span class="pl-c1">APP_KEY</span>,
|
||||
secret<span class="pl-k">:</span> <span class="pl-c1">APP_SECRET</span>
|
||||
domain<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">'</span>localhost<span class="pl-pds">'</span></span>,
|
||||
port<span class="pl-k">:</span> <span class="pl-c1">80</span>
|
||||
});
|
||||
</pre></div>
|
||||
|
||||
<h1>
|
||||
<a id="logging" class="anchor" href="#logging" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Logging</h1>
|
||||
|
||||
<p>This software uses the <a href="https://github.com/golang/glog">glog</a> library</p>
|
||||
|
||||
<p>for more information about logging type the following in console.</p>
|
||||
|
||||
<div class="highlight highlight-text-shell-session"><pre>$ <span class="pl-s1">ipe -h</span></pre></div>
|
||||
|
||||
<h1>
|
||||
<a id="when-use-this-software" class="anchor" href="#when-use-this-software" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>When use this software?</h1>
|
||||
|
||||
<ul>
|
||||
<li>When you are offline;</li>
|
||||
<li>When you want to control your infrastructure;</li>
|
||||
<li>When you do not want to have external dependencies;</li>
|
||||
<li>When you want extend the protocol;</li>
|
||||
</ul>
|
||||
|
||||
<h1>
|
||||
<a id="contributing" class="anchor" href="#contributing" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Contributing.</h1>
|
||||
|
||||
<p>Feel free to fork this repo.</p>
|
||||
|
||||
<h1>
|
||||
<a id="pusher" class="anchor" href="#pusher" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Pusher</h1>
|
||||
|
||||
<p>Pusher is an excelent service, their service is very reliable. I recomend for everyone.</p>
|
||||
|
||||
<h1>
|
||||
<a id="where-this-name-came-from" class="anchor" href="#where-this-name-came-from" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Where this name came from?</h1>
|
||||
|
||||
<p>Here in Brazil we have this beautiful tree called <a href="http://en.wikipedia.org/wiki/Tabebuia_aurea">Ipê</a>, it comes in differente colors: yellow, pink, white, purple.</p>
|
||||
|
||||
<p><a href="https://www.flickr.com/search/?q=ipe">I want to see pictures</a></p>
|
||||
|
||||
<h1>
|
||||
<a id="author" class="anchor" href="#author" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Author</h1>
|
||||
|
||||
<p>Claudemiro Alves Feitosa Neto</p>
|
||||
|
||||
<h1>
|
||||
<a id="license" class="anchor" href="#license" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>LICENSE</h1>
|
||||
|
||||
<p>Copyright 2014, 2015, 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.</p>
|
||||
|
||||
<footer class="site-footer">
|
||||
<span class="site-footer-owner"><a href="https://github.com/dimiro1/ipe">Ipe</a> is maintained by <a href="https://github.com/dimiro1">dimiro1</a>.</span>
|
||||
|
||||
<span class="site-footer-credits">This page was generated by <a href="https://pages.github.com">GitHub Pages</a> using the <a href="https://github.com/jasonlong/cayman-theme">Cayman theme</a> by <a href="https://twitter.com/jasonlong">Jason Long</a>.</span>
|
||||
</footer>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Ipe
|
||||
After=syslog.target network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=ipe
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=ipe
|
||||
ExecStart=/path/to/ipe -logtostderr -config="path_to_config.json"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,14 @@
|
||||
description "Ipê Upstart job definition"
|
||||
author "Claudemiro Alves Feitosa Neto"
|
||||
|
||||
start on (net-device-up
|
||||
and local-filesystems
|
||||
and runlevel [2345])
|
||||
|
||||
stop on runlevel [016]
|
||||
|
||||
respawn
|
||||
|
||||
setuid ipe
|
||||
|
||||
exec /path/to/ipe -config="path_to_config.json" -log_dir="path_to_log_dir"
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
// 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
@@ -0,0 +1,252 @@
|
||||
// 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
@@ -0,0 +1,224 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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 "testing"
|
||||
|
||||
func TestIsOccupied(t *testing.T) {
|
||||
c := newChannel("ID")
|
||||
|
||||
if c.IsOccupied() {
|
||||
t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), false)
|
||||
}
|
||||
|
||||
c.Subscriptions["ID"] = newSubscription(newConnection("ID", mockSocket{}), "")
|
||||
|
||||
if !c.IsOccupied() {
|
||||
t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPrivate(t *testing.T) {
|
||||
c := newChannel("private-channel")
|
||||
|
||||
if !c.IsPrivate() {
|
||||
t.Errorf("c.IsPrivate() == %t, wants %t", c.IsPrivate(), true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPresence(t *testing.T) {
|
||||
c := newChannel("presence-channel")
|
||||
|
||||
if !c.IsPresence() {
|
||||
t.Errorf("c.IsPresence() == %t, wants %t", c.IsPresence(), true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPublic(t *testing.T) {
|
||||
c := newChannel("channel")
|
||||
|
||||
if !c.IsPublic() {
|
||||
t.Errorf("c.IsPublic() == %t, wants %t", c.IsPublic(), true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPrivateOrPresence(t *testing.T) {
|
||||
c := newChannel("private-channel")
|
||||
|
||||
if !c.IsPresenceOrPrivate() {
|
||||
t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
|
||||
}
|
||||
|
||||
c = newChannel("presence-channel")
|
||||
|
||||
if !c.IsPresenceOrPrivate() {
|
||||
t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTotalSubscriptions(t *testing.T) {
|
||||
c := newChannel("ID")
|
||||
|
||||
if c.TotalSubscriptions() != len(c.Subscriptions) {
|
||||
t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.Subscriptions))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTotalUsers(t *testing.T) {
|
||||
c := newChannel("ID")
|
||||
|
||||
c.Subscriptions["1"] = newSubscription(newConnection("ID", mockSocket{}), "")
|
||||
c.Subscriptions["2"] = newSubscription(newConnection("ID", mockSocket{}), "")
|
||||
|
||||
if c.TotalSubscriptions() != len(c.Subscriptions) {
|
||||
t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.Subscriptions))
|
||||
}
|
||||
|
||||
if c.TotalUsers() != 1 {
|
||||
t.Errorf("c.TotalUsers() == %d, wants %d", c.TotalUsers(), 1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIsSubscribed(t *testing.T) {
|
||||
c := newChannel("ID")
|
||||
conn := newConnection("ID", mockSocket{})
|
||||
|
||||
if c.IsSubscribed(conn) {
|
||||
t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), false)
|
||||
}
|
||||
|
||||
c.Subscriptions["ID"] = newSubscription(conn, "")
|
||||
|
||||
if !c.IsSubscribed(conn) {
|
||||
t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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"
|
||||
|
||||
// mockSocket is a mock implementation of socket
|
||||
// used in the test suite
|
||||
type mockSocket struct{}
|
||||
|
||||
func (s mockSocket) WriteJSON(i interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestNewConnection(t *testing.T) {
|
||||
expectedSocketID := "socketID"
|
||||
expectedSocket := mockSocket{}
|
||||
|
||||
c := newConnection(expectedSocketID, expectedSocket)
|
||||
|
||||
if c.SocketID != expectedSocketID {
|
||||
t.Errorf("c.SocketID == %s, wants %s", c.SocketID, expectedSocketID)
|
||||
}
|
||||
|
||||
if c.Socket != expectedSocket {
|
||||
t.Errorf("c.Socket == %v, wants %v", c.Socket, expectedSocket)
|
||||
}
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
t.Errorf("c.CreatedAt.IsZero() == %t, wants %t", c.CreatedAt.IsZero(), false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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
|
||||
@@ -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 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")
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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
@@ -0,0 +1,118 @@
|
||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package 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
@@ -0,0 +1,278 @@
|
||||
// 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}
|
||||
}
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
// 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"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
goji "goji.io"
|
||||
|
||||
"goji.io/pat"
|
||||
|
||||
log "github.com/golang/glog"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/dimiro1/ipe/utils"
|
||||
)
|
||||
|
||||
// Prepare Querystring
|
||||
func prepareQueryString(params url.Values) string {
|
||||
var keys []string
|
||||
|
||||
for key := range params {
|
||||
keys = append(keys, strings.ToLower(key))
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
var pieces []string
|
||||
|
||||
for _, key := range keys {
|
||||
pieces = append(pieces, key+"="+params.Get(key))
|
||||
}
|
||||
|
||||
return strings.Join(pieces, "&")
|
||||
}
|
||||
|
||||
// Authenticate pusher
|
||||
// see: https://gist.github.com/mloughran/376898
|
||||
//
|
||||
// The signature is a HMAC SHA256 hex digest.
|
||||
// This is generated by signing a string made up of the following components concatenated with newline characters \n.
|
||||
//
|
||||
// * The uppercase request method (e.g. POST)
|
||||
// * The request path (e.g. /some/resource)
|
||||
// * The query parameters sorted by key, with keys converted to lowercase, then joined as in the query string.
|
||||
// Note that the string must not be url escaped (e.g. given the keys auth_key: foo, Name: Something else, you get auth_key=foo&name=Something else)
|
||||
func restAuthenticationHandler(DB db, next goji.Handler) goji.HandlerFunc {
|
||||
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
appID := pat.Param(ctx, "app_id")
|
||||
|
||||
app, err := DB.GetAppByAppID(appID)
|
||||
|
||||
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.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)
|
||||
return
|
||||
}
|
||||
}()
|
||||
next.ServeHTTPC(ctx, w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// This is conventionally known as triggering an event.
|
||||
//
|
||||
// The body should contain a Hash of parameters encoded as JSON where data parameter itself is JSON encoded.
|
||||
//
|
||||
// Not Implemented:
|
||||
// Note that these parameters may be provided in the query string, although this is discouraged.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// {"name":"foo","channels":["project-3"],"data":"{\"some\":\"data\"}"}
|
||||
//
|
||||
// Response is an empty JSON hash.
|
||||
//
|
||||
// POST /apps/{app_id}/events
|
||||
func (h *postEventsHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
appID := pat.Param(ctx, "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)
|
||||
}
|
||||
|
||||
var input struct {
|
||||
Name string `json:"name"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Channels []string `json:"channels,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
SocketID string `json:"socket_id,omitempty"`
|
||||
}
|
||||
|
||||
err = json.NewDecoder(r.Body).Decode(&input)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// The event data should not be larger than 10KB.
|
||||
if len(input.Data) > maxDataEventSize {
|
||||
http.Error(w, "Request too large.", http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info(input.Channels)
|
||||
if len(input.Channel) > 0 && len(input.Channels) == 0 {
|
||||
input.Channels = append(input.Channels, input.Channel)
|
||||
}
|
||||
|
||||
for _, c := range input.Channels {
|
||||
channel := app.FindOrCreateChannelByChannelID(c)
|
||||
|
||||
app.Publish(channel, rawEvent{Event: input.Name, Channel: c, Data: input.Data}, input.SocketID)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
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),
|
||||
// and optionally one or more attributes for each channel.
|
||||
//
|
||||
// Notes:
|
||||
// 'user_count' is the only attribute documented on the Pusher API
|
||||
//
|
||||
// Example:
|
||||
// {
|
||||
// "channels": {
|
||||
// "presence-foobar": {
|
||||
// user_count: 42
|
||||
// },
|
||||
// "presence-another": {
|
||||
// user_count: 123
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// GET /apps/{app_id}/channels
|
||||
func (h *getChannelsHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
|
||||
appID := pat.Param(ctx, "app_id")
|
||||
filter := query.Get("filter_by_prefix")
|
||||
info := query.Get("info")
|
||||
|
||||
attributes := strings.Split(info, ",")
|
||||
|
||||
requestedUserCount := false
|
||||
|
||||
for _, a := range attributes {
|
||||
if a == "user_count" {
|
||||
requestedUserCount = true
|
||||
}
|
||||
}
|
||||
|
||||
// If an attribute such as user_count is requested, and the request is not limited
|
||||
// to presence channels, the API will return an error (400 code)
|
||||
if requestedUserCount && filter != "presence-" {
|
||||
http.Error(w, "Attribute user_count is restricted to presence channels", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
app, err := h.DB.GetAppByAppID(appID)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
channels := make(map[string]interface{})
|
||||
|
||||
switch filter {
|
||||
case "presence-":
|
||||
for _, c := range app.PresenceChannels() {
|
||||
if requestedUserCount {
|
||||
channels[c.ChannelID] = struct {
|
||||
UserCount int `json:"user_count"`
|
||||
}{
|
||||
c.TotalUsers(),
|
||||
}
|
||||
} else {
|
||||
channels[c.ChannelID] = struct{}{}
|
||||
}
|
||||
}
|
||||
case "public-":
|
||||
for _, c := range app.PublicChannels() {
|
||||
channels[c.ChannelID] = struct{}{}
|
||||
}
|
||||
case "private-":
|
||||
for _, c := range app.PrivateChannels() {
|
||||
channels[c.ChannelID] = struct{}{}
|
||||
}
|
||||
default:
|
||||
for _, c := range app.Channels {
|
||||
channels[c.ChannelID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
js := make(map[string]interface{}, 1)
|
||||
js["channels"] = channels
|
||||
|
||||
if err := json.NewEncoder(w).Encode(js); err != nil {
|
||||
log.Error(err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func newGetChannelHandler(DB db) goji.HandlerFunc {
|
||||
return commonHandlers(DB, &getChannelHandler{DB})
|
||||
}
|
||||
|
||||
type getChannelHandler struct{ DB db }
|
||||
|
||||
// Fetch info for one channel
|
||||
//
|
||||
// Example:
|
||||
// {
|
||||
// occupied: true,
|
||||
// user_count: 42,
|
||||
// subscription_count: 42
|
||||
// }
|
||||
//
|
||||
// GET /apps/{app_id}/channels/{channel_name}
|
||||
func (h *getChannelHandler) ServeHTTPC(ctx context.Context, 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")
|
||||
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")
|
||||
|
||||
// Channel name could not be empty
|
||||
if strings.TrimSpace(channelName) == "" {
|
||||
http.Error(w, "Empty channel name", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
info := query.Get("info")
|
||||
attributes := strings.Split(info, ",")
|
||||
|
||||
// Attributes requested
|
||||
requestedUserCount := false
|
||||
requestedSubscriptionCount := false
|
||||
|
||||
for _, a := range attributes {
|
||||
switch a {
|
||||
case "subscription_count":
|
||||
requestedSubscriptionCount = true
|
||||
case "user_count":
|
||||
requestedUserCount = true
|
||||
}
|
||||
}
|
||||
|
||||
channel, err := app.FindChannelByChannelID(channelName)
|
||||
|
||||
// Channel exists?
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Could not find a channel with id %s", channelName), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// If an attribute such as user_count is requested, and the request is not limited
|
||||
// to presence channels, the API will return an error (400 code)
|
||||
if requestedUserCount && !channel.IsPresence() {
|
||||
http.Error(w, "Attribute user_count is restricted to presence channels", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Output
|
||||
dtoChannel := struct {
|
||||
Occupied bool `json:"occupied"`
|
||||
UserCount int `json:"user_count,omitempty"`
|
||||
SubscriptionCount int `json:"subscription_count,omitempty"`
|
||||
}{Occupied: channel.IsOccupied()}
|
||||
|
||||
switch {
|
||||
case requestedSubscriptionCount && requestedUserCount:
|
||||
dtoChannel.UserCount = channel.TotalUsers()
|
||||
dtoChannel.SubscriptionCount = channel.TotalSubscriptions()
|
||||
|
||||
case requestedUserCount:
|
||||
dtoChannel.UserCount = channel.TotalUsers()
|
||||
|
||||
case requestedSubscriptionCount:
|
||||
dtoChannel.SubscriptionCount = channel.TotalSubscriptions()
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(dtoChannel); err != nil {
|
||||
log.Error(err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func newGetChannelUsersHandler(DB db) goji.HandlerFunc {
|
||||
return commonHandlers(DB, &getChannelUsersHandler{DB})
|
||||
}
|
||||
|
||||
type getChannelUsersHandler struct{ DB db }
|
||||
|
||||
// Allowed only for presence-channels
|
||||
//
|
||||
// Example:
|
||||
// {
|
||||
// "users": [
|
||||
// { "id": "1" },
|
||||
// { "id": "2" }
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// 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")
|
||||
|
||||
isPresence := utils.IsPresenceChannel(channelName)
|
||||
|
||||
if !isPresence {
|
||||
http.Error(w, "This api endpoint is restricted to presence channels.", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Get the channel
|
||||
channel, err := app.FindChannelByChannelID(channelName)
|
||||
|
||||
// Channel exists?
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Could not find a channel with id %s", channelName), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
result := make(map[string][]interface{})
|
||||
|
||||
var users []interface{}
|
||||
|
||||
for _, s := range channel.Subscriptions {
|
||||
users = append(users, struct {
|
||||
ID string `json:"id"`
|
||||
}{s.ID})
|
||||
}
|
||||
|
||||
result["users"] = users
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(result); err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"goji.io/pattern"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var (
|
||||
testApp *app
|
||||
database db
|
||||
)
|
||||
|
||||
func init() {
|
||||
testApp = newTestApp()
|
||||
|
||||
channel := newChannel("presence-c1")
|
||||
testApp.AddChannel(channel)
|
||||
testApp.AddChannel(newChannel("c2"))
|
||||
testApp.AddChannel(newChannel("private-c3"))
|
||||
|
||||
conn := newConnection("123.456", mockSocket{})
|
||||
testApp.Subscribe(channel, conn, "{}")
|
||||
|
||||
conn = newConnection("321.654", mockSocket{})
|
||||
testApp.Subscribe(channel, conn, "{}")
|
||||
|
||||
db := newMemdb()
|
||||
db.AddApp(testApp)
|
||||
|
||||
database = db
|
||||
}
|
||||
|
||||
// All Channels
|
||||
func Test_getChannels_all(t *testing.T) {
|
||||
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)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
json.Unmarshal(w.Body.Bytes(), &data)
|
||||
|
||||
channels := data["channels"].(map[string]interface{})
|
||||
|
||||
if len(channels) != 3 {
|
||||
t.Errorf("len(%q) == %d, want %d", channels, len(channels), 3)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-", appID), nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
json.Unmarshal(w.Body.Bytes(), &data)
|
||||
|
||||
channels := data["channels"].(map[string]interface{})
|
||||
|
||||
if len(channels) != 1 {
|
||||
t.Errorf("len(%q) == %d, want %d", channels, len(channels), 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Only presence channels and user_count
|
||||
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)
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-&info=user_count", appID), nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
json.Unmarshal(w.Body.Bytes(), &data)
|
||||
|
||||
channels := data["channels"].(map[string]interface{})
|
||||
|
||||
if len(channels) != 1 {
|
||||
t.Errorf("len(%q) == %d, want %d", channels, len(channels), 1)
|
||||
}
|
||||
|
||||
c, exists := channels["presence-c1"]
|
||||
|
||||
if !exists {
|
||||
t.Errorf("!exists == %t, want %t", !exists, false)
|
||||
}
|
||||
|
||||
_channel := c.(map[string]interface{})
|
||||
|
||||
if _channel["user_count"] != float64(1) {
|
||||
t.Errorf("_channel['user_count'] == %f, want %d", _channel["user_count"], 1)
|
||||
}
|
||||
}
|
||||
|
||||
// User count only alowed in Presence channels
|
||||
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)
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-&info=user_count", appID), nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=public-", appID), nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
|
||||
json.Unmarshal(w.Body.Bytes(), &data)
|
||||
|
||||
channels := data["channels"].(map[string]interface{})
|
||||
|
||||
if len(channels) != 1 {
|
||||
t.Errorf("len(%q) == %d, want %d", channels, len(channels), 1)
|
||||
}
|
||||
|
||||
_, exists := channels["c2"]
|
||||
|
||||
if !exists {
|
||||
t.Errorf("!exists == %t, want %t", !exists, false)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-", appID), nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
|
||||
json.Unmarshal(w.Body.Bytes(), &data)
|
||||
|
||||
channels := data["channels"].(map[string]interface{})
|
||||
|
||||
if len(channels) != 1 {
|
||||
t.Errorf("len(%q) == %d, want %d", channels, len(channels), 1)
|
||||
}
|
||||
|
||||
_, exists := channels["private-c3"]
|
||||
|
||||
if !exists {
|
||||
t.Errorf("!exists == %t, want %t", !exists, false)
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
// 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))
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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}
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/dimiro1/ipe/utils"
|
||||
log "github.com/golang/glog"
|
||||
)
|
||||
|
||||
// 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:
|
||||
// {
|
||||
// "time_ms": 1327078148132
|
||||
// "events": [
|
||||
// { "name": "event_name", "some": "data" }
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// Security
|
||||
// Encryption
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// Since anyone could in principle send WebHooks to your application, it’s important to verify that these WebHooks originated from Pusher. Valid WebHooks will therefore contain these headers which contain a HMAC signature of the WebHook payload (body):
|
||||
//
|
||||
// X-Pusher-Key: The App Key.
|
||||
// X-Pusher-Signature: A HMAC SHA256 hex digest formed by signing the POST payload (body) with the token’s secret.
|
||||
type webHook struct {
|
||||
TimeMs int64 `json:"time_ms"`
|
||||
Events []hookEvent `json:"events"`
|
||||
}
|
||||
|
||||
type hookEvent struct {
|
||||
Name string `json:"name"`
|
||||
Channel string `json:"channel"`
|
||||
Event string `json:"event,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
SocketID string `json:"socket_id,omitempty"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
func newChannelOcuppiedHook(channel *channel) hookEvent {
|
||||
return hookEvent{Name: "channel_occupied", Channel: channel.ChannelID}
|
||||
}
|
||||
|
||||
func newChannelVacatedHook(channel *channel) hookEvent {
|
||||
return hookEvent{Name: "channel_vacated", Channel: channel.ChannelID}
|
||||
}
|
||||
|
||||
func newMemberAddedHook(channel *channel, s *subscription) hookEvent {
|
||||
return hookEvent{Name: "member_added", Channel: channel.ChannelID, UserID: s.ID}
|
||||
}
|
||||
|
||||
func newMemberRemovedHook(channel *channel, s *subscription) hookEvent {
|
||||
return hookEvent{Name: "member_removed", Channel: channel.ChannelID, UserID: s.ID}
|
||||
}
|
||||
|
||||
func newClientHook(channel *channel, s *subscription, event string, data interface{}) hookEvent {
|
||||
return hookEvent{Name: "client_event", Channel: channel.ChannelID, Event: event, Data: data, SocketID: s.Connection.SocketID}
|
||||
}
|
||||
|
||||
// channel_occupied
|
||||
// { "name": "channel_occupied", "channel": "test_channel" }
|
||||
func (a *app) TriggerChannelOccupiedHook(c *channel) {
|
||||
event := newChannelOcuppiedHook(c)
|
||||
triggerHook(event.Name, a, c, event)
|
||||
}
|
||||
|
||||
// channel_vacated
|
||||
// { "name": "channel_vacated", "channel": "test_channel" }
|
||||
func (a *app) TriggerChannelVacatedHook(c *channel) {
|
||||
event := newChannelVacatedHook(c)
|
||||
triggerHook(event.Name, a, c, event)
|
||||
}
|
||||
|
||||
// {
|
||||
// "name": "client_event",
|
||||
// "channel": "name of the channel the event was published on",
|
||||
// "event": "name of the event",
|
||||
// "data": "data associated with the event",
|
||||
// "socket_id": "socket_id of the sending socket",
|
||||
// "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{}) {
|
||||
event := newClientHook(c, s, clientEvent, data)
|
||||
|
||||
if c.IsPresence() {
|
||||
event.UserID = s.ID
|
||||
}
|
||||
|
||||
triggerHook(event.Name, a, c, event)
|
||||
}
|
||||
|
||||
// {
|
||||
// "name": "member_added",
|
||||
// "channel": "presence-your_channel_name",
|
||||
// "user_id": "a_user_id"
|
||||
// }
|
||||
func (a *app) TriggerMemberAddedHook(c *channel, s *subscription) {
|
||||
event := newMemberAddedHook(c, s)
|
||||
triggerHook(event.Name, a, c, event)
|
||||
}
|
||||
|
||||
// {
|
||||
// "name": "member_removed",
|
||||
// "channel": "presence-your_channel_name",
|
||||
// "user_id": "a_user_id"
|
||||
// }
|
||||
func (a *app) TriggerMemberRemovedHook(c *channel, s *subscription) {
|
||||
event := newMemberRemovedHook(c, s)
|
||||
triggerHook(event.Name, a, c, event)
|
||||
}
|
||||
|
||||
func triggerHook(name string, a *app, c *channel, event hookEvent) {
|
||||
if !a.WebHooks {
|
||||
log.Infof("Webhooks are not enabled for app: %s", a.Name)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Infof("Triggering %s event", name)
|
||||
|
||||
hook := webHook{TimeMs: time.Now().Unix()}
|
||||
|
||||
hook.Events = append(hook.Events, event)
|
||||
|
||||
var js []byte
|
||||
var err error
|
||||
|
||||
js, err = json.Marshal(hook)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Error decoding json: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
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.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)
|
||||
req.Header.Set("X-Pusher-Signature", utils.HashMAC(js, []byte(a.Secret)))
|
||||
|
||||
log.V(1).Infof("%+v", req.Header)
|
||||
log.V(1).Infof("%+v", string(js))
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
|
||||
// See: http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#close_http_resp_body
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Error posting %s event: %+v", name, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
console.log('This would be the main JS file.');
|
||||
@@ -1,17 +0,0 @@
|
||||
var metas = document.getElementsByTagName('meta');
|
||||
var i;
|
||||
if (navigator.userAgent.match(/iPhone/i)) {
|
||||
for (i=0; i<metas.length; i++) {
|
||||
if (metas[i].name == "viewport") {
|
||||
metas[i].content = "width=device-width, minimum-scale=1.0, maximum-scale=1.0";
|
||||
}
|
||||
}
|
||||
document.addEventListener("gesturestart", gestureStart, false);
|
||||
}
|
||||
function gestureStart() {
|
||||
for (i=0; i<metas.length; i++) {
|
||||
if (metas[i].name == "viewport") {
|
||||
metas[i].content = "width=device-width, minimum-scale=0.25, maximum-scale=1.6";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/dimiro1/ipe/ipe"
|
||||
)
|
||||
|
||||
// These variables are generated by the linker
|
||||
// please see the makefile for mor information.
|
||||
var (
|
||||
version = "version"
|
||||
buildstamp = "buildstamp"
|
||||
githash = "githash"
|
||||
)
|
||||
|
||||
// Main function, initialize the system
|
||||
func main() {
|
||||
var filename = flag.String("config", "config.json", "Config file location")
|
||||
flag.Parse()
|
||||
|
||||
printBanner()
|
||||
|
||||
ipe.Start(*filename)
|
||||
}
|
||||
|
||||
// Print a beautiful banner
|
||||
func printBanner() {
|
||||
fmt.Print("\033[31m")
|
||||
fmt.Print(`
|
||||
d8b
|
||||
Y8P
|
||||
|
||||
888 88888b. .d88b.
|
||||
888 888 "88b d8P Y8b
|
||||
888 888 888 88888888
|
||||
888 888 d88P Y8b.
|
||||
888 88888P" "Y8888
|
||||
888
|
||||
888
|
||||
888
|
||||
`)
|
||||
fmt.Println("\033[0m")
|
||||
fmt.Println("\033[32mWelcome to Ipê - Yet another Pusher server clone (https://github.com/dimiro1/ipe)\033[0m")
|
||||
fmt.Printf("\033[32mVersion %s+%s.git.%s\033[0m\n", version, buildstamp, githash)
|
||||
fmt.Println("\033[33mBy: Claudemiro Alves Feitosa Neto <[email protected]>\033[0m")
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"name":"Ipe","tagline":"A Pusher server implementation compatible with Pusher client libraries written in GO","body":"[](http://goreportcard.com/report/dimiro1/ipe)\r\n\r\n# IPÊ\r\n\r\nThis software is written in Go - the WYSIWYG lang\r\n\r\n# Why I wrote this software?\r\n\r\n1. I wanted to learn Go and I needed a non trivial application;\r\n2. I use Pusher in some projects;\r\n3. I really like Pusher;\r\n\r\n# Features\r\n\r\n* Public Channels;\r\n* Private Channels;\r\n* Presence Channels;\r\n* Web Hooks;\r\n* Client events;\r\n* Complete REST API;\r\n* Easy instalation;\r\n* A single binary without dependencies;\r\n* Easy configuration;\r\n* Protocol version 7;\r\n* Multiple apps in the same instance;\r\n* Drop in replacement for pusher server;\r\n\r\n# Download pre built binaries\r\n\r\nYou can download pre built binaries from the [releases tab](https://github.com/dimiro1/ipe/releases).\r\n\r\nI do not have a Windows machine, so I can only distribute binaries for amd64 linux and amd64 darwin.\r\n\r\n# Building\r\n\r\n```console\r\n$ go get github.com/dimiro1/ipe\r\n```\r\n\r\nor simply\r\n\r\n```console\r\n$ go install github.com/dimiro1/ipe\r\n```\r\n\r\n# How to configure?\r\n\r\n## The server\r\n\r\n```json\r\n{\r\n\t\"Host\": \":8080\",\r\n\t\"SSL\": false,\r\n\t\"SSLHost\": \":4433\",\r\n\t\"SSLKeyFile\": \"A key.pem file\",\r\n\t\"SSLCertFile\": \"A cert.pem file\",\r\n\t\"Apps\": [\r\n\t\t{\r\n\t\t\t\"ApplicationDisabled\": false,\r\n\t\t\t\"Secret\": \"A really secret random string\",\r\n\t\t\t\"Key\": \"A random Key string\",\r\n\t\t\t\"OnlySSL\": false,\r\n\t\t\t\"Name\": \"The app name\",\r\n\t\t\t\"AppID\": \"The app ID\",\r\n\t\t\t\"UserEvents\": true,\r\n\t\t\t\"WebHooks\": true,\r\n\t\t\t\"URLWebHook\": \"Some URL to send webhooks\"\r\n\t\t}\r\n\t]\r\n}\r\n\r\n```\r\n\r\n## Libraries\r\n\r\n### Client javascript library\r\n\r\n```javascript\r\nvar pusher = new Pusher(APP_KEY, {\r\n wsHost: 'localhost',\r\n wsPort: 8080,\r\n enabledTransports: [\"ws\", \"flash\"],\r\n disabledTransports: [\"flash\"]\r\n});\r\n```\r\n\r\n### Client server libraries\r\n\r\nRuby\r\n\r\n```ruby\r\nPusher.host = 'localhost'\r\nPusher.port = 8080\r\n```\r\n\r\nPHP\r\n\r\n```php\r\n$pusher = new Pusher(APP_KEY, APP_SECRET, APP_ID, DEBUG, \"http://localhost\", \"8080\");\r\n```\r\n\r\nNodeJS\r\n\r\n```javascript\r\nvar pusher = new Pusher({\r\n appId: APP_ID,\r\n key: APP_KEY,\r\n secret: APP_SECRET\r\n domain: 'localhost',\r\n port: 80\r\n});\r\n\r\n```\r\n\r\n# Logging\r\n\r\nThis software uses the [glog](https://github.com/golang/glog) library\r\n\r\nfor more information about logging type the following in console.\r\n\r\n```console\r\n$ ipe -h\r\n```\r\n\r\n# When use this software?\r\n\r\n* When you are offline;\r\n* When you want to control your infrastructure;\r\n* When you do not want to have external dependencies;\r\n* When you want extend the protocol;\r\n\r\n# Contributing.\r\n\r\nFeel free to fork this repo.\r\n\r\n# Pusher\r\n\r\nPusher is an excelent service, their service is very reliable. I recomend for everyone.\r\n\r\n# Where this name came from?\r\n\r\nHere in Brazil we have this beautiful tree called [Ipê](http://en.wikipedia.org/wiki/Tabebuia_aurea), it comes in differente colors: yellow, pink, white, purple.\r\n\r\n[I want to see pictures](https://www.flickr.com/search/?q=ipe)\r\n\r\n# Author\r\n\r\nClaudemiro Alves Feitosa Neto\r\n\r\n# LICENSE\r\n\r\nCopyright 2014, 2015, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.\r\nUse of this source code is governed by a MIT-style\r\nlicense that can be found in the LICENSE file.\r\n\r\n","google":"","note":"Don't delete this file! It's used internally to help with page regeneration."}
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 GitHub Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
*/
|
||||
|
||||
.pl-c /* comment */ {
|
||||
color: #969896;
|
||||
}
|
||||
|
||||
.pl-c1 /* constant, markup.raw, meta.diff.header, meta.module-reference, meta.property-name, support, support.constant, support.variable, variable.other.constant */,
|
||||
.pl-s .pl-v /* string variable */ {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.pl-e /* entity */,
|
||||
.pl-en /* entity.name */ {
|
||||
color: #795da3;
|
||||
}
|
||||
|
||||
.pl-s .pl-s1 /* string source */,
|
||||
.pl-smi /* storage.modifier.import, storage.modifier.package, storage.type.java, variable.other, variable.parameter.function */ {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.pl-ent /* entity.name.tag */ {
|
||||
color: #63a35c;
|
||||
}
|
||||
|
||||
.pl-k /* keyword, storage, storage.type */ {
|
||||
color: #a71d5d;
|
||||
}
|
||||
|
||||
.pl-pds /* punctuation.definition.string, string.regexp.character-class */,
|
||||
.pl-s /* string */,
|
||||
.pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */,
|
||||
.pl-sr /* string.regexp */,
|
||||
.pl-sr .pl-cce /* string.regexp constant.character.escape */,
|
||||
.pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */,
|
||||
.pl-sr .pl-sre /* string.regexp source.ruby.embedded */ {
|
||||
color: #183691;
|
||||
}
|
||||
|
||||
.pl-v /* variable */ {
|
||||
color: #ed6a43;
|
||||
}
|
||||
|
||||
.pl-id /* invalid.deprecated */ {
|
||||
color: #b52a1d;
|
||||
}
|
||||
|
||||
.pl-ii /* invalid.illegal */ {
|
||||
background-color: #b52a1d;
|
||||
color: #f8f8f8;
|
||||
}
|
||||
|
||||
.pl-sr .pl-cce /* string.regexp constant.character.escape */ {
|
||||
color: #63a35c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pl-ml /* markup.list */ {
|
||||
color: #693a17;
|
||||
}
|
||||
|
||||
.pl-mh /* markup.heading */,
|
||||
.pl-mh .pl-en /* markup.heading entity.name */,
|
||||
.pl-ms /* meta.separator */ {
|
||||
color: #1d3e81;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pl-mq /* markup.quote */ {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.pl-mi /* markup.italic */ {
|
||||
color: #333;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.pl-mb /* markup.bold */ {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pl-md /* markup.deleted, meta.diff.header.from-file */ {
|
||||
background-color: #ffecec;
|
||||
color: #bd2c00;
|
||||
}
|
||||
|
||||
.pl-mi1 /* markup.inserted, meta.diff.header.to-file */ {
|
||||
background-color: #eaffea;
|
||||
color: #55a532;
|
||||
}
|
||||
|
||||
.pl-mdr /* meta.diff.range */ {
|
||||
color: #795da3;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pl-mo /* meta.output */ {
|
||||
color: #1d3e81;
|
||||
}
|
||||
|
||||
Vendored
-424
@@ -1,424 +0,0 @@
|
||||
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
|
||||
|
||||
/**
|
||||
* 1. Set default font family to sans-serif.
|
||||
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
||||
* user zoom.
|
||||
*/
|
||||
|
||||
html {
|
||||
font-family: sans-serif; /* 1 */
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default margin.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* HTML5 display definitions
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Correct `block` display not defined for any HTML5 element in IE 8/9.
|
||||
* Correct `block` display not defined for `details` or `summary` in IE 10/11
|
||||
* and Firefox.
|
||||
* Correct `block` display not defined for `main` in IE 11.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
menu,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `inline-block` display not defined in IE 8/9.
|
||||
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
progress,
|
||||
video {
|
||||
display: inline-block; /* 1 */
|
||||
vertical-align: baseline; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent modern browsers from displaying `audio` without controls.
|
||||
* Remove excess height in iOS 5 devices.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `[hidden]` styling not present in IE 8/9/10.
|
||||
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
|
||||
*/
|
||||
|
||||
[hidden],
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Links
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background color from active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve readability when focused and also mouse hovered in all browsers.
|
||||
*/
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in Safari and Chrome.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address variable `h1` font-size and margin within `section` and `article`
|
||||
* contexts in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent and variable font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove border when inside `a` element in IE 8/9/10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct overflow not hidden in IE 9/10/11.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address margin not present in IE 8/9 and Safari.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address differences between Firefox and other browsers.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contain overflow in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address odd `em`-unit font size rendering in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Known limitation: by default, Chrome and Safari on OS X allow very limited
|
||||
* styling of `select`, unless a `border` property is set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 1. Correct color not being inherited.
|
||||
* Known issue: affects color of disabled elements.
|
||||
* 2. Correct font properties not being inherited.
|
||||
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
color: inherit; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
margin: 0; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
button {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||
* All other form control elements do not inherit `text-transform` values.
|
||||
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
|
||||
* Correct `select` style inheritance in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||
* and `video` controls.
|
||||
* 2. Correct inability to style clickable `input` types in iOS.
|
||||
* 3. Improve usability and consistency of cursor style between image-type
|
||||
* `input` and others.
|
||||
*/
|
||||
|
||||
button,
|
||||
html input[type="button"], /* 1 */
|
||||
input[type="reset"],
|
||||
input[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
cursor: pointer; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-set default cursor for disabled elements.
|
||||
*/
|
||||
|
||||
button[disabled],
|
||||
html input[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and border in Firefox 4+.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||
* the UA stylesheet.
|
||||
*/
|
||||
|
||||
input {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended that you don't attempt to style these elements.
|
||||
* Firefox's implementation doesn't respect box-sizing, padding, or width.
|
||||
*
|
||||
* 1. Address box sizing set to `content-box` in IE 8/9/10.
|
||||
* 2. Remove excess padding in IE 8/9/10.
|
||||
*/
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
|
||||
* `font-size` values of the `input`, it causes the cursor style of the
|
||||
* decrement button to change from `default` to `text`.
|
||||
*/
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
|
||||
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
|
||||
* (include `-moz` to future-proof).
|
||||
*/
|
||||
|
||||
input[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */ /* 2 */
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
|
||||
* Safari (but not Chrome) clips the cancel button when the search input has
|
||||
* padding (and `textfield` appearance).
|
||||
*/
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define consistent border, margin, and padding.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #c0c0c0;
|
||||
margin: 0 2px;
|
||||
padding: 0.35em 0.625em 0.75em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `color` not being inherited in IE 8/9/10/11.
|
||||
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||
*/
|
||||
|
||||
legend {
|
||||
border: 0; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default vertical scrollbar in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't inherit the `font-weight` (applied by a rule above).
|
||||
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
|
||||
*/
|
||||
|
||||
optgroup {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Tables
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove most spacing between table cells.
|
||||
*/
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
.highlight .c { color: #999988; font-style: italic } /* Comment */
|
||||
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
|
||||
.highlight .k { font-weight: bold } /* Keyword */
|
||||
.highlight .o { font-weight: bold } /* Operator */
|
||||
.highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */
|
||||
.highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */
|
||||
.highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */
|
||||
.highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */
|
||||
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
|
||||
.highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */
|
||||
.highlight .ge { font-style: italic } /* Generic.Emph */
|
||||
.highlight .gr { color: #aa0000 } /* Generic.Error */
|
||||
.highlight .gh { color: #999999 } /* Generic.Heading */
|
||||
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
|
||||
.highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */
|
||||
.highlight .go { color: #888888 } /* Generic.Output */
|
||||
.highlight .gp { color: #555555 } /* Generic.Prompt */
|
||||
.highlight .gs { font-weight: bold } /* Generic.Strong */
|
||||
.highlight .gu { color: #800080; font-weight: bold; } /* Generic.Subheading */
|
||||
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
|
||||
.highlight .kc { font-weight: bold } /* Keyword.Constant */
|
||||
.highlight .kd { font-weight: bold } /* Keyword.Declaration */
|
||||
.highlight .kn { font-weight: bold } /* Keyword.Namespace */
|
||||
.highlight .kp { font-weight: bold } /* Keyword.Pseudo */
|
||||
.highlight .kr { font-weight: bold } /* Keyword.Reserved */
|
||||
.highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */
|
||||
.highlight .m { color: #009999 } /* Literal.Number */
|
||||
.highlight .s { color: #d14 } /* Literal.String */
|
||||
.highlight .na { color: #008080 } /* Name.Attribute */
|
||||
.highlight .nb { color: #0086B3 } /* Name.Builtin */
|
||||
.highlight .nc { color: #445588; font-weight: bold } /* Name.Class */
|
||||
.highlight .no { color: #008080 } /* Name.Constant */
|
||||
.highlight .ni { color: #800080 } /* Name.Entity */
|
||||
.highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */
|
||||
.highlight .nf { color: #990000; font-weight: bold } /* Name.Function */
|
||||
.highlight .nn { color: #555555 } /* Name.Namespace */
|
||||
.highlight .nt { color: #CBDFFF } /* Name.Tag */
|
||||
.highlight .nv { color: #008080 } /* Name.Variable */
|
||||
.highlight .ow { font-weight: bold } /* Operator.Word */
|
||||
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
|
||||
.highlight .mf { color: #009999 } /* Literal.Number.Float */
|
||||
.highlight .mh { color: #009999 } /* Literal.Number.Hex */
|
||||
.highlight .mi { color: #009999 } /* Literal.Number.Integer */
|
||||
.highlight .mo { color: #009999 } /* Literal.Number.Oct */
|
||||
.highlight .sb { color: #d14 } /* Literal.String.Backtick */
|
||||
.highlight .sc { color: #d14 } /* Literal.String.Char */
|
||||
.highlight .sd { color: #d14 } /* Literal.String.Doc */
|
||||
.highlight .s2 { color: #d14 } /* Literal.String.Double */
|
||||
.highlight .se { color: #d14 } /* Literal.String.Escape */
|
||||
.highlight .sh { color: #d14 } /* Literal.String.Heredoc */
|
||||
.highlight .si { color: #d14 } /* Literal.String.Interpol */
|
||||
.highlight .sx { color: #d14 } /* Literal.String.Other */
|
||||
.highlight .sr { color: #009926 } /* Literal.String.Regex */
|
||||
.highlight .s1 { color: #d14 } /* Literal.String.Single */
|
||||
.highlight .ss { color: #990073 } /* Literal.String.Symbol */
|
||||
.highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */
|
||||
.highlight .vc { color: #008080 } /* Name.Variable.Class */
|
||||
.highlight .vg { color: #008080 } /* Name.Variable.Global */
|
||||
.highlight .vi { color: #008080 } /* Name.Variable.Instance */
|
||||
.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */
|
||||
|
||||
.type-csharp .highlight .k { color: #0000FF }
|
||||
.type-csharp .highlight .kt { color: #0000FF }
|
||||
.type-csharp .highlight .nf { color: #000000; font-weight: normal }
|
||||
.type-csharp .highlight .nc { color: #2B91AF }
|
||||
.type-csharp .highlight .nn { color: #000000 }
|
||||
.type-csharp .highlight .s { color: #A31515 }
|
||||
.type-csharp .highlight .sc { color: #A31515 }
|
||||
@@ -1,324 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
src: url('../fonts/Noto-Sans-regular/Noto-Sans-regular.eot');
|
||||
src: url('../fonts/Noto-Sans-regular/Noto-Sans-regular.eot?#iefix') format('embedded-opentype'),
|
||||
local('Noto Sans'),
|
||||
local('Noto-Sans-regular'),
|
||||
url('../fonts/Noto-Sans-regular/Noto-Sans-regular.woff2') format('woff2'),
|
||||
url('../fonts/Noto-Sans-regular/Noto-Sans-regular.woff') format('woff'),
|
||||
url('../fonts/Noto-Sans-regular/Noto-Sans-regular.ttf') format('truetype'),
|
||||
url('../fonts/Noto-Sans-regular/Noto-Sans-regular.svg#NotoSans') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
src: url('../fonts/Noto-Sans-700/Noto-Sans-700.eot');
|
||||
src: url('../fonts/Noto-Sans-700/Noto-Sans-700.eot?#iefix') format('embedded-opentype'),
|
||||
local('Noto Sans Bold'),
|
||||
local('Noto-Sans-700'),
|
||||
url('../fonts/Noto-Sans-700/Noto-Sans-700.woff2') format('woff2'),
|
||||
url('../fonts/Noto-Sans-700/Noto-Sans-700.woff') format('woff'),
|
||||
url('../fonts/Noto-Sans-700/Noto-Sans-700.ttf') format('truetype'),
|
||||
url('../fonts/Noto-Sans-700/Noto-Sans-700.svg#NotoSans') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
src: url('../fonts/Noto-Sans-italic/Noto-Sans-italic.eot');
|
||||
src: url('../fonts/Noto-Sans-italic/Noto-Sans-italic.eot?#iefix') format('embedded-opentype'),
|
||||
local('Noto Sans Italic'),
|
||||
local('Noto-Sans-italic'),
|
||||
url('../fonts/Noto-Sans-italic/Noto-Sans-italic.woff2') format('woff2'),
|
||||
url('../fonts/Noto-Sans-italic/Noto-Sans-italic.woff') format('woff'),
|
||||
url('../fonts/Noto-Sans-italic/Noto-Sans-italic.ttf') format('truetype'),
|
||||
url('../fonts/Noto-Sans-italic/Noto-Sans-italic.svg#NotoSans') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
src: url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.eot');
|
||||
src: url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.eot?#iefix') format('embedded-opentype'),
|
||||
local('Noto Sans Bold Italic'),
|
||||
local('Noto-Sans-700italic'),
|
||||
url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.woff2') format('woff2'),
|
||||
url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.woff') format('woff'),
|
||||
url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.ttf') format('truetype'),
|
||||
url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.svg#NotoSans') format('svg');
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #fff;
|
||||
padding:50px;
|
||||
font: 14px/1.5 "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color:#727272;
|
||||
font-weight:400;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color:#222;
|
||||
margin:0 0 20px;
|
||||
}
|
||||
|
||||
p, ul, ol, table, pre, dl {
|
||||
margin:0 0 20px;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
line-height:1.1;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size:28px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color:#393939;
|
||||
}
|
||||
|
||||
h3, h4, h5, h6 {
|
||||
color:#494949;
|
||||
}
|
||||
|
||||
a {
|
||||
color:#39c;
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color:#069;
|
||||
}
|
||||
|
||||
a small {
|
||||
font-size:11px;
|
||||
color:#777;
|
||||
margin-top:-0.3em;
|
||||
display:block;
|
||||
}
|
||||
|
||||
a:hover small {
|
||||
color:#777;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width:860px;
|
||||
margin:0 auto;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left:1px solid #e5e5e5;
|
||||
margin:0;
|
||||
padding:0 0 0 20px;
|
||||
font-style:italic;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal, Consolas, Liberation Mono, DejaVu Sans Mono, Courier New, monospace;
|
||||
color:#333;
|
||||
font-size:12px;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding:8px 15px;
|
||||
background: #f8f8f8;
|
||||
border-radius:5px;
|
||||
border:1px solid #e5e5e5;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width:100%;
|
||||
border-collapse:collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align:left;
|
||||
padding:5px 10px;
|
||||
border-bottom:1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
dt {
|
||||
color:#444;
|
||||
font-weight:700;
|
||||
}
|
||||
|
||||
th {
|
||||
color:#444;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width:100%;
|
||||
}
|
||||
|
||||
header {
|
||||
width:270px;
|
||||
float:left;
|
||||
position:fixed;
|
||||
-webkit-font-smoothing:subpixel-antialiased;
|
||||
}
|
||||
|
||||
header ul {
|
||||
list-style:none;
|
||||
height:40px;
|
||||
padding:0;
|
||||
background: #f4f4f4;
|
||||
border-radius:5px;
|
||||
border:1px solid #e0e0e0;
|
||||
width:270px;
|
||||
}
|
||||
|
||||
header li {
|
||||
width:89px;
|
||||
float:left;
|
||||
border-right:1px solid #e0e0e0;
|
||||
height:40px;
|
||||
}
|
||||
|
||||
header li:first-child a {
|
||||
border-radius:5px 0 0 5px;
|
||||
}
|
||||
|
||||
header li:last-child a {
|
||||
border-radius:0 5px 5px 0;
|
||||
}
|
||||
|
||||
header ul a {
|
||||
line-height:1;
|
||||
font-size:11px;
|
||||
color:#999;
|
||||
display:block;
|
||||
text-align:center;
|
||||
padding-top:6px;
|
||||
height:34px;
|
||||
}
|
||||
|
||||
header ul a:hover {
|
||||
color:#999;
|
||||
}
|
||||
|
||||
header ul a:active {
|
||||
background-color:#f0f0f0;
|
||||
}
|
||||
|
||||
strong {
|
||||
color:#222;
|
||||
font-weight:700;
|
||||
}
|
||||
|
||||
header ul li + li + li {
|
||||
border-right:none;
|
||||
width:89px;
|
||||
}
|
||||
|
||||
header ul a strong {
|
||||
font-size:14px;
|
||||
display:block;
|
||||
color:#222;
|
||||
}
|
||||
|
||||
section {
|
||||
width:500px;
|
||||
float:right;
|
||||
padding-bottom:50px;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size:11px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border:0;
|
||||
background:#e5e5e5;
|
||||
height:1px;
|
||||
margin:0 0 20px;
|
||||
}
|
||||
|
||||
footer {
|
||||
width:270px;
|
||||
float:left;
|
||||
position:fixed;
|
||||
bottom:50px;
|
||||
-webkit-font-smoothing:subpixel-antialiased;
|
||||
}
|
||||
|
||||
@media print, screen and (max-width: 960px) {
|
||||
|
||||
div.wrapper {
|
||||
width:auto;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
header, section, footer {
|
||||
float:none;
|
||||
position:static;
|
||||
width:auto;
|
||||
}
|
||||
|
||||
header {
|
||||
padding-right:320px;
|
||||
}
|
||||
|
||||
section {
|
||||
border:1px solid #e5e5e5;
|
||||
border-width:1px 0;
|
||||
padding:20px 0;
|
||||
margin:0 0 20px;
|
||||
}
|
||||
|
||||
header a small {
|
||||
display:inline;
|
||||
}
|
||||
|
||||
header ul {
|
||||
position:absolute;
|
||||
right:50px;
|
||||
top:52px;
|
||||
}
|
||||
}
|
||||
|
||||
@media print, screen and (max-width: 720px) {
|
||||
body {
|
||||
word-wrap:break-word;
|
||||
}
|
||||
|
||||
header {
|
||||
padding:0;
|
||||
}
|
||||
|
||||
header ul, header p.view {
|
||||
position:static;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
word-wrap:normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media print, screen and (max-width: 480px) {
|
||||
body {
|
||||
padding:15px;
|
||||
}
|
||||
|
||||
header ul {
|
||||
width:99%;
|
||||
}
|
||||
|
||||
header li, header ul li + li + li {
|
||||
width:33%;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
padding:0.4in;
|
||||
font-size:12pt;
|
||||
color:#444;
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
* {
|
||||
box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: #606c71; }
|
||||
|
||||
a {
|
||||
color: #1e6bb8;
|
||||
text-decoration: none; }
|
||||
a:hover {
|
||||
text-decoration: underline; }
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
margin-bottom: 1rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 0.3rem;
|
||||
transition: color 0.2s, background-color 0.2s, border-color 0.2s; }
|
||||
.btn + .btn {
|
||||
margin-left: 1rem; }
|
||||
|
||||
.btn:hover {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-decoration: none;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.3); }
|
||||
|
||||
@media screen and (min-width: 64em) {
|
||||
.btn {
|
||||
padding: 0.75rem 1rem; } }
|
||||
|
||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
||||
.btn {
|
||||
padding: 0.6rem 0.9rem;
|
||||
font-size: 0.9rem; } }
|
||||
|
||||
@media screen and (max-width: 42em) {
|
||||
.btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
font-size: 0.9rem; }
|
||||
.btn + .btn {
|
||||
margin-top: 1rem;
|
||||
margin-left: 0; } }
|
||||
|
||||
.page-header {
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-color: #159957;
|
||||
background-image: linear-gradient(120deg, #155799, #159957); }
|
||||
|
||||
@media screen and (min-width: 64em) {
|
||||
.page-header {
|
||||
padding: 5rem 6rem; } }
|
||||
|
||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
||||
.page-header {
|
||||
padding: 3rem 4rem; } }
|
||||
|
||||
@media screen and (max-width: 42em) {
|
||||
.page-header {
|
||||
padding: 2rem 1rem; } }
|
||||
|
||||
.project-name {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.1rem; }
|
||||
|
||||
@media screen and (min-width: 64em) {
|
||||
.project-name {
|
||||
font-size: 3.25rem; } }
|
||||
|
||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
||||
.project-name {
|
||||
font-size: 2.25rem; } }
|
||||
|
||||
@media screen and (max-width: 42em) {
|
||||
.project-name {
|
||||
font-size: 1.75rem; } }
|
||||
|
||||
.project-tagline {
|
||||
margin-bottom: 2rem;
|
||||
font-weight: normal;
|
||||
opacity: 0.7; }
|
||||
|
||||
@media screen and (min-width: 64em) {
|
||||
.project-tagline {
|
||||
font-size: 1.25rem; } }
|
||||
|
||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
||||
.project-tagline {
|
||||
font-size: 1.15rem; } }
|
||||
|
||||
@media screen and (max-width: 42em) {
|
||||
.project-tagline {
|
||||
font-size: 1rem; } }
|
||||
|
||||
.main-content :first-child {
|
||||
margin-top: 0; }
|
||||
.main-content img {
|
||||
max-width: 100%; }
|
||||
.main-content h1, .main-content h2, .main-content h3, .main-content h4, .main-content h5, .main-content h6 {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: normal;
|
||||
color: #159957; }
|
||||
.main-content p {
|
||||
margin-bottom: 1em; }
|
||||
.main-content code {
|
||||
padding: 2px 4px;
|
||||
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
font-size: 0.9rem;
|
||||
color: #383e41;
|
||||
background-color: #f3f6fa;
|
||||
border-radius: 0.3rem; }
|
||||
.main-content pre {
|
||||
padding: 0.8rem;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
font: 1rem Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
color: #567482;
|
||||
word-wrap: normal;
|
||||
background-color: #f3f6fa;
|
||||
border: solid 1px #dce6f0;
|
||||
border-radius: 0.3rem; }
|
||||
.main-content pre > code {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: #567482;
|
||||
word-break: normal;
|
||||
white-space: pre;
|
||||
background: transparent;
|
||||
border: 0; }
|
||||
.main-content .highlight {
|
||||
margin-bottom: 1rem; }
|
||||
.main-content .highlight pre {
|
||||
margin-bottom: 0;
|
||||
word-break: normal; }
|
||||
.main-content .highlight pre, .main-content pre {
|
||||
padding: 0.8rem;
|
||||
overflow: auto;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.45;
|
||||
border-radius: 0.3rem; }
|
||||
.main-content pre code, .main-content pre tt {
|
||||
display: inline;
|
||||
max-width: initial;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: initial;
|
||||
line-height: inherit;
|
||||
word-wrap: normal;
|
||||
background-color: transparent;
|
||||
border: 0; }
|
||||
.main-content pre code:before, .main-content pre code:after, .main-content pre tt:before, .main-content pre tt:after {
|
||||
content: normal; }
|
||||
.main-content ul, .main-content ol {
|
||||
margin-top: 0; }
|
||||
.main-content blockquote {
|
||||
padding: 0 1rem;
|
||||
margin-left: 0;
|
||||
color: #819198;
|
||||
border-left: 0.3rem solid #dce6f0; }
|
||||
.main-content blockquote > :first-child {
|
||||
margin-top: 0; }
|
||||
.main-content blockquote > :last-child {
|
||||
margin-bottom: 0; }
|
||||
.main-content table {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
word-break: normal;
|
||||
word-break: keep-all; }
|
||||
.main-content table th {
|
||||
font-weight: bold; }
|
||||
.main-content table th, .main-content table td {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #e9ebec; }
|
||||
.main-content dl {
|
||||
padding: 0; }
|
||||
.main-content dl dt {
|
||||
padding: 0;
|
||||
margin-top: 1rem;
|
||||
font-size: 1rem;
|
||||
font-weight: bold; }
|
||||
.main-content dl dd {
|
||||
padding: 0;
|
||||
margin-bottom: 1rem; }
|
||||
.main-content hr {
|
||||
height: 2px;
|
||||
padding: 0;
|
||||
margin: 1rem 0;
|
||||
background-color: #eff0f1;
|
||||
border: 0; }
|
||||
|
||||
@media screen and (min-width: 64em) {
|
||||
.main-content {
|
||||
max-width: 64rem;
|
||||
padding: 2rem 6rem;
|
||||
margin: 0 auto;
|
||||
font-size: 1.1rem; } }
|
||||
|
||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
||||
.main-content {
|
||||
padding: 2rem 4rem;
|
||||
font-size: 1.1rem; } }
|
||||
|
||||
@media screen and (max-width: 42em) {
|
||||
.main-content {
|
||||
padding: 2rem 1rem;
|
||||
font-size: 1rem; } }
|
||||
|
||||
.site-footer {
|
||||
padding-top: 2rem;
|
||||
margin-top: 2rem;
|
||||
border-top: solid 1px #eff0f1; }
|
||||
|
||||
.site-footer-owner {
|
||||
display: block;
|
||||
font-weight: bold; }
|
||||
|
||||
.site-footer-credits {
|
||||
color: #819198; }
|
||||
|
||||
@media screen and (min-width: 64em) {
|
||||
.site-footer {
|
||||
font-size: 1rem; } }
|
||||
|
||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
||||
.site-footer {
|
||||
font-size: 1rem; } }
|
||||
|
||||
@media screen and (max-width: 42em) {
|
||||
.site-footer {
|
||||
font-size: 0.9rem; } }
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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 utils
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var channelValidationRegex = regexp.MustCompile("^[A-Za-z0-9_\\-=@,.;]+$")
|
||||
|
||||
// HashMAC Calculates the MAC signing with the given key and returns the hexadecimal encoded Result
|
||||
func HashMAC(message, key []byte) string {
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write(message)
|
||||
expected := mac.Sum(nil)
|
||||
|
||||
return hex.EncodeToString(expected)
|
||||
}
|
||||
|
||||
// GenerateSessionID Generate a new random Hash
|
||||
func GenerateSessionID() string {
|
||||
return fmt.Sprintf("%d.%d", rand.Intn(math.MaxInt64), rand.Intn(math.MaxInt64))
|
||||
}
|
||||
|
||||
// IsChannelNameValid Verify if the channel name is valid
|
||||
func IsChannelNameValid(channelName string) bool {
|
||||
return channelValidationRegex.MatchString(channelName)
|
||||
}
|
||||
|
||||
// IsPrivateChannel Verify if the channel name represents a private channel
|
||||
func IsPrivateChannel(channelName string) bool {
|
||||
return strings.HasPrefix(channelName, "private-")
|
||||
}
|
||||
|
||||
// IsPresenceChannel Verify if the channel name represents a presence channel
|
||||
func IsPresenceChannel(channelName string) bool {
|
||||
return strings.HasPrefix(channelName, "presence-")
|
||||
}
|
||||
|
||||
// IsClientEvent Verify if the event name represents a client event type
|
||||
func IsClientEvent(event string) bool {
|
||||
return strings.HasPrefix(event, "client-")
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// 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 utils
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkGenerateSession(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
GenerateSessionID()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIsChannelNameValid(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
IsChannelNameValid("hello-world")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateSession(t *testing.T) {
|
||||
sessionID := GenerateSessionID()
|
||||
|
||||
if matched, _ := regexp.MatchString("^\\d+\\.\\d+$", sessionID); !matched {
|
||||
t.Errorf("Must match ^\\d+\\.\\d+$, value: '%s'", sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidChannelName(t *testing.T) {
|
||||
name := "#@#hhh**sasas"
|
||||
ok := IsChannelNameValid(name)
|
||||
|
||||
if ok {
|
||||
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, false)
|
||||
}
|
||||
|
||||
name = "private-hello"
|
||||
ok = IsChannelNameValid(name)
|
||||
|
||||
if !ok {
|
||||
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, true)
|
||||
}
|
||||
|
||||
name = "presence-hello"
|
||||
ok = IsChannelNameValid(name)
|
||||
|
||||
if !ok {
|
||||
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, true)
|
||||
}
|
||||
|
||||
name = "public"
|
||||
ok = IsChannelNameValid(name)
|
||||
|
||||
if !ok {
|
||||
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPrivateChannel_valid(t *testing.T) {
|
||||
name := "private-hello"
|
||||
ok := IsPrivateChannel(name)
|
||||
|
||||
if !ok {
|
||||
t.Errorf("IsPrivateChannel(%s) == %t, wants %t", name, ok, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPrivateChannel_invalid(t *testing.T) {
|
||||
name := "hello"
|
||||
ok := IsPrivateChannel(name)
|
||||
|
||||
if ok {
|
||||
t.Errorf("IsPrivateChannel(%s) == %t, wants %t", name, ok, false)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsIsPresenceChannel_valid(t *testing.T) {
|
||||
name := "presence-hello"
|
||||
ok := IsPresenceChannel(name)
|
||||
|
||||
if !ok {
|
||||
t.Errorf("IsPresenceChannel(%s) == %t, wants %t", name, ok, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPresenceChannel_invalid(t *testing.T) {
|
||||
name := "hello"
|
||||
ok := IsPresenceChannel(name)
|
||||
|
||||
if ok {
|
||||
t.Errorf("IsPresenceChannel(%s) == %t, wants %t", name, ok, false)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsClientEvent_valid(t *testing.T) {
|
||||
name := "client-hello"
|
||||
ok := IsClientEvent(name)
|
||||
|
||||
if !ok {
|
||||
t.Errorf("IsClientEvent(%s) == %t, wants %t", name, ok, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsClientEvent_invalid(t *testing.T) {
|
||||
name := "hello"
|
||||
ok := IsClientEvent(name)
|
||||
|
||||
if ok {
|
||||
t.Errorf("IsClientEvent(%s) == %t, wants %t", name, ok, false)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashMAC(t *testing.T) {
|
||||
message := []byte("hello world")
|
||||
key := []byte("my super secret key")
|
||||
digest := HashMAC(message, key)
|
||||
|
||||
// See: http://www.freeformatter.com/hmac-generator.html
|
||||
expected := "0811b8affc185a01e1a65b80089ebb1f7f68d287fc3b64581da9ec99136ad1db"
|
||||
|
||||
if digest != expected {
|
||||
t.Errorf("HashMAC(%s, %q) == %s, wants %s", message, key, digest, expected)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user