Compare commits
170 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7deb3edc3 | |||
| ea6e116836 | |||
| 3cb9eb27e1 | |||
| 0ff81caae4 | |||
| 0de46888eb | |||
| 05c088f158 | |||
| 3c51d0b614 | |||
| 1a9fb1a706 | |||
| d412759c16 | |||
| f4781ee483 | |||
| 964df8a5dd | |||
| 8da763ec2f | |||
| 4523549f71 | |||
| fe1eb8c5eb | |||
| 481311061f | |||
| b117386591 | |||
| 746b978e52 | |||
| da48528e51 | |||
| 065d6ab286 | |||
| 1d1ffa00b5 | |||
| 983f490737 | |||
| 5c9528bce4 | |||
| 8fb96f3cbf | |||
| 9d732fb01a | |||
| 0bd4fa02e9 | |||
| 4a305b32a6 | |||
| 4c5d5302ec | |||
| 994e8e00f1 | |||
| 9ad14daeeb | |||
| 0ac6c7a55c | |||
| 015cbdf07a | |||
| 431a19fe7b | |||
| 6225d8006f | |||
| 7a4568e92d | |||
| 6e46bcb2aa | |||
| 0c599ea5fb | |||
| 46ebd1b62b | |||
| 06449fdb58 | |||
| 6092cd85ca | |||
| 9c49ae16d3 | |||
| 7a794c97d7 | |||
| f0b3aadf74 | |||
| 2eac54a0d3 | |||
| a6f44d2175 | |||
| 6c479ced3c | |||
| 178bbc9b8e | |||
| 9f47b4fe2b | |||
| e9db8ea35e | |||
| 24425a2a9a | |||
| 952983a0f8 | |||
| de869b3e63 | |||
| abf9a980eb | |||
| e15a8a84e2 | |||
| 8c54491e27 | |||
| 03483592fc | |||
| edabc10008 | |||
| bfa96ebbfb | |||
| 5ce17c8856 | |||
| e71294de18 | |||
| a7c5813501 | |||
| 383c02de1c | |||
| 1bae7f13ad | |||
| f07549fb6a | |||
| c272361861 | |||
| 9dfc8c9cc0 | |||
| 6c55af2b5c | |||
| 679ff1e589 | |||
| cbdd4e428a | |||
| 805e9e3957 | |||
| 44bbc1b1ea | |||
| e02773bbeb | |||
| 5fa621d743 | |||
| 880d7bda2a | |||
| 7dad2de275 | |||
| 081611fd87 | |||
| 8810e523f0 | |||
| baf551ecdf | |||
| f3dba811b7 | |||
| 55a7a0b96c | |||
| cadee4e26d | |||
| 03e94887a7 | |||
| e93145efe7 | |||
| 051da185e5 | |||
| 51eacdcf51 | |||
| 412d8be451 | |||
| 68f2e15320 | |||
| a77fb791f7 | |||
| 8bf0786cb5 | |||
| ba3b699517 | |||
| 517462a7af | |||
| 56da1dea01 | |||
| 1971da652f | |||
| f81a365e0c | |||
| 1de947cedc | |||
| 45cffc17a8 | |||
| 59abf6ac5b | |||
| cbb34a928f | |||
| 403138e28f | |||
| 8dea85e71c | |||
| 3fc072373a | |||
| cad813e729 | |||
| 1054411b60 | |||
| b972058622 | |||
| f28e9da982 | |||
| 341909e600 | |||
| 0bf660183b | |||
| c697311be3 | |||
| f910433853 | |||
| 883376b0c8 | |||
| 8dbd3ccbea | |||
| 228ed284d9 | |||
| 10e6048d11 | |||
| e2d07e9760 | |||
| 485bc01abe | |||
| 7b578c4fb4 | |||
| 03607062aa | |||
| 846231f5f3 | |||
| 201cba86a9 | |||
| d74426127c | |||
| cd2e58d0c5 | |||
| eca114dcf1 | |||
| a3465f332e | |||
| e9bdc0859e | |||
| b426bc2534 | |||
| cd1fd8995d | |||
| c7f757e517 | |||
| 8d671751b3 | |||
| 018391ad66 | |||
| 8586b64bda | |||
| ef756d789a | |||
| 67676742a9 | |||
| 5218fb2ca4 | |||
| d1473197b8 | |||
| b93da8ccc8 | |||
| 8def052b9d | |||
| 0f5d238065 | |||
| 7367f06ccb | |||
| a4e08d1e85 | |||
| f6e1993eb6 | |||
| 1dc3105155 | |||
| a51629f7b5 | |||
| 0d4e9742b9 | |||
| 7032e69ffb | |||
| edf4b421bf | |||
| 874c07267f | |||
| 414c1d98ee | |||
| 5043b05c7b | |||
| 5695bb445d | |||
| dd78126371 | |||
| 431812f695 | |||
| 3999ad5aa0 | |||
| 2394f167a5 | |||
| 0e3440bc35 | |||
| 59457cc734 | |||
| 1f5ec97f57 | |||
| 4b08b0c658 | |||
| 7e91fbe35d | |||
| 2cb8b13dc6 | |||
| 0aec168ab3 | |||
| be66978479 | |||
| c125bc7206 | |||
| dc28fa90ff | |||
| 3a9b786a46 | |||
| ef1070887b | |||
| cadc72d482 | |||
| 71d5099ac3 | |||
| 387a2ae28d | |||
| e2412827cf | |||
| ff2fcf3c54 | |||
| e1b7bd8252 |
+164
@@ -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
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
[](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
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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, it’s important to verify that these WebHooks originated from Pusher. Valid WebHooks will therefore contain these headers which contain a HMAC signature of the webHook payload (body):
|
||||||
|
//
|
||||||
|
// X-Pusher-Key: The App Key.
|
||||||
|
// X-Pusher-Signature: A HMAC SHA256 hex digest formed by signing the POST payload (body) with the token’s secret.
|
||||||
|
type webHook struct {
|
||||||
|
TimeMs int64 `json:"time_ms"`
|
||||||
|
Events []hookEvent `json:"events"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type hookEvent struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Event string `json:"event,omitempty"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
SocketID string `json:"socket_id,omitempty"`
|
||||||
|
UserID string `json:"user_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newChannelOcuppiedHook(channel *channel.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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
@@ -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")
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
client: go run client.go
|
||||||
|
server: go run ../cmd/main.go -config ./functional-config.yml -alsologtostderr
|
||||||
@@ -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>
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
@@ -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
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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=
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 268 B |
-194
@@ -1,194 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en-us">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Ipe by dimiro1</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="stylesheet" type="text/css" href="stylesheets/normalize.css" media="screen">
|
|
||||||
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'>
|
|
||||||
<link rel="stylesheet" type="text/css" href="stylesheets/stylesheet.css" media="screen">
|
|
||||||
<link rel="stylesheet" type="text/css" href="stylesheets/github-light.css" media="screen">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<section class="page-header">
|
|
||||||
<h1 class="project-name">Ipe</h1>
|
|
||||||
<h2 class="project-tagline">A Pusher server implementation compatible with Pusher client libraries written in GO</h2>
|
|
||||||
<a href="https://github.com/dimiro1/ipe" class="btn">View on GitHub</a>
|
|
||||||
<a href="https://github.com/dimiro1/ipe/zipball/master" class="btn">Download .zip</a>
|
|
||||||
<a href="https://github.com/dimiro1/ipe/tarball/master" class="btn">Download .tar.gz</a>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="main-content">
|
|
||||||
<p><a href="http://goreportcard.com/report/dimiro1/ipe"><img src="http://goreportcard.com/badge/dimiro1/ipe" alt="Quality code report"></a></p>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
<a id="ipÊ" class="anchor" href="#ip%C3%8A" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>IPÊ</h1>
|
|
||||||
|
|
||||||
<p>This software is written in Go - the WYSIWYG lang</p>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
<a id="why-i-wrote-this-software" class="anchor" href="#why-i-wrote-this-software" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Why I wrote this software?</h1>
|
|
||||||
|
|
||||||
<ol>
|
|
||||||
<li>I wanted to learn Go and I needed a non trivial application;</li>
|
|
||||||
<li>I use Pusher in some projects;</li>
|
|
||||||
<li>I really like Pusher;</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
<a id="features" class="anchor" href="#features" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Features</h1>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>Public Channels;</li>
|
|
||||||
<li>Private Channels;</li>
|
|
||||||
<li>Presence Channels;</li>
|
|
||||||
<li>Web Hooks;</li>
|
|
||||||
<li>Client events;</li>
|
|
||||||
<li>Complete REST API;</li>
|
|
||||||
<li>Easy instalation;</li>
|
|
||||||
<li>A single binary without dependencies;</li>
|
|
||||||
<li>Easy configuration;</li>
|
|
||||||
<li>Protocol version 7;</li>
|
|
||||||
<li>Multiple apps in the same instance;</li>
|
|
||||||
<li>Drop in replacement for pusher server;</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
<a id="download-pre-built-binaries" class="anchor" href="#download-pre-built-binaries" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Download pre built binaries</h1>
|
|
||||||
|
|
||||||
<p>You can download pre built binaries from the <a href="https://github.com/dimiro1/ipe/releases">releases tab</a>.</p>
|
|
||||||
|
|
||||||
<p>I do not have a Windows machine, so I can only distribute binaries for amd64 linux and amd64 darwin.</p>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
<a id="building" class="anchor" href="#building" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Building</h1>
|
|
||||||
|
|
||||||
<div class="highlight highlight-text-shell-session"><pre>$ <span class="pl-s1">go get github.com/dimiro1/ipe</span></pre></div>
|
|
||||||
|
|
||||||
<p>or simply</p>
|
|
||||||
|
|
||||||
<div class="highlight highlight-text-shell-session"><pre>$ <span class="pl-s1">go install github.com/dimiro1/ipe</span></pre></div>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
<a id="how-to-configure" class="anchor" href="#how-to-configure" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>How to configure?</h1>
|
|
||||||
|
|
||||||
<h2>
|
|
||||||
<a id="the-server" class="anchor" href="#the-server" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>The server</h2>
|
|
||||||
|
|
||||||
<div class="highlight highlight-source-json"><pre>{
|
|
||||||
<span class="pl-s"><span class="pl-pds">"</span>Host<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>:8080<span class="pl-pds">"</span></span>,
|
|
||||||
<span class="pl-s"><span class="pl-pds">"</span>SSL<span class="pl-pds">"</span></span>: <span class="pl-c1">false</span>,
|
|
||||||
<span class="pl-s"><span class="pl-pds">"</span>SSLHost<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>:4433<span class="pl-pds">"</span></span>,
|
|
||||||
<span class="pl-s"><span class="pl-pds">"</span>SSLKeyFile<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>A key.pem file<span class="pl-pds">"</span></span>,
|
|
||||||
<span class="pl-s"><span class="pl-pds">"</span>SSLCertFile<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>A cert.pem file<span class="pl-pds">"</span></span>,
|
|
||||||
<span class="pl-s"><span class="pl-pds">"</span>Apps<span class="pl-pds">"</span></span>: [
|
|
||||||
{
|
|
||||||
<span class="pl-s"><span class="pl-pds">"</span>ApplicationDisabled<span class="pl-pds">"</span></span>: <span class="pl-c1">false</span>,
|
|
||||||
<span class="pl-s"><span class="pl-pds">"</span>Secret<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>A really secret random string<span class="pl-pds">"</span></span>,
|
|
||||||
<span class="pl-s"><span class="pl-pds">"</span>Key<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>A random Key string<span class="pl-pds">"</span></span>,
|
|
||||||
<span class="pl-s"><span class="pl-pds">"</span>OnlySSL<span class="pl-pds">"</span></span>: <span class="pl-c1">false</span>,
|
|
||||||
<span class="pl-s"><span class="pl-pds">"</span>Name<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>The app name<span class="pl-pds">"</span></span>,
|
|
||||||
<span class="pl-s"><span class="pl-pds">"</span>AppID<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>The app ID<span class="pl-pds">"</span></span>,
|
|
||||||
<span class="pl-s"><span class="pl-pds">"</span>UserEvents<span class="pl-pds">"</span></span>: <span class="pl-c1">true</span>,
|
|
||||||
<span class="pl-s"><span class="pl-pds">"</span>WebHooks<span class="pl-pds">"</span></span>: <span class="pl-c1">true</span>,
|
|
||||||
<span class="pl-s"><span class="pl-pds">"</span>URLWebHook<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>Some URL to send webhooks<span class="pl-pds">"</span></span>
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</pre></div>
|
|
||||||
|
|
||||||
<h2>
|
|
||||||
<a id="libraries" class="anchor" href="#libraries" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Libraries</h2>
|
|
||||||
|
|
||||||
<h3>
|
|
||||||
<a id="client-javascript-library" class="anchor" href="#client-javascript-library" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Client javascript library</h3>
|
|
||||||
|
|
||||||
<div class="highlight highlight-source-js"><pre><span class="pl-k">var</span> pusher <span class="pl-k">=</span> <span class="pl-k">new</span> <span class="pl-en">Pusher</span>(<span class="pl-c1">APP_KEY</span>, {
|
|
||||||
wsHost<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">'</span>localhost<span class="pl-pds">'</span></span>,
|
|
||||||
wsPort<span class="pl-k">:</span> <span class="pl-c1">8080</span>,
|
|
||||||
enabledTransports<span class="pl-k">:</span> [<span class="pl-s"><span class="pl-pds">"</span>ws<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>flash<span class="pl-pds">"</span></span>],
|
|
||||||
disabledTransports<span class="pl-k">:</span> [<span class="pl-s"><span class="pl-pds">"</span>flash<span class="pl-pds">"</span></span>]
|
|
||||||
});</pre></div>
|
|
||||||
|
|
||||||
<h3>
|
|
||||||
<a id="client-server-libraries" class="anchor" href="#client-server-libraries" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Client server libraries</h3>
|
|
||||||
|
|
||||||
<p>Ruby</p>
|
|
||||||
|
|
||||||
<div class="highlight highlight-source-ruby"><pre><span class="pl-c1">Pusher</span>.host <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">'</span>localhost<span class="pl-pds">'</span></span>
|
|
||||||
<span class="pl-c1">Pusher</span>.port <span class="pl-k">=</span> <span class="pl-c1">8080</span></pre></div>
|
|
||||||
|
|
||||||
<p>PHP</p>
|
|
||||||
|
|
||||||
<div class="highlight highlight-text-html-php"><pre><span class="pl-s1"><span class="pl-smi">$pusher</span> <span class="pl-k">=</span> <span class="pl-k">new</span> <span class="pl-c1">Pusher</span>(<span class="pl-c1">APP_KEY</span>, <span class="pl-c1">APP_SECRET</span>, <span class="pl-c1">APP_ID</span>, <span class="pl-c1">DEBUG</span>, <span class="pl-s"><span class="pl-pds">"</span>http://localhost<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>8080<span class="pl-pds">"</span></span>);</span></pre></div>
|
|
||||||
|
|
||||||
<p>NodeJS</p>
|
|
||||||
|
|
||||||
<div class="highlight highlight-source-js"><pre><span class="pl-k">var</span> pusher <span class="pl-k">=</span> <span class="pl-k">new</span> <span class="pl-en">Pusher</span>({
|
|
||||||
appId<span class="pl-k">:</span> <span class="pl-c1">APP_ID</span>,
|
|
||||||
key<span class="pl-k">:</span> <span class="pl-c1">APP_KEY</span>,
|
|
||||||
secret<span class="pl-k">:</span> <span class="pl-c1">APP_SECRET</span>
|
|
||||||
domain<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">'</span>localhost<span class="pl-pds">'</span></span>,
|
|
||||||
port<span class="pl-k">:</span> <span class="pl-c1">80</span>
|
|
||||||
});
|
|
||||||
</pre></div>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
<a id="logging" class="anchor" href="#logging" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Logging</h1>
|
|
||||||
|
|
||||||
<p>This software uses the <a href="https://github.com/golang/glog">glog</a> library</p>
|
|
||||||
|
|
||||||
<p>for more information about logging type the following in console.</p>
|
|
||||||
|
|
||||||
<div class="highlight highlight-text-shell-session"><pre>$ <span class="pl-s1">ipe -h</span></pre></div>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
<a id="when-use-this-software" class="anchor" href="#when-use-this-software" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>When use this software?</h1>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>When you are offline;</li>
|
|
||||||
<li>When you want to control your infrastructure;</li>
|
|
||||||
<li>When you do not want to have external dependencies;</li>
|
|
||||||
<li>When you want extend the protocol;</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
<a id="contributing" class="anchor" href="#contributing" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Contributing.</h1>
|
|
||||||
|
|
||||||
<p>Feel free to fork this repo.</p>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
<a id="pusher" class="anchor" href="#pusher" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Pusher</h1>
|
|
||||||
|
|
||||||
<p>Pusher is an excelent service, their service is very reliable. I recomend for everyone.</p>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
<a id="where-this-name-came-from" class="anchor" href="#where-this-name-came-from" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Where this name came from?</h1>
|
|
||||||
|
|
||||||
<p>Here in Brazil we have this beautiful tree called <a href="http://en.wikipedia.org/wiki/Tabebuia_aurea">Ipê</a>, it comes in differente colors: yellow, pink, white, purple.</p>
|
|
||||||
|
|
||||||
<p><a href="https://www.flickr.com/search/?q=ipe">I want to see pictures</a></p>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
<a id="author" class="anchor" href="#author" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Author</h1>
|
|
||||||
|
|
||||||
<p>Claudemiro Alves Feitosa Neto</p>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
<a id="license" class="anchor" href="#license" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>LICENSE</h1>
|
|
||||||
|
|
||||||
<p>Copyright 2014, 2015, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
|
||||||
Use of this source code is governed by a MIT-style
|
|
||||||
license that can be found in the LICENSE file.</p>
|
|
||||||
|
|
||||||
<footer class="site-footer">
|
|
||||||
<span class="site-footer-owner"><a href="https://github.com/dimiro1/ipe">Ipe</a> is maintained by <a href="https://github.com/dimiro1">dimiro1</a>.</span>
|
|
||||||
|
|
||||||
<span class="site-footer-credits">This page was generated by <a href="https://pages.github.com">GitHub Pages</a> using the <a href="https://github.com/jasonlong/cayman-theme">Cayman theme</a> by <a href="https://twitter.com/jasonlong">Jason Long</a>.</span>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Ipe
|
||||||
|
After=syslog.target network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=ipe
|
||||||
|
StandardOutput=syslog
|
||||||
|
StandardError=syslog
|
||||||
|
SyslogIdentifier=ipe
|
||||||
|
ExecStart=/path/to/ipe -logtostderr -config="path_to_config.json"
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
description "Ipê Upstart job definition"
|
||||||
|
author "Claudemiro Alves Feitosa Neto"
|
||||||
|
|
||||||
|
start on (net-device-up
|
||||||
|
and local-filesystems
|
||||||
|
and runlevel [2345])
|
||||||
|
|
||||||
|
stop on runlevel [016]
|
||||||
|
|
||||||
|
respawn
|
||||||
|
|
||||||
|
setuid ipe
|
||||||
|
|
||||||
|
exec /path/to/ipe -config="path_to_config.json" -log_dir="path_to_log_dir"
|
||||||
@@ -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 +0,0 @@
|
|||||||
console.log('This would be the main JS file.');
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
var metas = document.getElementsByTagName('meta');
|
|
||||||
var i;
|
|
||||||
if (navigator.userAgent.match(/iPhone/i)) {
|
|
||||||
for (i=0; i<metas.length; i++) {
|
|
||||||
if (metas[i].name == "viewport") {
|
|
||||||
metas[i].content = "width=device-width, minimum-scale=1.0, maximum-scale=1.0";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener("gesturestart", gestureStart, false);
|
|
||||||
}
|
|
||||||
function gestureStart() {
|
|
||||||
for (i=0; i<metas.length; i++) {
|
|
||||||
if (metas[i].name == "viewport") {
|
|
||||||
metas[i].content = "width=device-width, minimum-scale=0.25, maximum-scale=1.6";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,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 +0,0 @@
|
|||||||
{"name":"Ipe","tagline":"A Pusher server implementation compatible with Pusher client libraries written in GO","body":"[](http://goreportcard.com/report/dimiro1/ipe)\r\n\r\n# IPÊ\r\n\r\nThis software is written in Go - the WYSIWYG lang\r\n\r\n# Why I wrote this software?\r\n\r\n1. I wanted to learn Go and I needed a non trivial application;\r\n2. I use Pusher in some projects;\r\n3. I really like Pusher;\r\n\r\n# Features\r\n\r\n* Public Channels;\r\n* Private Channels;\r\n* Presence Channels;\r\n* Web Hooks;\r\n* Client events;\r\n* Complete REST API;\r\n* Easy instalation;\r\n* A single binary without dependencies;\r\n* Easy configuration;\r\n* Protocol version 7;\r\n* Multiple apps in the same instance;\r\n* Drop in replacement for pusher server;\r\n\r\n# Download pre built binaries\r\n\r\nYou can download pre built binaries from the [releases tab](https://github.com/dimiro1/ipe/releases).\r\n\r\nI do not have a Windows machine, so I can only distribute binaries for amd64 linux and amd64 darwin.\r\n\r\n# Building\r\n\r\n```console\r\n$ go get github.com/dimiro1/ipe\r\n```\r\n\r\nor simply\r\n\r\n```console\r\n$ go install github.com/dimiro1/ipe\r\n```\r\n\r\n# How to configure?\r\n\r\n## The server\r\n\r\n```json\r\n{\r\n\t\"Host\": \":8080\",\r\n\t\"SSL\": false,\r\n\t\"SSLHost\": \":4433\",\r\n\t\"SSLKeyFile\": \"A key.pem file\",\r\n\t\"SSLCertFile\": \"A cert.pem file\",\r\n\t\"Apps\": [\r\n\t\t{\r\n\t\t\t\"ApplicationDisabled\": false,\r\n\t\t\t\"Secret\": \"A really secret random string\",\r\n\t\t\t\"Key\": \"A random Key string\",\r\n\t\t\t\"OnlySSL\": false,\r\n\t\t\t\"Name\": \"The app name\",\r\n\t\t\t\"AppID\": \"The app ID\",\r\n\t\t\t\"UserEvents\": true,\r\n\t\t\t\"WebHooks\": true,\r\n\t\t\t\"URLWebHook\": \"Some URL to send webhooks\"\r\n\t\t}\r\n\t]\r\n}\r\n\r\n```\r\n\r\n## Libraries\r\n\r\n### Client javascript library\r\n\r\n```javascript\r\nvar pusher = new Pusher(APP_KEY, {\r\n wsHost: 'localhost',\r\n wsPort: 8080,\r\n enabledTransports: [\"ws\", \"flash\"],\r\n disabledTransports: [\"flash\"]\r\n});\r\n```\r\n\r\n### Client server libraries\r\n\r\nRuby\r\n\r\n```ruby\r\nPusher.host = 'localhost'\r\nPusher.port = 8080\r\n```\r\n\r\nPHP\r\n\r\n```php\r\n$pusher = new Pusher(APP_KEY, APP_SECRET, APP_ID, DEBUG, \"http://localhost\", \"8080\");\r\n```\r\n\r\nNodeJS\r\n\r\n```javascript\r\nvar pusher = new Pusher({\r\n appId: APP_ID,\r\n key: APP_KEY,\r\n secret: APP_SECRET\r\n domain: 'localhost',\r\n port: 80\r\n});\r\n\r\n```\r\n\r\n# Logging\r\n\r\nThis software uses the [glog](https://github.com/golang/glog) library\r\n\r\nfor more information about logging type the following in console.\r\n\r\n```console\r\n$ ipe -h\r\n```\r\n\r\n# When use this software?\r\n\r\n* When you are offline;\r\n* When you want to control your infrastructure;\r\n* When you do not want to have external dependencies;\r\n* When you want extend the protocol;\r\n\r\n# Contributing.\r\n\r\nFeel free to fork this repo.\r\n\r\n# Pusher\r\n\r\nPusher is an excelent service, their service is very reliable. I recomend for everyone.\r\n\r\n# Where this name came from?\r\n\r\nHere in Brazil we have this beautiful tree called [Ipê](http://en.wikipedia.org/wiki/Tabebuia_aurea), it comes in differente colors: yellow, pink, white, purple.\r\n\r\n[I want to see pictures](https://www.flickr.com/search/?q=ipe)\r\n\r\n# Author\r\n\r\nClaudemiro Alves Feitosa Neto\r\n\r\n# LICENSE\r\n\r\nCopyright 2014, 2015, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.\r\nUse of this source code is governed by a MIT-style\r\nlicense that can be found in the LICENSE file.\r\n\r\n","google":"","note":"Don't delete this file! It's used internally to help with page regeneration."}
|
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 GitHub Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
.pl-c /* comment */ {
|
|
||||||
color: #969896;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-c1 /* constant, markup.raw, meta.diff.header, meta.module-reference, meta.property-name, support, support.constant, support.variable, variable.other.constant */,
|
|
||||||
.pl-s .pl-v /* string variable */ {
|
|
||||||
color: #0086b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-e /* entity */,
|
|
||||||
.pl-en /* entity.name */ {
|
|
||||||
color: #795da3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-s .pl-s1 /* string source */,
|
|
||||||
.pl-smi /* storage.modifier.import, storage.modifier.package, storage.type.java, variable.other, variable.parameter.function */ {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-ent /* entity.name.tag */ {
|
|
||||||
color: #63a35c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-k /* keyword, storage, storage.type */ {
|
|
||||||
color: #a71d5d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-pds /* punctuation.definition.string, string.regexp.character-class */,
|
|
||||||
.pl-s /* string */,
|
|
||||||
.pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */,
|
|
||||||
.pl-sr /* string.regexp */,
|
|
||||||
.pl-sr .pl-cce /* string.regexp constant.character.escape */,
|
|
||||||
.pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */,
|
|
||||||
.pl-sr .pl-sre /* string.regexp source.ruby.embedded */ {
|
|
||||||
color: #183691;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-v /* variable */ {
|
|
||||||
color: #ed6a43;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-id /* invalid.deprecated */ {
|
|
||||||
color: #b52a1d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-ii /* invalid.illegal */ {
|
|
||||||
background-color: #b52a1d;
|
|
||||||
color: #f8f8f8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-sr .pl-cce /* string.regexp constant.character.escape */ {
|
|
||||||
color: #63a35c;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-ml /* markup.list */ {
|
|
||||||
color: #693a17;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-mh /* markup.heading */,
|
|
||||||
.pl-mh .pl-en /* markup.heading entity.name */,
|
|
||||||
.pl-ms /* meta.separator */ {
|
|
||||||
color: #1d3e81;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-mq /* markup.quote */ {
|
|
||||||
color: #008080;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-mi /* markup.italic */ {
|
|
||||||
color: #333;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-mb /* markup.bold */ {
|
|
||||||
color: #333;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-md /* markup.deleted, meta.diff.header.from-file */ {
|
|
||||||
background-color: #ffecec;
|
|
||||||
color: #bd2c00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-mi1 /* markup.inserted, meta.diff.header.to-file */ {
|
|
||||||
background-color: #eaffea;
|
|
||||||
color: #55a532;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-mdr /* meta.diff.range */ {
|
|
||||||
color: #795da3;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-mo /* meta.output */ {
|
|
||||||
color: #1d3e81;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vendored
-424
@@ -1,424 +0,0 @@
|
|||||||
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Set default font family to sans-serif.
|
|
||||||
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
|
||||||
* user zoom.
|
|
||||||
*/
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-family: sans-serif; /* 1 */
|
|
||||||
-ms-text-size-adjust: 100%; /* 2 */
|
|
||||||
-webkit-text-size-adjust: 100%; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove default margin.
|
|
||||||
*/
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* HTML5 display definitions
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Correct `block` display not defined for any HTML5 element in IE 8/9.
|
|
||||||
* Correct `block` display not defined for `details` or `summary` in IE 10/11
|
|
||||||
* and Firefox.
|
|
||||||
* Correct `block` display not defined for `main` in IE 11.
|
|
||||||
*/
|
|
||||||
|
|
||||||
article,
|
|
||||||
aside,
|
|
||||||
details,
|
|
||||||
figcaption,
|
|
||||||
figure,
|
|
||||||
footer,
|
|
||||||
header,
|
|
||||||
hgroup,
|
|
||||||
main,
|
|
||||||
menu,
|
|
||||||
nav,
|
|
||||||
section,
|
|
||||||
summary {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Correct `inline-block` display not defined in IE 8/9.
|
|
||||||
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
|
|
||||||
*/
|
|
||||||
|
|
||||||
audio,
|
|
||||||
canvas,
|
|
||||||
progress,
|
|
||||||
video {
|
|
||||||
display: inline-block; /* 1 */
|
|
||||||
vertical-align: baseline; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prevent modern browsers from displaying `audio` without controls.
|
|
||||||
* Remove excess height in iOS 5 devices.
|
|
||||||
*/
|
|
||||||
|
|
||||||
audio:not([controls]) {
|
|
||||||
display: none;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Address `[hidden]` styling not present in IE 8/9/10.
|
|
||||||
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
|
|
||||||
*/
|
|
||||||
|
|
||||||
[hidden],
|
|
||||||
template {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Links
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the gray background color from active links in IE 10.
|
|
||||||
*/
|
|
||||||
|
|
||||||
a {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Improve readability when focused and also mouse hovered in all browsers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
a:active,
|
|
||||||
a:hover {
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Text-level semantics
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
|
|
||||||
*/
|
|
||||||
|
|
||||||
abbr[title] {
|
|
||||||
border-bottom: 1px dotted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
|
|
||||||
*/
|
|
||||||
|
|
||||||
b,
|
|
||||||
strong {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Address styling not present in Safari and Chrome.
|
|
||||||
*/
|
|
||||||
|
|
||||||
dfn {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Address variable `h1` font-size and margin within `section` and `article`
|
|
||||||
* contexts in Firefox 4+, Safari, and Chrome.
|
|
||||||
*/
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2em;
|
|
||||||
margin: 0.67em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Address styling not present in IE 8/9.
|
|
||||||
*/
|
|
||||||
|
|
||||||
mark {
|
|
||||||
background: #ff0;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Address inconsistent and variable font size in all browsers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
small {
|
|
||||||
font-size: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
sub,
|
|
||||||
sup {
|
|
||||||
font-size: 75%;
|
|
||||||
line-height: 0;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
sup {
|
|
||||||
top: -0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub {
|
|
||||||
bottom: -0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Embedded content
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove border when inside `a` element in IE 8/9/10.
|
|
||||||
*/
|
|
||||||
|
|
||||||
img {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Correct overflow not hidden in IE 9/10/11.
|
|
||||||
*/
|
|
||||||
|
|
||||||
svg:not(:root) {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Grouping content
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Address margin not present in IE 8/9 and Safari.
|
|
||||||
*/
|
|
||||||
|
|
||||||
figure {
|
|
||||||
margin: 1em 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Address differences between Firefox and other browsers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
hr {
|
|
||||||
box-sizing: content-box;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contain overflow in all browsers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
pre {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Address odd `em`-unit font size rendering in all browsers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
code,
|
|
||||||
kbd,
|
|
||||||
pre,
|
|
||||||
samp {
|
|
||||||
font-family: monospace, monospace;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Forms
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Known limitation: by default, Chrome and Safari on OS X allow very limited
|
|
||||||
* styling of `select`, unless a `border` property is set.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Correct color not being inherited.
|
|
||||||
* Known issue: affects color of disabled elements.
|
|
||||||
* 2. Correct font properties not being inherited.
|
|
||||||
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
|
||||||
*/
|
|
||||||
|
|
||||||
button,
|
|
||||||
input,
|
|
||||||
optgroup,
|
|
||||||
select,
|
|
||||||
textarea {
|
|
||||||
color: inherit; /* 1 */
|
|
||||||
font: inherit; /* 2 */
|
|
||||||
margin: 0; /* 3 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
|
||||||
*/
|
|
||||||
|
|
||||||
button {
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
|
||||||
* All other form control elements do not inherit `text-transform` values.
|
|
||||||
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
|
|
||||||
* Correct `select` style inheritance in Firefox.
|
|
||||||
*/
|
|
||||||
|
|
||||||
button,
|
|
||||||
select {
|
|
||||||
text-transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
|
||||||
* and `video` controls.
|
|
||||||
* 2. Correct inability to style clickable `input` types in iOS.
|
|
||||||
* 3. Improve usability and consistency of cursor style between image-type
|
|
||||||
* `input` and others.
|
|
||||||
*/
|
|
||||||
|
|
||||||
button,
|
|
||||||
html input[type="button"], /* 1 */
|
|
||||||
input[type="reset"],
|
|
||||||
input[type="submit"] {
|
|
||||||
-webkit-appearance: button; /* 2 */
|
|
||||||
cursor: pointer; /* 3 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-set default cursor for disabled elements.
|
|
||||||
*/
|
|
||||||
|
|
||||||
button[disabled],
|
|
||||||
html input[disabled] {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove inner padding and border in Firefox 4+.
|
|
||||||
*/
|
|
||||||
|
|
||||||
button::-moz-focus-inner,
|
|
||||||
input::-moz-focus-inner {
|
|
||||||
border: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
|
||||||
* the UA stylesheet.
|
|
||||||
*/
|
|
||||||
|
|
||||||
input {
|
|
||||||
line-height: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* It's recommended that you don't attempt to style these elements.
|
|
||||||
* Firefox's implementation doesn't respect box-sizing, padding, or width.
|
|
||||||
*
|
|
||||||
* 1. Address box sizing set to `content-box` in IE 8/9/10.
|
|
||||||
* 2. Remove excess padding in IE 8/9/10.
|
|
||||||
*/
|
|
||||||
|
|
||||||
input[type="checkbox"],
|
|
||||||
input[type="radio"] {
|
|
||||||
box-sizing: border-box; /* 1 */
|
|
||||||
padding: 0; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
|
|
||||||
* `font-size` values of the `input`, it causes the cursor style of the
|
|
||||||
* decrement button to change from `default` to `text`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
input[type="number"]::-webkit-inner-spin-button,
|
|
||||||
input[type="number"]::-webkit-outer-spin-button {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
|
|
||||||
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
|
|
||||||
* (include `-moz` to future-proof).
|
|
||||||
*/
|
|
||||||
|
|
||||||
input[type="search"] {
|
|
||||||
-webkit-appearance: textfield; /* 1 */ /* 2 */
|
|
||||||
box-sizing: content-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
|
|
||||||
* Safari (but not Chrome) clips the cancel button when the search input has
|
|
||||||
* padding (and `textfield` appearance).
|
|
||||||
*/
|
|
||||||
|
|
||||||
input[type="search"]::-webkit-search-cancel-button,
|
|
||||||
input[type="search"]::-webkit-search-decoration {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define consistent border, margin, and padding.
|
|
||||||
*/
|
|
||||||
|
|
||||||
fieldset {
|
|
||||||
border: 1px solid #c0c0c0;
|
|
||||||
margin: 0 2px;
|
|
||||||
padding: 0.35em 0.625em 0.75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Correct `color` not being inherited in IE 8/9/10/11.
|
|
||||||
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
|
||||||
*/
|
|
||||||
|
|
||||||
legend {
|
|
||||||
border: 0; /* 1 */
|
|
||||||
padding: 0; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove default vertical scrollbar in IE 8/9/10/11.
|
|
||||||
*/
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Don't inherit the `font-weight` (applied by a rule above).
|
|
||||||
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
|
|
||||||
*/
|
|
||||||
|
|
||||||
optgroup {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tables
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove most spacing between table cells.
|
|
||||||
*/
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
border-spacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
.highlight .c { color: #999988; font-style: italic } /* Comment */
|
|
||||||
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
|
|
||||||
.highlight .k { font-weight: bold } /* Keyword */
|
|
||||||
.highlight .o { font-weight: bold } /* Operator */
|
|
||||||
.highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */
|
|
||||||
.highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */
|
|
||||||
.highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */
|
|
||||||
.highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */
|
|
||||||
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
|
|
||||||
.highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */
|
|
||||||
.highlight .ge { font-style: italic } /* Generic.Emph */
|
|
||||||
.highlight .gr { color: #aa0000 } /* Generic.Error */
|
|
||||||
.highlight .gh { color: #999999 } /* Generic.Heading */
|
|
||||||
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
|
|
||||||
.highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */
|
|
||||||
.highlight .go { color: #888888 } /* Generic.Output */
|
|
||||||
.highlight .gp { color: #555555 } /* Generic.Prompt */
|
|
||||||
.highlight .gs { font-weight: bold } /* Generic.Strong */
|
|
||||||
.highlight .gu { color: #800080; font-weight: bold; } /* Generic.Subheading */
|
|
||||||
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
|
|
||||||
.highlight .kc { font-weight: bold } /* Keyword.Constant */
|
|
||||||
.highlight .kd { font-weight: bold } /* Keyword.Declaration */
|
|
||||||
.highlight .kn { font-weight: bold } /* Keyword.Namespace */
|
|
||||||
.highlight .kp { font-weight: bold } /* Keyword.Pseudo */
|
|
||||||
.highlight .kr { font-weight: bold } /* Keyword.Reserved */
|
|
||||||
.highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */
|
|
||||||
.highlight .m { color: #009999 } /* Literal.Number */
|
|
||||||
.highlight .s { color: #d14 } /* Literal.String */
|
|
||||||
.highlight .na { color: #008080 } /* Name.Attribute */
|
|
||||||
.highlight .nb { color: #0086B3 } /* Name.Builtin */
|
|
||||||
.highlight .nc { color: #445588; font-weight: bold } /* Name.Class */
|
|
||||||
.highlight .no { color: #008080 } /* Name.Constant */
|
|
||||||
.highlight .ni { color: #800080 } /* Name.Entity */
|
|
||||||
.highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */
|
|
||||||
.highlight .nf { color: #990000; font-weight: bold } /* Name.Function */
|
|
||||||
.highlight .nn { color: #555555 } /* Name.Namespace */
|
|
||||||
.highlight .nt { color: #CBDFFF } /* Name.Tag */
|
|
||||||
.highlight .nv { color: #008080 } /* Name.Variable */
|
|
||||||
.highlight .ow { font-weight: bold } /* Operator.Word */
|
|
||||||
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
|
|
||||||
.highlight .mf { color: #009999 } /* Literal.Number.Float */
|
|
||||||
.highlight .mh { color: #009999 } /* Literal.Number.Hex */
|
|
||||||
.highlight .mi { color: #009999 } /* Literal.Number.Integer */
|
|
||||||
.highlight .mo { color: #009999 } /* Literal.Number.Oct */
|
|
||||||
.highlight .sb { color: #d14 } /* Literal.String.Backtick */
|
|
||||||
.highlight .sc { color: #d14 } /* Literal.String.Char */
|
|
||||||
.highlight .sd { color: #d14 } /* Literal.String.Doc */
|
|
||||||
.highlight .s2 { color: #d14 } /* Literal.String.Double */
|
|
||||||
.highlight .se { color: #d14 } /* Literal.String.Escape */
|
|
||||||
.highlight .sh { color: #d14 } /* Literal.String.Heredoc */
|
|
||||||
.highlight .si { color: #d14 } /* Literal.String.Interpol */
|
|
||||||
.highlight .sx { color: #d14 } /* Literal.String.Other */
|
|
||||||
.highlight .sr { color: #009926 } /* Literal.String.Regex */
|
|
||||||
.highlight .s1 { color: #d14 } /* Literal.String.Single */
|
|
||||||
.highlight .ss { color: #990073 } /* Literal.String.Symbol */
|
|
||||||
.highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */
|
|
||||||
.highlight .vc { color: #008080 } /* Name.Variable.Class */
|
|
||||||
.highlight .vg { color: #008080 } /* Name.Variable.Global */
|
|
||||||
.highlight .vi { color: #008080 } /* Name.Variable.Instance */
|
|
||||||
.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */
|
|
||||||
|
|
||||||
.type-csharp .highlight .k { color: #0000FF }
|
|
||||||
.type-csharp .highlight .kt { color: #0000FF }
|
|
||||||
.type-csharp .highlight .nf { color: #000000; font-weight: normal }
|
|
||||||
.type-csharp .highlight .nc { color: #2B91AF }
|
|
||||||
.type-csharp .highlight .nn { color: #000000 }
|
|
||||||
.type-csharp .highlight .s { color: #A31515 }
|
|
||||||
.type-csharp .highlight .sc { color: #A31515 }
|
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: 'Noto Sans';
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
src: url('../fonts/Noto-Sans-regular/Noto-Sans-regular.eot');
|
|
||||||
src: url('../fonts/Noto-Sans-regular/Noto-Sans-regular.eot?#iefix') format('embedded-opentype'),
|
|
||||||
local('Noto Sans'),
|
|
||||||
local('Noto-Sans-regular'),
|
|
||||||
url('../fonts/Noto-Sans-regular/Noto-Sans-regular.woff2') format('woff2'),
|
|
||||||
url('../fonts/Noto-Sans-regular/Noto-Sans-regular.woff') format('woff'),
|
|
||||||
url('../fonts/Noto-Sans-regular/Noto-Sans-regular.ttf') format('truetype'),
|
|
||||||
url('../fonts/Noto-Sans-regular/Noto-Sans-regular.svg#NotoSans') format('svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Noto Sans';
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
src: url('../fonts/Noto-Sans-700/Noto-Sans-700.eot');
|
|
||||||
src: url('../fonts/Noto-Sans-700/Noto-Sans-700.eot?#iefix') format('embedded-opentype'),
|
|
||||||
local('Noto Sans Bold'),
|
|
||||||
local('Noto-Sans-700'),
|
|
||||||
url('../fonts/Noto-Sans-700/Noto-Sans-700.woff2') format('woff2'),
|
|
||||||
url('../fonts/Noto-Sans-700/Noto-Sans-700.woff') format('woff'),
|
|
||||||
url('../fonts/Noto-Sans-700/Noto-Sans-700.ttf') format('truetype'),
|
|
||||||
url('../fonts/Noto-Sans-700/Noto-Sans-700.svg#NotoSans') format('svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Noto Sans';
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: italic;
|
|
||||||
src: url('../fonts/Noto-Sans-italic/Noto-Sans-italic.eot');
|
|
||||||
src: url('../fonts/Noto-Sans-italic/Noto-Sans-italic.eot?#iefix') format('embedded-opentype'),
|
|
||||||
local('Noto Sans Italic'),
|
|
||||||
local('Noto-Sans-italic'),
|
|
||||||
url('../fonts/Noto-Sans-italic/Noto-Sans-italic.woff2') format('woff2'),
|
|
||||||
url('../fonts/Noto-Sans-italic/Noto-Sans-italic.woff') format('woff'),
|
|
||||||
url('../fonts/Noto-Sans-italic/Noto-Sans-italic.ttf') format('truetype'),
|
|
||||||
url('../fonts/Noto-Sans-italic/Noto-Sans-italic.svg#NotoSans') format('svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Noto Sans';
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: italic;
|
|
||||||
src: url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.eot');
|
|
||||||
src: url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.eot?#iefix') format('embedded-opentype'),
|
|
||||||
local('Noto Sans Bold Italic'),
|
|
||||||
local('Noto-Sans-700italic'),
|
|
||||||
url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.woff2') format('woff2'),
|
|
||||||
url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.woff') format('woff'),
|
|
||||||
url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.ttf') format('truetype'),
|
|
||||||
url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.svg#NotoSans') format('svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #fff;
|
|
||||||
padding:50px;
|
|
||||||
font: 14px/1.5 "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
||||||
color:#727272;
|
|
||||||
font-weight:400;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
color:#222;
|
|
||||||
margin:0 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p, ul, ol, table, pre, dl {
|
|
||||||
margin:0 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3 {
|
|
||||||
line-height:1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size:28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
color:#393939;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3, h4, h5, h6 {
|
|
||||||
color:#494949;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color:#39c;
|
|
||||||
text-decoration:none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color:#069;
|
|
||||||
}
|
|
||||||
|
|
||||||
a small {
|
|
||||||
font-size:11px;
|
|
||||||
color:#777;
|
|
||||||
margin-top:-0.3em;
|
|
||||||
display:block;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover small {
|
|
||||||
color:#777;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
width:860px;
|
|
||||||
margin:0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
border-left:1px solid #e5e5e5;
|
|
||||||
margin:0;
|
|
||||||
padding:0 0 0 20px;
|
|
||||||
font-style:italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
code, pre {
|
|
||||||
font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal, Consolas, Liberation Mono, DejaVu Sans Mono, Courier New, monospace;
|
|
||||||
color:#333;
|
|
||||||
font-size:12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
padding:8px 15px;
|
|
||||||
background: #f8f8f8;
|
|
||||||
border-radius:5px;
|
|
||||||
border:1px solid #e5e5e5;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width:100%;
|
|
||||||
border-collapse:collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
text-align:left;
|
|
||||||
padding:5px 10px;
|
|
||||||
border-bottom:1px solid #e5e5e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
dt {
|
|
||||||
color:#444;
|
|
||||||
font-weight:700;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
color:#444;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width:100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
width:270px;
|
|
||||||
float:left;
|
|
||||||
position:fixed;
|
|
||||||
-webkit-font-smoothing:subpixel-antialiased;
|
|
||||||
}
|
|
||||||
|
|
||||||
header ul {
|
|
||||||
list-style:none;
|
|
||||||
height:40px;
|
|
||||||
padding:0;
|
|
||||||
background: #f4f4f4;
|
|
||||||
border-radius:5px;
|
|
||||||
border:1px solid #e0e0e0;
|
|
||||||
width:270px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header li {
|
|
||||||
width:89px;
|
|
||||||
float:left;
|
|
||||||
border-right:1px solid #e0e0e0;
|
|
||||||
height:40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header li:first-child a {
|
|
||||||
border-radius:5px 0 0 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header li:last-child a {
|
|
||||||
border-radius:0 5px 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header ul a {
|
|
||||||
line-height:1;
|
|
||||||
font-size:11px;
|
|
||||||
color:#999;
|
|
||||||
display:block;
|
|
||||||
text-align:center;
|
|
||||||
padding-top:6px;
|
|
||||||
height:34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header ul a:hover {
|
|
||||||
color:#999;
|
|
||||||
}
|
|
||||||
|
|
||||||
header ul a:active {
|
|
||||||
background-color:#f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
color:#222;
|
|
||||||
font-weight:700;
|
|
||||||
}
|
|
||||||
|
|
||||||
header ul li + li + li {
|
|
||||||
border-right:none;
|
|
||||||
width:89px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header ul a strong {
|
|
||||||
font-size:14px;
|
|
||||||
display:block;
|
|
||||||
color:#222;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
width:500px;
|
|
||||||
float:right;
|
|
||||||
padding-bottom:50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
small {
|
|
||||||
font-size:11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
border:0;
|
|
||||||
background:#e5e5e5;
|
|
||||||
height:1px;
|
|
||||||
margin:0 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
width:270px;
|
|
||||||
float:left;
|
|
||||||
position:fixed;
|
|
||||||
bottom:50px;
|
|
||||||
-webkit-font-smoothing:subpixel-antialiased;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print, screen and (max-width: 960px) {
|
|
||||||
|
|
||||||
div.wrapper {
|
|
||||||
width:auto;
|
|
||||||
margin:0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header, section, footer {
|
|
||||||
float:none;
|
|
||||||
position:static;
|
|
||||||
width:auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
padding-right:320px;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
border:1px solid #e5e5e5;
|
|
||||||
border-width:1px 0;
|
|
||||||
padding:20px 0;
|
|
||||||
margin:0 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header a small {
|
|
||||||
display:inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
header ul {
|
|
||||||
position:absolute;
|
|
||||||
right:50px;
|
|
||||||
top:52px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print, screen and (max-width: 720px) {
|
|
||||||
body {
|
|
||||||
word-wrap:break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
padding:0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header ul, header p.view {
|
|
||||||
position:static;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre, code {
|
|
||||||
word-wrap:normal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print, screen and (max-width: 480px) {
|
|
||||||
body {
|
|
||||||
padding:15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header ul {
|
|
||||||
width:99%;
|
|
||||||
}
|
|
||||||
|
|
||||||
header li, header ul li + li + li {
|
|
||||||
width:33%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
body {
|
|
||||||
padding:0.4in;
|
|
||||||
font-size:12pt;
|
|
||||||
color:#444;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
* {
|
|
||||||
box-sizing: border-box; }
|
|
||||||
|
|
||||||
body {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #606c71; }
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #1e6bb8;
|
|
||||||
text-decoration: none; }
|
|
||||||
a:hover {
|
|
||||||
text-decoration: underline; }
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
display: inline-block;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
background-color: rgba(255, 255, 255, 0.08);
|
|
||||||
border-color: rgba(255, 255, 255, 0.2);
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 1px;
|
|
||||||
border-radius: 0.3rem;
|
|
||||||
transition: color 0.2s, background-color 0.2s, border-color 0.2s; }
|
|
||||||
.btn + .btn {
|
|
||||||
margin-left: 1rem; }
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
text-decoration: none;
|
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
|
||||||
border-color: rgba(255, 255, 255, 0.3); }
|
|
||||||
|
|
||||||
@media screen and (min-width: 64em) {
|
|
||||||
.btn {
|
|
||||||
padding: 0.75rem 1rem; } }
|
|
||||||
|
|
||||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
|
||||||
.btn {
|
|
||||||
padding: 0.6rem 0.9rem;
|
|
||||||
font-size: 0.9rem; } }
|
|
||||||
|
|
||||||
@media screen and (max-width: 42em) {
|
|
||||||
.btn {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.75rem;
|
|
||||||
font-size: 0.9rem; }
|
|
||||||
.btn + .btn {
|
|
||||||
margin-top: 1rem;
|
|
||||||
margin-left: 0; } }
|
|
||||||
|
|
||||||
.page-header {
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
background-color: #159957;
|
|
||||||
background-image: linear-gradient(120deg, #155799, #159957); }
|
|
||||||
|
|
||||||
@media screen and (min-width: 64em) {
|
|
||||||
.page-header {
|
|
||||||
padding: 5rem 6rem; } }
|
|
||||||
|
|
||||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
|
||||||
.page-header {
|
|
||||||
padding: 3rem 4rem; } }
|
|
||||||
|
|
||||||
@media screen and (max-width: 42em) {
|
|
||||||
.page-header {
|
|
||||||
padding: 2rem 1rem; } }
|
|
||||||
|
|
||||||
.project-name {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0.1rem; }
|
|
||||||
|
|
||||||
@media screen and (min-width: 64em) {
|
|
||||||
.project-name {
|
|
||||||
font-size: 3.25rem; } }
|
|
||||||
|
|
||||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
|
||||||
.project-name {
|
|
||||||
font-size: 2.25rem; } }
|
|
||||||
|
|
||||||
@media screen and (max-width: 42em) {
|
|
||||||
.project-name {
|
|
||||||
font-size: 1.75rem; } }
|
|
||||||
|
|
||||||
.project-tagline {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
font-weight: normal;
|
|
||||||
opacity: 0.7; }
|
|
||||||
|
|
||||||
@media screen and (min-width: 64em) {
|
|
||||||
.project-tagline {
|
|
||||||
font-size: 1.25rem; } }
|
|
||||||
|
|
||||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
|
||||||
.project-tagline {
|
|
||||||
font-size: 1.15rem; } }
|
|
||||||
|
|
||||||
@media screen and (max-width: 42em) {
|
|
||||||
.project-tagline {
|
|
||||||
font-size: 1rem; } }
|
|
||||||
|
|
||||||
.main-content :first-child {
|
|
||||||
margin-top: 0; }
|
|
||||||
.main-content img {
|
|
||||||
max-width: 100%; }
|
|
||||||
.main-content h1, .main-content h2, .main-content h3, .main-content h4, .main-content h5, .main-content h6 {
|
|
||||||
margin-top: 2rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
font-weight: normal;
|
|
||||||
color: #159957; }
|
|
||||||
.main-content p {
|
|
||||||
margin-bottom: 1em; }
|
|
||||||
.main-content code {
|
|
||||||
padding: 2px 4px;
|
|
||||||
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #383e41;
|
|
||||||
background-color: #f3f6fa;
|
|
||||||
border-radius: 0.3rem; }
|
|
||||||
.main-content pre {
|
|
||||||
padding: 0.8rem;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
font: 1rem Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
|
||||||
color: #567482;
|
|
||||||
word-wrap: normal;
|
|
||||||
background-color: #f3f6fa;
|
|
||||||
border: solid 1px #dce6f0;
|
|
||||||
border-radius: 0.3rem; }
|
|
||||||
.main-content pre > code {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #567482;
|
|
||||||
word-break: normal;
|
|
||||||
white-space: pre;
|
|
||||||
background: transparent;
|
|
||||||
border: 0; }
|
|
||||||
.main-content .highlight {
|
|
||||||
margin-bottom: 1rem; }
|
|
||||||
.main-content .highlight pre {
|
|
||||||
margin-bottom: 0;
|
|
||||||
word-break: normal; }
|
|
||||||
.main-content .highlight pre, .main-content pre {
|
|
||||||
padding: 0.8rem;
|
|
||||||
overflow: auto;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.45;
|
|
||||||
border-radius: 0.3rem; }
|
|
||||||
.main-content pre code, .main-content pre tt {
|
|
||||||
display: inline;
|
|
||||||
max-width: initial;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
overflow: initial;
|
|
||||||
line-height: inherit;
|
|
||||||
word-wrap: normal;
|
|
||||||
background-color: transparent;
|
|
||||||
border: 0; }
|
|
||||||
.main-content pre code:before, .main-content pre code:after, .main-content pre tt:before, .main-content pre tt:after {
|
|
||||||
content: normal; }
|
|
||||||
.main-content ul, .main-content ol {
|
|
||||||
margin-top: 0; }
|
|
||||||
.main-content blockquote {
|
|
||||||
padding: 0 1rem;
|
|
||||||
margin-left: 0;
|
|
||||||
color: #819198;
|
|
||||||
border-left: 0.3rem solid #dce6f0; }
|
|
||||||
.main-content blockquote > :first-child {
|
|
||||||
margin-top: 0; }
|
|
||||||
.main-content blockquote > :last-child {
|
|
||||||
margin-bottom: 0; }
|
|
||||||
.main-content table {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
word-break: normal;
|
|
||||||
word-break: keep-all; }
|
|
||||||
.main-content table th {
|
|
||||||
font-weight: bold; }
|
|
||||||
.main-content table th, .main-content table td {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border: 1px solid #e9ebec; }
|
|
||||||
.main-content dl {
|
|
||||||
padding: 0; }
|
|
||||||
.main-content dl dt {
|
|
||||||
padding: 0;
|
|
||||||
margin-top: 1rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: bold; }
|
|
||||||
.main-content dl dd {
|
|
||||||
padding: 0;
|
|
||||||
margin-bottom: 1rem; }
|
|
||||||
.main-content hr {
|
|
||||||
height: 2px;
|
|
||||||
padding: 0;
|
|
||||||
margin: 1rem 0;
|
|
||||||
background-color: #eff0f1;
|
|
||||||
border: 0; }
|
|
||||||
|
|
||||||
@media screen and (min-width: 64em) {
|
|
||||||
.main-content {
|
|
||||||
max-width: 64rem;
|
|
||||||
padding: 2rem 6rem;
|
|
||||||
margin: 0 auto;
|
|
||||||
font-size: 1.1rem; } }
|
|
||||||
|
|
||||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
|
||||||
.main-content {
|
|
||||||
padding: 2rem 4rem;
|
|
||||||
font-size: 1.1rem; } }
|
|
||||||
|
|
||||||
@media screen and (max-width: 42em) {
|
|
||||||
.main-content {
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
font-size: 1rem; } }
|
|
||||||
|
|
||||||
.site-footer {
|
|
||||||
padding-top: 2rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
border-top: solid 1px #eff0f1; }
|
|
||||||
|
|
||||||
.site-footer-owner {
|
|
||||||
display: block;
|
|
||||||
font-weight: bold; }
|
|
||||||
|
|
||||||
.site-footer-credits {
|
|
||||||
color: #819198; }
|
|
||||||
|
|
||||||
@media screen and (min-width: 64em) {
|
|
||||||
.site-footer {
|
|
||||||
font-size: 1rem; } }
|
|
||||||
|
|
||||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
|
||||||
.site-footer {
|
|
||||||
font-size: 1rem; } }
|
|
||||||
|
|
||||||
@media screen and (max-width: 42em) {
|
|
||||||
.site-footer {
|
|
||||||
font-size: 0.9rem; } }
|
|
||||||
@@ -0,0 +1,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}
|
||||||
|
}
|
||||||
@@ -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-")
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"}
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user