140 Commits
Author SHA1 Message Date
claudemiro 015cbdf07a Release 1.2.0 2016-08-11 21:13:44 -03:00
claudemiro 431a19fe7b Updated go report card badge 2016-08-11 21:10:40 -03:00
ClaudemiroandGitHub 6225d8006f Replaces gorilla mux to goji and decied to remove vendor dependencies from git (#28)
* Replaces gorilla mux to goji

Removed the applicationContext stuff. Now every handler is a struct, and each one hold its dependencies.

* handler not handle

* Ignoring vendor dir

* Hide handlers complexity instantiation.

* Websocket handler does not need common handlers

* Use maps instead of for loops to search in memdb for apps (#29)

* Use maps instead of for loops to search in memdb for apps; use mutexes more efficiently by immediately unlocking when lock is no longer needed, not just at the end of the function via defer

* Fixed assignment to entry in nil map

* Minor impovements in source code

* Replaces gorilla mux to goji

Removed the applicationContext stuff. Now every handler is a struct, and each one hold its dependencies.

* handler not handle

* Ignoring vendor dir

* Hide handlers complexity instantiation.

* Websocket handler does not need common handlers

* renamed IdMutex to IDMutex to follow go convention
2016-08-11 21:06:05 -03:00
ZoltanandClaudemiro 7a4568e92d Use maps instead of for loops to search in memdb for apps (#29)
* Use maps instead of for loops to search in memdb for apps; use mutexes more efficiently by immediately unlocking when lock is no longer needed, not just at the end of the function via defer

* Fixed assignment to entry in nil map

* Minor impovements in source code
2016-08-11 20:57:51 -03:00
claudemiro 6e46bcb2aa Moved location of mockSocket 2016-08-10 20:26:27 -03:00
claudemiro 0c599ea5fb Type definition is not necessary 2016-08-10 20:26:13 -03:00
ClaudemiroandGitHub 46ebd1b62b Merge pull request #26 from tssajo/tssajo5
Added *.sublime-project to .gitignore
2016-08-10 20:14:20 -03:00
tssajo 06449fdb58 Added *.sublime-project to .gitignore 2016-08-10 21:57:10 +02:00
claudemiro 6092cd85ca using regex MatchString instead 2016-08-10 00:17:12 -03:00
claudemiro 9c49ae16d3 Renamed regex to validate the channel name 2016-08-09 23:54:06 -03:00
claudemiro 7a794c97d7 Added util method IsClientEvent 2016-08-09 23:42:44 -03:00
claudemiro f0b3aadf74 Updated dependencies 2016-08-09 23:22:23 -03:00
ClaudemiroandGitHub 2eac54a0d3 Merge pull request #25 from sj26/fix-pusher-submodule
Fix submodule reference
2016-08-07 10:11:25 -03:00
ClaudemiroandGitHub a6f44d2175 Merge pull request #24 from tssajo/tssajo4
Fixed a bug in ipe/websocket.go
2016-08-07 10:11:02 -03:00
ClaudemiroandGitHub 6c479ced3c Merge pull request #22 from tssajo/tssajo3
Do not overuse defer unnecessarily
2016-08-07 10:10:18 -03:00
ClaudemiroandGitHub 178bbc9b8e Merge pull request #20 from tssajo/tssajo
Added missing defer file.Close() to func Start
2016-08-07 10:06:59 -03:00
Samuel Cochran 9f47b4fe2b Fix submodule reference 2016-07-29 11:15:47 +10:00
tssajo e9db8ea35e Fixed a bug in ipe/websocket.go (TotalConnections were growing infinitely connection was not closed when a channel listener refreshed the browser tab or simply closed the browser tab) 2016-07-23 15:20:35 +02:00
tssajo 24425a2a9a While I love defer, I think the program is better readable and maybe even a bit faster when defer is not used when Locking/Unlocking 2016-07-20 02:15:04 +02:00
tssajo 952983a0f8 Added missing defer file.Close() to func Start 2016-07-20 02:07:39 +02:00
claudemiro de869b3e63 Bump version 2016-03-21 22:21:48 -03:00
claudemiro abf9a980eb Sending messages without a goroutine 2016-03-21 20:05:30 -03:00
claudemiro e15a8a84e2 Testing cluster option 2016-03-17 08:44:58 -03:00
claudemiro 8c54491e27 Not necessary close body at this point 2016-03-07 08:08:56 -03:00
claudemiro 03483592fc Testing util functions 2016-03-07 08:02:37 -03:00
claudemiro edabc10008 Closing request body in webhook call 2016-03-06 23:30:35 -03:00
Claudemiro bfa96ebbfb Merge pull request #15 from dimiro1/remove-global-config
Remove global config and a lot of fixes
2016-03-06 20:35:08 -03:00
claudemiro 5ce17c8856 Code cleanup 2016-03-06 20:21:04 -03:00
claudemiro e71294de18 Standardized error messages 2016-03-06 18:12:03 -03:00
claudemiro a7c5813501 Testing http handlers 2016-03-06 17:53:51 -03:00
claudemiro 383c02de1c Added params as a parameter for each handler. 2016-03-06 16:09:21 -03:00
claudemiro 1bae7f13ad Internal refactoring. Removing the global config. 2016-03-06 15:34:12 -03:00
claudemiro f07549fb6a Changed syntax to javascript to fix the README 2016-03-06 00:18:18 -03:00
claudemiro c272361861 Added comments in config example section in README 2016-03-06 00:17:19 -03:00
claudemiro 9dfc8c9cc0 Ignoring vscode files 2016-03-05 16:59:17 -03:00
claudemiro 6c55af2b5c Adding README and LICENSE to distribution 2016-03-05 09:43:02 -03:00
claudemiro 679ff1e589 Adding the version in the generated binary 2016-03-05 09:30:36 -03:00
claudemiro cbdd4e428a Better config example 2016-03-05 09:29:57 -03:00
claudemiro 805e9e3957 Removed Makefile and Add a Rakefile 2016-03-05 09:27:40 -03:00
claudemiro 44bbc1b1ea Renamed constants, avoid export uncessary constants 2016-03-04 08:33:18 -03:00
claudemiro e02773bbeb function init is not necessary 2016-03-04 08:32:39 -03:00
claudemiro 5fa621d743 Added user-agent header with the url of the project 2016-03-04 08:32:08 -03:00
claudemiro 880d7bda2a Using reader for reading config file 2016-03-04 08:31:32 -03:00
claudemiro 7dad2de275 Added SSL port 2016-03-04 08:31:03 -03:00
claudemiro 081611fd87 Changed the banner, fixed spelling 2016-03-04 08:30:26 -03:00
claudemiro 8810e523f0 Adding config for functional tests 2016-03-03 21:39:15 -03:00
claudemiro baf551ecdf Updated TODO.org 2016-03-03 21:07:57 -03:00
claudemiro f3dba811b7 Improved Handling SSL
* Panic is betten than using channels in this case
* Changed the names to a more recognized names
* Improved the README with a full config file example
2016-03-03 21:01:21 -03:00
claudemiro 55a7a0b96c Fixed SSL verification 2016-03-03 20:46:37 -03:00
Claudemiro cadee4e26d Merge pull request #10 from zaeznet/SSL
SSL Implementation
2016-03-03 20:45:42 -03:00
Welington Sampaio 03e94887a7 Both server running. SSL and Non SSL 2016-03-03 17:12:51 -03:00
Welington Sampaio e93145efe7 Both server running. SSL and Non SSL 2016-03-03 14:54:53 -03:00
Welington Sampaio 051da185e5 SSL Implementation 2016-03-03 08:37:00 -03:00
claudemiro 51eacdcf51 Managing vendor dependencies with glide 2016-02-21 20:15:10 -03:00
Claudemiro 412d8be451 Merge pull request #7 from jweslley/private-channel-payload
Private channel payload
2016-01-28 07:41:57 -02:00
Claudemiro 68f2e15320 Merge pull request #6 from jweslley/master
minor improvements
2016-01-27 09:10:52 -02:00
Jonhnny Weslley a77fb791f7 sign private channels with additional user data payload
Pusher accepts js clients to use the same auth endpoint to both presence
and private channels.
2016-01-24 12:43:06 -03:00
Jonhnny Weslley 8bf0786cb5 add systemd config file 2016-01-24 12:06:38 -03:00
Jonhnny Weslley ba3b699517 refactor common logic 2016-01-24 11:54:06 -03:00
claudemiro 517462a7af Cache of the valid channel name regex
* Added a Benchmark to verify the performance
* Now it is 10 times faster
2016-01-23 22:25:14 -02:00
claudemiro 56da1dea01 Starting doing integration tests 2016-01-18 22:52:03 -02:00
claudemiro 1971da652f Basic integration test 2016-01-18 00:41:45 -02:00
claudemiro f81a365e0c Changed the max number to the max in64 value 2016-01-18 00:40:06 -02:00
claudemiro 1de947cedc The random Seed should be executed only once.
If two users connect to the system in the same second both would receive the same id. This fix this error.
2016-01-18 00:37:55 -02:00
claudemiro 45cffc17a8 Added Link to badge, fix indentation. 2016-01-17 00:32:11 -02:00
claudemiro 59abf6ac5b Added Test for newConnection. 2016-01-17 00:27:21 -02:00
claudemiro cbb34a928f Verify if the exist the channel before 2016-01-17 00:12:12 -02:00
claudemiro 403138e28f APP_KEY not API_KEY 2016-01-16 21:48:45 -02:00
claudemiro 8dea85e71c Added badge quality report 2016-01-16 21:26:52 -02:00
claudemiro 3fc072373a Fix lint suggestions 2016-01-16 21:20:23 -02:00
claudemiro cad813e729 Removed unused file 2016-01-16 20:50:42 -02:00
Claudemiro 1054411b60 Fixed typo
Fixed typo
2015-11-07 23:49:03 -02:00
Claudemiro b972058622 Merge pull request #4 from orthographic-pedant/spell_check/beautiful
Fix typographical error(s)
2015-10-05 14:26:20 -03:00
orthographic-pedant f28e9da982 Fix typographical error(s)
Changed beatiful to beautiful in README.
2015-10-05 10:43:47 -04:00
claudemiro 341909e600 Changed the way vendor imports, using the new GO15VENDOREXPERIMENT feature 2015-08-27 23:48:05 -03:00
claudemiro 0bf660183b Avoid test vendor packages. 2015-06-13 18:44:21 -03:00
claudemiro c697311be3 Fixed a failing test. 2015-06-13 11:24:38 -03:00
claudemiro f910433853 Updated config example.
Removed OnlySSL as this feature is not implemented yet and removed
username and password.
2015-06-13 11:20:12 -03:00
claudemiro 883376b0c8 Updated Readme.
Updated the readme to explain how to install the package.
2015-06-13 09:23:29 -03:00
claudemiro 8dbd3ccbea Removing the expvar handler.
I am removing the expvar handler, this is uncessary to me. I was using
the goji/httpauth package to authenticate this handler, so, I am
removing the dependency too.
2015-06-13 09:18:40 -03:00
claudemiro 228ed284d9 Removed godep dependency.
Golang community are discuting a vendor aproach to go dependencies. This
change will come as experimental in go 1.5. For now I am removing the
godep dependency manager and using a vendor aproach.

I still have to use the full path when importing dependencies, but when
go1.5 released I just have to rename the imports and use a -vendor
prefix when building the project.
2015-06-13 09:11:55 -03:00
claudemiro 10e6048d11 Removed forgotten printf 2015-05-17 13:16:59 -03:00
claudemiro e2d07e9760 Validating channel name when connecting. 2015-05-17 09:51:17 -03:00
claudemiro 485bc01abe Changed SessionID format.
Pusher libs are validating the sessionID format. So I need to conform
with its format.
2015-05-17 09:25:57 -03:00
claudemiro 7b578c4fb4 Added Created at to connection. 2015-02-02 14:12:11 -03:00
claudemiro 03607062aa Fixed a litte comment explanation 2015-02-01 23:03:55 -03:00
claudemiro 846231f5f3 Not necessary export conf. 2015-01-31 15:18:38 -03:00
claudemiro 201cba86a9 There is no reason to export this functions and this types. 2015-01-31 15:12:54 -03:00
claudemiro d74426127c Updated go version in godep do 1.4.1 2015-01-31 10:57:48 -03:00
claudemiro cd2e58d0c5 No need to export NewChannel function. 2015-01-31 09:19:00 -03:00
claudemiro eca114dcf1 Fixed tests 2015-01-31 09:16:47 -03:00
claudemiro a3465f332e There is no reason to export these functions. 2015-01-31 09:13:24 -03:00
claudemiro e9bdc0859e Updated comments in constants.
* The maximun event size is 10kb the comment said it was 20kb
* Commented the error codes that are not implemented.
2015-01-31 08:55:06 -03:00
claudemiro b426bc2534 Added ellips to test all packages at once. 2015-01-31 08:30:10 -03:00
claudemiro cd1fd8995d File structure organization.
Create a new package ipe and put everything n there except for the main function.
2015-01-31 08:29:01 -03:00
claudemiro c7f757e517 Updated the ipe upstart job definition
Added the setuid option.
2015-01-30 11:58:52 -03:00
claudemiro 8d671751b3 Fixed an warning that go vet found. 2015-01-27 11:46:28 -03:00
claudemiro 018391ad66 Added some channels tests 2015-01-21 22:34:45 -03:00
claudemiro 8586b64bda Added tests to app 2015-01-20 08:33:02 -03:00
claudemiro ef756d789a Updated the README
Explained how to use godep to get the source and dependencies.
2015-01-19 03:43:25 -03:00
claudemiro 67676742a9 Removed comments in example json. 2015-01-18 08:40:36 -03:00
claudemiro 5218fb2ca4 Updated Readme 2015-01-18 08:01:15 -03:00
claudemiro d1473197b8 Better file organization.
Moved connection and channel types for its own files.
2015-01-17 19:58:46 -03:00
claudemiro b93da8ccc8 Fixed Tests 2015-01-17 08:21:49 -03:00
claudemiro 8def052b9d Added a command line banner. 2015-01-17 08:21:23 -03:00
claudemiro 0f5d238065 Removed the log from HTTP. 2015-01-16 22:37:45 -03:00
claudemiro 7367f06ccb Added an upstart init file example. 2015-01-16 22:36:04 -03:00
claudemiro a4e08d1e85 Updated Dependencies 2015-01-16 22:35:41 -03:00
claudemiro f6e1993eb6 The websocket must be closed only when it is alive 2015-01-16 13:18:50 -03:00
claudemiro 1dc3105155 Moved function to generate sessions Ids from websocket to Utils 2015-01-16 13:18:24 -03:00
claudemiro a51629f7b5 Removing empty channels.
Removing empty channels when the channel becomes vacated.
2015-01-16 13:17:22 -03:00
claudemiro 0d4e9742b9 Added flymake files in gitignore. 2015-01-16 13:16:08 -03:00
claudemiro 7032e69ffb Changed Type from Subsriber to Connection.
I think Connection is a better name.
2015-01-15 20:13:42 -03:00
claudemiro edf4b421bf Updated TODO list 2015-01-15 20:03:07 -03:00
claudemiro 874c07267f Just little changes. 2015-01-15 07:53:18 -03:00
claudemiro 414c1d98ee Calculating the total of users in a channel 2015-01-15 07:52:23 -03:00
claudemiro 5043b05c7b Triggering Presence channels events. 2015-01-14 13:19:11 -03:00
claudemiro 5695bb445d Updated TODO list 2015-01-14 13:17:47 -03:00
claudemiro dd78126371 Fixed Makefile amd64 arch. 2015-01-14 13:16:54 -03:00
claudemiro 431812f695 Added Godep as a dependency manager. 2015-01-13 21:51:12 -03:00
claudemiro 3999ad5aa0 Removed empty import 2015-01-13 20:53:12 -03:00
claudemiro 2394f167a5 Changes in TODO 2015-01-13 20:52:20 -03:00
claudemiro 0e3440bc35 Fixed app and connection tests.
The tests was not updated.
2015-01-13 13:29:52 -03:00
claudemiro 59457cc734 The channel could be created in the Rest POST Event 2015-01-13 13:09:45 -03:00
claudemiro 1f5ec97f57 Fixed typo error in README 2015-01-13 13:09:14 -03:00
claudemiro 4b08b0c658 Fixed error in Makefile. 2015-01-13 08:59:33 -03:00
claudemiro 7e91fbe35d user_id in client events when channels is presence
The channels must be presence
2015-01-13 08:44:41 -03:00
claudemiro 2cb8b13dc6 CLeanup the User and Password checking
Added a Method in ConfigFile
2015-01-13 08:10:47 -03:00
claudemiro 0aec168ab3 Added basic authentication in debug/vars
Using a simple http auth middleware.
https://github.com/goji/httpauth
2015-01-13 08:01:02 -03:00
claudemiro be66978479 Changed the data field from Client WebHook
The value correct is the value associated with the event, not the data
attached to the subscription.
2015-01-13 00:38:48 -03:00
claudemiro c125bc7206 Updated TODOS 2015-01-13 00:24:29 -03:00
claudemiro dc28fa90ff Updated example config file.
Added the URL called on WebHooks.
2015-01-13 00:11:16 -03:00
claudemiro 3a9b786a46 Length of bytes that are read from the crypto/rand
Now the value is 25
2015-01-13 00:08:37 -03:00
claudemiro ef1070887b Refactor Export variables
Now the variables are separated by app and other stats were added.
2015-01-13 00:00:34 -03:00
claudemiro cadc72d482 Exporting Variables for Debug
The variables are

* Total Channels
* Total Subscribers
* Total of unique Messages received by the system.
2015-01-12 22:41:42 -03:00
claudemiro 71d5099ac3 Refactor IsPublic function
A Function to check is the channel is private or presence already exists
2015-01-12 21:50:34 -03:00
claudemiro 387a2ae28d Changed slices to maps in app, channel 2015-01-12 21:41:22 -03:00
claudemiro e2412827cf Second Import 2015-01-12 21:15:36 -03:00
claudemiro ff2fcf3c54 Fixed Tests and added GetChannels test 2015-01-02 18:27:05 -03:00
claudemiro e1b7bd8252 First import 2015-01-02 18:20:27 -03:00
47 changed files with 3656 additions and 1390 deletions
+163
View File
@@ -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/*
+21
View File
@@ -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.
+157
View File
@@ -0,0 +1,157 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/dimiro1/ipe)](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.
+58
View 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
+41
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
client: go run client.go
server: go run ../main.go -config ./functional-config.json -alsologtostderr
+24
View File
@@ -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>
+66
View File
@@ -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)
}
+20
View File
@@ -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"
}
]
}
+136
View File
@@ -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
View File
@@ -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
View File
@@ -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
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 B

-194
View File
@@ -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>
+14
View File
@@ -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
+14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+98
View File
@@ -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)
}
}
+20
View File
@@ -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"
}
]
}
+43
View File
@@ -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,
)
}
+35
View File
@@ -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)
}
+34
View File
@@ -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)
}
}
+47
View File
@@ -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
+67
View File
@@ -0,0 +1,67 @@
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import (
"errors"
"sync"
)
// db represents a app database
// For now it there is only one memory database implementation
// but in the future I can write a sql implementation
type db interface {
GetAppByAppID(appID string) (*app, error)
GetAppByKey(key string) (*app, error)
AddApp(*app) error
}
// memdb is an in memory implementation of db interface
type memdb struct {
IDMutex sync.Mutex
KeyMutex sync.Mutex
AppsByAppID map[string]*app
AppsByKey map[string]*app
}
func newMemdb() *memdb {
return &memdb{
AppsByAppID: make(map[string]*app),
AppsByKey: make(map[string]*app),
}
}
func (db *memdb) AddApp(a *app) error {
db.IDMutex.Lock()
db.AppsByAppID[a.AppID] = a
db.IDMutex.Unlock()
db.KeyMutex.Lock()
db.AppsByKey[a.Key] = a
db.KeyMutex.Unlock()
return nil
}
// GetAppByAppID returns an App with by appID
func (db *memdb) GetAppByAppID(appID string) (*app, error) {
db.IDMutex.Lock()
a, ok := db.AppsByAppID[appID]
db.IDMutex.Unlock()
if ok {
return a, nil
}
return nil, errors.New("App not found")
}
// GetAppByKey returns an App with by key
func (db *memdb) GetAppByKey(key string) (*app, error) {
db.KeyMutex.Lock()
a, ok := db.AppsByKey[key]
db.KeyMutex.Unlock()
if ok {
return a, nil
}
return nil, errors.New("App not found")
}
+59
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
+214
View File
@@ -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
View File
@@ -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))
}
+17
View File
@@ -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
View File
@@ -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, its important to verify that these WebHooks originated from Pusher. Valid WebHooks will therefore contain these headers which contain a HMAC signature of the WebHook payload (body):
//
// X-Pusher-Key: The App Key.
// X-Pusher-Signature: A HMAC SHA256 hex digest formed by signing the POST payload (body) with the tokens 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)
}
}()
}
+262
View File
@@ -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
View File
@@ -1 +0,0 @@
console.log('This would be the main JS file.');
-17
View 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";
}
}
}
+52
View File
@@ -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
View File
@@ -1 +0,0 @@
{"name":"Ipe","tagline":"A Pusher server implementation compatible with Pusher client libraries written in GO","body":"[![Quality code report](http://goreportcard.com/badge/dimiro1/ipe)](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."}
-116
View File
@@ -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;
}
-424
View File
@@ -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;
}
-68
View File
@@ -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 }
-324
View File
@@ -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;
}
}
-245
View File
@@ -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; } }
+52
View File
@@ -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-")
}
+127
View File
@@ -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)
}
}