170 Commits

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

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

* created function validateAuthKey

* Using go default error interface.
2016-08-13 12:25:38 -03:00
claudemiro 0ac6c7a55c Started working on v1.3.0 2016-08-11 22:00:19 -03:00
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
Claudemiro 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
Zoltan 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
Claudemiro 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
Claudemiro 2eac54a0d3 Merge pull request #25 from sj26/fix-pusher-submodule
Fix submodule reference
2016-08-07 10:11:25 -03:00
Claudemiro a6f44d2175 Merge pull request #24 from tssajo/tssajo4
Fixed a bug in ipe/websocket.go
2016-08-07 10:11:02 -03:00
Claudemiro 6c479ced3c Merge pull request #22 from tssajo/tssajo3
Do not overuse defer unnecessarily
2016-08-07 10:10:18 -03:00
Claudemiro 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
48 changed files with 3919 additions and 1390 deletions
+164
View File
@@ -0,0 +1,164 @@
# 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
config.yml
*.pem
build
.vscode/*
debug
vendor/*
+20
View File
@@ -0,0 +1,20 @@
FROM golang:alpine as builder
RUN mkdir /build
ADD . /build/
WORKDIR /build
RUN apk add git gcc musl-dev
RUN go build -o ipe ./cmd
FROM alpine
USER root
RUN mkdir -p /config
RUN adduser -S -D -H -h /app appuser
COPY ./entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
USER appuser
WORKDIR /app
COPY --from=builder /build/ipe /app/
COPY --from=builder /build/config-example.yml /app/config-example.yml
VOLUME /config
CMD ["/bin/sh", "/app/entrypoint.sh"]
EXPOSE 4343
EXPOSE 8080
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2015 Claudemiro Alves Feitosa Neto <dimiro1@gmail.com>
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.
+185
View File
@@ -0,0 +1,185 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/dimiro1/ipe)](https://goreportcard.com/report/github.com/dimiro1/ipe)
Try browsing [the code on Sourcegraph](https://sourcegraph.com/github.com/dimiro1/ipe)!
# IPÊ
An open source Pusher server implementation compatible with Pusher client libraries written in Go.
# 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;
4. I was using Pusher on some projects behind a firewall;
# Features
* Public Channels;
* Private Channels;
* Presence Channels;
* Web Hooks;
* Client events;
* Complete REST API;
* Easy installation;
* 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).
# Building
```console
$ go get github.com/dimiro1/ipe
```
or simply
```console
$ go install github.com/dimiro1/ipe
```
Building from a local checkout
```console
$ git clone https://github.com/dimiro1/ipe.git
$ cd ipe/cmd
$ go build -o ipe
```
# How to configure?
## The server
```yaml
---
host: ":8080"
profiling: false
ssl:
enabled: false
host: ":4343"
key_file: "key.pem"
cert_file: "cert.pem"
apps:
- name: "Sample Application"
enabled: true
only_ssl: false
key: "278d525bdf162c739803"
secret: "${APP_SECRET}" # Expand env vars
app_id: "1"
user_events: true
webhooks:
enabled: true
url: "http://127.0.0.1:5000/hook"
```
## Libraries
### Client javascript library
```javascript
let 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");
```
Go
```go
package main
import "github.com/pusher/pusher-http-go"
func main() {
client := pusher.Client{
AppId: "APP_ID",
Key: "APP_KEY",
Secret: "APP_SECRET",
Host: ":8080",
}
// use the client
}
```
NodeJS
```javascript
let 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 excellent service, their service is very reliable. I recommend 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, 2018 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.
+454
View File
@@ -0,0 +1,454 @@
// 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 api
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"sort"
"strings"
log "github.com/golang/glog"
"github.com/gorilla/mux"
"ipe/events"
"ipe/storage"
"ipe/utils"
)
// // Maximum event size permitted 10 kB
// See: http://blogs.gnome.org/cneumair/2008/09/30/1-kb-1024-bytes-no-1-kb-1000-bytes/
const maxDataEventSize = 10 * 1000
// Prepare QueryString
func prepareQueryString(params url.Values) string {
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, "&")
}
// Authentication 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 Authentication(storage storage.Storage) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
var (
pathVars = mux.Vars(r)
appID = pathVars["app_id"]
)
app, err := storage.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.ServeHTTP(w, r)
} else {
log.Error("Not authorized")
http.Error(w, "Not authorized", http.StatusUnauthorized)
}
}
return http.HandlerFunc(fn)
}
}
// CheckAppDisabled Check if the application is disabled
func CheckAppDisabled(storage storage.Storage) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
var (
pathVars = mux.Vars(r)
appID = pathVars["app_id"]
)
currentApp, err := storage.GetAppByAppID(appID)
if err != nil {
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusForbidden)
return
}
if !currentApp.Enabled {
http.Error(w, "Application disabled", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}
// PostEvents handle post events
type PostEvents struct{ storage storage.Storage }
// NewPostEvents return a new PostEvents handler
func NewPostEvents(storage storage.Storage) *PostEvents {
return &PostEvents{storage: storage}
}
// ServeHTTP 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 *PostEvents) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
pathVars = mux.Vars(r)
appID = pathVars["app_id"]
)
app, err := h.storage.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)
if err := app.Publish(channel, events.Raw{Event: input.Name, Channel: c, Data: input.Data}, input.SocketID); err != nil {
log.Errorf("error publishing event %+v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte("{}")); err != nil {
log.Errorf("unexpected error while writing into response %+v", err)
}
}
// GetChannels handle get channels
type GetChannels struct{ storage storage.Storage }
// NewGetChannels return a new GetChannels handler
func NewGetChannels(storage storage.Storage) *GetChannels {
return &GetChannels{storage: storage}
}
// ServeHTTP 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 *GetChannels) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
pathVars = mux.Vars(r)
queryVars = r.URL.Query()
appID = pathVars["app_id"]
filter = queryVars.Get("filter_by_prefix")
info = queryVars.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.storage.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.ID] = struct {
UserCount int `json:"user_count"`
}{
c.TotalUsers(),
}
} else {
channels[c.ID] = struct{}{}
}
}
case "public-":
for _, c := range app.PublicChannels() {
channels[c.ID] = struct{}{}
}
case "private-":
for _, c := range app.PrivateChannels() {
channels[c.ID] = struct{}{}
}
default:
for _, c := range app.Channels() {
channels[c.ID] = struct{}{}
}
}
w.Header().Set("Content-Type", "application/json")
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)
}
}
// GetChannel handle get channel
type GetChannel struct{ storage storage.Storage }
// NewGetChannel return a new GetChannel handler
func NewGetChannel(storage storage.Storage) *GetChannel {
return &GetChannel{storage: storage}
}
// ServeHTTP Fetch info for one channel
//
// Example:
// {
// occupied: true,
// user_count: 42,
// subscription_count: 42
// }
//
// GET /apps/{app_id}/channels/{channel_name}
func (h *GetChannel) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
pathVars = mux.Vars(r)
queryVars = r.URL.Query()
appID = pathVars["app_id"]
channelName = pathVars["channel_name"]
info = queryVars.Get("info")
attributes = strings.Split(info, ",")
)
app, err := h.storage.GetAppByAppID(appID)
if err != nil {
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
}
// Channel name could not be empty
if strings.TrimSpace(channelName) == "" {
http.Error(w, "Empty channel name", http.StatusBadRequest)
return
}
// 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")
if err := json.NewEncoder(w).Encode(dtoChannel); err != nil {
log.Error(err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
// GetChannelUsers handle get users from a channel
type GetChannelUsers struct{ storage storage.Storage }
// NewGetChannelUsers return a new GetChannelUsers handler
func NewGetChannelUsers(storage storage.Storage) *GetChannelUsers {
return &GetChannelUsers{storage: storage}
}
// ServeHTTP Allowed only for presence-channels
//
// Example:
// {
// "users": [
// { "id": "1" },
// { "id": "2" }
// ]
// }
//
// GET /apps/{app_id}/channels/{channel_name}/users
func (h *GetChannelUsers) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
pathVars = mux.Vars(r)
appID = pathVars["app_id"]
channelName = pathVars["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.storage.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")
if err := json.NewEncoder(w).Encode(result); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
log.Error(err)
}
}
+227
View File
@@ -0,0 +1,227 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"github.com/gorilla/mux"
"ipe/app"
channel2 "ipe/channel"
"ipe/connection"
"ipe/mocks"
"ipe/storage"
)
var (
testApp *app.Application
database storage.Storage
id = 0
)
func newTestApp() *app.Application {
a := app.NewApplication("Test", strconv.Itoa(id), "123", "123", false, false, true, false, "")
id++
return a
}
func init() {
testApp = newTestApp()
channel := channel2.New("presence-c1")
testApp.AddChannel(channel)
testApp.AddChannel(channel2.New("c2"))
testApp.AddChannel(channel2.New("private-c3"))
conn := connection.New("123.456", mocks.MockSocket{})
_ = testApp.Subscribe(channel, conn, "{}")
conn = connection.New("321.654", mocks.MockSocket{})
_ = testApp.Subscribe(channel, conn, "{}")
_storage := storage.NewInMemory()
_ = _storage.AddApp(testApp)
database = _storage
}
// All channels
func Test_getChannels_all(t *testing.T) {
appID := testApp.AppID
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels", appID), nil)
r = mux.SetURLVars(r, map[string]string{
"app_id": appID,
})
w := httptest.NewRecorder()
handler := &GetChannels{database}
handler.ServeHTTP(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
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-", appID), nil)
r = mux.SetURLVars(r, map[string]string{
"app_id": appID,
})
w := httptest.NewRecorder()
handler := &GetChannels{database}
handler.ServeHTTP(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
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-&info=user_count", appID), nil)
r = mux.SetURLVars(r, map[string]string{
"app_id": appID,
})
w := httptest.NewRecorder()
handler := &GetChannels{database}
handler.ServeHTTP(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 allowed in Presence channels
func Test_getChannels_filter_by_private_prefix_and_info_user_count(t *testing.T) {
appID := testApp.AppID
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-&info=user_count", appID), nil)
r = mux.SetURLVars(r, map[string]string{
"app_id": appID,
})
w := httptest.NewRecorder()
handler := &GetChannels{database}
handler.ServeHTTP(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
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=public-", appID), nil)
r = mux.SetURLVars(r, map[string]string{
"app_id": appID,
})
w := httptest.NewRecorder()
handler := &GetChannels{database}
handler.ServeHTTP(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
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-", appID), nil)
r = mux.SetURLVars(r, map[string]string{
"app_id": appID,
})
w := httptest.NewRecorder()
handler := &GetChannels{database}
handler.ServeHTTP(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)
}
}
+314
View File
@@ -0,0 +1,314 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package app
import (
"errors"
"expvar"
"fmt"
"sync"
log "github.com/golang/glog"
"ipe/channel"
"ipe/connection"
"ipe/events"
"ipe/subscription"
)
// Application represents a Pusher application
type Application struct {
sync.RWMutex
Name string
AppID string
Key string
Secret string
OnlySSL bool
Enabled bool
UserEvents bool
WebHooks bool
URLWebHook string
channels map[string]*channel.Channel
connections map[string]*connection.Connection
Stats *expvar.Map `json:"-"`
}
// NewApplication returns a new Application
func NewApplication(
name,
appID,
key,
secret string,
onlySSL,
enabled,
userEvents,
webHooks bool,
webHookURL string,
) *Application {
a := &Application{
Name: name,
AppID: appID,
Key: key,
Secret: secret,
OnlySSL: onlySSL,
Enabled: enabled,
UserEvents: userEvents,
WebHooks: webHooks,
URLWebHook: webHookURL,
}
a.connections = make(map[string]*connection.Connection)
a.channels = make(map[string]*channel.Channel)
a.Stats = expvar.NewMap(fmt.Sprintf("%s (%s)", a.Name, a.AppID))
return a
}
// Channels returns the full list of channels
func (a *Application) Channels() []*channel.Channel {
a.RLock()
defer a.RUnlock()
var channels []*channel.Channel
for _, c := range a.channels {
channels = append(channels, c)
}
return channels
}
// PresenceChannels Only Presence channels
func (a *Application) PresenceChannels() []*channel.Channel {
a.RLock()
defer a.RUnlock()
var channels []*channel.Channel
for _, c := range a.channels {
if c.IsPresence() {
channels = append(channels, c)
}
}
return channels
}
// PrivateChannels Only Private channels
func (a *Application) PrivateChannels() []*channel.Channel {
a.RLock()
defer a.RUnlock()
var channels []*channel.Channel
for _, c := range a.channels {
if c.IsPrivate() {
channels = append(channels, c)
}
}
return channels
}
// PublicChannels Only Public channels
func (a *Application) PublicChannels() []*channel.Channel {
a.RLock()
defer a.RUnlock()
var channels []*channel.Channel
for _, c := range a.channels {
if c.IsPublic() {
channels = append(channels, c)
}
}
return channels
}
// Disconnect Socket
func (a *Application) Disconnect(socketID string) {
log.Infof("disconnecting socket %+v", socketID)
conn, err := a.FindConnection(socketID)
if err != nil {
log.Infof("socket not found, %+v", err)
return
}
// Unsubscribe from channels
for _, c := range a.channels {
if c.IsSubscribed(conn) {
if err := c.Unsubscribe(conn); err != nil {
log.Errorf("error while calling Channel.Unsubscribe, %+v", err)
continue
}
}
}
// Remove from Application
a.Lock()
_, exists := a.connections[conn.SocketID]
a.Unlock()
if !exists {
return
}
a.Lock()
delete(a.connections, conn.SocketID)
a.Unlock()
a.Stats.Add("TotalConnections", -1)
}
// Connect a new Subscriber
func (a *Application) Connect(conn *connection.Connection) {
log.Infof("adding a new Connection %s to Application %s", conn.SocketID, a.Name)
a.Lock()
defer a.Unlock()
a.connections[conn.SocketID] = conn
a.Stats.Add("TotalConnections", 1)
}
// FindConnection Find a Connection on this Application
func (a *Application) FindConnection(socketID string) (*connection.Connection, error) {
a.RLock()
defer a.RUnlock()
conn, exists := a.connections[socketID]
if exists {
return conn, nil
}
return nil, errors.New("connection not found")
}
// RemoveChannel removes the Channel from Application
func (a *Application) RemoveChannel(c *channel.Channel) {
log.Infof("remove the Channel %s from Application %s", c.ID, a.Name)
a.Lock()
defer a.Unlock()
delete(a.channels, c.ID)
if c.IsPresence() {
a.Stats.Add("TotalPresenceChannels", -1)
}
if c.IsPrivate() {
a.Stats.Add("TotalPrivateChannels", -1)
}
if c.IsPublic() {
a.Stats.Add("TotalPublicChannels", -1)
}
a.Stats.Add("TotalChannels", -1)
}
// AddChannel Add a new Channel to this APP
func (a *Application) AddChannel(c *channel.Channel) {
log.Infof("adding a new Channel %s to Application %s", c.ID, a.Name)
a.Lock()
defer a.Unlock()
a.channels[c.ID] = c
if c.IsPresence() {
a.Stats.Add("TotalPresenceChannels", 1)
}
if c.IsPrivate() {
a.Stats.Add("TotalPrivateChannels", 1)
}
if c.IsPublic() {
a.Stats.Add("TotalPublicChannels", 1)
}
a.Stats.Add("TotalChannels", 1)
}
// FindOrCreateChannelByChannelID Returns a Channel from this Application
// If not found then the Channel is created and added to this Application
func (a *Application) FindOrCreateChannelByChannelID(n string) *channel.Channel {
c, err := a.FindChannelByChannelID(n)
if err != nil {
c = channel.New(
n,
channel.WithChannelOccupiedListener(func(c *channel.Channel, s *subscription.Subscription) {
a.TriggerChannelOccupiedHook(c)
}),
channel.WithChannelVacatedListener(func(c *channel.Channel, s *subscription.Subscription) {
a.TriggerChannelVacatedHook(c)
}),
channel.WithMemberAddedListener(func(c *channel.Channel, s *subscription.Subscription) {
a.TriggerMemberAddedHook(c, s)
}),
channel.WithMemberRemovedListener(func(c *channel.Channel, s *subscription.Subscription) {
a.TriggerMemberRemovedHook(c, s)
}),
channel.WithClientEventListener(func(c *channel.Channel, s *subscription.Subscription, event string, data interface{}) {
a.TriggerClientEventHook(c, s, event, data)
}),
)
a.AddChannel(c)
}
return c
}
// FindChannelByChannelID Find the Channel by Channel ID
func (a *Application) FindChannelByChannelID(n string) (*channel.Channel, error) {
a.RLock()
defer a.RUnlock()
c, exists := a.channels[n]
if exists {
return c, nil
}
return nil, errors.New("channel does not exists")
}
// Publish an event into the channel
// skip the ignore connection
func (a *Application) Publish(c *channel.Channel, event events.Raw, ignore string) error {
a.Stats.Add("TotalUniqueMessages", 1)
return c.Publish(event, ignore)
}
// Unsubscribe unsubscribe the given connection from the channel
// remove the channel from the application if it is empty
func (a *Application) Unsubscribe(c *channel.Channel, conn *connection.Connection) error {
err := c.Unsubscribe(conn)
if err != nil {
return err
}
if !c.IsOccupied() {
a.RemoveChannel(c)
}
return nil
}
// Subscribe the connection into the given channel
func (a *Application) Subscribe(c *channel.Channel, conn *connection.Connection, data string) error {
return c.Subscribe(conn, data)
}
+255
View File
@@ -0,0 +1,255 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package app
import (
"strconv"
"testing"
channel2 "ipe/channel"
"ipe/connection"
"ipe/mocks"
)
var id = 0
func newTestApp() *Application {
a := NewApplication("Test", strconv.Itoa(id), "123", "123", false, false, true, false, "")
id++
return a
}
func TestConnect(t *testing.T) {
app := newTestApp()
app.Connect(connection.New("socketID", mocks.MockSocket{}))
if len(app.connections) != 1 {
t.Errorf("len(Application.connections) == %d, wants %d", len(app.connections), 1)
}
}
func TestDisconnect(t *testing.T) {
app := newTestApp()
app.Connect(connection.New("socketID", mocks.MockSocket{}))
app.Disconnect("socketID")
if len(app.connections) != 0 {
t.Errorf("len(Application.connections) == %d, wants %d", len(app.connections), 0)
}
}
func TestFindConnection(t *testing.T) {
app := newTestApp()
app.Connect(connection.New("socketID", mocks.MockSocket{}))
if _, err := app.FindConnection("socketID"); err != nil {
t.Errorf("Application.FindConnection('socketID') == _, %q, wants %v", err, nil)
}
if _, err := app.FindConnection("NotFound"); err == nil {
t.Errorf("Application.FindConnection('socketID') == _, %q, wants !nil", err)
}
}
func TestFindChannelByChannelID(t *testing.T) {
app := newTestApp()
channel := channel2.New("ID")
app.AddChannel(channel)
if _, err := app.FindChannelByChannelID("ID"); err != nil {
t.Errorf("Application.FindChannelByChannelID('ID') == _, %q, wants %v", err, nil)
}
}
func TestFindOrCreateChannelByChannelID(t *testing.T) {
app := newTestApp()
if len(app.channels) != 0 {
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 0)
}
app.FindOrCreateChannelByChannelID("ID")
if len(app.channels) != 1 {
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 1)
}
}
func TestRemoveChannel(t *testing.T) {
app := newTestApp()
if len(app.channels) != 0 {
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 0)
}
channel := channel2.New("ID")
app.AddChannel(channel)
if len(app.channels) != 1 {
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 1)
}
app.RemoveChannel(channel)
if len(app.channels) != 0 {
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 0)
}
}
func Test_add_channels(t *testing.T) {
app := newTestApp()
// Public
if len(app.PublicChannels()) != 0 {
t.Errorf("len(Application.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
}
app.AddChannel(channel2.New("ID"))
if len(app.PublicChannels()) != 1 {
t.Errorf("len(Application.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
}
// Presence
if len(app.PresenceChannels()) != 0 {
t.Errorf("len(Application.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
}
app.AddChannel(channel2.New("presence-test"))
if len(app.PresenceChannels()) != 1 {
t.Errorf("len(Application.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
}
// Private
if len(app.PrivateChannels()) != 0 {
t.Errorf("len(Application.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
}
app.AddChannel(channel2.New("private-test"))
if len(app.PrivateChannels()) != 1 {
t.Errorf("len(Application.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
}
}
func Test_AllChannels(t *testing.T) {
app := newTestApp()
app.AddChannel(channel2.New("private-test"))
app.AddChannel(channel2.New("presence-test"))
app.AddChannel(channel2.New("test"))
if len(app.channels) != 3 {
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 3)
}
}
func Test_New_Subscriber(t *testing.T) {
app := newTestApp()
if len(app.connections) != 0 {
t.Errorf("len(Application.connections) == %d, wants %d", len(app.connections), 0)
}
conn := connection.New("1", mocks.MockSocket{})
app.Connect(conn)
if len(app.connections) != 1 {
t.Errorf("len(Application.connections) == %d, wants %d", len(app.connections), 1)
}
}
func Test_find_subscriber(t *testing.T) {
app := newTestApp()
conn := connection.New("1", mocks.MockSocket{})
app.Connect(conn)
conn, err := app.FindConnection("1")
if err != nil {
t.Error(err)
}
if conn.SocketID != "1" {
t.Errorf("conn.SocketID == %s, wants %s", conn.SocketID, "1")
}
// Find a wrong subscriber
conn, err = app.FindConnection("DoesNotExists")
if err == nil {
t.Errorf("err == %q, wants !nil", err)
}
if conn != nil {
t.Errorf("conn == %q, wants nil", conn)
}
}
func Test_find_or_create_channels(t *testing.T) {
app := newTestApp()
// Public
if len(app.PublicChannels()) != 0 {
t.Errorf("len(Application.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
}
c := app.FindOrCreateChannelByChannelID("id")
if len(app.PublicChannels()) != 1 {
t.Errorf("len(Application.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
}
if c.ID != "id" {
t.Errorf("c.id == %s, wants %s", c.ID, "id")
}
// Presence
if len(app.PresenceChannels()) != 0 {
t.Errorf("len(Application.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
}
c = app.FindOrCreateChannelByChannelID("presence-test")
if len(app.PresenceChannels()) != 1 {
t.Errorf("len(Application.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
}
if c.ID != "presence-test" {
t.Errorf("c.id == %s, wants %s", c.ID, "presence-test")
}
// Private
if len(app.PrivateChannels()) != 0 {
t.Errorf("len(Application.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
}
c = app.FindOrCreateChannelByChannelID("private-test")
if len(app.PrivateChannels()) != 1 {
t.Errorf("len(Application.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
}
if c.ID != "private-test" {
t.Errorf("c.id == %s, wants %s", c.ID, "private-test")
}
}
+226
View File
@@ -0,0 +1,226 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package app
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
log "github.com/golang/glog"
"ipe/channel"
"ipe/subscription"
"ipe/utils"
)
const maxTimeout = 3 * time.Second
// A webHook is sent as a HTTP POST request to the url which you specify.
// The POST request payload (body) contains a JSON document, and follows the following format:
// {
// "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.Channel) hookEvent {
return hookEvent{Name: "channel_occupied", Channel: channel.ID}
}
func newChannelVacatedHook(channel *channel.Channel) hookEvent {
return hookEvent{Name: "channel_vacated", Channel: channel.ID}
}
func newMemberAddedHook(channel *channel.Channel, s *subscription.Subscription) hookEvent {
return hookEvent{Name: "member_added", Channel: channel.ID, UserID: s.ID}
}
func newMemberRemovedHook(channel *channel.Channel, s *subscription.Subscription) hookEvent {
return hookEvent{Name: "member_removed", Channel: channel.ID, UserID: s.ID}
}
func newClientHook(channel *channel.Channel, s *subscription.Subscription, event string, data interface{}) hookEvent {
return hookEvent{Name: "client_event", Channel: channel.ID, Event: event, Data: data, SocketID: s.Connection.SocketID}
}
// TriggerChannelOccupiedHook channel_occupied
// { "name": "channel_occupied", "channel": "test_channel" }
func (a *Application) TriggerChannelOccupiedHook(c *channel.Channel) {
event := newChannelOcuppiedHook(c)
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
defer cancel()
if err := triggerHook(ctx, a, event); err != nil {
log.Errorf("triggering webhook %+v", err)
}
}
// TriggerChannelVacatedHook channel_vacated
// { "name": "channel_vacated", "channel": "test_channel" }
func (a *Application) TriggerChannelVacatedHook(c *channel.Channel) {
event := newChannelVacatedHook(c)
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
defer cancel()
if err := triggerHook(ctx, a, event); err != nil {
log.Errorf("triggering webhook %+v", err)
}
}
// TriggerClientEventHook client_events
// {
// "name": "client_event",
// "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 *Application) TriggerClientEventHook(c *channel.Channel, s *subscription.Subscription, clientEvent string, data interface{}) {
event := newClientHook(c, s, clientEvent, data)
if c.IsPresence() {
event.UserID = s.ID
}
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
defer cancel()
if err := triggerHook(ctx, a, event); err != nil {
log.Errorf("triggering webhook %+v", err)
}
}
// TriggerMemberAddedHook member_added
// {
// "name": "member_added",
// "channel": "presence-your_channel_name",
// "user_id": "a_user_id"
// }
func (a *Application) TriggerMemberAddedHook(c *channel.Channel, s *subscription.Subscription) {
event := newMemberAddedHook(c, s)
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
defer cancel()
if err := triggerHook(ctx, a, event); err != nil {
log.Errorf("triggering webhook %+v", err)
}
}
// TriggerMemberRemovedHook member_removed
// {
// "name": "member_removed",
// "channel": "presence-your_channel_name",
// "user_id": "a_user_id"
// }
func (a *Application) TriggerMemberRemovedHook(c *channel.Channel, s *subscription.Subscription) {
event := newMemberRemovedHook(c, s)
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
defer cancel()
if err := triggerHook(ctx, a, event); err != nil {
log.Errorf("triggering webhook %+v", err)
}
}
func triggerHook(ctx context.Context, a *Application, event hookEvent) error {
if !a.WebHooks {
log.Infof("webhook are not enabled for app: %s", a.Name)
return fmt.Errorf("webhooks are not enabled for app: %s", a.Name)
}
done := make(chan bool)
go func() {
log.Infof("Triggering %s event", 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 = req.WithContext(ctx)
req.Header.Set("User-Agent", "Ipe UA; (+https://github.com/dimiro1/ipe)")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Pusher-Key", a.Key)
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 func() {
if err := resp.Body.Close(); err != nil {
log.Errorf("error closing response body %+v", err)
}
}()
}
if err != nil {
log.Errorf("error posting %s event: %+v", event.Name, err)
}
// Successfully terminated
done <- true
}()
select {
case <-ctx.Done():
return ctx.Err()
case <-done:
return nil
}
}
+324
View File
@@ -0,0 +1,324 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package channel
import (
"encoding/json"
"errors"
"sync"
"time"
log "github.com/golang/glog"
"ipe/connection"
"ipe/events"
"ipe/subscription"
"ipe/utils"
)
// Option constructor function for Channel
type Option func(*Channel)
// ListenerFunc listener function
type ListenerFunc func(*Channel, *subscription.Subscription)
// ClientEventListenerFunc listener for client events
type ClientEventListenerFunc func(*Channel, *subscription.Subscription, string, interface{})
// Channel represents an application channel
type Channel struct {
sync.RWMutex
ID string
subscriptions map[string]*subscription.Subscription
createdAt time.Time
memberAddedListeners []ListenerFunc
memberRemovedListeners []ListenerFunc
channelOccupiedListeners []ListenerFunc
channelVacatedListeners []ListenerFunc
clientEventListeners []ClientEventListenerFunc
}
// New Create a new Channel
func New(channelID string, options ...Option) *Channel {
log.Infof("Creating a new Channel: %s", channelID)
c := &Channel{ID: channelID, createdAt: time.Now(), subscriptions: make(map[string]*subscription.Subscription)}
for _, option := range options {
option(c)
}
return c
}
// WithMemberAddedListener appends the given ListenerFunc into the memberAddedListeners list
func WithMemberAddedListener(f ListenerFunc) func(*Channel) {
return func(c *Channel) {
c.memberAddedListeners = append(c.memberAddedListeners, f)
}
}
// WithMemberRemovedListener appends the given ListenerFunc into the memberRemovedListeners list
func WithMemberRemovedListener(f ListenerFunc) func(*Channel) {
return func(c *Channel) {
c.memberRemovedListeners = append(c.memberRemovedListeners, f)
}
}
// WithChannelOccupiedListener appends the given ListenerFunc into the channelOccupiedListeners list
func WithChannelOccupiedListener(f ListenerFunc) func(*Channel) {
return func(c *Channel) {
c.channelOccupiedListeners = append(c.channelOccupiedListeners, f)
}
}
// WithChannelVacatedListener appends the given ListenerFunc into the channelVacatedListeners list
func WithChannelVacatedListener(f ListenerFunc) func(*Channel) {
return func(c *Channel) {
c.channelVacatedListeners = append(c.channelVacatedListeners, f)
}
}
// WithClientEventListener appends the given ListenerFunc into the clientEventListeners list
func WithClientEventListener(f ClientEventListenerFunc) func(*Channel) {
return func(c *Channel) {
c.clientEventListeners = append(c.clientEventListeners, f)
}
}
// Subscriptions returns a slice of subscriptions
func (c *Channel) Subscriptions() []*subscription.Subscription {
c.RLock()
defer c.RUnlock()
var subscriptions []*subscription.Subscription
for _, sub := range c.subscriptions {
subscriptions = append(subscriptions, sub)
}
return subscriptions
}
// IsOccupied Return true if the Channel has at least one subscriber
func (c *Channel) IsOccupied() bool {
return c.TotalSubscriptions() > 0
}
// IsPresenceOrPrivate Check if the type of the Channel is presence or is private
func (c *Channel) IsPresenceOrPrivate() bool {
return c.IsPresence() || c.IsPrivate()
}
// IsPublic Check if the type of the Channel is public
func (c *Channel) IsPublic() bool {
return !c.IsPresenceOrPrivate()
}
// IsPresence Check if the type of the Channel is presence
func (c *Channel) IsPresence() bool {
return utils.IsPresenceChannel(c.ID)
}
// IsPrivate Check if the type of the Channel is private
func (c *Channel) IsPrivate() bool {
return utils.IsPrivateChannel(c.ID)
}
// TotalSubscriptions Get the total of subscribers
func (c *Channel) TotalSubscriptions() int {
c.RLock()
defer c.RUnlock()
return len(c.subscriptions)
}
// TotalUsers Get the total of users.
func (c *Channel) TotalUsers() int {
c.RLock()
defer c.RUnlock()
total := make(map[string]int)
for _, s := range c.subscriptions {
total[s.ID]++
}
return len(total)
}
// Subscribe Add a new subscriber to the Channel
func (c *Channel) Subscribe(conn *connection.Connection, channelData string) error {
log.Infof("Subscribing %s to Channel %s", conn.SocketID, c.ID)
_subscription := subscription.New(conn, channelData)
c.Lock()
c.subscriptions[conn.SocketID] = _subscription
c.Unlock()
if c.IsPresence() {
// User Info Data
var info struct {
UserID string `json:"user_id"`
UserInfo json.RawMessage `json:"user_info"`
}
log.Infof("%+v", channelData)
if err := json.Unmarshal([]byte(channelData), &info); err != nil {
log.Error(err)
return err
}
js, err := info.UserInfo.MarshalJSON()
if err != nil {
log.Error(err)
return err
}
c.Lock()
// Update the Subscription
_subscription.ID = info.UserID
_subscription.Data = string(js)
c.Unlock()
// Publish pusher_internal:member_added
c.PublishMemberAddedEvent(channelData, _subscription)
for _, hook := range c.memberAddedListeners {
hook(c, _subscription)
}
// pusher_internal:subscription_succeeded
data := make(map[string]events.SubscriptionSucceededPresenceData)
data["presence"] = events.NewSubscriptionSucceedPresenceData(c.subscriptions)
js, err = json.Marshal(data)
if err != nil {
log.Error(err)
return err
}
conn.Publish(events.NewSubscriptionSucceeded(c.ID, string(js)))
} else {
conn.Publish(events.NewSubscriptionSucceeded(c.ID, "{}"))
}
if c.TotalSubscriptions() == 1 {
for _, hook := range c.channelOccupiedListeners {
hook(c, _subscription)
}
}
return nil
}
// IsSubscribed check if the user is subscribed
func (c *Channel) IsSubscribed(conn *connection.Connection) bool {
c.RLock()
defer c.RUnlock()
_, exists := c.subscriptions[conn.SocketID]
return exists
}
// Unsubscribe Remove the subscriber from the Channel
// It destroy the Channel if the channels does not have any subscribers.
func (c *Channel) Unsubscribe(conn *connection.Connection) error {
log.Infof("unsubscribe %s from Channel %s", conn.SocketID, c.ID)
c.RLock()
_subscription, exists := c.subscriptions[conn.SocketID]
c.RUnlock()
if !exists {
return errors.New("_subscription not found")
}
c.Lock()
delete(c.subscriptions, conn.SocketID)
c.Unlock()
if c.IsPresence() {
// Publish pusher_internal:member_removed
c.PublishMemberRemovedEvent(_subscription)
for _, hook := range c.memberRemovedListeners {
hook(c, _subscription)
}
}
if !c.IsOccupied() {
for _, hook := range c.channelVacatedListeners {
hook(c, _subscription)
}
}
return nil
}
// PublishMemberAddedEvent Publish a MemberAddedEvent to all subscriptions
func (c *Channel) PublishMemberAddedEvent(data string, subscription *subscription.Subscription) {
c.RLock()
defer c.RUnlock()
for _, subs := range c.subscriptions {
if subs != subscription {
subs.Connection.Publish(events.NewMemberAdded(c.ID, data))
}
}
}
// PublishMemberRemovedEvent Publish a MemberRemovedEvent to all subscriptions
func (c *Channel) PublishMemberRemovedEvent(subscription *subscription.Subscription) {
c.RLock()
defer c.RUnlock()
for _, subs := range c.subscriptions {
if subs != subscription {
subs.Connection.Publish(events.NewMemberRemoved(c.ID, subscription.ID))
}
}
}
// Publish messages to all Subscribers
// skip the ignore connection
func (c *Channel) Publish(event events.Raw, ignore string) error {
c.RLock()
defer c.RUnlock()
b, err := event.Data.MarshalJSON()
if err != nil {
return err
}
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
log.Infof("Publishing message %+v to Channel %s", v, c.ID)
for _, subs := range c.subscriptions {
if subs.Connection.SocketID != ignore {
subs.Connection.Publish(events.NewResponse(event.Event, event.Channel, v))
} else {
if utils.IsClientEvent(event.Event) {
for _, hook := range c.clientEventListeners {
hook(c, subs, event.Event, v)
}
}
}
}
return nil
}
+103
View File
@@ -0,0 +1,103 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package channel
import (
"ipe/connection"
"ipe/mocks"
"ipe/subscription"
"testing"
)
func TestIsOccupied(t *testing.T) {
c := New("ID")
if c.IsOccupied() {
t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), false)
}
c.subscriptions["ID"] = subscription.New(connection.New("ID", mocks.MockSocket{}), "")
if !c.IsOccupied() {
t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), true)
}
}
func TestIsPrivate(t *testing.T) {
c := New("private-Channel")
if !c.IsPrivate() {
t.Errorf("c.IsPrivate() == %t, wants %t", c.IsPrivate(), true)
}
}
func TestIsPresence(t *testing.T) {
c := New("presence-Channel")
if !c.IsPresence() {
t.Errorf("c.IsPresence() == %t, wants %t", c.IsPresence(), true)
}
}
func TestIsPublic(t *testing.T) {
c := New("Channel")
if !c.IsPublic() {
t.Errorf("c.IsPublic() == %t, wants %t", c.IsPublic(), true)
}
}
func TestIsPrivateOrPresence(t *testing.T) {
c := New("private-Channel")
if !c.IsPresenceOrPrivate() {
t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
}
c = New("presence-Channel")
if !c.IsPresenceOrPrivate() {
t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
}
}
func TestTotalSubscriptions(t *testing.T) {
c := New("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 := New("ID")
c.subscriptions["1"] = subscription.New(connection.New("ID", mocks.MockSocket{}), "")
c.subscriptions["2"] = subscription.New(connection.New("ID", mocks.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 := New("ID")
conn := connection.New("ID", mocks.MockSocket{})
if c.IsSubscribed(conn) {
t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), false)
}
c.subscriptions["ID"] = subscription.New(conn, "")
if !c.IsSubscribed(conn) {
t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), true)
}
}
Executable
BIN
View File
Binary file not shown.
+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"
"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.yml", "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 <dimiro1@gmail.com>\033[0m")
}
+39
View File
@@ -0,0 +1,39 @@
// Copyright 2014, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package config
// File config file
type File struct {
Host string `yaml:"host"` // The host, eg: :8080 will start on 0.0.0.0:8080
SSL SSL `yaml:"ssl"`
Profiling bool `yaml:"profiling"`
Apps []Application `yaml:"apps"`
}
// SSL related configuration options
type SSL struct {
Enabled bool `yaml:"enabled"`
Host string `yaml:"host"`
KeyFile string `yaml:"key_file"`
CertFile string `yaml:"cert_file"`
}
// Application related configuration options
type Application struct {
Name string `yaml:"name"`
AppID string `yaml:"app_id"`
Key string `yaml:"key"`
Secret string `yaml:"secret"`
OnlySSL bool `yaml:"only_ssl"`
Enabled bool `yaml:"enabled"`
UserEvents bool `yaml:"user_events"`
WebHooks Webhooks `yaml:"webhooks"`
}
// Webhooks related configuration options
type Webhooks struct {
Enabled bool `yaml:"enabled"`
URL string `yaml:"url"`
}
+43
View File
@@ -0,0 +1,43 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package connection
import (
"sync"
"time"
log "github.com/golang/glog"
)
// Socket interface to write to the client
type Socket interface {
WriteJSON(interface{}) error
}
// Connection An user connection
type Connection struct {
sync.Mutex
SocketID string
Socket Socket
CreatedAt time.Time
}
// New Create a new Subscriber
func New(socketID string, s Socket) *Connection {
log.Infof("Creating a new Subscriber %+v", socketID)
return &Connection{SocketID: socketID, Socket: s, CreatedAt: time.Now()}
}
// Publish the message to websocket attached to this client
func (conn *Connection) Publish(m interface{}) {
conn.Lock()
defer conn.Unlock()
if err := conn.Socket.WriteJSON(m); err != nil {
log.Errorf("error writing json into Socket, %+v", err)
}
}
+29
View File
@@ -0,0 +1,29 @@
// 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 connection
import (
"ipe/mocks"
"testing"
)
func TestNewConnection(t *testing.T) {
expectedSocketID := "socketID"
expectedSocket := mocks.MockSocket{}
c := New(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)
}
}
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
if [ -z "$(ls -A /config)" ]; then
cp /app/config-example.yml /config/config.yml
fi
/app/ipe --config=/config/config.yml
+289
View File
@@ -0,0 +1,289 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package events
import (
"encoding/json"
log "github.com/golang/glog"
"ipe/subscription"
)
// SubscribeData data for Subscribe event
type SubscribeData struct {
Channel string `json:"channel"`
Auth string `json:"auth,omitempty"`
ChannelData string `json:"channel_data,omitempty"`
}
// Subscribe event
// {
// "event": "pusher:subscribe",
// "data": {
// "channel": "the channel",
// "auth": "the auth",
// "channelData": "extra data"
// }
// }
type Subscribe struct {
Event string `json:"event"`
Data SubscribeData `json:"data"`
}
// NewSubscribe Create a new subscribe event with the specified channel and data
func NewSubscribe(channel, auth, channelData string) Subscribe {
data := SubscribeData{Channel: channel, Auth: auth, ChannelData: channelData}
return Subscribe{Event: "pusher:subscribe", Data: data}
}
// UnsubscribeData event data
type UnsubscribeData struct {
Channel string `json:"channel"`
}
// Unsubscribe event
// {
// "event": "pusher:unsubscribe",
// "data": {
// "channel": "The channel"
// }
// }
type Unsubscribe struct {
Event string `json:"event"`
Data UnsubscribeData `json:"data"`
}
// NewUnsubscribe Create a new unsubscribe event for the specified channel
func NewUnsubscribe(channel string) Unsubscribe {
data := UnsubscribeData{Channel: channel}
return Unsubscribe{Event: "pusher:unsubscribe", Data: data}
}
// SubscriptionSucceeded event
// {
// "event": "pusher_internal:subscription_succeeded",
// "channel": "the channel"
// }
type SubscriptionSucceeded struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data string `json:"data"`
}
// NewSubscriptionSucceeded Create a new subscription succeed event for the specified channel
func NewSubscriptionSucceeded(channel, data string) SubscriptionSucceeded {
return SubscriptionSucceeded{Event: "pusher_internal:subscription_succeeded", Channel: channel, Data: data}
}
// SubscriptionSucceededPresenceData Data Subscription Succeed
// "{
// \"presence\": {
// \"ids\": [\"11814b369700141b222a3f3791cec2d9\",\"71dd6a29da2a4833336d2a964becf820\"],
// \"hash\": {
// \"11814b369700141b222a3f3791cec2d9\": {
// \"name\":\"Phil Leggetter\",
// \"twitter\": \"@leggetter\"
// },
// \"71dd6a29da2a4833336d2a964becf820\": {
// \"name\":\"Max Williams\",
// \"twitter\": \"@maxthelion\"
// }
// },
// \"count\": 2
// }
// }"
type SubscriptionSucceededPresenceData struct {
Ids []string `json:"ids"`
Hash map[string]interface{} `json:"hash"`
Count int `json:"count"`
}
// NewSubscriptionSucceedPresenceData returns new SubscriptionSucceededPresenceData
func NewSubscriptionSucceedPresenceData(subscriptions map[string]*subscription.Subscription) SubscriptionSucceededPresenceData {
event := SubscriptionSucceededPresenceData{}
var (
ids []string
hash = make(map[string]interface{}, len(subscriptions))
)
for _, s := range subscriptions {
// Do you have any other idea?
var js interface{}
if err := json.Unmarshal([]byte(s.Data), &js); err != nil {
continue
}
hash[s.ID] = js
ids = append(ids, s.ID)
}
event.Ids = ids
event.Hash = hash
event.Count = len(subscriptions)
return event
}
// Pong event
// {
// "event": "pusher:pong",
// "data": {}
// }
type Pong struct {
Event string `json:"event"`
Data string `json:"data"`
}
// NewPong Create a new pong event
func NewPong() Pong {
return Pong{Event: "pusher:pong", Data: "{}"}
}
// Ping event
// {
// "event": "pusher:ping",
// "data": {}
// }
type Ping struct {
Event string `json:"event"`
Data string `json:"data"`
}
// NewPing Create a new ping event
func NewPing() Ping {
return Ping{Event: "pusher:ping", Data: "{}"}
}
// Error event
// {
// "event": "pusher:error",
// "data": {
// "message": "A Message",
// "code": 4000
// }
// }
type Error struct {
Event string `json:"event"`
Data interface{} `json:"data"`
}
// NewError Create a new error event
// Pusher protocol is very strange in some parts
// It send null in some errors.
func NewError(code int, message string) Error {
var data = struct {
Code *int `json:"code"`
Message string `json:"message"`
}{
Message: message,
}
if code == 0 {
data.Code = nil
} else {
data.Code = &code
}
return Error{Event: "pusher:error", Data: data}
}
// ConnectionEstablished event
// {
// "event" : "pusher:connection_established",
// "data" : {
// "socket_id" : "123456",
// "activity_timeout" : 120
// }
// }
type ConnectionEstablished struct {
Event string `json:"event"`
Data string `json:"data"`
}
// NewConnectionEstablished Create a new connection established event using the specified socketId
func NewConnectionEstablished(socketID string) ConnectionEstablished {
b, err := json.Marshal(struct {
SocketID string `json:"socket_id"`
ActivityTimeout int `json:"activity_timeout"`
}{
SocketID: socketID, ActivityTimeout: 120,
})
if err != nil {
panic("events: Could not Marshal json ConnectionEstablishedEvent")
}
return ConnectionEstablished{Event: "pusher:connection_established", Data: string(b)}
}
// MemberAdded event
// {
// "event": "pusher_internal:member_added",
// "channel": "presence-example-channel",
// "data": String
// }
type MemberAdded struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data string `json:"data"`
}
// NewMemberAdded creates a new MemberAdded event
func NewMemberAdded(channel, data string) MemberAdded {
return MemberAdded{Event: "pusher_internal:member_added", Channel: channel, Data: data}
}
// MemberRemoved event
// {
// "event": "pusher_internal:member_removed",
// "channel": "presence-example-channel",
// "data": String
// }
type MemberRemoved struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data string `json:"data"`
}
// NewMemberRemoved returns a new MemberRemoved event
func NewMemberRemoved(channel string, userID string) MemberRemoved {
data, err := json.Marshal(struct {
UserID string `json:"user_id"`
}{
UserID: userID,
})
if err != nil {
log.Error(err)
}
return MemberRemoved{Event: "pusher_internal:member_removed", Channel: channel, Data: string(data)}
}
// Raw event, usually used for client events
// {
// "event": "client-?",
// "channel": "The channel",
// "data": {}
// }
type Raw struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data json.RawMessage `json:"data"`
}
// Response event
type Response struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data interface{} `json:"data"`
}
// NewResponse The response event that is broadcasted to the client sockets
func NewResponse(name, channel string, data interface{}) Response {
return Response{Event: name, Channel: channel, Data: data}
}
+29
View File
@@ -0,0 +1,29 @@
package events
import (
"bytes"
"encoding/json"
"testing"
)
func Test_newErrorEvent_with_invalid_code(t *testing.T) {
event := NewError(0, "The error message")
data, _ := json.Marshal(event)
expected := `{"event":"pusher:error","data":{"code":null,"message":"The error message"}}`
if bytes.Compare(data, []byte(expected)) != 0 {
t.Errorf("%s != %s", string(data), expected)
}
}
func Test_newErrorEvent_with_valid_code(t *testing.T) {
event := NewError(4007, "Unsupported protocol version")
data, _ := json.Marshal(event)
expected := `{"event":"pusher:error","data":{"code":4007,"message":"Unsupported protocol version"}}`
if bytes.Compare(data, []byte(expected)) != 0 {
t.Errorf("%s != %s", string(data), expected)
}
}
+2
View File
@@ -0,0 +1,2 @@
client: go run client.go
server: go run ../cmd/main.go -config ./functional-config.yml -alsologtostderr
+23
View File
@@ -0,0 +1,23 @@
<html>
<head>
<meta charset="utf-8">
<title>Pusher Spec</title>
<link href="//cdnjs.cloudflare.com/ajax/libs/mocha/5.2.0/mocha.min.css" rel="stylesheet"/>
</head>
<body>
<div id="mocha"></div>
<script src="//cdnjs.cloudflare.com/ajax/libs/mocha/5.2.0/mocha.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/chai/4.2.0/chai.min.js"></script>
<script src="//js.pusher.com/4.3.1/pusher.min.js"></script>
<script>mocha.setup('bdd')</script>
<script src="test.pusher.js"></script>
<script>
mocha.checkLeaks();
mocha.globals(['Pusher']);
mocha.run();
</script>
</body>
</html>
+98
View File
@@ -0,0 +1,98 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"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.Fprint(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.Fprint(res, string(response))
}
func triggerMessage(res http.ResponseWriter, _ *http.Request) {
_, err := client.Trigger("private-messages", "messages", "The message from server")
if err != nil {
panic(err)
}
_, _ = fmt.Fprint(res, "OK")
}
func hookcallback(res http.ResponseWriter, r *http.Request) {
bytes, err := httputil.DumpRequest(r, true)
if err != nil {
panic(err)
}
fmt.Println(string(bytes))
event := struct {
Events []struct {
Name string `json:"name"`
} `json:"events"`
}{}
err = json.NewDecoder(r.Body).Decode(&event)
if err != nil {
panic(err)
}
_, err = client.Trigger("private-webhook", event.Events[0].Name, "The Webhoook from server")
if err != nil {
panic(err)
}
_, _ = fmt.Fprint(res, "OK")
}
func main() {
http.HandleFunc("/pusher/presence/auth", pusherPresenceAuth)
http.HandleFunc("/pusher/private/auth", pusherPrivateAuth)
http.HandleFunc("/trigger", triggerMessage)
http.HandleFunc("/hook", hookcallback)
http.Handle("/", http.FileServer(http.Dir("./")))
_ = http.ListenAndServe(":5000", nil)
}
+19
View File
@@ -0,0 +1,19 @@
---
host: ':8080'
profiling: false
ssl:
enabled: false
host: ':4343'
key_file: 'key.pem'
cert_file: 'cert.pem'
apps:
- name: 'Sample Application'
enabled: true
only_ssl: false
key: '278d525bdf162c739803'
secret: '7ad3753142a6693b25b9'
app_id: '1'
user_events: true
webhooks:
enabled: true # Default is false
url: 'http://127.0.0.1:5000/hook'
+158
View File
@@ -0,0 +1,158 @@
"use strict";
let assert = chai.assert;
let APP_KEY = "278d525bdf162c739803";
let HOST = "localhost";
let PORT = 8080;
let AUTH = "http://localhost:5000/pusher/private/auth";
let 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"],
});
}
describe("Pusher", function () {
describe("connection", function () {
it("should connect sucessfully with correct config", function (done) {
let pusher = getPusher(AUTH);
pusher.connection.bind('connected', function () {
assert.ok(true, "Connected");
done();
});
});
it("should not connect without the correct config", function (done) {
let 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) {
let pusher = getPusher(AUTH);
let channel = pusher.subscribe('public-channel');
channel.bind("pusher:subscription_succeeded", function () {
assert.ok(true, "Connected to the channel");
done();
});
});
it("should subscribe to a private channel", function (done) {
let pusher = getPusher(AUTH);
let channel = pusher.subscribe('private-channel');
channel.bind("pusher:subscription_succeeded", function () {
assert.ok(true, "Connected to the channel");
done();
});
});
it("should subscribe to a presence channel", function (done) {
let pusher = getPusher(AUTH_PRESENCE);
let channel = pusher.subscribe('presence-channel');
channel.bind("pusher:subscription_succeeded", function () {
assert.ok(true, "Connected to the channel");
done();
});
});
}); // subscription
describe("hooks", function () {
it('should receive hook', function (done) {
let pusher = getPusher(AUTH);
let channel = pusher.subscribe('private-webhook');
channel.bind("pusher:subscription_succeeded", function () {
console.log("subscribed");
});
channel.bind("channel_occupied", function (data) {
assert.equal(data, "The Webhoook from server");
pusher.unsubscribe('private-webhook');
done();
});
});
}); // hooks
describe("events", function () {
it('should not allowed client events on public channels', function (done) {
let pusher = getPusher(AUTH);
let channel = pusher.subscribe('public-channel');
channel.bind("pusher:subscription_succeeded", function () {
channel.trigger("client-message", "The message");
});
pusher.bind("pusher:error", function () {
assert.ok(true, "Expected error");
done();
});
});
it('should allow client events on private channels', function (done) {
let pusher_a = getPusher(AUTH);
let pusher_b = getPusher(AUTH);
let channel_a = pusher_a.subscribe('private-channel');
let 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) {
let pusher_a = getPusher(AUTH);
let pusher_b = getPusher(AUTH);
let channel_a = pusher_a.subscribe('private-messages');
let channel_b = pusher_b.subscribe('private-messages');
channel_a.bind("pusher:subscription_succeeded", function () {
console.log("channel_a connected");
let xhttp = new XMLHttpRequest();
xhttp.open("GET", "/trigger", true);
xhttp.send();
});
channel_b.bind("pusher:subscription_succeeded", function () {
console.log("channel_b connected");
});
channel_b.bind("messages", function (data) {
assert.equal(data, "The message from server");
done();
});
});
}); // events
});
+15
View File
@@ -0,0 +1,15 @@
module ipe
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/handlers v1.4.0
github.com/gorilla/mux v1.6.2
github.com/gorilla/websocket v1.4.0
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pusher/pusher-http-go v1.3.0
github.com/stretchr/testify v1.2.2 // indirect
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 // indirect
gopkg.in/yaml.v2 v2.2.1
)
+23
View File
@@ -0,0 +1,23 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA=
github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pusher/pusher-http-go v1.3.0 h1:dWrjIsNheCUEo6YE9qlS8pIl3XzJa5yM8VlBu/LUl3M=
github.com/pusher/pusher-http-go v1.3.0/go.mod h1:XAv1fxRmVTI++2xsfofDhg7whapsLRG/gH/DXbF3a18=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 h1:kkXA53yGe04D0adEYJwEVQjeBppL01Exg+fnMjfUraU=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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"
+106
View File
@@ -0,0 +1,106 @@
// Copyright 2015 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import (
"io/ioutil"
"math/rand"
"net/http"
"os"
"time"
log "github.com/golang/glog"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"gopkg.in/yaml.v2"
"ipe/api"
"ipe/app"
"ipe/config"
"ipe/storage"
"ipe/websockets"
)
// Start Parse the configuration file and starts the ipe server
// It Panic if could not start the HTTP or HTTPS server
func Start(filename string) {
var conf config.File
rand.Seed(time.Now().Unix())
data, err := ioutil.ReadFile(filename)
if err != nil {
log.Error(err)
return
}
// Expand env vars
data = []byte(os.ExpandEnv(string(data)))
// Decoding config
if err := yaml.UnmarshalStrict(data, &conf); err != nil {
log.Error(err)
return
}
// Using a in memory database
inMemoryStorage := storage.NewInMemory()
// Adding applications
for _, a := range conf.Apps {
application := app.NewApplication(
a.Name,
a.AppID,
a.Key,
a.Secret,
a.OnlySSL,
a.Enabled,
a.UserEvents,
a.WebHooks.Enabled,
a.WebHooks.URL,
)
if err := inMemoryStorage.AddApp(application); err != nil {
log.Error(err)
return
}
}
router := mux.NewRouter()
router.Use(handlers.RecoveryHandler())
router.Path("/app/{key}").Methods("GET").Handler(
websockets.NewWebsocket(inMemoryStorage),
)
appsRouter := router.PathPrefix("/apps/{app_id}").Subrouter()
appsRouter.Use(
api.CheckAppDisabled(inMemoryStorage),
api.Authentication(inMemoryStorage),
)
appsRouter.Path("/events").Methods("POST").Handler(
api.NewPostEvents(inMemoryStorage),
)
appsRouter.Path("/channels").Methods("GET").Handler(
api.NewGetChannels(inMemoryStorage),
)
appsRouter.Path("/channels/{channel_name}").Methods("GET").Handler(
api.NewGetChannel(inMemoryStorage),
)
appsRouter.Path("/channels/{channel_name}/users").Methods("GET").Handler(
api.NewGetChannelUsers(inMemoryStorage),
)
if conf.SSL.Enabled {
go func() {
log.Infof("Starting HTTPS service on %s ...", conf.SSL.Host)
log.Fatal(http.ListenAndServeTLS(conf.SSL.Host, conf.SSL.CertFile, conf.SSL.KeyFile, router))
}()
}
log.Infof("Starting HTTP service on %s ...", conf.Host)
log.Fatal(http.ListenAndServe(conf.Host, router))
}
-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";
}
}
}
+11
View File
@@ -0,0 +1,11 @@
package mocks
// MockSocket is a mock implementation of Socket
// used in the test suite
type MockSocket struct{}
// WriteJSON always returns nil
// used in the test suite
func (s MockSocket) WriteJSON(i interface{}) error {
return nil
}
-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."}
+67
View File
@@ -0,0 +1,67 @@
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package storage
import (
"errors"
"ipe/app"
"sync"
)
// Storage represents a app database
// For now it there is only one memory database implementation
// but in the future I can write a sql implementation
type Storage interface {
GetAppByAppID(appID string) (*app.Application, error)
GetAppByKey(key string) (*app.Application, error)
AddApp(application *app.Application) error
}
// InMemory in memory implementation of Storage
type InMemory struct {
sync.RWMutex
Apps []*app.Application
}
// NewInMemory returns an InMemory storage
func NewInMemory() Storage {
return &InMemory{}
}
// AddApp adds app into memory
func (db *InMemory) AddApp(application *app.Application) error {
db.Lock()
defer db.Unlock()
db.Apps = append(db.Apps, application)
return nil
}
// GetAppByAppID returns an App with by appID
func (db *InMemory) GetAppByAppID(appID string) (*app.Application, error) {
db.RLock()
defer db.RUnlock()
for _, a := range db.Apps {
if a.AppID == appID {
return a, nil
}
}
return nil, errors.New("app not found")
}
// GetAppByKey returns an App with by key
func (db *InMemory) GetAppByKey(key string) (*app.Application, error) {
db.RLock()
defer db.RUnlock()
for _, a := range db.Apps {
if a.Key == key {
return a, nil
}
}
return nil, errors.New("app not found")
}
+75
View File
@@ -0,0 +1,75 @@
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package storage
import (
"ipe/app"
"testing"
)
func Benchmark_memdb_GetAppByAppID(b *testing.B) {
storage := NewInMemory()
_ = storage.AddApp(&app.Application{AppID: "123456", Name: "Example"})
_ = storage.AddApp(&app.Application{AppID: "654321", Name: "Example2"})
_ = storage.AddApp(&app.Application{AppID: "678901", Name: "Example3"})
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = storage.GetAppByAppID("123456")
}
}
func Test_db_GetAppByAppID(t *testing.T) {
_app := &app.Application{AppID: "123456", Name: "Example"}
storage := NewInMemory()
_ = storage.AddApp(_app)
a, err := storage.GetAppByAppID("123456")
if err != nil {
t.Errorf("GetAppByAppID(%q) == %+v, want %+v", "123456", a, _app)
}
}
func Test_db_GetAppByAppID__error(t *testing.T) {
_app := &app.Application{AppID: "123456", Name: "Example"}
storage := NewInMemory()
_ = storage.AddApp(_app)
a, err := storage.GetAppByAppID("not-found")
if err == nil {
t.Errorf("GetAppByAppID(%q) == %+v, want %+v", "123456", a, _app)
}
}
func Test_db_GetAppByKey(t *testing.T) {
_app := &app.Application{AppID: "123456", Name: "Example", Key: "654321"}
storage := NewInMemory()
_ = storage.AddApp(_app)
a, err := storage.GetAppByKey("654321")
if err != nil {
t.Errorf("GetAppByKey(%q) == %+v, want %+v", "654321", a, _app)
}
}
func Test_db_GetAppByKey__error(t *testing.T) {
_app := &app.Application{AppID: "123456", Name: "Example", Key: "654321"}
storage := NewInMemory()
_ = storage.AddApp(_app)
a, err := storage.GetAppByKey("not-found")
if err == nil {
t.Errorf("GetAppByKey(%q) == %+v, want %+v", "not-found", a, nil)
}
}
-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; } }
+19
View File
@@ -0,0 +1,19 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package subscription
import "ipe/connection"
// Subscription A Channel Subscription
type Subscription struct {
Connection *connection.Connection
ID string
Data string
}
// New Create a new Subscription
func New(conn *connection.Connection, data string) *Subscription {
return &Subscription{Connection: conn, Data: data}
}
+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.MaxInt32), rand.Intn(math.MaxInt32))
}
// 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)
}
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package websockets
type websocketError struct {
Code int
Msg string
}
var (
applicationOnlyAcceptsSSL = &websocketError{Code: 4000, Msg: "Application only accepts SSL connections, reconnect using wss://"}
applicationDoesNotExists = &websocketError{Code: 4001, Msg: "Could not found an app with the given key"}
applicationDisabled = &websocketError{Code: 4003, Msg: "Application disabled"}
invalidVersionStringFormat = &websocketError{Code: 4006, Msg: "Invalid version string format"}
unsupportedProtocolVersion = &websocketError{Code: 4007, Msg: "Unsupported protocol version"}
noProtocolVersionSupplied = &websocketError{Code: 4008, Msg: "No protocol version supplied"}
reconnectImmediately = &websocketError{Code: 4200, Msg: "Generic reconnect immediately"}
)
+296
View File
@@ -0,0 +1,296 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package websockets
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
log "github.com/golang/glog"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"ipe/app"
"ipe/connection"
"ipe/events"
"ipe/storage"
"ipe/utils"
)
// Only this version is supported
const supportedProtocolVersion = 7
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(_ *http.Request) bool {
return true
},
}
// Websocket handler for real time websocket messages
type Websocket struct {
storage storage.Storage
}
// NewWebsocket returns a new Websocket handler
func NewWebsocket(storage storage.Storage) *Websocket {
return &Websocket{storage: storage}
}
// ServeHTTP Websocket GET /app/{key}
func (h *Websocket) ServeHTTP(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
defer func() {
if conn != nil {
if err := conn.Close(); err != nil {
log.Errorf("closing the websocket connection %+v", err)
}
}
}()
if err != nil {
log.Error(err)
return
}
var (
pathVars = mux.Vars(r)
appKey = pathVars["key"]
)
_app, err := h.storage.GetAppByKey(appKey)
if err != nil {
log.Error(err)
emitError(applicationDoesNotExists, conn)
return
}
sessionID := utils.GenerateSessionID()
if err := onOpen(conn, r, sessionID, _app); err != nil {
emitError(err, conn)
return
}
handleMessages(conn, sessionID, _app)
}
func handleMessages(conn *websocket.Conn, sessionID string, app *app.Application) {
var event struct {
Event string `json:"event"`
}
for {
_, message, err := conn.ReadMessage()
if err != nil {
handleError(conn, sessionID, app, err)
return
}
if err := json.Unmarshal(message, &event); err != nil {
emitError(reconnectImmediately, conn)
return
}
log.Infof("websocket: Handling %s event", event.Event)
switch event.Event {
case "pusher:ping":
handlePing(conn)
case "pusher:subscribe":
handleSubscribe(conn, sessionID, app, message)
case "pusher:unsubscribe":
handleUnsubscribe(conn, sessionID, app, message)
default:
if utils.IsClientEvent(event.Event) {
handleClientEvent(conn, sessionID, app, message)
}
}
}
}
// Emit an Websocket ErrorEvent
func emitError(err *websocketError, conn *websocket.Conn) {
event := events.NewError(err.Code, err.Msg)
if err := conn.WriteJSON(event); err != nil {
log.Error(err)
}
}
func handleError(conn *websocket.Conn, sessionID string, app *app.Application, err error) {
log.Errorf("%+v", err)
if err == io.EOF {
onClose(sessionID, app)
} else if _, ok := err.(*websocket.CloseError); ok {
onClose(sessionID, app)
} else {
emitError(reconnectImmediately, conn)
}
}
func onOpen(conn *websocket.Conn, r *http.Request, sessionID string, app *app.Application) *websocketError {
var (
queryVars = r.URL.Query()
strProtocol = queryVars.Get("protocol")
)
protocol, err := strconv.Atoi(strProtocol)
if err != nil {
return invalidVersionStringFormat
}
switch {
case strings.TrimSpace(strProtocol) == "":
return noProtocolVersionSupplied
case protocol != supportedProtocolVersion:
return unsupportedProtocolVersion
case !app.Enabled:
return applicationDisabled
case app.OnlySSL:
if r.TLS == nil {
return applicationOnlyAcceptsSSL
}
}
// Create the new Subscriber
_connection := connection.New(sessionID, conn)
app.Connect(_connection)
// Everything went fine.
if err := conn.WriteJSON(events.NewConnectionEstablished(_connection.SocketID)); err != nil {
return reconnectImmediately
}
return nil
}
func onClose(sessionID string, app *app.Application) {
app.Disconnect(sessionID)
}
func handlePing(conn *websocket.Conn) {
if err := conn.WriteJSON(events.NewPong()); err != nil {
emitError(reconnectImmediately, conn)
}
}
func handleClientEvent(conn *websocket.Conn, sessionID string, app *app.Application, message []byte) {
if !app.UserEvents {
emitError(&websocketError{Code: 0, Msg: "To send client events, you must enable this feature in the Settings."}, conn)
}
clientEvent := events.Raw{}
if err := json.Unmarshal(message, &clientEvent); err != nil {
log.Error(err)
emitError(reconnectImmediately, conn)
return
}
channel, err := app.FindChannelByChannelID(clientEvent.Channel)
if err != nil {
emitError(&websocketError{Code: 0, Msg: fmt.Sprintf("Could not find a channel with the id %s", clientEvent.Channel)}, conn)
return
}
if !channel.IsPresenceOrPrivate() {
emitError(&websocketError{Code: 0, Msg: "Client event rejected - only supported on private and presence channels"}, conn)
return
}
if err := app.Publish(channel, clientEvent, sessionID); err != nil {
log.Error(err)
emitError(reconnectImmediately, conn)
return
}
}
func handleUnsubscribe(conn *websocket.Conn, sessionID string, app *app.Application, message []byte) {
unsubscribeEvent := events.Unsubscribe{}
if err := json.Unmarshal(message, &unsubscribeEvent); err != nil {
emitError(reconnectImmediately, conn)
return
}
_connection, err := app.FindConnection(sessionID)
if err != nil {
emitError(&websocketError{Code: 0, Msg: fmt.Sprintf("Could not find a connection with the id %s", sessionID)}, conn)
return
}
channel, err := app.FindChannelByChannelID(unsubscribeEvent.Data.Channel)
if err != nil {
emitError(&websocketError{Code: 0, Msg: fmt.Sprintf("Could not find a channel with the id %s", unsubscribeEvent.Data.Channel)}, conn)
return
}
if err := app.Unsubscribe(channel, _connection); err != nil {
emitError(reconnectImmediately, conn)
return
}
}
func handleSubscribe(conn *websocket.Conn, sessionID string, app *app.Application, message []byte) {
subscribeEvent := events.Subscribe{}
if err := json.Unmarshal(message, &subscribeEvent); err != nil {
emitError(reconnectImmediately, conn)
return
}
_connection, err := app.FindConnection(sessionID)
if err != nil {
emitError(reconnectImmediately, conn)
return
}
channelName := strings.TrimSpace(subscribeEvent.Data.Channel)
if !utils.IsChannelNameValid(channelName) {
emitError(&websocketError{Code: 0, Msg: "This channel name is not valid"}, conn)
return
}
isPresence := utils.IsPresenceChannel(channelName)
isPrivate := utils.IsPrivateChannel(channelName)
if isPresence || isPrivate {
toSign := []string{_connection.SocketID, channelName}
if isPresence || len(subscribeEvent.Data.ChannelData) > 0 {
toSign = append(toSign, subscribeEvent.Data.ChannelData)
}
if !validateAuthKey(subscribeEvent.Data.Auth, toSign, app) {
emitError(&websocketError{Code: 0, Msg: fmt.Sprintf("Auth value for subscription to %s is invalid", channelName)}, conn)
return
}
}
channel := app.FindOrCreateChannelByChannelID(channelName)
log.Info(subscribeEvent.Data.ChannelData)
if err := app.Subscribe(channel, _connection, subscribeEvent.Data.ChannelData); err != nil {
emitError(reconnectImmediately, conn)
}
}
func validateAuthKey(givenAuthKey string, toSign []string, app *app.Application) bool {
expectedAuthKey := fmt.Sprintf("%s:%s", app.Key, utils.HashMAC([]byte(strings.Join(toSign, ":")), []byte(app.Secret)))
return givenAuthKey == expectedAuthKey
}