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

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

* created function validateAuthKey

* Using go default error interface.
2016-08-13 12:25:38 -03:00
claudemiro 0ac6c7a55c Started working on v1.3.0 2016-08-11 22:00:19 -03:00
claudemiro 015cbdf07a Release 1.2.0 2016-08-11 21:13:44 -03:00
claudemiro 431a19fe7b Updated go report card badge 2016-08-11 21:10:40 -03:00
ClaudemiroandGitHub 6225d8006f Replaces gorilla mux to goji and decied to remove vendor dependencies from git (#28)
* Replaces gorilla mux to goji

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

* handler not handle

* Ignoring vendor dir

* Hide handlers complexity instantiation.

* Websocket handler does not need common handlers

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

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

* Fixed assignment to entry in nil map

* Minor impovements in source code

* Replaces gorilla mux to goji

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

* handler not handle

* Ignoring vendor dir

* Hide handlers complexity instantiation.

* Websocket handler does not need common handlers

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

* Fixed assignment to entry in nil map

* Minor impovements in source code
2016-08-11 20:57:51 -03:00
claudemiro 6e46bcb2aa Moved location of mockSocket 2016-08-10 20:26:27 -03:00
claudemiro 0c599ea5fb Type definition is not necessary 2016-08-10 20:26:13 -03:00
ClaudemiroandGitHub 46ebd1b62b Merge pull request #26 from tssajo/tssajo5
Added *.sublime-project to .gitignore
2016-08-10 20:14:20 -03:00
tssajo 06449fdb58 Added *.sublime-project to .gitignore 2016-08-10 21:57:10 +02:00
claudemiro 6092cd85ca using regex MatchString instead 2016-08-10 00:17:12 -03:00
claudemiro 9c49ae16d3 Renamed regex to validate the channel name 2016-08-09 23:54:06 -03:00
claudemiro 7a794c97d7 Added util method IsClientEvent 2016-08-09 23:42:44 -03:00
claudemiro f0b3aadf74 Updated dependencies 2016-08-09 23:22:23 -03:00
ClaudemiroandGitHub 2eac54a0d3 Merge pull request #25 from sj26/fix-pusher-submodule
Fix submodule reference
2016-08-07 10:11:25 -03:00
ClaudemiroandGitHub a6f44d2175 Merge pull request #24 from tssajo/tssajo4
Fixed a bug in ipe/websocket.go
2016-08-07 10:11:02 -03:00
ClaudemiroandGitHub 6c479ced3c Merge pull request #22 from tssajo/tssajo3
Do not overuse defer unnecessarily
2016-08-07 10:10:18 -03:00
ClaudemiroandGitHub 178bbc9b8e Merge pull request #20 from tssajo/tssajo
Added missing defer file.Close() to func Start
2016-08-07 10:06:59 -03:00
Samuel Cochran 9f47b4fe2b Fix submodule reference 2016-07-29 11:15:47 +10:00
tssajo e9db8ea35e Fixed a bug in ipe/websocket.go (TotalConnections were growing infinitely connection was not closed when a channel listener refreshed the browser tab or simply closed the browser tab) 2016-07-23 15:20:35 +02:00
tssajo 24425a2a9a While I love defer, I think the program is better readable and maybe even a bit faster when defer is not used when Locking/Unlocking 2016-07-20 02:15:04 +02:00
tssajo 952983a0f8 Added missing defer file.Close() to func Start 2016-07-20 02:07:39 +02:00
112 changed files with 2584 additions and 12977 deletions
+5 -2
View File
@@ -38,7 +38,7 @@ _testmain.go
# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project
*.sublime-project
# sftp configuration file
sftp-config.json
@@ -155,7 +155,10 @@ flymake*
ignore_http/*
config.json
config.yml
*.pem
build
.vscode/*
debug
debug
vendor/*
+20
View File
@@ -0,0 +1,20 @@
FROM golang:alpine as builder
RUN mkdir /build
ADD . /build/
WORKDIR /build
RUN apk add git gcc musl-dev
RUN go build -o ipe ./cmd
FROM alpine
USER root
RUN mkdir -p /config
RUN adduser -S -D -H -h /app appuser
COPY ./entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
USER appuser
WORKDIR /app
COPY --from=builder /build/ipe /app/
COPY --from=builder /build/config-example.yml /app/config-example.yml
VOLUME /config
CMD ["/bin/sh", "/app/entrypoint.sh"]
EXPOSE 4343
EXPOSE 8080
+58 -30
View File
@@ -1,14 +1,17 @@
[![Quality code report](http://goreportcard.com/badge/dimiro1/ipe)](http://goreportcard.com/report/dimiro1/ipe)
[![Go Report Card](https://goreportcard.com/badge/github.com/dimiro1/ipe)](https://goreportcard.com/report/github.com/dimiro1/ipe)
Try browsing [the code on Sourcegraph](https://sourcegraph.com/github.com/dimiro1/ipe)!
# IPÊ
This software is written in Go - the WYSIWYG lang
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
@@ -18,7 +21,7 @@ This software is written in Go - the WYSIWYG lang
* Web Hooks;
* Client events;
* Complete REST API;
* Easy instalation;
* Easy installation;
* A single binary without dependencies;
* Easy configuration;
* Protocol version 7;
@@ -29,8 +32,6 @@ This software is written in Go - the WYSIWYG lang
You can download pre built binaries from the [releases tab](https://github.com/dimiro1/ipe/releases).
I do not have a Windows machine, so I can only distribute binaries for amd64 linux and amd64 darwin.
# Building
```console
@@ -43,31 +44,39 @@ or simply
$ 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
```javascript
{
"Host": ":8080", // Required
"SSL": false, // Required but can be false
"SSLHost": ":4433", // Required if SSL is true
"SSLKeyFile": "A key.pem file", // Required if SSL is true
"SSLCertFile": "A cert.pem file", // Required if SSL is true
"Apps": [ // Required, A Json arrays with multiple apps
{
"ApplicationDisabled": false, // Required but can be false
"Secret": "A really secret random string", // Required
"Key": "A random Key string", // Required
"OnlySSL": false, // Required but can be false
"Name": "The app name", // Required
"AppID": "The app ID", // Required
"UserEvents": true, // Required but can be false
"WebHooks": true, // Required but can be false
"URLWebHook": "Some URL to send webhooks" // Required if WebHooks is true
}
]
}
```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"
```
@@ -76,7 +85,7 @@ $ go install github.com/dimiro1/ipe
### Client javascript library
```javascript
var pusher = new Pusher(APP_KEY, {
let pusher = new Pusher(APP_KEY, {
wsHost: 'localhost',
wsPort: 8080,
wssPort: 4433, // Required if encrypted is true
@@ -101,10 +110,29 @@ 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
var pusher = new Pusher({
let pusher = new Pusher({
appId: APP_ID,
key: APP_KEY,
secret: APP_SECRET
@@ -137,7 +165,7 @@ Feel free to fork this repo.
# Pusher
Pusher is an excelent service, their service is very reliable. I recomend for everyone.
Pusher is an excellent service, their service is very reliable. I recommend for everyone.
# Where this name came from?
@@ -151,7 +179,7 @@ Claudemiro Alves Feitosa Neto
# LICENSE
Copyright 2014, 2015, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
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.
-58
View File
@@ -1,58 +0,0 @@
# Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
# Use of this source code is governed by a MIT-style
# license that can be found in the LICENSE file.
require 'rake/clean'
VERSION = 'v1.1.0'
GITHASH = `git rev-parse --short HEAD`
DATE = Time.now.strftime '%Y%m%d%H%M%S'
CLOBBER.include 'build'
task :default => [:'run-debug']
desc 'Build a debug version'
task :debug do
sh "GO15VENDOREXPERIMENT=1 go install -ldflags '-w -X main.version=DEBUG -X main.buildstamp=DEBUG -X main.githash=DEBUG' github.com/dimiro1/ipe"
end
desc 'Build and run debug version'
task :'run-debug' => :debug do
sh '$GOPATH/bin/ipe --config $GOPATH/src/github.com/dimiro1/ipe/config.json -logtostderr=true -v=2'
end
desc 'Run test suite'
task :test do
sh 'GO15VENDOREXPERIMENT=1 go test . `glide nv`'
end
desc 'Download the development dependencies'
task :'dev-deps' do
sh 'go get github.com/pusher/pusher-http-go'
end
desc 'Generate distributions'
task :distribute => [:linux, :darwin]
desc 'Generate a linux distribution'
task :linux do
Rake::Task['build'].invoke 'linux'
end
desc 'Generate a darwin distribution'
task :darwin do
Rake::Task['build'].invoke 'darwin'
end
task :build, [:os] do |t, args|
t.reenable
os = args[:os]
sh "mkdir -p build/#{os}"
sh "GO15VENDOREXPERIMENT=1 GOOS=#{os} GOARCH=amd64 go build -ldflags '-X main.version=#{VERSION} -X main.buildstamp=#{DATE} -X main.githash=#{GITHASH}' -o build/#{os}/ipe github.com/dimiro1/ipe"
sh "cp ipe/config-example.json build/#{os}/config.json"
sh "cp LICENSE build/#{os}/"
sh "cp README.md build/#{os}/"
sh "tar -C build/#{os} -czf build/ipe_#{VERSION}_#{os}_amd64.tar.gz ."
end
-41
View File
@@ -1,41 +0,0 @@
IPÊ
---
* TODO [12/14]
* [X] Autenticação API Rest
* [X] Autenticação Websockets
* [X] Ping e Pong
* [ ] Escrever testes automatizados
* [X] SSL
* [X] Expvar - Canais, inscritos
* [X] Otimizações [3/3]
* [X] Refatorar partes do código, remover repetições
* [X] Alterar tipos de dados de slices para mapas em alguns locais.
* [X] Remover Canais vazios.
* [ ] Segurança, tempo de expiração, etc
* [X] Dados extra na conexão do usuário. Ver Websockets onOpen
* [X] Webhooks [5/5]
* [X] Member added
* [X] Member removed
* [X] Channel Occupied
* [X] Channel vacated
* [X] Clients Events
* [X] Events Presence channels [3/3]
* [X] pusher_internal:subscription_succeeded para canais de presença
* [X] pusher_internal:member_added
* [X] pusher_internal:member_removed
* [X] Remover inscrições quando o web socket for fechado
* [X] Alterar os dados extras da inscrição são relacionados ao canal e não diretamente a inscrição
* [X] Escrever um README
* Dúvidas
* [X] Atualmente o ID do usuário em canais de presença está como string, o protocolo não deixa claro, já ví exemplos onde a chave é um inteiro. Nesse caso essa chave terá que ser um RawMessage, O campo Data inteiro??
* Objetivos [7/7]
* [X] Implementação Funcional.
* [X] WebHooks
* [X] Presence channels
* [X] Private Channels
* [X] Public Channels
* [X] Easy Instalation
* [X] Easy configuration
+149 -93
View File
@@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
package api
import (
"encoding/json"
@@ -13,11 +13,18 @@ import (
"strings"
log "github.com/golang/glog"
"github.com/gorilla/mux"
"github.com/dimiro1/ipe/utils"
"ipe/events"
"ipe/storage"
"ipe/utils"
)
// Prepare Querystring
// // 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
@@ -36,7 +43,7 @@ func prepareQueryString(params url.Values) string {
return strings.Join(pieces, "&")
}
// Authenticate pusher
// Authentication Authenticate pusher
// see: https://gist.github.com/mloughran/376898
//
// The signature is a HMAC SHA256 hex digest.
@@ -46,63 +53,79 @@ func prepareQueryString(params url.Values) string {
// * The request path (e.g. /some/resource)
// * The query parameters sorted by key, with keys converted to lowercase, then joined as in the query string.
// Note that the string must not be url escaped (e.g. given the keys auth_key: foo, Name: Something else, you get auth_key=foo&name=Something else)
func restAuthenticationHandler(ctx *applicationContext, h contextHandler) contextHandler {
return contextHandlerFunc(func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
appID := p.Get("app_id")
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 := ctx.DB.GetAppByAppID(appID)
app, err := storage.GetAppByAppID(appID)
if err != nil {
log.Error(err)
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
if err != nil {
log.Error(err)
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
query := r.URL.Query()
signature := query.Get("auth_signature")
query.Del("auth_signature")
queryString := prepareQueryString(query)
toSign := strings.ToUpper(r.Method) + "\n" + r.URL.Path + "\n" + queryString
if utils.HashMAC([]byte(toSign), []byte(app.Secret)) == signature {
next.ServeHTTP(w, r)
} else {
log.Error("Not authorized")
http.Error(w, "Not authorized", http.StatusUnauthorized)
}
}
query := r.URL.Query()
signature := query.Get("auth_signature")
query.Del("auth_signature")
queryString := prepareQueryString(query)
toSign := strings.ToUpper(r.Method) + "\n" + r.URL.Path + "\n" + queryString
if utils.HashMAC([]byte(toSign), []byte(app.Secret)) == signature {
h.ServeWithContext(ctx, p, w, r)
} else {
log.Error("Not authorized")
http.Error(w, "Not authorized", http.StatusUnauthorized)
}
})
return http.HandlerFunc(fn)
}
}
// Check if the application is disabled
func restCheckAppDisabledHandler(ctx *applicationContext, h contextHandler) contextHandler {
return contextHandlerFunc(func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
appID := p.Get("app_id")
// 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 := ctx.DB.GetAppByAppID(appID)
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 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)
}
if currentApp.ApplicationDisabled {
http.Error(w, "Application disabled", http.StatusForbidden)
return
}
h.ServeWithContext(ctx, p, w, r)
})
return http.HandlerFunc(fn)
}
}
// commonHandlers combine restCheckAppDisabledHandler and restAuthenticationHandler handlers
func commonHandlers(ctx *applicationContext, h contextHandlerFunc) contextHandler {
return restCheckAppDisabledHandler(ctx, restAuthenticationHandler(ctx, h))
// 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}
}
// An event consists of a name and data (typically JSON) which may be sent to all subscribers to a particular channel or channels.
// 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.
@@ -117,10 +140,13 @@ func commonHandlers(ctx *applicationContext, h contextHandlerFunc) contextHandle
// Response is an empty JSON hash.
//
// POST /apps/{app_id}/events
func postEvents(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
appID := p.Get("app_id")
func (h *PostEvents) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
pathVars = mux.Vars(r)
appID = pathVars["app_id"]
)
app, err := ctx.DB.GetAppByAppID(appID)
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)
@@ -155,15 +181,28 @@ func postEvents(ctx *applicationContext, p params, w http.ResponseWriter, r *htt
for _, c := range input.Channels {
channel := app.FindOrCreateChannelByChannelID(c)
app.Publish(channel, rawEvent{Event: input.Name, Channel: c, Data: input.Data}, input.SocketID)
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;charset=UTF-8")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte("{}"))
if _, err := w.Write([]byte("{}")); err != nil {
log.Errorf("unexpected error while writing into response %+v", err)
}
}
// Allows fetching a hash of occupied channels (optionally filtered by prefix),
// 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:
@@ -182,14 +221,15 @@ func postEvents(ctx *applicationContext, p params, w http.ResponseWriter, r *htt
// }
//
// GET /apps/{app_id}/channels
func getChannels(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
appID := p.Get("app_id")
filter := query.Get("filter_by_prefix")
info := query.Get("info")
attributes := strings.Split(info, ",")
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
@@ -206,7 +246,7 @@ func getChannels(ctx *applicationContext, p params, w http.ResponseWriter, r *ht
return
}
app, err := ctx.DB.GetAppByAppID(appID)
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)
@@ -218,30 +258,30 @@ func getChannels(ctx *applicationContext, p params, w http.ResponseWriter, r *ht
case "presence-":
for _, c := range app.PresenceChannels() {
if requestedUserCount {
channels[c.ChannelID] = struct {
channels[c.ID] = struct {
UserCount int `json:"user_count"`
}{
c.TotalUsers(),
}
} else {
channels[c.ChannelID] = struct{}{}
channels[c.ID] = struct{}{}
}
}
case "public-":
for _, c := range app.PublicChannels() {
channels[c.ChannelID] = struct{}{}
channels[c.ID] = struct{}{}
}
case "private-":
for _, c := range app.PrivateChannels() {
channels[c.ChannelID] = struct{}{}
channels[c.ID] = struct{}{}
}
default:
for _, c := range app.Channels {
channels[c.ChannelID] = struct{}{}
for _, c := range app.Channels() {
channels[c.ID] = struct{}{}
}
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
w.Header().Set("Content-Type", "application/json")
js := make(map[string]interface{}, 1)
js["channels"] = channels
@@ -252,7 +292,15 @@ func getChannels(ctx *applicationContext, p params, w http.ResponseWriter, r *ht
}
}
// Fetch info for one channel
// 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:
// {
@@ -262,29 +310,28 @@ func getChannels(ctx *applicationContext, p params, w http.ResponseWriter, r *ht
// }
//
// GET /apps/{app_id}/channels/{channel_name}
func getChannel(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
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, ",")
)
query := r.URL.Query()
appID := p.Get("app_id")
app, err := ctx.DB.GetAppByAppID(appID)
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)
}
channelName := p.Get("channel_name")
// Channel name could not be empty
if strings.TrimSpace(channelName) == "" {
http.Error(w, "Empty channel name", http.StatusBadRequest)
return
}
info := query.Get("info")
attributes := strings.Split(info, ",")
// Attributes requested
requestedUserCount := false
requestedSubscriptionCount := false
@@ -332,15 +379,22 @@ func getChannel(ctx *applicationContext, p params, w http.ResponseWriter, r *htt
dtoChannel.SubscriptionCount = channel.TotalSubscriptions()
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
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)
}
}
// Allowed only for presence-channels
// 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:
// {
@@ -351,9 +405,12 @@ func getChannel(ctx *applicationContext, p params, w http.ResponseWriter, r *htt
// }
//
// GET /apps/{app_id}/channels/{channel_name}/users
func getChannelUsers(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
appID := p.Get("app_id")
channelName := p.Get("channel_name")
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)
@@ -362,7 +419,7 @@ func getChannelUsers(ctx *applicationContext, p params, w http.ResponseWriter, r
return
}
app, err := ctx.DB.GetAppByAppID(appID)
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)
@@ -381,7 +438,7 @@ func getChannelUsers(ctx *applicationContext, p params, w http.ResponseWriter, r
var users []interface{}
for _, s := range channel.Subscriptions {
for _, s := range channel.Subscriptions() {
users = append(users, struct {
ID string `json:"id"`
}{s.ID})
@@ -389,8 +446,7 @@ func getChannelUsers(ctx *applicationContext, p params, w http.ResponseWriter, r
result["users"] = users
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
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)
@@ -1,57 +1,74 @@
package ipe
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
ctx *applicationContext
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 := newChannel("presence-c1")
channel := channel2.New("presence-c1")
testApp.AddChannel(channel)
testApp.AddChannel(newChannel("c2"))
testApp.AddChannel(newChannel("private-c3"))
testApp.AddChannel(channel2.New("c2"))
testApp.AddChannel(channel2.New("private-c3"))
conn := newConnection("123.456", mockSocket{})
testApp.Subscribe(channel, conn, "{}")
conn := connection.New("123.456", mocks.MockSocket{})
_ = testApp.Subscribe(channel, conn, "{}")
conn = newConnection("321.654", mockSocket{})
testApp.Subscribe(channel, conn, "{}")
conn = connection.New("321.654", mocks.MockSocket{})
_ = testApp.Subscribe(channel, conn, "{}")
db := newMemdb()
db.AddApp(testApp)
_storage := storage.NewInMemory()
_ = _storage.AddApp(testApp)
ctx = &applicationContext{DB: db}
database = _storage
}
// All Channels
// All channels
func Test_getChannels_all(t *testing.T) {
appID := testApp.AppID
p := map[string]string{}
p["app_id"] = 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()
getChannels(ctx, params(p), w, r)
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)
_ = json.Unmarshal(w.Body.Bytes(), &data)
channels := data["channels"].(map[string]interface{})
@@ -63,20 +80,21 @@ func Test_getChannels_all(t *testing.T) {
func Test_getChannels_filter_by_presence_prefix(t *testing.T) {
appID := testApp.AppID
p := map[string]string{}
p["app_id"] = 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()
getChannels(ctx, params(p), w, r)
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)
_ = json.Unmarshal(w.Body.Bytes(), &data)
channels := data["channels"].(map[string]interface{})
@@ -87,23 +105,23 @@ func Test_getChannels_filter_by_presence_prefix(t *testing.T) {
// Only presence channels and user_count
func Test_getChannels_filter_by_presence_prefix_and_user_count(t *testing.T) {
appID := testApp.AppID
p := map[string]string{}
p["app_id"] = 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()
getChannels(ctx, params(p), w, r)
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)
_ = json.Unmarshal(w.Body.Bytes(), &data)
channels := data["channels"].(map[string]interface{})
@@ -124,17 +142,18 @@ func Test_getChannels_filter_by_presence_prefix_and_user_count(t *testing.T) {
}
}
// User count only alowed in Presence channels
// User count only allowed in Presence channels
func Test_getChannels_filter_by_private_prefix_and_info_user_count(t *testing.T) {
appID := testApp.AppID
p := map[string]string{}
p["app_id"] = 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()
getChannels(ctx, params(p), w, r)
handler := &GetChannels{database}
handler.ServeHTTP(w, r)
if w.Code != http.StatusBadRequest {
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusBadRequest)
@@ -144,13 +163,14 @@ func Test_getChannels_filter_by_private_prefix_and_info_user_count(t *testing.T)
func Test_getChannels_filter_by_public_prefix(t *testing.T) {
appID := testApp.AppID
p := map[string]string{}
p["app_id"] = 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()
getChannels(ctx, params(p), w, r)
handler := &GetChannels{database}
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
@@ -158,7 +178,7 @@ func Test_getChannels_filter_by_public_prefix(t *testing.T) {
data := make(map[string]interface{})
json.Unmarshal(w.Body.Bytes(), &data)
_ = json.Unmarshal(w.Body.Bytes(), &data)
channels := data["channels"].(map[string]interface{})
@@ -174,16 +194,16 @@ func Test_getChannels_filter_by_public_prefix(t *testing.T) {
}
func Test_getChannels_filter_by_private_prefix(t *testing.T) {
appID := testApp.AppID
p := map[string]string{}
p["app_id"] = 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()
getChannels(ctx, params(p), w, r)
handler := &GetChannels{database}
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
@@ -191,7 +211,7 @@ func Test_getChannels_filter_by_private_prefix(t *testing.T) {
data := make(map[string]interface{})
json.Unmarshal(w.Body.Bytes(), &data)
_ = json.Unmarshal(w.Body.Bytes(), &data)
channels := data["channels"].(map[string]interface{})
+314
View File
@@ -0,0 +1,314 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package app
import (
"errors"
"expvar"
"fmt"
"sync"
log "github.com/golang/glog"
"ipe/channel"
"ipe/connection"
"ipe/events"
"ipe/subscription"
)
// Application represents a Pusher application
type Application struct {
sync.RWMutex
Name string
AppID string
Key string
Secret string
OnlySSL bool
Enabled bool
UserEvents bool
WebHooks bool
URLWebHook string
channels map[string]*channel.Channel
connections map[string]*connection.Connection
Stats *expvar.Map `json:"-"`
}
// NewApplication returns a new Application
func NewApplication(
name,
appID,
key,
secret string,
onlySSL,
enabled,
userEvents,
webHooks bool,
webHookURL string,
) *Application {
a := &Application{
Name: name,
AppID: appID,
Key: key,
Secret: secret,
OnlySSL: onlySSL,
Enabled: enabled,
UserEvents: userEvents,
WebHooks: webHooks,
URLWebHook: webHookURL,
}
a.connections = make(map[string]*connection.Connection)
a.channels = make(map[string]*channel.Channel)
a.Stats = expvar.NewMap(fmt.Sprintf("%s (%s)", a.Name, a.AppID))
return a
}
// Channels returns the full list of channels
func (a *Application) Channels() []*channel.Channel {
a.RLock()
defer a.RUnlock()
var channels []*channel.Channel
for _, c := range a.channels {
channels = append(channels, c)
}
return channels
}
// PresenceChannels Only Presence channels
func (a *Application) PresenceChannels() []*channel.Channel {
a.RLock()
defer a.RUnlock()
var channels []*channel.Channel
for _, c := range a.channels {
if c.IsPresence() {
channels = append(channels, c)
}
}
return channels
}
// PrivateChannels Only Private channels
func (a *Application) PrivateChannels() []*channel.Channel {
a.RLock()
defer a.RUnlock()
var channels []*channel.Channel
for _, c := range a.channels {
if c.IsPrivate() {
channels = append(channels, c)
}
}
return channels
}
// PublicChannels Only Public channels
func (a *Application) PublicChannels() []*channel.Channel {
a.RLock()
defer a.RUnlock()
var channels []*channel.Channel
for _, c := range a.channels {
if c.IsPublic() {
channels = append(channels, c)
}
}
return channels
}
// Disconnect Socket
func (a *Application) Disconnect(socketID string) {
log.Infof("disconnecting socket %+v", socketID)
conn, err := a.FindConnection(socketID)
if err != nil {
log.Infof("socket not found, %+v", err)
return
}
// Unsubscribe from channels
for _, c := range a.channels {
if c.IsSubscribed(conn) {
if err := c.Unsubscribe(conn); err != nil {
log.Errorf("error while calling Channel.Unsubscribe, %+v", err)
continue
}
}
}
// Remove from Application
a.Lock()
_, exists := a.connections[conn.SocketID]
a.Unlock()
if !exists {
return
}
a.Lock()
delete(a.connections, conn.SocketID)
a.Unlock()
a.Stats.Add("TotalConnections", -1)
}
// Connect a new Subscriber
func (a *Application) Connect(conn *connection.Connection) {
log.Infof("adding a new Connection %s to Application %s", conn.SocketID, a.Name)
a.Lock()
defer a.Unlock()
a.connections[conn.SocketID] = conn
a.Stats.Add("TotalConnections", 1)
}
// FindConnection Find a Connection on this Application
func (a *Application) FindConnection(socketID string) (*connection.Connection, error) {
a.RLock()
defer a.RUnlock()
conn, exists := a.connections[socketID]
if exists {
return conn, nil
}
return nil, errors.New("connection not found")
}
// RemoveChannel removes the Channel from Application
func (a *Application) RemoveChannel(c *channel.Channel) {
log.Infof("remove the Channel %s from Application %s", c.ID, a.Name)
a.Lock()
defer a.Unlock()
delete(a.channels, c.ID)
if c.IsPresence() {
a.Stats.Add("TotalPresenceChannels", -1)
}
if c.IsPrivate() {
a.Stats.Add("TotalPrivateChannels", -1)
}
if c.IsPublic() {
a.Stats.Add("TotalPublicChannels", -1)
}
a.Stats.Add("TotalChannels", -1)
}
// AddChannel Add a new Channel to this APP
func (a *Application) AddChannel(c *channel.Channel) {
log.Infof("adding a new Channel %s to Application %s", c.ID, a.Name)
a.Lock()
defer a.Unlock()
a.channels[c.ID] = c
if c.IsPresence() {
a.Stats.Add("TotalPresenceChannels", 1)
}
if c.IsPrivate() {
a.Stats.Add("TotalPrivateChannels", 1)
}
if c.IsPublic() {
a.Stats.Add("TotalPublicChannels", 1)
}
a.Stats.Add("TotalChannels", 1)
}
// FindOrCreateChannelByChannelID Returns a Channel from this Application
// If not found then the Channel is created and added to this Application
func (a *Application) FindOrCreateChannelByChannelID(n string) *channel.Channel {
c, err := a.FindChannelByChannelID(n)
if err != nil {
c = channel.New(
n,
channel.WithChannelOccupiedListener(func(c *channel.Channel, s *subscription.Subscription) {
a.TriggerChannelOccupiedHook(c)
}),
channel.WithChannelVacatedListener(func(c *channel.Channel, s *subscription.Subscription) {
a.TriggerChannelVacatedHook(c)
}),
channel.WithMemberAddedListener(func(c *channel.Channel, s *subscription.Subscription) {
a.TriggerMemberAddedHook(c, s)
}),
channel.WithMemberRemovedListener(func(c *channel.Channel, s *subscription.Subscription) {
a.TriggerMemberRemovedHook(c, s)
}),
channel.WithClientEventListener(func(c *channel.Channel, s *subscription.Subscription, event string, data interface{}) {
a.TriggerClientEventHook(c, s, event, data)
}),
)
a.AddChannel(c)
}
return c
}
// FindChannelByChannelID Find the Channel by Channel ID
func (a *Application) FindChannelByChannelID(n string) (*channel.Channel, error) {
a.RLock()
defer a.RUnlock()
c, exists := a.channels[n]
if exists {
return c, nil
}
return nil, errors.New("channel does not exists")
}
// Publish an event into the channel
// skip the ignore connection
func (a *Application) Publish(c *channel.Channel, event events.Raw, ignore string) error {
a.Stats.Add("TotalUniqueMessages", 1)
return c.Publish(event, ignore)
}
// Unsubscribe unsubscribe the given connection from the channel
// remove the channel from the application if it is empty
func (a *Application) Unsubscribe(c *channel.Channel, conn *connection.Connection) error {
err := c.Unsubscribe(conn)
if err != nil {
return err
}
if !c.IsOccupied() {
a.RemoveChannel(c)
}
return nil
}
// Subscribe the connection into the given channel
func (a *Application) Subscribe(c *channel.Channel, conn *connection.Connection, data string) error {
return c.Subscribe(conn, data)
}
+255
View File
@@ -0,0 +1,255 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package app
import (
"strconv"
"testing"
channel2 "ipe/channel"
"ipe/connection"
"ipe/mocks"
)
var id = 0
func newTestApp() *Application {
a := NewApplication("Test", strconv.Itoa(id), "123", "123", false, false, true, false, "")
id++
return a
}
func TestConnect(t *testing.T) {
app := newTestApp()
app.Connect(connection.New("socketID", mocks.MockSocket{}))
if len(app.connections) != 1 {
t.Errorf("len(Application.connections) == %d, wants %d", len(app.connections), 1)
}
}
func TestDisconnect(t *testing.T) {
app := newTestApp()
app.Connect(connection.New("socketID", mocks.MockSocket{}))
app.Disconnect("socketID")
if len(app.connections) != 0 {
t.Errorf("len(Application.connections) == %d, wants %d", len(app.connections), 0)
}
}
func TestFindConnection(t *testing.T) {
app := newTestApp()
app.Connect(connection.New("socketID", mocks.MockSocket{}))
if _, err := app.FindConnection("socketID"); err != nil {
t.Errorf("Application.FindConnection('socketID') == _, %q, wants %v", err, nil)
}
if _, err := app.FindConnection("NotFound"); err == nil {
t.Errorf("Application.FindConnection('socketID') == _, %q, wants !nil", err)
}
}
func TestFindChannelByChannelID(t *testing.T) {
app := newTestApp()
channel := channel2.New("ID")
app.AddChannel(channel)
if _, err := app.FindChannelByChannelID("ID"); err != nil {
t.Errorf("Application.FindChannelByChannelID('ID') == _, %q, wants %v", err, nil)
}
}
func TestFindOrCreateChannelByChannelID(t *testing.T) {
app := newTestApp()
if len(app.channels) != 0 {
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 0)
}
app.FindOrCreateChannelByChannelID("ID")
if len(app.channels) != 1 {
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 1)
}
}
func TestRemoveChannel(t *testing.T) {
app := newTestApp()
if len(app.channels) != 0 {
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 0)
}
channel := channel2.New("ID")
app.AddChannel(channel)
if len(app.channels) != 1 {
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 1)
}
app.RemoveChannel(channel)
if len(app.channels) != 0 {
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 0)
}
}
func Test_add_channels(t *testing.T) {
app := newTestApp()
// Public
if len(app.PublicChannels()) != 0 {
t.Errorf("len(Application.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
}
app.AddChannel(channel2.New("ID"))
if len(app.PublicChannels()) != 1 {
t.Errorf("len(Application.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
}
// Presence
if len(app.PresenceChannels()) != 0 {
t.Errorf("len(Application.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
}
app.AddChannel(channel2.New("presence-test"))
if len(app.PresenceChannels()) != 1 {
t.Errorf("len(Application.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
}
// Private
if len(app.PrivateChannels()) != 0 {
t.Errorf("len(Application.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
}
app.AddChannel(channel2.New("private-test"))
if len(app.PrivateChannels()) != 1 {
t.Errorf("len(Application.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
}
}
func Test_AllChannels(t *testing.T) {
app := newTestApp()
app.AddChannel(channel2.New("private-test"))
app.AddChannel(channel2.New("presence-test"))
app.AddChannel(channel2.New("test"))
if len(app.channels) != 3 {
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 3)
}
}
func Test_New_Subscriber(t *testing.T) {
app := newTestApp()
if len(app.connections) != 0 {
t.Errorf("len(Application.connections) == %d, wants %d", len(app.connections), 0)
}
conn := connection.New("1", mocks.MockSocket{})
app.Connect(conn)
if len(app.connections) != 1 {
t.Errorf("len(Application.connections) == %d, wants %d", len(app.connections), 1)
}
}
func Test_find_subscriber(t *testing.T) {
app := newTestApp()
conn := connection.New("1", mocks.MockSocket{})
app.Connect(conn)
conn, err := app.FindConnection("1")
if err != nil {
t.Error(err)
}
if conn.SocketID != "1" {
t.Errorf("conn.SocketID == %s, wants %s", conn.SocketID, "1")
}
// Find a wrong subscriber
conn, err = app.FindConnection("DoesNotExists")
if err == nil {
t.Errorf("err == %q, wants !nil", err)
}
if conn != nil {
t.Errorf("conn == %q, wants nil", conn)
}
}
func Test_find_or_create_channels(t *testing.T) {
app := newTestApp()
// Public
if len(app.PublicChannels()) != 0 {
t.Errorf("len(Application.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
}
c := app.FindOrCreateChannelByChannelID("id")
if len(app.PublicChannels()) != 1 {
t.Errorf("len(Application.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
}
if c.ID != "id" {
t.Errorf("c.id == %s, wants %s", c.ID, "id")
}
// Presence
if len(app.PresenceChannels()) != 0 {
t.Errorf("len(Application.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
}
c = app.FindOrCreateChannelByChannelID("presence-test")
if len(app.PresenceChannels()) != 1 {
t.Errorf("len(Application.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
}
if c.ID != "presence-test" {
t.Errorf("c.id == %s, wants %s", c.ID, "presence-test")
}
// Private
if len(app.PrivateChannels()) != 0 {
t.Errorf("len(Application.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
}
c = app.FindOrCreateChannelByChannelID("private-test")
if len(app.PrivateChannels()) != 1 {
t.Errorf("len(Application.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
}
if c.ID != "private-test" {
t.Errorf("c.id == %s, wants %s", c.ID, "private-test")
}
}
+86 -32
View File
@@ -2,19 +2,26 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
package app
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/dimiro1/ipe/utils"
log "github.com/golang/glog"
"ipe/channel"
"ipe/subscription"
"ipe/utils"
)
// A WebHook is sent as a HTTP POST request to the url which you specify.
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
@@ -29,7 +36,7 @@ import (
// 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):
// 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.
@@ -47,40 +54,51 @@ type hookEvent struct {
UserID string `json:"user_id,omitempty"`
}
func newChannelOcuppiedHook(channel *channel) hookEvent {
return hookEvent{Name: "channel_occupied", Channel: channel.ChannelID}
func newChannelOcuppiedHook(channel *channel.Channel) hookEvent {
return hookEvent{Name: "channel_occupied", Channel: channel.ID}
}
func newChannelVacatedHook(channel *channel) hookEvent {
return hookEvent{Name: "channel_vacated", Channel: channel.ChannelID}
func newChannelVacatedHook(channel *channel.Channel) hookEvent {
return hookEvent{Name: "channel_vacated", Channel: channel.ID}
}
func newMemberAddedHook(channel *channel, s *subscription) hookEvent {
return hookEvent{Name: "member_added", Channel: channel.ChannelID, UserID: s.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, s *subscription) hookEvent {
return hookEvent{Name: "member_removed", Channel: channel.ChannelID, 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, s *subscription, event string, data interface{}) hookEvent {
return hookEvent{Name: "client_event", Channel: channel.ChannelID, Event: event, Data: data, SocketID: s.Connection.SocketID}
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}
}
// channel_occupied
// TriggerChannelOccupiedHook channel_occupied
// { "name": "channel_occupied", "channel": "test_channel" }
func (a *app) TriggerChannelOccupiedHook(c *channel) {
func (a *Application) TriggerChannelOccupiedHook(c *channel.Channel) {
event := newChannelOcuppiedHook(c)
triggerHook(event.Name, a, c, event)
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
defer cancel()
if err := triggerHook(ctx, a, event); err != nil {
log.Errorf("triggering webhook %+v", err)
}
}
// channel_vacated
// TriggerChannelVacatedHook channel_vacated
// { "name": "channel_vacated", "channel": "test_channel" }
func (a *app) TriggerChannelVacatedHook(c *channel) {
func (a *Application) TriggerChannelVacatedHook(c *channel.Channel) {
event := newChannelVacatedHook(c)
triggerHook(event.Name, a, c, event)
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",
@@ -89,44 +107,63 @@ func (a *app) TriggerChannelVacatedHook(c *channel) {
// "socket_id": "socket_id of the sending socket",
// "user_id": "user_id associated with the sending socket" # Only for presence channels
// }
func (a *app) TriggerClientEventHook(c *channel, s *subscription, clientEvent string, data interface{}) {
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
}
triggerHook(event.Name, a, c, event)
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 *app) TriggerMemberAddedHook(c *channel, s *subscription) {
func (a *Application) TriggerMemberAddedHook(c *channel.Channel, s *subscription.Subscription) {
event := newMemberAddedHook(c, s)
triggerHook(event.Name, a, c, event)
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 *app) TriggerMemberRemovedHook(c *channel, s *subscription) {
func (a *Application) TriggerMemberRemovedHook(c *channel.Channel, s *subscription.Subscription) {
event := newMemberRemovedHook(c, s)
triggerHook(event.Name, a, c, event)
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(name string, a *app, c *channel, event hookEvent) {
func triggerHook(ctx context.Context, a *Application, event hookEvent) error {
if !a.WebHooks {
log.Infof("Webhooks are not enabled for app: %s", a.Name)
return
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", name)
log.Infof("Triggering %s event", event.Name)
hook := webHook{TimeMs: time.Now().Unix()}
@@ -145,11 +182,14 @@ func triggerHook(name string, a *app, c *channel, event hookEvent) {
var req *http.Request
req, err = http.NewRequest("POST", a.URLWebHook, bytes.NewReader(js))
if err != nil {
log.Errorf("Error creating request: %+v", err)
return
}
req = 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)
@@ -162,11 +202,25 @@ func triggerHook(name string, a *app, c *channel, event hookEvent) {
// See: http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#close_http_resp_body
if resp != nil {
defer resp.Body.Close()
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", name, err)
log.Errorf("error posting %s event: %+v", event.Name, err)
}
// Successfully terminated
done <- true
}()
select {
case <-ctx.Done():
return ctx.Err()
case <-done:
return nil
}
}
+324
View File
@@ -0,0 +1,324 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package channel
import (
"encoding/json"
"errors"
"sync"
"time"
log "github.com/golang/glog"
"ipe/connection"
"ipe/events"
"ipe/subscription"
"ipe/utils"
)
// Option constructor function for Channel
type Option func(*Channel)
// ListenerFunc listener function
type ListenerFunc func(*Channel, *subscription.Subscription)
// ClientEventListenerFunc listener for client events
type ClientEventListenerFunc func(*Channel, *subscription.Subscription, string, interface{})
// Channel represents an application channel
type Channel struct {
sync.RWMutex
ID string
subscriptions map[string]*subscription.Subscription
createdAt time.Time
memberAddedListeners []ListenerFunc
memberRemovedListeners []ListenerFunc
channelOccupiedListeners []ListenerFunc
channelVacatedListeners []ListenerFunc
clientEventListeners []ClientEventListenerFunc
}
// New Create a new Channel
func New(channelID string, options ...Option) *Channel {
log.Infof("Creating a new Channel: %s", channelID)
c := &Channel{ID: channelID, createdAt: time.Now(), subscriptions: make(map[string]*subscription.Subscription)}
for _, option := range options {
option(c)
}
return c
}
// WithMemberAddedListener appends the given ListenerFunc into the memberAddedListeners list
func WithMemberAddedListener(f ListenerFunc) func(*Channel) {
return func(c *Channel) {
c.memberAddedListeners = append(c.memberAddedListeners, f)
}
}
// WithMemberRemovedListener appends the given ListenerFunc into the memberRemovedListeners list
func WithMemberRemovedListener(f ListenerFunc) func(*Channel) {
return func(c *Channel) {
c.memberRemovedListeners = append(c.memberRemovedListeners, f)
}
}
// WithChannelOccupiedListener appends the given ListenerFunc into the channelOccupiedListeners list
func WithChannelOccupiedListener(f ListenerFunc) func(*Channel) {
return func(c *Channel) {
c.channelOccupiedListeners = append(c.channelOccupiedListeners, f)
}
}
// WithChannelVacatedListener appends the given ListenerFunc into the channelVacatedListeners list
func WithChannelVacatedListener(f ListenerFunc) func(*Channel) {
return func(c *Channel) {
c.channelVacatedListeners = append(c.channelVacatedListeners, f)
}
}
// WithClientEventListener appends the given ListenerFunc into the clientEventListeners list
func WithClientEventListener(f ClientEventListenerFunc) func(*Channel) {
return func(c *Channel) {
c.clientEventListeners = append(c.clientEventListeners, f)
}
}
// Subscriptions returns a slice of subscriptions
func (c *Channel) Subscriptions() []*subscription.Subscription {
c.RLock()
defer c.RUnlock()
var subscriptions []*subscription.Subscription
for _, sub := range c.subscriptions {
subscriptions = append(subscriptions, sub)
}
return subscriptions
}
// IsOccupied Return true if the Channel has at least one subscriber
func (c *Channel) IsOccupied() bool {
return c.TotalSubscriptions() > 0
}
// IsPresenceOrPrivate Check if the type of the Channel is presence or is private
func (c *Channel) IsPresenceOrPrivate() bool {
return c.IsPresence() || c.IsPrivate()
}
// IsPublic Check if the type of the Channel is public
func (c *Channel) IsPublic() bool {
return !c.IsPresenceOrPrivate()
}
// IsPresence Check if the type of the Channel is presence
func (c *Channel) IsPresence() bool {
return utils.IsPresenceChannel(c.ID)
}
// IsPrivate Check if the type of the Channel is private
func (c *Channel) IsPrivate() bool {
return utils.IsPrivateChannel(c.ID)
}
// TotalSubscriptions Get the total of subscribers
func (c *Channel) TotalSubscriptions() int {
c.RLock()
defer c.RUnlock()
return len(c.subscriptions)
}
// TotalUsers Get the total of users.
func (c *Channel) TotalUsers() int {
c.RLock()
defer c.RUnlock()
total := make(map[string]int)
for _, s := range c.subscriptions {
total[s.ID]++
}
return len(total)
}
// Subscribe Add a new subscriber to the Channel
func (c *Channel) Subscribe(conn *connection.Connection, channelData string) error {
log.Infof("Subscribing %s to Channel %s", conn.SocketID, c.ID)
_subscription := subscription.New(conn, channelData)
c.Lock()
c.subscriptions[conn.SocketID] = _subscription
c.Unlock()
if c.IsPresence() {
// User Info Data
var info struct {
UserID string `json:"user_id"`
UserInfo json.RawMessage `json:"user_info"`
}
log.Infof("%+v", channelData)
if err := json.Unmarshal([]byte(channelData), &info); err != nil {
log.Error(err)
return err
}
js, err := info.UserInfo.MarshalJSON()
if err != nil {
log.Error(err)
return err
}
c.Lock()
// Update the Subscription
_subscription.ID = info.UserID
_subscription.Data = string(js)
c.Unlock()
// Publish pusher_internal:member_added
c.PublishMemberAddedEvent(channelData, _subscription)
for _, hook := range c.memberAddedListeners {
hook(c, _subscription)
}
// pusher_internal:subscription_succeeded
data := make(map[string]events.SubscriptionSucceededPresenceData)
data["presence"] = events.NewSubscriptionSucceedPresenceData(c.subscriptions)
js, err = json.Marshal(data)
if err != nil {
log.Error(err)
return err
}
conn.Publish(events.NewSubscriptionSucceeded(c.ID, string(js)))
} else {
conn.Publish(events.NewSubscriptionSucceeded(c.ID, "{}"))
}
if c.TotalSubscriptions() == 1 {
for _, hook := range c.channelOccupiedListeners {
hook(c, _subscription)
}
}
return nil
}
// IsSubscribed check if the user is subscribed
func (c *Channel) IsSubscribed(conn *connection.Connection) bool {
c.RLock()
defer c.RUnlock()
_, exists := c.subscriptions[conn.SocketID]
return exists
}
// Unsubscribe Remove the subscriber from the Channel
// It destroy the Channel if the channels does not have any subscribers.
func (c *Channel) Unsubscribe(conn *connection.Connection) error {
log.Infof("unsubscribe %s from Channel %s", conn.SocketID, c.ID)
c.RLock()
_subscription, exists := c.subscriptions[conn.SocketID]
c.RUnlock()
if !exists {
return errors.New("_subscription not found")
}
c.Lock()
delete(c.subscriptions, conn.SocketID)
c.Unlock()
if c.IsPresence() {
// Publish pusher_internal:member_removed
c.PublishMemberRemovedEvent(_subscription)
for _, hook := range c.memberRemovedListeners {
hook(c, _subscription)
}
}
if !c.IsOccupied() {
for _, hook := range c.channelVacatedListeners {
hook(c, _subscription)
}
}
return nil
}
// PublishMemberAddedEvent Publish a MemberAddedEvent to all subscriptions
func (c *Channel) PublishMemberAddedEvent(data string, subscription *subscription.Subscription) {
c.RLock()
defer c.RUnlock()
for _, subs := range c.subscriptions {
if subs != subscription {
subs.Connection.Publish(events.NewMemberAdded(c.ID, data))
}
}
}
// PublishMemberRemovedEvent Publish a MemberRemovedEvent to all subscriptions
func (c *Channel) PublishMemberRemovedEvent(subscription *subscription.Subscription) {
c.RLock()
defer c.RUnlock()
for _, subs := range c.subscriptions {
if subs != subscription {
subs.Connection.Publish(events.NewMemberRemoved(c.ID, subscription.ID))
}
}
}
// Publish messages to all Subscribers
// skip the ignore connection
func (c *Channel) Publish(event events.Raw, ignore string) error {
c.RLock()
defer c.RUnlock()
b, err := event.Data.MarshalJSON()
if err != nil {
return err
}
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
log.Infof("Publishing message %+v to Channel %s", v, c.ID)
for _, subs := range c.subscriptions {
if subs.Connection.SocketID != ignore {
subs.Connection.Publish(events.NewResponse(event.Event, event.Channel, v))
} else {
if utils.IsClientEvent(event.Event) {
for _, hook := range c.clientEventListeners {
hook(c, subs, event.Event, v)
}
}
}
}
return nil
}
@@ -2,18 +2,23 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
package channel
import "testing"
import (
"ipe/connection"
"ipe/mocks"
"ipe/subscription"
"testing"
)
func TestIsOccupied(t *testing.T) {
c := newChannel("ID")
c := New("ID")
if c.IsOccupied() {
t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), false)
}
c.Subscriptions["ID"] = newSubscription(newConnection("ID", mockSocket{}), "")
c.subscriptions["ID"] = subscription.New(connection.New("ID", mocks.MockSocket{}), "")
if !c.IsOccupied() {
t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), true)
@@ -21,7 +26,7 @@ func TestIsOccupied(t *testing.T) {
}
func TestIsPrivate(t *testing.T) {
c := newChannel("private-channel")
c := New("private-Channel")
if !c.IsPrivate() {
t.Errorf("c.IsPrivate() == %t, wants %t", c.IsPrivate(), true)
@@ -29,7 +34,7 @@ func TestIsPrivate(t *testing.T) {
}
func TestIsPresence(t *testing.T) {
c := newChannel("presence-channel")
c := New("presence-Channel")
if !c.IsPresence() {
t.Errorf("c.IsPresence() == %t, wants %t", c.IsPresence(), true)
@@ -37,7 +42,7 @@ func TestIsPresence(t *testing.T) {
}
func TestIsPublic(t *testing.T) {
c := newChannel("channel")
c := New("Channel")
if !c.IsPublic() {
t.Errorf("c.IsPublic() == %t, wants %t", c.IsPublic(), true)
@@ -45,13 +50,13 @@ func TestIsPublic(t *testing.T) {
}
func TestIsPrivateOrPresence(t *testing.T) {
c := newChannel("private-channel")
c := New("private-Channel")
if !c.IsPresenceOrPrivate() {
t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
}
c = newChannel("presence-channel")
c = New("presence-Channel")
if !c.IsPresenceOrPrivate() {
t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
@@ -59,21 +64,21 @@ func TestIsPrivateOrPresence(t *testing.T) {
}
func TestTotalSubscriptions(t *testing.T) {
c := newChannel("ID")
c := New("ID")
if c.TotalSubscriptions() != len(c.Subscriptions) {
t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.Subscriptions))
if c.TotalSubscriptions() != len(c.subscriptions) {
t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.subscriptions))
}
}
func TestTotalUsers(t *testing.T) {
c := newChannel("ID")
c := New("ID")
c.Subscriptions["1"] = newSubscription(newConnection("ID", mockSocket{}), "")
c.Subscriptions["2"] = newSubscription(newConnection("ID", mockSocket{}), "")
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.TotalSubscriptions() != len(c.subscriptions) {
t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.subscriptions))
}
if c.TotalUsers() != 1 {
@@ -83,14 +88,14 @@ func TestTotalUsers(t *testing.T) {
}
func TestIsSubscribed(t *testing.T) {
c := newChannel("ID")
conn := newConnection("ID", mockSocket{})
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"] = newSubscription(conn, "")
c.subscriptions["ID"] = subscription.New(conn, "")
if !c.IsSubscribed(conn) {
t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), true)
Executable
BIN
View File
Binary file not shown.
+5 -5
View File
@@ -8,20 +8,20 @@ import (
"flag"
"fmt"
"github.com/dimiro1/ipe/ipe"
"ipe"
)
// These variables are generated by the linker
// please see the makefile for mor information.
var (
version string = "version"
buildstamp string = "buildstamp"
githash string = "githash"
version = "version"
buildstamp = "buildstamp"
githash = "githash"
)
// Main function, initialize the system
func main() {
var filename = flag.String("config", "config.json", "Config file location")
var filename = flag.String("config", "config.yml", "Config file location")
flag.Parse()
printBanner()
+39
View File
@@ -0,0 +1,39 @@
// Copyright 2014, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package config
// File config file
type File struct {
Host string `yaml:"host"` // The host, eg: :8080 will start on 0.0.0.0:8080
SSL SSL `yaml:"ssl"`
Profiling bool `yaml:"profiling"`
Apps []Application `yaml:"apps"`
}
// SSL related configuration options
type SSL struct {
Enabled bool `yaml:"enabled"`
Host string `yaml:"host"`
KeyFile string `yaml:"key_file"`
CertFile string `yaml:"cert_file"`
}
// Application related configuration options
type Application struct {
Name string `yaml:"name"`
AppID string `yaml:"app_id"`
Key string `yaml:"key"`
Secret string `yaml:"secret"`
OnlySSL bool `yaml:"only_ssl"`
Enabled bool `yaml:"enabled"`
UserEvents bool `yaml:"user_events"`
WebHooks Webhooks `yaml:"webhooks"`
}
// Webhooks related configuration options
type Webhooks struct {
Enabled bool `yaml:"enabled"`
URL string `yaml:"url"`
}
+43
View File
@@ -0,0 +1,43 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package connection
import (
"sync"
"time"
log "github.com/golang/glog"
)
// Socket interface to write to the client
type Socket interface {
WriteJSON(interface{}) error
}
// Connection An user connection
type Connection struct {
sync.Mutex
SocketID string
Socket Socket
CreatedAt time.Time
}
// New Create a new Subscriber
func New(socketID string, s Socket) *Connection {
log.Infof("Creating a new Subscriber %+v", socketID)
return &Connection{SocketID: socketID, Socket: s, CreatedAt: time.Now()}
}
// Publish the message to websocket attached to this client
func (conn *Connection) Publish(m interface{}) {
conn.Lock()
defer conn.Unlock()
if err := conn.Socket.WriteJSON(m); err != nil {
log.Errorf("error writing json into Socket, %+v", err)
}
}
@@ -2,15 +2,18 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
package connection
import "testing"
import (
"ipe/mocks"
"testing"
)
func TestNewConnection(t *testing.T) {
expectedSocketID := "socketID"
expectedSocket := mockSocket{}
expectedSocket := mocks.MockSocket{}
c := newConnection(expectedSocketID, expectedSocket)
c := New(expectedSocketID, expectedSocket)
if c.SocketID != expectedSocketID {
t.Errorf("c.SocketID == %s, wants %s", c.SocketID, expectedSocketID)
@@ -21,6 +24,6 @@ func TestNewConnection(t *testing.T) {
}
if c.CreatedAt.IsZero() {
t.Errorf("c.CreatedAt.IsZero() == %t, wants %t", c.CreatedAt.IsZero(), false)
t.Errorf("c.createdAt.IsZero() == %t, wants %t", c.CreatedAt.IsZero(), false)
}
}
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
if [ -z "$(ls -A /config)" ]; then
cp /app/config-example.yml /config/config.yml
fi
/app/ipe --config=/config/config.yml
+289
View File
@@ -0,0 +1,289 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package events
import (
"encoding/json"
log "github.com/golang/glog"
"ipe/subscription"
)
// SubscribeData data for Subscribe event
type SubscribeData struct {
Channel string `json:"channel"`
Auth string `json:"auth,omitempty"`
ChannelData string `json:"channel_data,omitempty"`
}
// Subscribe event
// {
// "event": "pusher:subscribe",
// "data": {
// "channel": "the channel",
// "auth": "the auth",
// "channelData": "extra data"
// }
// }
type Subscribe struct {
Event string `json:"event"`
Data SubscribeData `json:"data"`
}
// NewSubscribe Create a new subscribe event with the specified channel and data
func NewSubscribe(channel, auth, channelData string) Subscribe {
data := SubscribeData{Channel: channel, Auth: auth, ChannelData: channelData}
return Subscribe{Event: "pusher:subscribe", Data: data}
}
// UnsubscribeData event data
type UnsubscribeData struct {
Channel string `json:"channel"`
}
// Unsubscribe event
// {
// "event": "pusher:unsubscribe",
// "data": {
// "channel": "The channel"
// }
// }
type Unsubscribe struct {
Event string `json:"event"`
Data UnsubscribeData `json:"data"`
}
// NewUnsubscribe Create a new unsubscribe event for the specified channel
func NewUnsubscribe(channel string) Unsubscribe {
data := UnsubscribeData{Channel: channel}
return Unsubscribe{Event: "pusher:unsubscribe", Data: data}
}
// SubscriptionSucceeded event
// {
// "event": "pusher_internal:subscription_succeeded",
// "channel": "the channel"
// }
type SubscriptionSucceeded struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data string `json:"data"`
}
// NewSubscriptionSucceeded Create a new subscription succeed event for the specified channel
func NewSubscriptionSucceeded(channel, data string) SubscriptionSucceeded {
return SubscriptionSucceeded{Event: "pusher_internal:subscription_succeeded", Channel: channel, Data: data}
}
// SubscriptionSucceededPresenceData Data Subscription Succeed
// "{
// \"presence\": {
// \"ids\": [\"11814b369700141b222a3f3791cec2d9\",\"71dd6a29da2a4833336d2a964becf820\"],
// \"hash\": {
// \"11814b369700141b222a3f3791cec2d9\": {
// \"name\":\"Phil Leggetter\",
// \"twitter\": \"@leggetter\"
// },
// \"71dd6a29da2a4833336d2a964becf820\": {
// \"name\":\"Max Williams\",
// \"twitter\": \"@maxthelion\"
// }
// },
// \"count\": 2
// }
// }"
type SubscriptionSucceededPresenceData struct {
Ids []string `json:"ids"`
Hash map[string]interface{} `json:"hash"`
Count int `json:"count"`
}
// NewSubscriptionSucceedPresenceData returns new SubscriptionSucceededPresenceData
func NewSubscriptionSucceedPresenceData(subscriptions map[string]*subscription.Subscription) SubscriptionSucceededPresenceData {
event := SubscriptionSucceededPresenceData{}
var (
ids []string
hash = make(map[string]interface{}, len(subscriptions))
)
for _, s := range subscriptions {
// Do you have any other idea?
var js interface{}
if err := json.Unmarshal([]byte(s.Data), &js); err != nil {
continue
}
hash[s.ID] = js
ids = append(ids, s.ID)
}
event.Ids = ids
event.Hash = hash
event.Count = len(subscriptions)
return event
}
// Pong event
// {
// "event": "pusher:pong",
// "data": {}
// }
type Pong struct {
Event string `json:"event"`
Data string `json:"data"`
}
// NewPong Create a new pong event
func NewPong() Pong {
return Pong{Event: "pusher:pong", Data: "{}"}
}
// Ping event
// {
// "event": "pusher:ping",
// "data": {}
// }
type Ping struct {
Event string `json:"event"`
Data string `json:"data"`
}
// NewPing Create a new ping event
func NewPing() Ping {
return Ping{Event: "pusher:ping", Data: "{}"}
}
// Error event
// {
// "event": "pusher:error",
// "data": {
// "message": "A Message",
// "code": 4000
// }
// }
type Error struct {
Event string `json:"event"`
Data interface{} `json:"data"`
}
// NewError Create a new error event
// Pusher protocol is very strange in some parts
// It send null in some errors.
func NewError(code int, message string) Error {
var data = struct {
Code *int `json:"code"`
Message string `json:"message"`
}{
Message: message,
}
if code == 0 {
data.Code = nil
} else {
data.Code = &code
}
return Error{Event: "pusher:error", Data: data}
}
// ConnectionEstablished event
// {
// "event" : "pusher:connection_established",
// "data" : {
// "socket_id" : "123456",
// "activity_timeout" : 120
// }
// }
type ConnectionEstablished struct {
Event string `json:"event"`
Data string `json:"data"`
}
// NewConnectionEstablished Create a new connection established event using the specified socketId
func NewConnectionEstablished(socketID string) ConnectionEstablished {
b, err := json.Marshal(struct {
SocketID string `json:"socket_id"`
ActivityTimeout int `json:"activity_timeout"`
}{
SocketID: socketID, ActivityTimeout: 120,
})
if err != nil {
panic("events: Could not Marshal json ConnectionEstablishedEvent")
}
return ConnectionEstablished{Event: "pusher:connection_established", Data: string(b)}
}
// MemberAdded event
// {
// "event": "pusher_internal:member_added",
// "channel": "presence-example-channel",
// "data": String
// }
type MemberAdded struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data string `json:"data"`
}
// NewMemberAdded creates a new MemberAdded event
func NewMemberAdded(channel, data string) MemberAdded {
return MemberAdded{Event: "pusher_internal:member_added", Channel: channel, Data: data}
}
// MemberRemoved event
// {
// "event": "pusher_internal:member_removed",
// "channel": "presence-example-channel",
// "data": String
// }
type MemberRemoved struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data string `json:"data"`
}
// NewMemberRemoved returns a new MemberRemoved event
func NewMemberRemoved(channel string, userID string) MemberRemoved {
data, err := json.Marshal(struct {
UserID string `json:"user_id"`
}{
UserID: userID,
})
if err != nil {
log.Error(err)
}
return MemberRemoved{Event: "pusher_internal:member_removed", Channel: channel, Data: string(data)}
}
// Raw event, usually used for client events
// {
// "event": "client-?",
// "channel": "The channel",
// "data": {}
// }
type Raw struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data json.RawMessage `json:"data"`
}
// Response event
type Response struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data interface{} `json:"data"`
}
// NewResponse The response event that is broadcasted to the client sockets
func NewResponse(name, channel string, data interface{}) Response {
return Response{Event: name, Channel: channel, Data: data}
}
+29
View File
@@ -0,0 +1,29 @@
package events
import (
"bytes"
"encoding/json"
"testing"
)
func Test_newErrorEvent_with_invalid_code(t *testing.T) {
event := NewError(0, "The error message")
data, _ := json.Marshal(event)
expected := `{"event":"pusher:error","data":{"code":null,"message":"The error message"}}`
if bytes.Compare(data, []byte(expected)) != 0 {
t.Errorf("%s != %s", string(data), expected)
}
}
func Test_newErrorEvent_with_valid_code(t *testing.T) {
event := NewError(4007, "Unsupported protocol version")
data, _ := json.Marshal(event)
expected := `{"event":"pusher:error","data":{"code":4007,"message":"Unsupported protocol version"}}`
if bytes.Compare(data, []byte(expected)) != 0 {
t.Errorf("%s != %s", string(data), expected)
}
}
+1 -1
View File
@@ -1,2 +1,2 @@
client: go run client.go
server: go run ../main.go -config ./functional-config.json -alsologtostderr
server: go run ../cmd/main.go -config ./functional-config.yml -alsologtostderr
+12 -13
View File
@@ -1,24 +1,23 @@
<html>
<head>
<meta charset="utf-8">
<title>Pusher Spec</title>
<link href="https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.css" rel="stylesheet" />
<meta charset="utf-8">
<title>Pusher Spec</title>
<link href="//cdnjs.cloudflare.com/ajax/libs/mocha/5.2.0/mocha.min.css" rel="stylesheet"/>
</head>
<body>
<div id="mocha"></div>
<div id="mocha"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.3.4/mocha.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.4.1/chai.min.js"></script>
<script src="//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="https://cdnjs.cloudflare.com/ajax/libs/pusher/3.0.0/pusher.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>
<script>mocha.setup('bdd')</script>
<script src="test.pusher.js"></script>
<script>
mocha.checkLeaks();
mocha.globals(['jQuery', 'Pusher']);
mocha.globals(['Pusher']);
mocha.run();
</script>
</script>
</body>
</html>
+43 -6
View File
@@ -1,9 +1,12 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"github.com/pusher/pusher-http-go"
)
@@ -20,6 +23,7 @@ func init() {
}
func pusherPresenceAuth(res http.ResponseWriter, req *http.Request) {
log.Println("Presence Request")
presenceData := pusher.MemberData{
UserId: "1",
UserInfo: map[string]string{},
@@ -32,30 +36,63 @@ func pusherPresenceAuth(res http.ResponseWriter, req *http.Request) {
panic(err)
}
fmt.Fprintf(res, string(response))
_, _ = fmt.Fprint(res, string(response))
}
func pusherPrivateAuth(res http.ResponseWriter, req *http.Request) {
params, _ := ioutil.ReadAll(req.Body)
response, err := client.AuthenticatePrivateChannel(params)
log.Printf("Private Request %s", params)
log.Printf("Auth %s", response)
if err != nil {
panic(err)
}
fmt.Fprintf(res, string(response))
_, _ = fmt.Fprint(res, string(response))
}
func triggerMessage(res http.ResponseWriter, req *http.Request) {
client.Trigger("private-messages", "messages", "The message from server")
func triggerMessage(res http.ResponseWriter, _ *http.Request) {
_, err := client.Trigger("private-messages", "messages", "The message from server")
if err != nil {
panic(err)
}
fmt.Fprintf(res, "OK")
_, _ = 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)
_ = http.ListenAndServe(":5000", nil)
}
-20
View File
@@ -1,20 +0,0 @@
{
"Host": ":8080",
"Encrypted": false,
"SSLHost": ":8090",
"SSLKeyFile": "key.pem",
"SSLCertFile": "cert.pem",
"Apps": [
{
"ApplicationDisabled": false,
"OnlySSL": false,
"Secret": "7ad3753142a6693b25b9",
"Key": "278d525bdf162c739803",
"Name": "App for Functional Test",
"AppID": "1",
"UserEvents": true,
"WebHooks": false,
"URLWebHook": "http://127.0.0.1:4567/php/hook.php"
}
]
}
+19
View File
@@ -0,0 +1,19 @@
---
host: ':8080'
profiling: false
ssl:
enabled: false
host: ':4343'
key_file: 'key.pem'
cert_file: 'cert.pem'
apps:
- name: 'Sample Application'
enabled: true
only_ssl: false
key: '278d525bdf162c739803'
secret: '7ad3753142a6693b25b9'
app_id: '1'
user_events: true
webhooks:
enabled: true # Default is false
url: 'http://127.0.0.1:5000/hook'
+127 -105
View File
@@ -1,136 +1,158 @@
"use strict";
var assert = chai.assert;
let assert = chai.assert;
var APP_KEY = "278d525bdf162c739803";
var HOST = "localhost";
var PORT = 8080;
var AUTH = "http://localhost:5000/pusher/private/auth"
var AUTH_PRESENCE = "http://localhost:5000/pusher/presence/auth"
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);
Pusher.log = function (msg) {
console.log(msg);
};
function getPusher(auth) {
return new Pusher(APP_KEY, {
wsHost: HOST,
wsPort: PORT,
authEndpoint: auth,
enabledTransports: ["ws"],
disabledTransports: ["flash"],
cluster: "hello", // Should be ignored
});
return new Pusher(APP_KEY, {
wsHost: HOST,
wsPort: PORT,
authEndpoint: auth,
enabledTransports: ["ws"],
disabledTransports: ["flash"],
});
}
describe("Pusher", function() {
describe("Pusher", function () {
describe("connection", function() {
it("should connect sucessfully with correct config", function(done) {
var pusher = getPusher(AUTH);
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();
});
});
pusher.connection.bind('connected', function () {
assert.ok(true, "Connected");
done();
});
});
it("should not connect without the correct config", function(done) {
var pusher = new Pusher("INVALID_APP_KEY", {
wsHost: HOST,
wsPort: PORT,
enabledTransports: ["ws"],
disabledTransports: ["flash"]
});
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
pusher.connection.bind('disconnected', function () {
assert.ok(true, "Not Connected");
done();
});
});
}); // connection
describe("subscription", function() {
it("should subscribe to a public channel", function(done) {
var pusher = getPusher(AUTH);
describe("subscription", function () {
it("should subscribe to a public channel", function (done) {
let pusher = getPusher(AUTH);
var channel = pusher.subscribe('public-channel');
channel.bind("pusher:subscription_succeeded", function(data) {
assert.ok(true, "Connected to the channel");
done();
});
});
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) {
var pusher = getPusher(AUTH);
it("should subscribe to a private channel", function (done) {
let pusher = getPusher(AUTH);
var channel = pusher.subscribe('private-channel');
channel.bind("pusher:subscription_succeeded", function(data) {
assert.ok(true, "Connected to the channel");
done();
});
});
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) {
var pusher = getPusher(AUTH_PRESENCE);
it("should subscribe to a presence channel", function (done) {
let pusher = getPusher(AUTH_PRESENCE);
var channel = pusher.subscribe('presence-channel');
channel.bind("pusher:subscription_succeeded", function(data) {
assert.ok(true, "Connected to the channel");
done();
});
});
}); // subscription
let channel = pusher.subscribe('presence-channel');
channel.bind("pusher:subscription_succeeded", function () {
assert.ok(true, "Connected to the channel");
done();
});
});
}); // subscription
describe("events", function() {
it('should not allowed client events on public channels', function(done) {
var pusher = getPusher(AUTH);
var channel = pusher.subscribe('public-channel');
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(data) {
channel.trigger("client-message", "The message");
});
channel.bind("pusher:subscription_succeeded", function () {
console.log("subscribed");
});
pusher.bind("pusher:error", function(data) {
assert.ok(true, "Expected error");
done();
});
});
channel.bind("channel_occupied", function (data) {
assert.equal(data, "The Webhoook from server");
pusher.unsubscribe('private-webhook');
done();
});
});
}); // hooks
it('should allow client events on private channels', function(done) {
var pusher_a = getPusher(AUTH);
var pusher_b = getPusher(AUTH);
describe("events", function () {
it('should not allowed client events on public channels', function (done) {
let pusher = getPusher(AUTH);
let channel = pusher.subscribe('public-channel');
var channel_a = pusher_a.subscribe('private-channel');
var channel_b = pusher_b.subscribe('private-channel');
channel.bind("pusher:subscription_succeeded", function () {
channel.trigger("client-message", "The message");
});
channel_a.bind("pusher:subscription_succeeded", function() {
channel_a.trigger("client-message", "The message");
});
pusher.bind("pusher:error", function () {
assert.ok(true, "Expected error");
done();
});
});
channel_b.bind("client-message", function(data) {
assert.equal(data, "The message");
done();
});
});
it('should allow client events on private channels', function (done) {
let pusher_a = getPusher(AUTH);
let pusher_b = getPusher(AUTH);
it('should publish event on private channel', function(done) {
var pusher_a = getPusher(AUTH);
var pusher_b = getPusher(AUTH);
let channel_a = pusher_a.subscribe('private-channel');
let channel_b = pusher_b.subscribe('private-channel');
var channel_a = pusher_a.subscribe('private-messages');
var channel_b = pusher_b.subscribe('private-messages');
channel_a.bind("pusher:subscription_succeeded", function () {
channel_a.trigger("client-message", "The message");
});
channel_a.bind("pusher:subscription_succeeded", function() {
var xhttp = new XMLHttpRequest();
xhttp.open("GET", "/trigger", true);
xhttp.send();
});
channel_b.bind("client-message", function (data) {
assert.equal(data, "The message");
done();
});
});
channel_b.bind("messages", function(data) {
assert.equal(data, "The message from server");
done();
});
});
it('should publish event on private channel', function (done) {
let pusher_a = getPusher(AUTH);
let pusher_b = getPusher(AUTH);
}); // events
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
});
Generated
-14
View File
@@ -1,14 +0,0 @@
hash: bfb508bdf4f85c71c7eef4bb9431148ee373dfaf31d001d050d933f80ac4288e
updated: 2016-02-21T20:13:57.03748112-03:00
imports:
- name: github.com/golang/glog
version: 23def4e6c14b4da8ac2ed8007337bc5eb5007998
- name: github.com/gorilla/context
version: 1c83b3eabd45b6d76072b66b746c20815fb2872d
- name: github.com/gorilla/mux
version: 26a6070f849969ba72b72256e9f14cf519751690
- name: github.com/gorilla/websocket
version: 5c91b59efa232fa9a87b705d54101832c498a172
- name: github.com/pusher/pusher-http-go
version: 8d4ffe157699620440932e4d03253a22533f2e43
devImports: []
-6
View File
@@ -1,6 +0,0 @@
package: github.com/dimiro1/ipe
import:
- package: github.com/golang/glog
- package: github.com/gorilla/mux
- package: github.com/gorilla/websocket
- package: github.com/pusher/pusher-http-go
+15
View File
@@ -0,0 +1,15 @@
module ipe
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/handlers v1.4.0
github.com/gorilla/mux v1.6.2
github.com/gorilla/websocket v1.4.0
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pusher/pusher-http-go v1.3.0
github.com/stretchr/testify v1.2.2 // indirect
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 // indirect
gopkg.in/yaml.v2 v2.2.1
)
+23
View File
@@ -0,0 +1,23 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA=
github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pusher/pusher-http-go v1.3.0 h1:dWrjIsNheCUEo6YE9qlS8pIl3XzJa5yM8VlBu/LUl3M=
github.com/pusher/pusher-http-go v1.3.0/go.mod h1:XAv1fxRmVTI++2xsfofDhg7whapsLRG/gH/DXbF3a18=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 h1:kkXA53yGe04D0adEYJwEVQjeBppL01Exg+fnMjfUraU=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+106
View File
@@ -0,0 +1,106 @@
// Copyright 2015 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import (
"io/ioutil"
"math/rand"
"net/http"
"os"
"time"
log "github.com/golang/glog"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"gopkg.in/yaml.v2"
"ipe/api"
"ipe/app"
"ipe/config"
"ipe/storage"
"ipe/websockets"
)
// Start Parse the configuration file and starts the ipe server
// It Panic if could not start the HTTP or HTTPS server
func Start(filename string) {
var conf config.File
rand.Seed(time.Now().Unix())
data, err := ioutil.ReadFile(filename)
if err != nil {
log.Error(err)
return
}
// Expand env vars
data = []byte(os.ExpandEnv(string(data)))
// Decoding config
if err := yaml.UnmarshalStrict(data, &conf); err != nil {
log.Error(err)
return
}
// Using a in memory database
inMemoryStorage := storage.NewInMemory()
// Adding applications
for _, a := range conf.Apps {
application := app.NewApplication(
a.Name,
a.AppID,
a.Key,
a.Secret,
a.OnlySSL,
a.Enabled,
a.UserEvents,
a.WebHooks.Enabled,
a.WebHooks.URL,
)
if err := inMemoryStorage.AddApp(application); err != nil {
log.Error(err)
return
}
}
router := mux.NewRouter()
router.Use(handlers.RecoveryHandler())
router.Path("/app/{key}").Methods("GET").Handler(
websockets.NewWebsocket(inMemoryStorage),
)
appsRouter := router.PathPrefix("/apps/{app_id}").Subrouter()
appsRouter.Use(
api.CheckAppDisabled(inMemoryStorage),
api.Authentication(inMemoryStorage),
)
appsRouter.Path("/events").Methods("POST").Handler(
api.NewPostEvents(inMemoryStorage),
)
appsRouter.Path("/channels").Methods("GET").Handler(
api.NewGetChannels(inMemoryStorage),
)
appsRouter.Path("/channels/{channel_name}").Methods("GET").Handler(
api.NewGetChannel(inMemoryStorage),
)
appsRouter.Path("/channels/{channel_name}/users").Methods("GET").Handler(
api.NewGetChannelUsers(inMemoryStorage),
)
if conf.SSL.Enabled {
go func() {
log.Infof("Starting HTTPS service on %s ...", conf.SSL.Host)
log.Fatal(http.ListenAndServeTLS(conf.SSL.Host, conf.SSL.CertFile, conf.SSL.KeyFile, router))
}()
}
log.Infof("Starting HTTP service on %s ...", conf.Host)
log.Fatal(http.ListenAndServe(conf.Host, router))
}
-234
View File
@@ -1,234 +0,0 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import (
"errors"
"expvar"
"fmt"
"sync"
log "github.com/golang/glog"
)
// An App
type app struct {
sync.Mutex
Name string
AppID string
Key string
Secret string
OnlySSL bool
ApplicationDisabled bool
UserEvents bool
WebHooks bool
URLWebHook string
Channels map[string]*channel
Connections map[string]*connection
Stats *expvar.Map
}
func newApp(name, appID, key, secret string, onlySSL, disabled, userEvents, webHooks bool, webHookURL string) *app {
a := &app{
Name: name,
AppID: appID,
Key: key,
Secret: secret,
OnlySSL: onlySSL,
ApplicationDisabled: disabled,
UserEvents: userEvents,
WebHooks: webHooks,
URLWebHook: webHookURL,
}
a.Connections = make(map[string]*connection)
a.Channels = make(map[string]*channel)
a.Stats = expvar.NewMap(fmt.Sprintf("%s (%s)", a.Name, a.AppID))
return a
}
// Only Presence channels
func (a *app) PresenceChannels() []*channel {
var channels []*channel
for _, c := range a.Channels {
if c.IsPresence() {
channels = append(channels, c)
}
}
return channels
}
// Only Private channels
func (a *app) PrivateChannels() []*channel {
var channels []*channel
for _, c := range a.Channels {
if c.IsPrivate() {
channels = append(channels, c)
}
}
return channels
}
// Only Public channels
func (a *app) PublicChannels() []*channel {
var channels []*channel
for _, c := range a.Channels {
if c.IsPublic() {
channels = append(channels, c)
}
}
return channels
}
// Disconnect Socket
func (a *app) Disconnect(socketID string) {
log.Infof("Disconnecting socket %+v", socketID)
conn, err := a.FindConnection(socketID)
if err != nil {
log.Infof("Socket not found, %+v", err)
return
}
// Unsubscribe from channels
for _, c := range a.Channels {
if c.IsSubscribed(conn) {
c.Unsubscribe(a, conn)
}
}
// Remove from app
a.Lock()
defer a.Unlock()
_, exists := a.Connections[conn.SocketID]
if !exists {
return
}
delete(a.Connections, conn.SocketID)
a.Stats.Add("TotalConnections", -1)
}
// Connect a new Subscriber
func (a *app) Connect(conn *connection) {
log.Infof("Adding a new Connection %s to app %s", conn.SocketID, a.Name)
a.Lock()
defer a.Unlock()
a.Connections[conn.SocketID] = conn
a.Stats.Add("TotalConnections", 1)
}
// Find a Connection on this app
func (a *app) FindConnection(socketID string) (*connection, error) {
conn, exists := a.Connections[socketID]
if exists {
return conn, nil
}
return nil, errors.New("Connection not found")
}
// DeleteChannel removes the channel from app
func (a *app) RemoveChannel(c *channel) {
log.Infof("Remove the channel %s from app %s", c.ChannelID, a.Name)
a.Lock()
defer a.Unlock()
delete(a.Channels, c.ChannelID)
if c.IsPresence() {
a.Stats.Add("TotalPresenceChannels", -1)
}
if c.IsPrivate() {
a.Stats.Add("TotalPrivateChannels", -1)
}
if c.IsPublic() {
a.Stats.Add("TotalPublicChannels", -1)
}
a.Stats.Add("TotalChannels", -1)
}
// Add a new Channel to this APP
func (a *app) AddChannel(c *channel) {
log.Infof("Adding a new channel %s to app %s", c.ChannelID, a.Name)
a.Lock()
defer a.Unlock()
a.Channels[c.ChannelID] = c
if c.IsPresence() {
a.Stats.Add("TotalPresenceChannels", 1)
}
if c.IsPrivate() {
a.Stats.Add("TotalPrivateChannels", 1)
}
if c.IsPublic() {
a.Stats.Add("TotalPublicChannels", 1)
}
a.Stats.Add("TotalChannels", 1)
}
// Returns a Channel from this app
// If not found then the channel is created and added to this app
func (a *app) FindOrCreateChannelByChannelID(n string) *channel {
c, err := a.FindChannelByChannelID(n)
if err != nil {
c = newChannel(n)
a.AddChannel(c)
}
return c
}
// Find the channel by channel ID
func (a *app) FindChannelByChannelID(n string) (*channel, error) {
c, exists := a.Channels[n]
if exists {
return c, nil
}
return nil, errors.New("Channel does not exists")
}
func (a *app) Publish(c *channel, event rawEvent, ignore string) error {
a.Stats.Add("TotalUniqueMessages", 1)
return c.Publish(a, event, ignore)
}
func (a *app) Unsubscribe(c *channel, conn *connection) error {
return c.Unsubscribe(a, conn)
}
func (a *app) Subscribe(c *channel, conn *connection, data string) error {
return c.Subscribe(a, conn, data)
}
-252
View File
@@ -1,252 +0,0 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import (
"strconv"
"testing"
)
var id = 0
func newTestApp() *app {
a := newApp("Test", strconv.Itoa(id), "123", "123", false, false, true, false, "")
id++
return a
}
func TestConnect(t *testing.T) {
app := newTestApp()
app.Connect(newConnection("socketID", mockSocket{}))
if len(app.Connections) != 1 {
t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 1)
}
}
func TestDisconnect(t *testing.T) {
app := newTestApp()
app.Connect(newConnection("socketID", mockSocket{}))
app.Disconnect("socketID")
if len(app.Connections) != 0 {
t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 0)
}
}
func TestFindConnection(t *testing.T) {
app := newTestApp()
app.Connect(newConnection("socketID", mockSocket{}))
if _, err := app.FindConnection("socketID"); err != nil {
t.Errorf("app.FindConnection('socketID') == _, %q, wants %v", err, nil)
}
if _, err := app.FindConnection("NotFound"); err == nil {
t.Errorf("app.FindConnection('socketID') == _, %q, wants !nil", err)
}
}
func TestFindChannelByChannelID(t *testing.T) {
app := newTestApp()
channel := newChannel("ID")
app.AddChannel(channel)
if _, err := app.FindChannelByChannelID("ID"); err != nil {
t.Errorf("app.FindChannelByChannelID('ID') == _, %q, wants %v", err, nil)
}
}
func TestFindOrCreateChannelByChannelID(t *testing.T) {
app := newTestApp()
if len(app.Channels) != 0 {
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0)
}
app.FindOrCreateChannelByChannelID("ID")
if len(app.Channels) != 1 {
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 1)
}
}
func TestRemoveChannel(t *testing.T) {
app := newTestApp()
if len(app.Channels) != 0 {
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0)
}
channel := newChannel("ID")
app.AddChannel(channel)
if len(app.Channels) != 1 {
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 1)
}
app.RemoveChannel(channel)
if len(app.Channels) != 0 {
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0)
}
}
func Test_add_channels(t *testing.T) {
app := newTestApp()
// Public
if len(app.PublicChannels()) != 0 {
t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
}
app.AddChannel(newChannel("ID"))
if len(app.PublicChannels()) != 1 {
t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
}
// Presence
if len(app.PresenceChannels()) != 0 {
t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
}
app.AddChannel(newChannel("presence-test"))
if len(app.PresenceChannels()) != 1 {
t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
}
// Private
if len(app.PrivateChannels()) != 0 {
t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
}
app.AddChannel(newChannel("private-test"))
if len(app.PrivateChannels()) != 1 {
t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
}
}
func Test_AllChannels(t *testing.T) {
app := newTestApp()
app.AddChannel(newChannel("private-test"))
app.AddChannel(newChannel("presence-test"))
app.AddChannel(newChannel("test"))
if len(app.Channels) != 3 {
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 3)
}
}
func Test_New_Subscriber(t *testing.T) {
app := newTestApp()
if len(app.Connections) != 0 {
t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 0)
}
conn := newConnection("1", mockSocket{})
app.Connect(conn)
if len(app.Connections) != 1 {
t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 1)
}
}
func Test_find_subscriber(t *testing.T) {
app := newTestApp()
conn := newConnection("1", mockSocket{})
app.Connect(conn)
conn, err := app.FindConnection("1")
if err != nil {
t.Error(err)
}
if conn.SocketID != "1" {
t.Errorf("conn.SocketID == %s, wants %s", conn.SocketID, "1")
}
// Find a wrong subscriber
conn, err = app.FindConnection("DoesNotExists")
if err == nil {
t.Errorf("err == %q, wants !nil", err)
}
if conn != nil {
t.Errorf("conn == %q, wants nil", conn)
}
}
func Test_find_or_create_channels(t *testing.T) {
app := newTestApp()
// Public
if len(app.PublicChannels()) != 0 {
t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
}
c := app.FindOrCreateChannelByChannelID("id")
if len(app.PublicChannels()) != 1 {
t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
}
if c.ChannelID != "id" {
t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "id")
}
// Presence
if len(app.PresenceChannels()) != 0 {
t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
}
c = app.FindOrCreateChannelByChannelID("presence-test")
if len(app.PresenceChannels()) != 1 {
t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
}
if c.ChannelID != "presence-test" {
t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "presence-test")
}
// Private
if len(app.PrivateChannels()) != 0 {
t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
}
c = app.FindOrCreateChannelByChannelID("private-test")
if len(app.PrivateChannels()) != 1 {
t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
}
if c.ChannelID != "private-test" {
t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "private-test")
}
}
-225
View File
@@ -1,225 +0,0 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import (
"encoding/json"
"errors"
"strings"
"sync"
"time"
"github.com/dimiro1/ipe/utils"
log "github.com/golang/glog"
)
// A Channel
type channel struct {
sync.Mutex
CreatedAt time.Time
ChannelID string
Subscriptions map[string]*subscription
}
// Return true if the channel has at least one subscriber
func (c *channel) IsOccupied() bool {
return c.TotalSubscriptions() > 0
}
// Check if the type of the channel is presence or is private
func (c *channel) IsPresenceOrPrivate() bool {
return c.IsPresence() || c.IsPrivate()
}
// Check if the type of the channel is public
func (c *channel) IsPublic() bool {
return !c.IsPresenceOrPrivate()
}
// Check if the type of the channel is presence
func (c *channel) IsPresence() bool {
return utils.IsPresenceChannel(c.ChannelID)
}
// Check if the type of the channel is private
func (c *channel) IsPrivate() bool {
return utils.IsPrivateChannel(c.ChannelID)
}
// Get the total of subscribers
func (c *channel) TotalSubscriptions() int {
return len(c.Subscriptions)
}
// Get the total of users.
func (c *channel) TotalUsers() int {
total := make(map[string]int)
for _, s := range c.Subscriptions {
total[s.ID]++
}
return len(total)
}
// Add a new subscriber to the channel
func (c *channel) Subscribe(a *app, conn *connection, channelData string) error {
log.Infof("Subscribing %s to channel %s", conn.SocketID, c.ChannelID)
c.Lock()
defer c.Unlock()
subscription := newSubscription(conn, channelData)
c.Subscriptions[conn.SocketID] = subscription
if c.IsPresence() {
// User Info Data
var info struct {
UserID string `json:"user_id"`
UserInfo json.RawMessage `json:"user_info"`
}
log.Infof("%+v", channelData)
if err := json.Unmarshal([]byte(channelData), &info); err != nil {
log.Error(err)
return err
}
js, err := info.UserInfo.MarshalJSON()
if err != nil {
log.Error(err)
return err
}
// Update the Subscription
subscription.ID = info.UserID
subscription.Data = string(js)
// Publish pusher_internal:member_added
c.PublishMemberAddedEvent(a, channelData, subscription)
// WebHook
a.TriggerMemberAddedHook(c, subscription)
// pusher_internal:subscription_succeeded
data := make(map[string]subscriptionSucceeedEventPresenceData)
data["presence"] = newSubscriptionSucceedEventPresenceData(c)
js, err = json.Marshal(data)
if err != nil {
log.Error(err)
return err
}
conn.Publish(newSubscriptionSucceededEvent(c.ChannelID, string(js)))
} else {
conn.Publish(newSubscriptionSucceededEvent(c.ChannelID, "{}"))
}
// WebHook
if c.TotalSubscriptions() == 1 {
a.TriggerChannelOccupiedHook(c)
}
return nil
}
// IsSubscribed check if the user is subscribed
func (c *channel) IsSubscribed(conn *connection) bool {
_, exists := c.Subscriptions[conn.SocketID]
return exists
}
// Remove the subscriber from the channel
// It destroy the channel if the channels does not have any subscribers.
func (c *channel) Unsubscribe(a *app, conn *connection) error {
log.Infof("Unsubscribing %s from channel %s", conn.SocketID, c.ChannelID)
c.Lock()
defer c.Unlock()
subscription, exists := c.Subscriptions[conn.SocketID]
if !exists {
return errors.New("Subscription not found")
}
delete(c.Subscriptions, conn.SocketID)
if c.IsPresence() {
// Publish pusher_internal:member_removed
c.PublishMemberRemovedEvent(a, subscription)
// Webhook
a.TriggerMemberRemovedHook(c, subscription)
}
if !c.IsOccupied() {
// WebHook
a.TriggerChannelVacatedHook(c)
// Remove the empty Channel
a.RemoveChannel(c)
}
return nil
}
// Create a new Channel
func newChannel(channelID string) *channel {
log.Infof("Creating a new channel: %s", channelID)
return &channel{ChannelID: channelID, CreatedAt: time.Now(), Subscriptions: make(map[string]*subscription)}
}
// Publish a MemberAddedEvent to all subscriptions
func (c *channel) PublishMemberAddedEvent(a *app, data string, subscription *subscription) {
for _, subs := range c.Subscriptions {
if subs != subscription {
subs.Connection.Publish(newMemberAddedEvent(c.ChannelID, data))
}
}
}
// Publish a MemberRemovedEvent to all subscriptions
func (c *channel) PublishMemberRemovedEvent(a *app, subscription *subscription) {
for _, subs := range c.Subscriptions {
if subs != subscription {
subs.Connection.Publish(newMemberRemovedEvent(c.ChannelID, subscription))
}
}
}
// Publish messages to all Subscribers
func (c *channel) Publish(a *app, event rawEvent, ignore string) error {
b, err := event.Data.MarshalJSON()
if err != nil {
return err
}
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
log.Infof("Publishing message %+v to channel %s", v, c.ChannelID)
for _, subs := range c.Subscriptions {
if subs.Connection.SocketID != ignore {
subs.Connection.Publish(newResponseEvent(event.Event, event.Channel, v))
} else {
// Webhook
if strings.HasPrefix(event.Event, "client-") {
a.TriggerClientEventHook(c, subs, event.Event, v)
}
}
}
return nil
}
-20
View File
@@ -1,20 +0,0 @@
{
"Host": ":8080",
"SSL": false,
"SSLHost": ":4433",
"SSLKeyFile": "A key.pem file",
"SSLCertFile": "A cert.pem file",
"Apps": [
{
"ApplicationDisabled": false,
"Secret": "A really secret random string",
"Key": "A random Key string",
"OnlySSL": false,
"Name": "The app name",
"AppID": "The app ID",
"UserEvents": true,
"WebHooks": true,
"URLWebHook": "Some URL to send webhooks"
}
]
}
-43
View File
@@ -1,43 +0,0 @@
// Copyright 2014, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
// The config file
type configFile struct {
Host string // The host, eg: :8080 will start on 0.0.0.0:8080
User string
SSL bool
SSLHost string
SSLKeyFile string
SSLCertFile string
Apps []configApp
}
type configApp struct {
Name string
AppID string
Key string
Secret string
OnlySSL bool
ApplicationDisabled bool
UserEvents bool
WebHooks bool
URLWebHook string
}
func newAppFromConfig(a configApp) *app {
return newApp(
a.Name,
a.AppID,
a.Key,
a.Secret,
a.OnlySSL,
a.ApplicationDisabled,
a.UserEvents,
a.WebHooks,
a.URLWebHook,
)
}
-43
View File
@@ -1,43 +0,0 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import (
"time"
log "github.com/golang/glog"
)
// socket interface to write to the client
type socket interface {
WriteJSON(interface{}) error
}
// mockSocket is a mock implementation of socket
// used in the test suite
type mockSocket struct{}
func (s mockSocket) WriteJSON(i interface{}) error {
return nil
}
// An User Connection
type connection struct {
SocketID string
Socket socket
CreatedAt time.Time
}
// Create a new Subscriber
func newConnection(socketID string, s socket) *connection {
log.Infof("Creating a new Subscriber %+v", socketID)
return &connection{SocketID: socketID, Socket: s, CreatedAt: time.Now()}
}
// Publish the message to websocket atached to this client
func (conn *connection) Publish(m interface{}) {
conn.Socket.WriteJSON(m)
}
-47
View File
@@ -1,47 +0,0 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
// Error Codes
const (
// 4000 - 4099
// Indicates an error resulting in the connection being closed by Pusher,
// and that attempting to reconnect using the same parameters will not succeed.
applicationOnlyAcceptsSSL = 4000
applicationDoesNotExists = 4001
applicationDisabled = 4003
applicationIsOverConnectionQuota = 4004 // Not Implemented
pathNotFound = 4005 // Not Implemented
invalidVersionStringFormat = 4006
unsupportedProtocolVersion = 4007
noProtocolVersionSupplied = 4008
// 4100 - 4199
// Indicates an error resulting in the connection being closed by Pusher,
// and the client may reconnect after 1s or more
overCapacity = 4100 // Not Implemented
// 4200 - 4299
// Indicate an error resulting in the connection being closed by Pusher,
// and the client my reconnect immediately
genericReconnectImmediately = 4200
pongReplyNotReceived = 4201 // Ping was sent to the client, but no reply was received; Not Implemented
closedAfterInactivity = 4202 // Client has been inactive for a long time (24 hours) and client does not suppot ping.; Not Implemented
// 4300 - 4399
// Any other type of error
clientRejectedDueToRateLimit = 4301 // Not Implemented
// Pusher send null, This app use this error code to send the null value
// see ErrorEvent
otherError = 0
)
// Only this version is supported
const supportedProtocolVersion = 7
// // Maximun event size permitted 10 kB
// See: http://blogs.gnome.org/cneumair/2008/09/30/1-kb-1024-bytes-no-1-kb-1000-bytes/
const maxDataEventSize = 10 * 1000
-29
View File
@@ -1,29 +0,0 @@
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import "net/http"
type applicationContext struct {
DB db
}
// url params
type params map[string]string
func (p params) Get(key string) string {
return p[key]
}
// A contextHandler responds to an HTTP request with custom application context.
type contextHandler interface {
ServeWithContext(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request)
}
type contextHandlerFunc func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request)
func (c contextHandlerFunc) ServeWithContext(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
c(ctx, p, w, r)
}
-58
View File
@@ -1,58 +0,0 @@
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import (
"errors"
"sync"
)
// db represents a app database
// For now it there is only one memory database implementation
// but in the future I can write a sql implementation
type db interface {
GetAppByAppID(appID string) (*app, error)
GetAppByKey(key string) (*app, error)
AddApp(*app) error
}
// memdb is a in memory implementation of db interface
type memdb struct {
sync.Mutex
Apps []*app
}
func newMemdb() *memdb {
return &memdb{}
}
func (db *memdb) AddApp(a *app) error {
db.Lock()
defer db.Unlock()
db.Apps = append(db.Apps, a)
return nil
}
// GetAppByAppID returns an App with by appID
func (db *memdb) GetAppByAppID(appID string) (*app, error) {
for _, a := range db.Apps {
if a.AppID == appID {
return a, nil
}
}
return nil, errors.New("App not found")
}
// GetAppByKey returns an App with by key
func (db *memdb) GetAppByKey(key string) (*app, error) {
for _, a := range db.Apps {
if a.Key == key {
return a, nil
}
}
return nil, errors.New("App not found")
}
-59
View File
@@ -1,59 +0,0 @@
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import "testing"
func Test_db_GetAppByAppID(t *testing.T) {
app := &app{AppID: "123456", Name: "Example"}
db := newMemdb()
db.AddApp(app)
a, err := db.GetAppByAppID("123456")
if err != nil {
t.Errorf("GetAppByAppID(%q) == %q, want %q", "123456", a, app)
}
}
func Test_db_GetAppByAppID__error(t *testing.T) {
app := &app{AppID: "123456", Name: "Example"}
db := newMemdb()
db.AddApp(app)
a, err := db.GetAppByAppID("not-found")
if err == nil {
t.Errorf("GetAppByAppID(%q) == %q, want %q", "123456", a, app)
}
}
func Test_db_GetAppByKey(t *testing.T) {
app := &app{AppID: "123456", Name: "Example", Key: "654321"}
db := newMemdb()
db.AddApp(app)
a, err := db.GetAppByKey("654321")
if err != nil {
t.Errorf("GetAppByKey(%q) == %q, want %q", "654321", a, app)
}
}
func Test_db_GetAppByKey__error(t *testing.T) {
app := &app{AppID: "123456", Name: "Example", Key: "654321"}
db := newMemdb()
db.AddApp(app)
a, err := db.GetAppByKey("not-found")
if err == nil {
t.Errorf("GetAppByKey(%q) == %q, want %v", "not-found", a, nil)
}
}
-118
View File
@@ -1,118 +0,0 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
// Base interface
type websocketError interface {
GetCode() int
GetMsg() string
}
// Base struct
type baseWebsocketError struct {
Code int
Msg string
}
func (e baseWebsocketError) GetCode() int {
return e.Code
}
func (e baseWebsocketError) GetMsg() string {
return e.Msg
}
// Unsupprted protocol version
type unsupportedProtocolVersionError struct {
baseWebsocketError
}
func newUnsupportedProtocolVersionError() unsupportedProtocolVersionError {
return unsupportedProtocolVersionError{
baseWebsocketError{Code: unsupportedProtocolVersion, Msg: "Unsupported protocol version"},
}
}
// The application does not exists
// See the configuration file
type applicationDoesNotExistsError struct {
baseWebsocketError
}
func newApplicationDoesNotExistsError() applicationDoesNotExistsError {
return applicationDoesNotExistsError{
baseWebsocketError{Code: applicationDoesNotExists, Msg: "Could not found an app with the given key"},
}
}
// The user did not send the protocol version
type noProtocolVersionSuppliedError struct {
baseWebsocketError
}
func newNoProtocolVersionSuppliedError() noProtocolVersionSuppliedError {
return noProtocolVersionSuppliedError{
baseWebsocketError{Code: noProtocolVersionSupplied, Msg: "No protocol version supplied"},
}
}
// When the application is disabled.
// See the configuration file
type applicationDisabledError struct {
baseWebsocketError
}
func newApplicationDisabledError() noProtocolVersionSuppliedError {
return noProtocolVersionSuppliedError{
baseWebsocketError{Code: applicationDisabled, Msg: "Application disabled"},
}
}
// When the application only accepts SSL connections
type applicationOnlyAccepsSSLError struct {
baseWebsocketError
}
func newApplicationOnlyAccepsSSLError() applicationOnlyAccepsSSLError {
return applicationOnlyAccepsSSLError{
baseWebsocketError{Code: applicationOnlyAcceptsSSL, Msg: "Application only accepts SSL connections, reconnect using wss://"},
}
}
// When the user send an invalid version
type invalidVersionStringFormatError struct {
baseWebsocketError
}
func newInvalidVersionStringFormatError() invalidVersionStringFormatError {
return invalidVersionStringFormatError{
baseWebsocketError{Code: invalidVersionStringFormat, Msg: "Invalid version string format"},
}
}
// Used when the error was internal
// * Decoding json
// * Writing to output
type genericReconnectImmediatelyError struct {
baseWebsocketError
}
func newGenericReconnectImmediatelyError() genericReconnectImmediatelyError {
return genericReconnectImmediatelyError{
baseWebsocketError{Code: genericReconnectImmediately, Msg: "Generic reconnect immediately"},
}
}
// When pusher wants to send an Generic error, it only send the message, the code become nil
// Currently I do not know how to send nil, so I send GENERIC_ERROR
type genericError struct {
baseWebsocketError
}
func newGenericError(msg string) genericError {
return genericError{
baseWebsocketError{Code: otherError, Msg: msg},
}
}
-278
View File
@@ -1,278 +0,0 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import (
"encoding/json"
log "github.com/golang/glog"
)
// {
// "event": "pusher:subscribe",
// "data": {
// "channel": "the channel",
// "auth": "the auth",
// "channelData": "extra data"
// }
// }
type subscribeEventData struct {
Channel string `json:"channel"`
Auth string `json:"auth,omitempty"`
ChannelData string `json:"channel_data,omitempty"`
}
type subscribeEvent struct {
Event string `json:"event"`
Data subscribeEventData `json:"data"`
}
// Create a new subscribe event with the specified channel and data
func newSubscribeEvent(channel, auth, channelData string) subscribeEvent {
data := subscribeEventData{Channel: channel, Auth: auth, ChannelData: channelData}
return subscribeEvent{Event: "pusher:subscribe", Data: data}
}
type unsubscribeEventData struct {
Channel string `json:"channel"`
}
// {
// "event": "pusher:unsubscribe",
// "data": {
// "channel": "The channel"
// }
// }
type unsubscribeEvent struct {
Event string `json:"event"`
Data unsubscribeEventData `json:"data"`
}
// Create a new unsubscribe event for the specified channel
func newUnsubscribeEvent(channel string) unsubscribeEvent {
data := unsubscribeEventData{Channel: channel}
return unsubscribeEvent{Event: "pusher:unsubscribe", Data: data}
}
// {
// "event": "pusher_internal:subscription_succeeded",
// "channel": "the channel"
// }
type subscriptionSucceededEvent struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data string `json:"data"`
}
// Create a new subscription succeed event for the specified channel
func newSubscriptionSucceededEvent(channel, data string) subscriptionSucceededEvent {
return subscriptionSucceededEvent{Event: "pusher_internal:subscription_succeeded", Channel: channel, Data: data}
}
// Data Subscription Succeed
// "{
// \"presence\": {
// \"ids\": [\"11814b369700141b222a3f3791cec2d9\",\"71dd6a29da2a4833336d2a964becf820\"],
// \"hash\": {
// \"11814b369700141b222a3f3791cec2d9\": {
// \"name\":\"Phil Leggetter\",
// \"twitter\": \"@leggetter\"
// },
// \"71dd6a29da2a4833336d2a964becf820\": {
// \"name\":\"Max Williams\",
// \"twitter\": \"@maxthelion\"
// }
// },
// \"count\": 2
// }
// }"
type subscriptionSucceeedEventPresenceData struct {
Ids []string `json:"ids"`
Hash map[string]interface{} `json:"hash"`
Count int `json:"count"`
}
func newSubscriptionSucceedEventPresenceData(c *channel) subscriptionSucceeedEventPresenceData {
event := subscriptionSucceeedEventPresenceData{}
var ids []string
hash := make(map[string]interface{}, c.TotalSubscriptions())
for _, s := range c.Subscriptions {
// Do you have any other idea?
var js interface{}
json.Unmarshal([]byte(s.Data), &js)
hash[s.ID] = js
ids = append(ids, s.ID)
}
event.Ids = ids
event.Hash = hash
event.Count = c.TotalSubscriptions()
return event
}
// {
// "event": "pusher:pong",
// "data": {}
// }
type pongEvent struct {
Event string `json:"event"`
Data string `json:"data"`
}
// Create a new pong event
func newPongEvent() pongEvent {
return pongEvent{Event: "pusher:pong", Data: "{}"}
}
// {
// "event": "pusher:ping",
// "data": {}
// }
type pingEvent struct {
Event string `json:"event"`
Data string `json:"data"`
}
// Create a new ping event
func newPingEvent() pingEvent {
return pingEvent{Event: "pusher:ping", Data: "{}"}
}
// {
// "event": "pusher:error",
// "data": {
// "message": "A Message",
// "code": 4000
// }
// }
type errorEvent struct {
Event string `json:"event"`
Data interface{} `json:"data"`
}
// Create a new error event
// Pusher protocol is very strange in some parts
// It send null in some errors.
// So I created this GENERIC_ERROR thing, just to verify if the json must have null on the error code
func newErrorEvent(code int, message string) errorEvent {
var data interface{}
if code == otherError {
data = struct {
Code *int `json:"code"`
Message string `json:"message"`
}{
nil,
message,
}
} else {
data = struct {
Code int `json:"code"`
Message string `json:"message"`
}{
code,
message,
}
}
return errorEvent{Event: "pusher:error", Data: data}
}
// {
// "event" : "pusher:connection_established",
// "data" : {
// "socket_id" : "123456",
// "activity_timeout" : 120
// }
// }
type connectionEstablishedEventData struct {
SocketID string `json:"socket_id"`
ActivityTimeout int `json:"activity_timeout"`
}
type connectionEstablishedEvent struct {
Event string `json:"event"`
Data string `json:"data"`
}
// Create a new connection established event using the specified socketId
func newConnectionEstablishedEvent(socketID string) connectionEstablishedEvent {
data := connectionEstablishedEventData{SocketID: socketID, ActivityTimeout: 120}
b, err := json.Marshal(data)
if err != nil {
panic("events: Could not Marshal json ConnectionEstablishedEvent")
}
return connectionEstablishedEvent{Event: "pusher:connection_established", Data: string(b)}
}
// {
// "event": "pusher_internal:member_added",
// "channel": "presence-example-channel",
// "data": String
// }
type memberAddedEvent struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data string `json:"data"`
}
func newMemberAddedEvent(channel, data string) memberAddedEvent {
return memberAddedEvent{Event: "pusher_internal:member_added", Channel: channel, Data: data}
}
// {
// "event": "pusher_internal:member_removed",
// "channel": "presence-example-channel",
// "data": String
// }
type memberRemovedEvent struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data string `json:"data"`
}
func newMemberRemovedEvent(channel string, s *subscription) memberRemovedEvent {
data, err := json.Marshal(struct {
UserID string `json:"user_id"`
}{
UserID: s.ID,
})
if err != nil {
log.Error(err)
}
return memberRemovedEvent{Event: "pusher_internal:member_removed", Channel: channel, Data: string(data)}
}
// {
// "event": "client-?",
// "channel": "The channel",
// "data": {}
// }
type rawEvent struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data json.RawMessage `json:"data"`
}
type responseEvent struct {
Event string `json:"event"`
Channel string `json:"channel"`
Data interface{} `json:"data"`
}
// The response event that is broadcasted to the client sockets
func newResponseEvent(name, channel string, data interface{}) responseEvent {
return responseEvent{Event: name, Channel: channel, Data: data}
}
-67
View File
@@ -1,67 +0,0 @@
// Copyright 2015 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import (
"encoding/json"
"math/rand"
"net/http"
"os"
"time"
log "github.com/golang/glog"
)
// Start Parse the configuration file and starts the ipe server
// It Panic if could not start the HTTP or HTTPS server
func Start(filename string) {
var conf configFile
rand.Seed(time.Now().Unix())
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
// Reading config
if err := json.NewDecoder(file).Decode(&conf); err != nil {
log.Fatal(err)
}
// Using a in memory database
db := newMemdb()
// Adding applications
for _, a := range conf.Apps {
db.AddApp(newAppFromConfig(a))
}
// Creating the global application context
ctx := &applicationContext{DB: db}
// The router
router := newRouter(ctx)
router.POST("/apps/{app_id}/events", commonHandlers(ctx, postEvents))
router.GET("/apps/{app_id}/channels", commonHandlers(ctx, getChannels))
router.GET("/apps/{app_id}/channels/{channel_name}", commonHandlers(ctx, getChannel))
router.GET("/apps/{app_id}/channels/{channel_name}/users", commonHandlers(ctx, getChannelUsers))
router.GET("/app/{key}", contextHandlerFunc(wsHandler))
if conf.SSL {
go func() {
log.Infof("Starting HTTPS service on %s ...", conf.SSLHost)
log.Fatal(http.ListenAndServeTLS(conf.SSLHost, conf.SSLCertFile, conf.SSLKeyFile, router))
}()
}
log.Infof("Starting HTTP service on %s ...", conf.Host)
log.Fatal(http.ListenAndServe(conf.Host, router))
}
-42
View File
@@ -1,42 +0,0 @@
// Copyright 2014, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import (
"net/http"
"github.com/gorilla/mux"
)
type router struct {
ctx *applicationContext
mux *mux.Router
routes map[string]contextHandler
}
func newRouter(ctx *applicationContext) *router {
return &router{
ctx: ctx,
mux: mux.NewRouter().StrictSlash(true),
}
}
func (a *router) GET(path string, handler contextHandler) {
a.Handle("GET", path, handler)
}
func (a *router) POST(path string, handler contextHandler) {
a.Handle("POST", path, handler)
}
func (a *router) Handle(method, path string, handler contextHandler) {
a.mux.Methods(method).Path(path).HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handler.ServeWithContext(a.ctx, params(mux.Vars(r)), w, r)
})
}
func (a router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
a.mux.ServeHTTP(w, r)
}
-17
View File
@@ -1,17 +0,0 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
// A Channel Subscription
type subscription struct {
Connection *connection
ID string
Data string
}
// Create a new Subscription
func newSubscription(conn *connection, data string) *subscription {
return &subscription{Connection: conn, Data: data}
}
-250
View File
@@ -1,250 +0,0 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ipe
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
log "github.com/golang/glog"
"github.com/gorilla/websocket"
"github.com/dimiro1/ipe/utils"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
// Handle open Subscriber.
func onOpen(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, sessionID string, app *app) websocketError {
params := r.URL.Query()
p := params.Get("protocol")
protocol, err := strconv.Atoi(p)
if err != nil {
return newInvalidVersionStringFormatError()
}
switch {
case strings.TrimSpace(p) == "":
return newNoProtocolVersionSuppliedError()
case protocol != supportedProtocolVersion:
return newUnsupportedProtocolVersionError()
case app.ApplicationDisabled:
return newApplicationDisabledError()
case app.OnlySSL:
if r.TLS == nil {
return newApplicationOnlyAccepsSSLError()
}
}
// Create the new Subscriber
connection := newConnection(sessionID, conn)
app.Connect(connection)
// Everything went fine. Huhu.
if err := conn.WriteJSON(newConnectionEstablishedEvent(connection.SocketID)); err != nil {
return newGenericReconnectImmediatelyError()
}
return nil
}
// Handle the close event
func onClose(sessionID string, app *app) {
app.Disconnect(sessionID)
}
// Handle messages
//
// If there is an unrecoverable error then break the loop,
// otherwise just keep going.
func onMessage(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, sessionID string, app *app) {
var event struct {
Event string `json:"event"`
}
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Errorf("%+v", err)
switch err {
case io.EOF:
onClose(sessionID, app)
default:
emitWSError(newGenericReconnectImmediatelyError(), conn)
}
break
}
if err := json.Unmarshal(message, &event); err != nil {
emitWSError(newGenericReconnectImmediatelyError(), conn)
break
}
log.Infof("websockets: Handling %s event", event.Event)
switch event.Event {
case "pusher:ping":
if err := conn.WriteJSON(newPongEvent()); err != nil {
emitWSError(newGenericReconnectImmediatelyError(), conn)
}
case "pusher:subscribe":
subscribeEvent := subscribeEvent{}
if err := json.Unmarshal(message, &subscribeEvent); err != nil {
emitWSError(newGenericReconnectImmediatelyError(), conn)
break
}
connection, err := app.FindConnection(sessionID)
if err != nil {
emitWSError(newGenericReconnectImmediatelyError(), conn)
break
}
channelName := strings.TrimSpace(subscribeEvent.Data.Channel)
if !utils.IsChannelNameValid(channelName) {
emitWSError(newGenericError(fmt.Sprintf("This channel name is not valid")), conn)
break
}
isPresence := utils.IsPresenceChannel(channelName)
isPrivate := utils.IsPrivateChannel(channelName)
if isPresence || isPrivate {
toSign := []string{connection.SocketID, channelName}
if isPresence || len(subscribeEvent.Data.ChannelData) > 0 {
toSign = append(toSign, subscribeEvent.Data.ChannelData)
}
expectedAuthKey := fmt.Sprintf("%s:%s", app.Key, utils.HashMAC([]byte(strings.Join(toSign, ":")), []byte(app.Secret)))
if subscribeEvent.Data.Auth != expectedAuthKey {
emitWSError(newGenericError(fmt.Sprintf("Auth value for subscription to %s is invalid", channelName)), conn)
continue
}
}
channel := app.FindOrCreateChannelByChannelID(channelName)
log.Info(subscribeEvent.Data.ChannelData)
if err := app.Subscribe(channel, connection, subscribeEvent.Data.ChannelData); err != nil {
emitWSError(newGenericReconnectImmediatelyError(), conn)
}
case "pusher:unsubscribe":
unsubscribeEvent := unsubscribeEvent{}
if err := json.Unmarshal(message, &unsubscribeEvent); err != nil {
emitWSError(newGenericReconnectImmediatelyError(), conn)
}
connection, err := app.FindConnection(sessionID)
if err != nil {
emitWSError(newGenericError(fmt.Sprintf("Could not find a connection with the id %s", sessionID)), conn)
}
channel, err := app.FindChannelByChannelID(unsubscribeEvent.Data.Channel)
if err != nil {
emitWSError(newGenericError(fmt.Sprintf("Could not find a channel with the id %s", unsubscribeEvent.Data.Channel)), conn)
}
if err := app.Unsubscribe(channel, connection); err != nil {
emitWSError(newGenericReconnectImmediatelyError(), conn)
break
}
default: // CLient Events ??
// see http://pusher.com/docs/client_api_guide/client_events#trigger-events
if strings.HasPrefix(event.Event, "client-") {
if !app.UserEvents {
emitWSError(newGenericError("To send client events, you must enable this feature in the Settings."), conn)
}
clientEvent := rawEvent{}
if err := json.Unmarshal(message, &clientEvent); err != nil {
log.Error(err)
emitWSError(newGenericReconnectImmediatelyError(), conn)
break
}
channel, err := app.FindChannelByChannelID(clientEvent.Channel)
if err != nil {
emitWSError(newGenericError(fmt.Sprintf("Could not find a channel with the id %s", clientEvent.Channel)), conn)
}
if !channel.IsPresenceOrPrivate() {
emitWSError(newGenericError("Client event rejected - only supported on private and presence channels"), conn)
break
}
if err := app.Publish(channel, clientEvent, sessionID); err != nil {
log.Error(err)
emitWSError(newGenericReconnectImmediatelyError(), conn)
break
}
}
} // switch
} // For
}
// Websocket GET /app/{key}
func wsHandler(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
defer func() {
if conn != nil {
conn.Close()
}
}()
if err != nil {
log.Error(err)
return
}
appKey := p.Get("key")
app, err := ctx.DB.GetAppByKey(appKey)
if err != nil {
log.Error(err)
emitWSError(newApplicationDoesNotExistsError(), conn)
return
}
sessionID := utils.GenerateSessionID()
if err := onOpen(conn, w, r, sessionID, app); err != nil {
emitWSError(err, conn)
return
}
onMessage(conn, w, r, sessionID, app)
}
// Emit an Websocket ErrorEvent
func emitWSError(err websocketError, conn *websocket.Conn) {
event := newErrorEvent(err.GetCode(), err.GetMsg())
if err := conn.WriteJSON(event); err != nil {
log.Error(err)
}
}
+11
View File
@@ -0,0 +1,11 @@
package mocks
// MockSocket is a mock implementation of Socket
// used in the test suite
type MockSocket struct{}
// WriteJSON always returns nil
// used in the test suite
func (s MockSocket) WriteJSON(i interface{}) error {
return nil
}
+67
View File
@@ -0,0 +1,67 @@
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package storage
import (
"errors"
"ipe/app"
"sync"
)
// Storage represents a app database
// For now it there is only one memory database implementation
// but in the future I can write a sql implementation
type Storage interface {
GetAppByAppID(appID string) (*app.Application, error)
GetAppByKey(key string) (*app.Application, error)
AddApp(application *app.Application) error
}
// InMemory in memory implementation of Storage
type InMemory struct {
sync.RWMutex
Apps []*app.Application
}
// NewInMemory returns an InMemory storage
func NewInMemory() Storage {
return &InMemory{}
}
// AddApp adds app into memory
func (db *InMemory) AddApp(application *app.Application) error {
db.Lock()
defer db.Unlock()
db.Apps = append(db.Apps, application)
return nil
}
// GetAppByAppID returns an App with by appID
func (db *InMemory) GetAppByAppID(appID string) (*app.Application, error) {
db.RLock()
defer db.RUnlock()
for _, a := range db.Apps {
if a.AppID == appID {
return a, nil
}
}
return nil, errors.New("app not found")
}
// GetAppByKey returns an App with by key
func (db *InMemory) GetAppByKey(key string) (*app.Application, error) {
db.RLock()
defer db.RUnlock()
for _, a := range db.Apps {
if a.Key == key {
return a, nil
}
}
return nil, errors.New("app not found")
}
+75
View File
@@ -0,0 +1,75 @@
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package storage
import (
"ipe/app"
"testing"
)
func Benchmark_memdb_GetAppByAppID(b *testing.B) {
storage := NewInMemory()
_ = storage.AddApp(&app.Application{AppID: "123456", Name: "Example"})
_ = storage.AddApp(&app.Application{AppID: "654321", Name: "Example2"})
_ = storage.AddApp(&app.Application{AppID: "678901", Name: "Example3"})
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = storage.GetAppByAppID("123456")
}
}
func Test_db_GetAppByAppID(t *testing.T) {
_app := &app.Application{AppID: "123456", Name: "Example"}
storage := NewInMemory()
_ = storage.AddApp(_app)
a, err := storage.GetAppByAppID("123456")
if err != nil {
t.Errorf("GetAppByAppID(%q) == %+v, want %+v", "123456", a, _app)
}
}
func Test_db_GetAppByAppID__error(t *testing.T) {
_app := &app.Application{AppID: "123456", Name: "Example"}
storage := NewInMemory()
_ = storage.AddApp(_app)
a, err := storage.GetAppByAppID("not-found")
if err == nil {
t.Errorf("GetAppByAppID(%q) == %+v, want %+v", "123456", a, _app)
}
}
func Test_db_GetAppByKey(t *testing.T) {
_app := &app.Application{AppID: "123456", Name: "Example", Key: "654321"}
storage := NewInMemory()
_ = storage.AddApp(_app)
a, err := storage.GetAppByKey("654321")
if err != nil {
t.Errorf("GetAppByKey(%q) == %+v, want %+v", "654321", a, _app)
}
}
func Test_db_GetAppByKey__error(t *testing.T) {
_app := &app.Application{AppID: "123456", Name: "Example", Key: "654321"}
storage := NewInMemory()
_ = storage.AddApp(_app)
a, err := storage.GetAppByKey("not-found")
if err == nil {
t.Errorf("GetAppByKey(%q) == %+v, want %+v", "not-found", a, nil)
}
}
+19
View File
@@ -0,0 +1,19 @@
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package subscription
import "ipe/connection"
// Subscription A Channel Subscription
type Subscription struct {
Connection *connection.Connection
ID string
Data string
}
// New Create a new Subscription
func New(conn *connection.Connection, data string) *Subscription {
return &Subscription{Connection: conn, Data: data}
}
+10 -7
View File
@@ -15,11 +15,7 @@ import (
"strings"
)
var validChannelName *regexp.Regexp
func init() {
validChannelName = regexp.MustCompile("^[A-Za-z0-9_\\-=@,.;]+$")
}
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 {
@@ -32,18 +28,25 @@ func HashMAC(message, key []byte) string {
// GenerateSessionID Generate a new random Hash
func GenerateSessionID() string {
return fmt.Sprintf("%d.%d", rand.Intn(math.MaxInt64), rand.Intn(math.MaxInt64))
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 validChannelName.Match([]byte(channelName))
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-")
}
+18
View File
@@ -95,6 +95,24 @@ func TestIsPresenceChannel_invalid(t *testing.T) {
}
}
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")
-191
View File
@@ -1,191 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
-44
View File
@@ -1,44 +0,0 @@
glog
====
Leveled execution logs for Go.
This is an efficient pure Go implementation of leveled logs in the
manner of the open source C++ package
https://github.com/google/glog
By binding methods to booleans it is possible to use the log package
without paying the expense of evaluating the arguments to the log.
Through the -vmodule flag, the package also provides fine-grained
control over logging at the file level.
The comment from glog.go introduces the ideas:
Package glog implements logging analogous to the Google-internal
C++ INFO/ERROR/V setup. It provides functions Info, Warning,
Error, Fatal, plus formatting variants such as Infof. It
also provides V-style logging controlled by the -v and
-vmodule=file=2 flags.
Basic examples:
glog.Info("Prepare to repel boarders")
glog.Fatalf("Initialization failed: %s", err)
See the documentation for the V function for an explanation
of these examples:
if glog.V(2) {
glog.Info("Starting transaction...")
}
glog.V(2).Infoln("Processed", nItems, "elements")
The repository contains an open source version of the log package
used inside Google. The master copy of the source lives inside
Google, not here. The code in this repo is for export only and is not itself
under development. Feature requests will be ignored.
Send bug reports to [email protected].
-1180
View File
File diff suppressed because it is too large Load Diff
-124
View File
@@ -1,124 +0,0 @@
// Go support for leveled logs, analogous to https://code.google.com/p/google-glog/
//
// Copyright 2013 Google Inc. All Rights Reserved.
//
// 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.
// File I/O for logs.
package glog
import (
"errors"
"flag"
"fmt"
"os"
"os/user"
"path/filepath"
"strings"
"sync"
"time"
)
// MaxSize is the maximum size of a log file in bytes.
var MaxSize uint64 = 1024 * 1024 * 1800
// logDirs lists the candidate directories for new log files.
var logDirs []string
// If non-empty, overrides the choice of directory in which to write logs.
// See createLogDirs for the full list of possible destinations.
var logDir = flag.String("log_dir", "", "If non-empty, write log files in this directory")
func createLogDirs() {
if *logDir != "" {
logDirs = append(logDirs, *logDir)
}
logDirs = append(logDirs, os.TempDir())
}
var (
pid = os.Getpid()
program = filepath.Base(os.Args[0])
host = "unknownhost"
userName = "unknownuser"
)
func init() {
h, err := os.Hostname()
if err == nil {
host = shortHostname(h)
}
current, err := user.Current()
if err == nil {
userName = current.Username
}
// Sanitize userName since it may contain filepath separators on Windows.
userName = strings.Replace(userName, `\`, "_", -1)
}
// shortHostname returns its argument, truncating at the first period.
// For instance, given "www.google.com" it returns "www".
func shortHostname(hostname string) string {
if i := strings.Index(hostname, "."); i >= 0 {
return hostname[:i]
}
return hostname
}
// logName returns a new log file name containing tag, with start time t, and
// the name for the symlink for tag.
func logName(tag string, t time.Time) (name, link string) {
name = fmt.Sprintf("%s.%s.%s.log.%s.%04d%02d%02d-%02d%02d%02d.%d",
program,
host,
userName,
tag,
t.Year(),
t.Month(),
t.Day(),
t.Hour(),
t.Minute(),
t.Second(),
pid)
return name, program + "." + tag
}
var onceLogDirs sync.Once
// create creates a new log file and returns the file and its filename, which
// contains tag ("INFO", "FATAL", etc.) and t. If the file is created
// successfully, create also attempts to update the symlink for that tag, ignoring
// errors.
func create(tag string, t time.Time) (f *os.File, filename string, err error) {
onceLogDirs.Do(createLogDirs)
if len(logDirs) == 0 {
return nil, "", errors.New("log: no log dirs")
}
name, link := logName(tag, t)
var lastErr error
for _, dir := range logDirs {
fname := filepath.Join(dir, name)
f, err := os.Create(fname)
if err == nil {
symlink := filepath.Join(dir, link)
os.Remove(symlink) // ignore err
os.Symlink(name, symlink) // ignore err
return f, fname, nil
}
lastErr = err
}
return nil, "", fmt.Errorf("log: cannot create log: %v", lastErr)
}
-415
View File
@@ -1,415 +0,0 @@
// Go support for leveled logs, analogous to https://code.google.com/p/google-glog/
//
// Copyright 2013 Google Inc. All Rights Reserved.
//
// 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.
package glog
import (
"bytes"
"fmt"
stdLog "log"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
"time"
)
// Test that shortHostname works as advertised.
func TestShortHostname(t *testing.T) {
for hostname, expect := range map[string]string{
"": "",
"host": "host",
"host.google.com": "host",
} {
if got := shortHostname(hostname); expect != got {
t.Errorf("shortHostname(%q): expected %q, got %q", hostname, expect, got)
}
}
}
// flushBuffer wraps a bytes.Buffer to satisfy flushSyncWriter.
type flushBuffer struct {
bytes.Buffer
}
func (f *flushBuffer) Flush() error {
return nil
}
func (f *flushBuffer) Sync() error {
return nil
}
// swap sets the log writers and returns the old array.
func (l *loggingT) swap(writers [numSeverity]flushSyncWriter) (old [numSeverity]flushSyncWriter) {
l.mu.Lock()
defer l.mu.Unlock()
old = l.file
for i, w := range writers {
logging.file[i] = w
}
return
}
// newBuffers sets the log writers to all new byte buffers and returns the old array.
func (l *loggingT) newBuffers() [numSeverity]flushSyncWriter {
return l.swap([numSeverity]flushSyncWriter{new(flushBuffer), new(flushBuffer), new(flushBuffer), new(flushBuffer)})
}
// contents returns the specified log value as a string.
func contents(s severity) string {
return logging.file[s].(*flushBuffer).String()
}
// contains reports whether the string is contained in the log.
func contains(s severity, str string, t *testing.T) bool {
return strings.Contains(contents(s), str)
}
// setFlags configures the logging flags how the test expects them.
func setFlags() {
logging.toStderr = false
}
// Test that Info works as advertised.
func TestInfo(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
Info("test")
if !contains(infoLog, "I", t) {
t.Errorf("Info has wrong character: %q", contents(infoLog))
}
if !contains(infoLog, "test", t) {
t.Error("Info failed")
}
}
func TestInfoDepth(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
f := func() { InfoDepth(1, "depth-test1") }
// The next three lines must stay together
_, _, wantLine, _ := runtime.Caller(0)
InfoDepth(0, "depth-test0")
f()
msgs := strings.Split(strings.TrimSuffix(contents(infoLog), "\n"), "\n")
if len(msgs) != 2 {
t.Fatalf("Got %d lines, expected 2", len(msgs))
}
for i, m := range msgs {
if !strings.HasPrefix(m, "I") {
t.Errorf("InfoDepth[%d] has wrong character: %q", i, m)
}
w := fmt.Sprintf("depth-test%d", i)
if !strings.Contains(m, w) {
t.Errorf("InfoDepth[%d] missing %q: %q", i, w, m)
}
// pull out the line number (between : and ])
msg := m[strings.LastIndex(m, ":")+1:]
x := strings.Index(msg, "]")
if x < 0 {
t.Errorf("InfoDepth[%d]: missing ']': %q", i, m)
continue
}
line, err := strconv.Atoi(msg[:x])
if err != nil {
t.Errorf("InfoDepth[%d]: bad line number: %q", i, m)
continue
}
wantLine++
if wantLine != line {
t.Errorf("InfoDepth[%d]: got line %d, want %d", i, line, wantLine)
}
}
}
func init() {
CopyStandardLogTo("INFO")
}
// Test that CopyStandardLogTo panics on bad input.
func TestCopyStandardLogToPanic(t *testing.T) {
defer func() {
if s, ok := recover().(string); !ok || !strings.Contains(s, "LOG") {
t.Errorf(`CopyStandardLogTo("LOG") should have panicked: %v`, s)
}
}()
CopyStandardLogTo("LOG")
}
// Test that using the standard log package logs to INFO.
func TestStandardLog(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
stdLog.Print("test")
if !contains(infoLog, "I", t) {
t.Errorf("Info has wrong character: %q", contents(infoLog))
}
if !contains(infoLog, "test", t) {
t.Error("Info failed")
}
}
// Test that the header has the correct format.
func TestHeader(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
defer func(previous func() time.Time) { timeNow = previous }(timeNow)
timeNow = func() time.Time {
return time.Date(2006, 1, 2, 15, 4, 5, .067890e9, time.Local)
}
pid = 1234
Info("test")
var line int
format := "I0102 15:04:05.067890 1234 glog_test.go:%d] test\n"
n, err := fmt.Sscanf(contents(infoLog), format, &line)
if n != 1 || err != nil {
t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(infoLog))
}
// Scanf treats multiple spaces as equivalent to a single space,
// so check for correct space-padding also.
want := fmt.Sprintf(format, line)
if contents(infoLog) != want {
t.Errorf("log format error: got:\n\t%q\nwant:\t%q", contents(infoLog), want)
}
}
// Test that an Error log goes to Warning and Info.
// Even in the Info log, the source character will be E, so the data should
// all be identical.
func TestError(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
Error("test")
if !contains(errorLog, "E", t) {
t.Errorf("Error has wrong character: %q", contents(errorLog))
}
if !contains(errorLog, "test", t) {
t.Error("Error failed")
}
str := contents(errorLog)
if !contains(warningLog, str, t) {
t.Error("Warning failed")
}
if !contains(infoLog, str, t) {
t.Error("Info failed")
}
}
// Test that a Warning log goes to Info.
// Even in the Info log, the source character will be W, so the data should
// all be identical.
func TestWarning(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
Warning("test")
if !contains(warningLog, "W", t) {
t.Errorf("Warning has wrong character: %q", contents(warningLog))
}
if !contains(warningLog, "test", t) {
t.Error("Warning failed")
}
str := contents(warningLog)
if !contains(infoLog, str, t) {
t.Error("Info failed")
}
}
// Test that a V log goes to Info.
func TestV(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
logging.verbosity.Set("2")
defer logging.verbosity.Set("0")
V(2).Info("test")
if !contains(infoLog, "I", t) {
t.Errorf("Info has wrong character: %q", contents(infoLog))
}
if !contains(infoLog, "test", t) {
t.Error("Info failed")
}
}
// Test that a vmodule enables a log in this file.
func TestVmoduleOn(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
logging.vmodule.Set("glog_test=2")
defer logging.vmodule.Set("")
if !V(1) {
t.Error("V not enabled for 1")
}
if !V(2) {
t.Error("V not enabled for 2")
}
if V(3) {
t.Error("V enabled for 3")
}
V(2).Info("test")
if !contains(infoLog, "I", t) {
t.Errorf("Info has wrong character: %q", contents(infoLog))
}
if !contains(infoLog, "test", t) {
t.Error("Info failed")
}
}
// Test that a vmodule of another file does not enable a log in this file.
func TestVmoduleOff(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
logging.vmodule.Set("notthisfile=2")
defer logging.vmodule.Set("")
for i := 1; i <= 3; i++ {
if V(Level(i)) {
t.Errorf("V enabled for %d", i)
}
}
V(2).Info("test")
if contents(infoLog) != "" {
t.Error("V logged incorrectly")
}
}
// vGlobs are patterns that match/don't match this file at V=2.
var vGlobs = map[string]bool{
// Easy to test the numeric match here.
"glog_test=1": false, // If -vmodule sets V to 1, V(2) will fail.
"glog_test=2": true,
"glog_test=3": true, // If -vmodule sets V to 1, V(3) will succeed.
// These all use 2 and check the patterns. All are true.
"*=2": true,
"?l*=2": true,
"????_*=2": true,
"??[mno]?_*t=2": true,
// These all use 2 and check the patterns. All are false.
"*x=2": false,
"m*=2": false,
"??_*=2": false,
"?[abc]?_*t=2": false,
}
// Test that vmodule globbing works as advertised.
func testVmoduleGlob(pat string, match bool, t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
defer logging.vmodule.Set("")
logging.vmodule.Set(pat)
if V(2) != Verbose(match) {
t.Errorf("incorrect match for %q: got %t expected %t", pat, V(2), match)
}
}
// Test that a vmodule globbing works as advertised.
func TestVmoduleGlob(t *testing.T) {
for glob, match := range vGlobs {
testVmoduleGlob(glob, match, t)
}
}
func TestRollover(t *testing.T) {
setFlags()
var err error
defer func(previous func(error)) { logExitFunc = previous }(logExitFunc)
logExitFunc = func(e error) {
err = e
}
defer func(previous uint64) { MaxSize = previous }(MaxSize)
MaxSize = 512
Info("x") // Be sure we have a file.
info, ok := logging.file[infoLog].(*syncBuffer)
if !ok {
t.Fatal("info wasn't created")
}
if err != nil {
t.Fatalf("info has initial error: %v", err)
}
fname0 := info.file.Name()
Info(strings.Repeat("x", int(MaxSize))) // force a rollover
if err != nil {
t.Fatalf("info has error after big write: %v", err)
}
// Make sure the next log file gets a file name with a different
// time stamp.
//
// TODO: determine whether we need to support subsecond log
// rotation. C++ does not appear to handle this case (nor does it
// handle Daylight Savings Time properly).
time.Sleep(1 * time.Second)
Info("x") // create a new file
if err != nil {
t.Fatalf("error after rotation: %v", err)
}
fname1 := info.file.Name()
if fname0 == fname1 {
t.Errorf("info.f.Name did not change: %v", fname0)
}
if info.nbytes >= MaxSize {
t.Errorf("file size was not reset: %d", info.nbytes)
}
}
func TestLogBacktraceAt(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
// The peculiar style of this code simplifies line counting and maintenance of the
// tracing block below.
var infoLine string
setTraceLocation := func(file string, line int, ok bool, delta int) {
if !ok {
t.Fatal("could not get file:line")
}
_, file = filepath.Split(file)
infoLine = fmt.Sprintf("%s:%d", file, line+delta)
err := logging.traceLocation.Set(infoLine)
if err != nil {
t.Fatal("error setting log_backtrace_at: ", err)
}
}
{
// Start of tracing block. These lines know about each other's relative position.
_, file, line, ok := runtime.Caller(0)
setTraceLocation(file, line, ok, +2) // Two lines between Caller and Info calls.
Info("we want a stack trace here")
}
numAppearances := strings.Count(contents(infoLog), infoLine)
if numAppearances < 2 {
// Need 2 appearances, one in the log header and one in the trace:
// log_test.go:281: I0511 16:36:06.952398 02238 log_test.go:280] we want a stack trace here
// ...
// github.com/glog/glog_test.go:280 (0x41ba91)
// ...
// We could be more precise but that would require knowing the details
// of the traceback format, which may not be dependable.
t.Fatal("got no trace back; log is ", contents(infoLog))
}
}
func BenchmarkHeader(b *testing.B) {
for i := 0; i < b.N; i++ {
buf, _, _ := logging.header(infoLog, 0)
logging.putBuffer(buf)
}
}
-8
View File
@@ -1,8 +0,0 @@
language: go
sudo: false
go:
- 1.3
- 1.4
- 1.5
- tip
-27
View File
@@ -1,27 +0,0 @@
Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-7
View File
@@ -1,7 +0,0 @@
context
=======
[![Build Status](https://travis-ci.org/gorilla/context.png?branch=master)](https://travis-ci.org/gorilla/context)
gorilla/context is a general purpose registry for global request variables.
Read the full documentation here: http://www.gorillatoolkit.org/pkg/context
-143
View File
@@ -1,143 +0,0 @@
// Copyright 2012 The Gorilla Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package context
import (
"net/http"
"sync"
"time"
)
var (
mutex sync.RWMutex
data = make(map[*http.Request]map[interface{}]interface{})
datat = make(map[*http.Request]int64)
)
// Set stores a value for a given key in a given request.
func Set(r *http.Request, key, val interface{}) {
mutex.Lock()
if data[r] == nil {
data[r] = make(map[interface{}]interface{})
datat[r] = time.Now().Unix()
}
data[r][key] = val
mutex.Unlock()
}
// Get returns a value stored for a given key in a given request.
func Get(r *http.Request, key interface{}) interface{} {
mutex.RLock()
if ctx := data[r]; ctx != nil {
value := ctx[key]
mutex.RUnlock()
return value
}
mutex.RUnlock()
return nil
}
// GetOk returns stored value and presence state like multi-value return of map access.
func GetOk(r *http.Request, key interface{}) (interface{}, bool) {
mutex.RLock()
if _, ok := data[r]; ok {
value, ok := data[r][key]
mutex.RUnlock()
return value, ok
}
mutex.RUnlock()
return nil, false
}
// GetAll returns all stored values for the request as a map. Nil is returned for invalid requests.
func GetAll(r *http.Request) map[interface{}]interface{} {
mutex.RLock()
if context, ok := data[r]; ok {
result := make(map[interface{}]interface{}, len(context))
for k, v := range context {
result[k] = v
}
mutex.RUnlock()
return result
}
mutex.RUnlock()
return nil
}
// GetAllOk returns all stored values for the request as a map and a boolean value that indicates if
// the request was registered.
func GetAllOk(r *http.Request) (map[interface{}]interface{}, bool) {
mutex.RLock()
context, ok := data[r]
result := make(map[interface{}]interface{}, len(context))
for k, v := range context {
result[k] = v
}
mutex.RUnlock()
return result, ok
}
// Delete removes a value stored for a given key in a given request.
func Delete(r *http.Request, key interface{}) {
mutex.Lock()
if data[r] != nil {
delete(data[r], key)
}
mutex.Unlock()
}
// Clear removes all values stored for a given request.
//
// This is usually called by a handler wrapper to clean up request
// variables at the end of a request lifetime. See ClearHandler().
func Clear(r *http.Request) {
mutex.Lock()
clear(r)
mutex.Unlock()
}
// clear is Clear without the lock.
func clear(r *http.Request) {
delete(data, r)
delete(datat, r)
}
// Purge removes request data stored for longer than maxAge, in seconds.
// It returns the amount of requests removed.
//
// If maxAge <= 0, all request data is removed.
//
// This is only used for sanity check: in case context cleaning was not
// properly set some request data can be kept forever, consuming an increasing
// amount of memory. In case this is detected, Purge() must be called
// periodically until the problem is fixed.
func Purge(maxAge int) int {
mutex.Lock()
count := 0
if maxAge <= 0 {
count = len(data)
data = make(map[*http.Request]map[interface{}]interface{})
datat = make(map[*http.Request]int64)
} else {
min := time.Now().Unix() - int64(maxAge)
for r := range data {
if datat[r] < min {
clear(r)
count++
}
}
}
mutex.Unlock()
return count
}
// ClearHandler wraps an http.Handler and clears request values at the end
// of a request lifetime.
func ClearHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer Clear(r)
h.ServeHTTP(w, r)
})
}
-161
View File
@@ -1,161 +0,0 @@
// Copyright 2012 The Gorilla Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package context
import (
"net/http"
"testing"
)
type keyType int
const (
key1 keyType = iota
key2
)
func TestContext(t *testing.T) {
assertEqual := func(val interface{}, exp interface{}) {
if val != exp {
t.Errorf("Expected %v, got %v.", exp, val)
}
}
r, _ := http.NewRequest("GET", "http://localhost:8080/", nil)
emptyR, _ := http.NewRequest("GET", "http://localhost:8080/", nil)
// Get()
assertEqual(Get(r, key1), nil)
// Set()
Set(r, key1, "1")
assertEqual(Get(r, key1), "1")
assertEqual(len(data[r]), 1)
Set(r, key2, "2")
assertEqual(Get(r, key2), "2")
assertEqual(len(data[r]), 2)
//GetOk
value, ok := GetOk(r, key1)
assertEqual(value, "1")
assertEqual(ok, true)
value, ok = GetOk(r, "not exists")
assertEqual(value, nil)
assertEqual(ok, false)
Set(r, "nil value", nil)
value, ok = GetOk(r, "nil value")
assertEqual(value, nil)
assertEqual(ok, true)
// GetAll()
values := GetAll(r)
assertEqual(len(values), 3)
// GetAll() for empty request
values = GetAll(emptyR)
if values != nil {
t.Error("GetAll didn't return nil value for invalid request")
}
// GetAllOk()
values, ok = GetAllOk(r)
assertEqual(len(values), 3)
assertEqual(ok, true)
// GetAllOk() for empty request
values, ok = GetAllOk(emptyR)
assertEqual(value, nil)
assertEqual(ok, false)
// Delete()
Delete(r, key1)
assertEqual(Get(r, key1), nil)
assertEqual(len(data[r]), 2)
Delete(r, key2)
assertEqual(Get(r, key2), nil)
assertEqual(len(data[r]), 1)
// Clear()
Clear(r)
assertEqual(len(data), 0)
}
func parallelReader(r *http.Request, key string, iterations int, wait, done chan struct{}) {
<-wait
for i := 0; i < iterations; i++ {
Get(r, key)
}
done <- struct{}{}
}
func parallelWriter(r *http.Request, key, value string, iterations int, wait, done chan struct{}) {
<-wait
for i := 0; i < iterations; i++ {
Set(r, key, value)
}
done <- struct{}{}
}
func benchmarkMutex(b *testing.B, numReaders, numWriters, iterations int) {
b.StopTimer()
r, _ := http.NewRequest("GET", "http://localhost:8080/", nil)
done := make(chan struct{})
b.StartTimer()
for i := 0; i < b.N; i++ {
wait := make(chan struct{})
for i := 0; i < numReaders; i++ {
go parallelReader(r, "test", iterations, wait, done)
}
for i := 0; i < numWriters; i++ {
go parallelWriter(r, "test", "123", iterations, wait, done)
}
close(wait)
for i := 0; i < numReaders+numWriters; i++ {
<-done
}
}
}
func BenchmarkMutexSameReadWrite1(b *testing.B) {
benchmarkMutex(b, 1, 1, 32)
}
func BenchmarkMutexSameReadWrite2(b *testing.B) {
benchmarkMutex(b, 2, 2, 32)
}
func BenchmarkMutexSameReadWrite4(b *testing.B) {
benchmarkMutex(b, 4, 4, 32)
}
func BenchmarkMutex1(b *testing.B) {
benchmarkMutex(b, 2, 8, 32)
}
func BenchmarkMutex2(b *testing.B) {
benchmarkMutex(b, 16, 4, 64)
}
func BenchmarkMutex3(b *testing.B) {
benchmarkMutex(b, 1, 2, 128)
}
func BenchmarkMutex4(b *testing.B) {
benchmarkMutex(b, 128, 32, 256)
}
func BenchmarkMutex5(b *testing.B) {
benchmarkMutex(b, 1024, 2048, 64)
}
func BenchmarkMutex6(b *testing.B) {
benchmarkMutex(b, 2048, 1024, 512)
}
-82
View File
@@ -1,82 +0,0 @@
// Copyright 2012 The Gorilla Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package context stores values shared during a request lifetime.
For example, a router can set variables extracted from the URL and later
application handlers can access those values, or it can be used to store
sessions values to be saved at the end of a request. There are several
others common uses.
The idea was posted by Brad Fitzpatrick to the go-nuts mailing list:
http://groups.google.com/group/golang-nuts/msg/e2d679d303aa5d53
Here's the basic usage: first define the keys that you will need. The key
type is interface{} so a key can be of any type that supports equality.
Here we define a key using a custom int type to avoid name collisions:
package foo
import (
"github.com/gorilla/context"
)
type key int
const MyKey key = 0
Then set a variable. Variables are bound to an http.Request object, so you
need a request instance to set a value:
context.Set(r, MyKey, "bar")
The application can later access the variable using the same key you provided:
func MyHandler(w http.ResponseWriter, r *http.Request) {
// val is "bar".
val := context.Get(r, foo.MyKey)
// returns ("bar", true)
val, ok := context.GetOk(r, foo.MyKey)
// ...
}
And that's all about the basic usage. We discuss some other ideas below.
Any type can be stored in the context. To enforce a given type, make the key
private and wrap Get() and Set() to accept and return values of a specific
type:
type key int
const mykey key = 0
// GetMyKey returns a value for this package from the request values.
func GetMyKey(r *http.Request) SomeType {
if rv := context.Get(r, mykey); rv != nil {
return rv.(SomeType)
}
return nil
}
// SetMyKey sets a value for this package in the request values.
func SetMyKey(r *http.Request, val SomeType) {
context.Set(r, mykey, val)
}
Variables must be cleared at the end of a request, to remove all values
that were stored. This can be done in an http.Handler, after a request was
served. Just call Clear() passing the request:
context.Clear(r)
...or use ClearHandler(), which conveniently wraps an http.Handler to clear
variables at the end of a request lifetime.
The Routers from the packages gorilla/mux and gorilla/pat call Clear()
so if you are using either of them you don't need to clear the context manually.
*/
package context
-14
View File
@@ -1,14 +0,0 @@
language: go
sudo: false
go:
- 1.3
- 1.4
- 1.5
- tip
install:
- go get golang.org/x/tools/cmd/vet
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d -s .)
- go tool vet .
- go test -v -race ./...
-27
View File
@@ -1,27 +0,0 @@
Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-240
View File
@@ -1,240 +0,0 @@
mux
===
[![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux)
[![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux)
Package `gorilla/mux` implements a request router and dispatcher.
The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are:
* Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers.
* URL hosts and paths can have variables with an optional regular expression.
* Registered URLs can be built, or "reversed", which helps maintaining references to resources.
* Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching.
* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`.
Let's start registering a couple of URL paths and handlers:
```go
func main() {
r := mux.NewRouter()
r.HandleFunc("/", HomeHandler)
r.HandleFunc("/products", ProductsHandler)
r.HandleFunc("/articles", ArticlesHandler)
http.Handle("/", r)
}
```
Here we register three routes mapping URL paths to handlers. This is equivalent to how `http.HandleFunc()` works: if an incoming request URL matches one of the paths, the corresponding handler is called passing (`http.ResponseWriter`, `*http.Request`) as parameters.
Paths can have variables. They are defined using the format `{name}` or `{name:pattern}`. If a regular expression pattern is not defined, the matched variable will be anything until the next slash. For example:
```go
r := mux.NewRouter()
r.HandleFunc("/products/{key}", ProductHandler)
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
```
The names are used to create a map of route variables which can be retrieved calling `mux.Vars()`:
```go
vars := mux.Vars(request)
category := vars["category"]
```
And this is all you need to know about the basic usage. More advanced options are explained below.
Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables:
```go
r := mux.NewRouter()
// Only matches if domain is "www.example.com".
r.Host("www.example.com")
// Matches a dynamic subdomain.
r.Host("{subdomain:[a-z]+}.domain.com")
```
There are several other matchers that can be added. To match path prefixes:
```go
r.PathPrefix("/products/")
```
...or HTTP methods:
```go
r.Methods("GET", "POST")
```
...or URL schemes:
```go
r.Schemes("https")
```
...or header values:
```go
r.Headers("X-Requested-With", "XMLHttpRequest")
```
...or query values:
```go
r.Queries("key", "value")
```
...or to use a custom matcher function:
```go
r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool {
return r.ProtoMajor == 0
})
```
...and finally, it is possible to combine several matchers in a single route:
```go
r.HandleFunc("/products", ProductsHandler).
Host("www.example.com").
Methods("GET").
Schemes("http")
```
Setting the same matching conditions again and again can be boring, so we have a way to group several routes that share the same requirements. We call it "subrouting".
For example, let's say we have several URLs that should only match when the host is `www.example.com`. Create a route for that host and get a "subrouter" from it:
```go
r := mux.NewRouter()
s := r.Host("www.example.com").Subrouter()
```
Then register routes in the subrouter:
```go
s.HandleFunc("/products/", ProductsHandler)
s.HandleFunc("/products/{key}", ProductHandler)
s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
```
The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route.
Subrouters can be used to create domain or path "namespaces": you define subrouters in a central place and then parts of the app can register its paths relatively to a given subrouter.
There's one more thing about subroutes. When a subrouter has a path prefix, the inner routes use it as base for their paths:
```go
r := mux.NewRouter()
s := r.PathPrefix("/products").Subrouter()
// "/products/"
s.HandleFunc("/", ProductsHandler)
// "/products/{key}/"
s.HandleFunc("/{key}/", ProductHandler)
// "/products/{key}/details"
s.HandleFunc("/{key}/details", ProductDetailsHandler)
```
Now let's see how to build registered URLs.
Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example:
```go
r := mux.NewRouter()
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
Name("article")
```
To build a URL, get the route and call the `URL()` method, passing a sequence of key/value pairs for the route variables. For the previous route, we would do:
```go
url, err := r.Get("article").URL("category", "technology", "id", "42")
```
...and the result will be a `url.URL` with the following path:
```
"/articles/technology/42"
```
This also works for host variables:
```go
r := mux.NewRouter()
r.Host("{subdomain}.domain.com").
Path("/articles/{category}/{id:[0-9]+}").
HandlerFunc(ArticleHandler).
Name("article")
// url.String() will be "http://news.domain.com/articles/technology/42"
url, err := r.Get("article").URL("subdomain", "news",
"category", "technology",
"id", "42")
```
All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match.
Regex support also exists for matching Headers within a route. For example, we could do:
```go
r.HeadersRegexp("Content-Type", "application/(text|json)")
```
...and the route will match both requests with a Content-Type of `application/json` as well as `application/text`
There's also a way to build only the URL host or path for a route: use the methods `URLHost()` or `URLPath()` instead. For the previous route, we would do:
```go
// "http://news.domain.com/"
host, err := r.Get("article").URLHost("subdomain", "news")
// "/articles/technology/42"
path, err := r.Get("article").URLPath("category", "technology", "id", "42")
```
And if you use subrouters, host and path defined separately can be built as well:
```go
r := mux.NewRouter()
s := r.Host("{subdomain}.domain.com").Subrouter()
s.Path("/articles/{category}/{id:[0-9]+}").
HandlerFunc(ArticleHandler).
Name("article")
// "http://news.domain.com/articles/technology/42"
url, err := r.Get("article").URL("subdomain", "news",
"category", "technology",
"id", "42")
```
## Full Example
Here's a complete, runnable example of a small `mux` based server:
```go
package main
import (
"net/http"
"github.com/gorilla/mux"
)
func YourHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Gorilla!\n"))
}
func main() {
r := mux.NewRouter()
// Routes consist of a path and a handler function.
r.HandleFunc("/", YourHandler)
// Bind to a port and pass our router in
http.ListenAndServe(":8000", r)
}
```
## License
BSD licensed. See the LICENSE file for details.
-21
View File
@@ -1,21 +0,0 @@
// Copyright 2012 The Gorilla Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mux
import (
"net/http"
"testing"
)
func BenchmarkMux(b *testing.B) {
router := new(Router)
handler := func(w http.ResponseWriter, r *http.Request) {}
router.HandleFunc("/v1/{v1}", handler)
request, _ := http.NewRequest("GET", "/v1/anything", nil)
for i := 0; i < b.N; i++ {
router.ServeHTTP(nil, request)
}
}
-206
View File
@@ -1,206 +0,0 @@
// Copyright 2012 The Gorilla Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package gorilla/mux implements a request router and dispatcher.
The name mux stands for "HTTP request multiplexer". Like the standard
http.ServeMux, mux.Router matches incoming requests against a list of
registered routes and calls a handler for the route that matches the URL
or other conditions. The main features are:
* Requests can be matched based on URL host, path, path prefix, schemes,
header and query values, HTTP methods or using custom matchers.
* URL hosts and paths can have variables with an optional regular
expression.
* Registered URLs can be built, or "reversed", which helps maintaining
references to resources.
* Routes can be used as subrouters: nested routes are only tested if the
parent route matches. This is useful to define groups of routes that
share common conditions like a host, a path prefix or other repeated
attributes. As a bonus, this optimizes request matching.
* It implements the http.Handler interface so it is compatible with the
standard http.ServeMux.
Let's start registering a couple of URL paths and handlers:
func main() {
r := mux.NewRouter()
r.HandleFunc("/", HomeHandler)
r.HandleFunc("/products", ProductsHandler)
r.HandleFunc("/articles", ArticlesHandler)
http.Handle("/", r)
}
Here we register three routes mapping URL paths to handlers. This is
equivalent to how http.HandleFunc() works: if an incoming request URL matches
one of the paths, the corresponding handler is called passing
(http.ResponseWriter, *http.Request) as parameters.
Paths can have variables. They are defined using the format {name} or
{name:pattern}. If a regular expression pattern is not defined, the matched
variable will be anything until the next slash. For example:
r := mux.NewRouter()
r.HandleFunc("/products/{key}", ProductHandler)
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
The names are used to create a map of route variables which can be retrieved
calling mux.Vars():
vars := mux.Vars(request)
category := vars["category"]
And this is all you need to know about the basic usage. More advanced options
are explained below.
Routes can also be restricted to a domain or subdomain. Just define a host
pattern to be matched. They can also have variables:
r := mux.NewRouter()
// Only matches if domain is "www.example.com".
r.Host("www.example.com")
// Matches a dynamic subdomain.
r.Host("{subdomain:[a-z]+}.domain.com")
There are several other matchers that can be added. To match path prefixes:
r.PathPrefix("/products/")
...or HTTP methods:
r.Methods("GET", "POST")
...or URL schemes:
r.Schemes("https")
...or header values:
r.Headers("X-Requested-With", "XMLHttpRequest")
...or query values:
r.Queries("key", "value")
...or to use a custom matcher function:
r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool {
return r.ProtoMajor == 0
})
...and finally, it is possible to combine several matchers in a single route:
r.HandleFunc("/products", ProductsHandler).
Host("www.example.com").
Methods("GET").
Schemes("http")
Setting the same matching conditions again and again can be boring, so we have
a way to group several routes that share the same requirements.
We call it "subrouting".
For example, let's say we have several URLs that should only match when the
host is "www.example.com". Create a route for that host and get a "subrouter"
from it:
r := mux.NewRouter()
s := r.Host("www.example.com").Subrouter()
Then register routes in the subrouter:
s.HandleFunc("/products/", ProductsHandler)
s.HandleFunc("/products/{key}", ProductHandler)
s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
The three URL paths we registered above will only be tested if the domain is
"www.example.com", because the subrouter is tested first. This is not
only convenient, but also optimizes request matching. You can create
subrouters combining any attribute matchers accepted by a route.
Subrouters can be used to create domain or path "namespaces": you define
subrouters in a central place and then parts of the app can register its
paths relatively to a given subrouter.
There's one more thing about subroutes. When a subrouter has a path prefix,
the inner routes use it as base for their paths:
r := mux.NewRouter()
s := r.PathPrefix("/products").Subrouter()
// "/products/"
s.HandleFunc("/", ProductsHandler)
// "/products/{key}/"
s.HandleFunc("/{key}/", ProductHandler)
// "/products/{key}/details"
s.HandleFunc("/{key}/details", ProductDetailsHandler)
Now let's see how to build registered URLs.
Routes can be named. All routes that define a name can have their URLs built,
or "reversed". We define a name calling Name() on a route. For example:
r := mux.NewRouter()
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
Name("article")
To build a URL, get the route and call the URL() method, passing a sequence of
key/value pairs for the route variables. For the previous route, we would do:
url, err := r.Get("article").URL("category", "technology", "id", "42")
...and the result will be a url.URL with the following path:
"/articles/technology/42"
This also works for host variables:
r := mux.NewRouter()
r.Host("{subdomain}.domain.com").
Path("/articles/{category}/{id:[0-9]+}").
HandlerFunc(ArticleHandler).
Name("article")
// url.String() will be "http://news.domain.com/articles/technology/42"
url, err := r.Get("article").URL("subdomain", "news",
"category", "technology",
"id", "42")
All variables defined in the route are required, and their values must
conform to the corresponding patterns. These requirements guarantee that a
generated URL will always match a registered route -- the only exception is
for explicitly defined "build-only" routes which never match.
Regex support also exists for matching Headers within a route. For example, we could do:
r.HeadersRegexp("Content-Type", "application/(text|json)")
...and the route will match both requests with a Content-Type of `application/json` as well as
`application/text`
There's also a way to build only the URL host or path for a route:
use the methods URLHost() or URLPath() instead. For the previous route,
we would do:
// "http://news.domain.com/"
host, err := r.Get("article").URLHost("subdomain", "news")
// "/articles/technology/42"
path, err := r.Get("article").URLPath("category", "technology", "id", "42")
And if you use subrouters, host and path defined separately can be built
as well:
r := mux.NewRouter()
s := r.Host("{subdomain}.domain.com").Subrouter()
s.Path("/articles/{category}/{id:[0-9]+}").
HandlerFunc(ArticleHandler).
Name("article")
// "http://news.domain.com/articles/technology/42"
url, err := r.Get("article").URL("subdomain", "news",
"category", "technology",
"id", "42")
*/
package mux
-481
View File
@@ -1,481 +0,0 @@
// Copyright 2012 The Gorilla Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mux
import (
"errors"
"fmt"
"net/http"
"path"
"regexp"
"github.com/gorilla/context"
)
// NewRouter returns a new router instance.
func NewRouter() *Router {
return &Router{namedRoutes: make(map[string]*Route), KeepContext: false}
}
// Router registers routes to be matched and dispatches a handler.
//
// It implements the http.Handler interface, so it can be registered to serve
// requests:
//
// var router = mux.NewRouter()
//
// func main() {
// http.Handle("/", router)
// }
//
// Or, for Google App Engine, register it in a init() function:
//
// func init() {
// http.Handle("/", router)
// }
//
// This will send all incoming requests to the router.
type Router struct {
// Configurable Handler to be used when no route matches.
NotFoundHandler http.Handler
// Parent route, if this is a subrouter.
parent parentRoute
// Routes to be matched, in order.
routes []*Route
// Routes by name for URL building.
namedRoutes map[string]*Route
// See Router.StrictSlash(). This defines the flag for new routes.
strictSlash bool
// If true, do not clear the request context after handling the request
KeepContext bool
}
// Match matches registered routes against the request.
func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
for _, route := range r.routes {
if route.Match(req, match) {
return true
}
}
// Closest match for a router (includes sub-routers)
if r.NotFoundHandler != nil {
match.Handler = r.NotFoundHandler
return true
}
return false
}
// ServeHTTP dispatches the handler registered in the matched route.
//
// When there is a match, the route variables can be retrieved calling
// mux.Vars(request).
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Clean path to canonical form and redirect.
if p := cleanPath(req.URL.Path); p != req.URL.Path {
// Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query.
// This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue:
// http://code.google.com/p/go/issues/detail?id=5252
url := *req.URL
url.Path = p
p = url.String()
w.Header().Set("Location", p)
w.WriteHeader(http.StatusMovedPermanently)
return
}
var match RouteMatch
var handler http.Handler
if r.Match(req, &match) {
handler = match.Handler
setVars(req, match.Vars)
setCurrentRoute(req, match.Route)
}
if handler == nil {
handler = http.NotFoundHandler()
}
if !r.KeepContext {
defer context.Clear(req)
}
handler.ServeHTTP(w, req)
}
// Get returns a route registered with the given name.
func (r *Router) Get(name string) *Route {
return r.getNamedRoutes()[name]
}
// GetRoute returns a route registered with the given name. This method
// was renamed to Get() and remains here for backwards compatibility.
func (r *Router) GetRoute(name string) *Route {
return r.getNamedRoutes()[name]
}
// StrictSlash defines the trailing slash behavior for new routes. The initial
// value is false.
//
// When true, if the route path is "/path/", accessing "/path" will redirect
// to the former and vice versa. In other words, your application will always
// see the path as specified in the route.
//
// When false, if the route path is "/path", accessing "/path/" will not match
// this route and vice versa.
//
// Special case: when a route sets a path prefix using the PathPrefix() method,
// strict slash is ignored for that route because the redirect behavior can't
// be determined from a prefix alone. However, any subrouters created from that
// route inherit the original StrictSlash setting.
func (r *Router) StrictSlash(value bool) *Router {
r.strictSlash = value
return r
}
// ----------------------------------------------------------------------------
// parentRoute
// ----------------------------------------------------------------------------
// getNamedRoutes returns the map where named routes are registered.
func (r *Router) getNamedRoutes() map[string]*Route {
if r.namedRoutes == nil {
if r.parent != nil {
r.namedRoutes = r.parent.getNamedRoutes()
} else {
r.namedRoutes = make(map[string]*Route)
}
}
return r.namedRoutes
}
// getRegexpGroup returns regexp definitions from the parent route, if any.
func (r *Router) getRegexpGroup() *routeRegexpGroup {
if r.parent != nil {
return r.parent.getRegexpGroup()
}
return nil
}
func (r *Router) buildVars(m map[string]string) map[string]string {
if r.parent != nil {
m = r.parent.buildVars(m)
}
return m
}
// ----------------------------------------------------------------------------
// Route factories
// ----------------------------------------------------------------------------
// NewRoute registers an empty route.
func (r *Router) NewRoute() *Route {
route := &Route{parent: r, strictSlash: r.strictSlash}
r.routes = append(r.routes, route)
return route
}
// Handle registers a new route with a matcher for the URL path.
// See Route.Path() and Route.Handler().
func (r *Router) Handle(path string, handler http.Handler) *Route {
return r.NewRoute().Path(path).Handler(handler)
}
// HandleFunc registers a new route with a matcher for the URL path.
// See Route.Path() and Route.HandlerFunc().
func (r *Router) HandleFunc(path string, f func(http.ResponseWriter,
*http.Request)) *Route {
return r.NewRoute().Path(path).HandlerFunc(f)
}
// Headers registers a new route with a matcher for request header values.
// See Route.Headers().
func (r *Router) Headers(pairs ...string) *Route {
return r.NewRoute().Headers(pairs...)
}
// Host registers a new route with a matcher for the URL host.
// See Route.Host().
func (r *Router) Host(tpl string) *Route {
return r.NewRoute().Host(tpl)
}
// MatcherFunc registers a new route with a custom matcher function.
// See Route.MatcherFunc().
func (r *Router) MatcherFunc(f MatcherFunc) *Route {
return r.NewRoute().MatcherFunc(f)
}
// Methods registers a new route with a matcher for HTTP methods.
// See Route.Methods().
func (r *Router) Methods(methods ...string) *Route {
return r.NewRoute().Methods(methods...)
}
// Path registers a new route with a matcher for the URL path.
// See Route.Path().
func (r *Router) Path(tpl string) *Route {
return r.NewRoute().Path(tpl)
}
// PathPrefix registers a new route with a matcher for the URL path prefix.
// See Route.PathPrefix().
func (r *Router) PathPrefix(tpl string) *Route {
return r.NewRoute().PathPrefix(tpl)
}
// Queries registers a new route with a matcher for URL query values.
// See Route.Queries().
func (r *Router) Queries(pairs ...string) *Route {
return r.NewRoute().Queries(pairs...)
}
// Schemes registers a new route with a matcher for URL schemes.
// See Route.Schemes().
func (r *Router) Schemes(schemes ...string) *Route {
return r.NewRoute().Schemes(schemes...)
}
// BuildVars registers a new route with a custom function for modifying
// route variables before building a URL.
func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route {
return r.NewRoute().BuildVarsFunc(f)
}
// Walk walks the router and all its sub-routers, calling walkFn for each route
// in the tree. The routes are walked in the order they were added. Sub-routers
// are explored depth-first.
func (r *Router) Walk(walkFn WalkFunc) error {
return r.walk(walkFn, []*Route{})
}
// SkipRouter is used as a return value from WalkFuncs to indicate that the
// router that walk is about to descend down to should be skipped.
var SkipRouter = errors.New("skip this router")
// WalkFunc is the type of the function called for each route visited by Walk.
// At every invocation, it is given the current route, and the current router,
// and a list of ancestor routes that lead to the current route.
type WalkFunc func(route *Route, router *Router, ancestors []*Route) error
func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error {
for _, t := range r.routes {
if t.regexp == nil || t.regexp.path == nil || t.regexp.path.template == "" {
continue
}
err := walkFn(t, r, ancestors)
if err == SkipRouter {
continue
}
for _, sr := range t.matchers {
if h, ok := sr.(*Router); ok {
err := h.walk(walkFn, ancestors)
if err != nil {
return err
}
}
}
if h, ok := t.handler.(*Router); ok {
ancestors = append(ancestors, t)
err := h.walk(walkFn, ancestors)
if err != nil {
return err
}
ancestors = ancestors[:len(ancestors)-1]
}
}
return nil
}
// ----------------------------------------------------------------------------
// Context
// ----------------------------------------------------------------------------
// RouteMatch stores information about a matched route.
type RouteMatch struct {
Route *Route
Handler http.Handler
Vars map[string]string
}
type contextKey int
const (
varsKey contextKey = iota
routeKey
)
// Vars returns the route variables for the current request, if any.
func Vars(r *http.Request) map[string]string {
if rv := context.Get(r, varsKey); rv != nil {
return rv.(map[string]string)
}
return nil
}
// CurrentRoute returns the matched route for the current request, if any.
// This only works when called inside the handler of the matched route
// because the matched route is stored in the request context which is cleared
// after the handler returns, unless the KeepContext option is set on the
// Router.
func CurrentRoute(r *http.Request) *Route {
if rv := context.Get(r, routeKey); rv != nil {
return rv.(*Route)
}
return nil
}
func setVars(r *http.Request, val interface{}) {
if val != nil {
context.Set(r, varsKey, val)
}
}
func setCurrentRoute(r *http.Request, val interface{}) {
if val != nil {
context.Set(r, routeKey, val)
}
}
// ----------------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------------
// cleanPath returns the canonical path for p, eliminating . and .. elements.
// Borrowed from the net/http package.
func cleanPath(p string) string {
if p == "" {
return "/"
}
if p[0] != '/' {
p = "/" + p
}
np := path.Clean(p)
// path.Clean removes trailing slash except for root;
// put the trailing slash back if necessary.
if p[len(p)-1] == '/' && np != "/" {
np += "/"
}
return np
}
// uniqueVars returns an error if two slices contain duplicated strings.
func uniqueVars(s1, s2 []string) error {
for _, v1 := range s1 {
for _, v2 := range s2 {
if v1 == v2 {
return fmt.Errorf("mux: duplicated route variable %q", v2)
}
}
}
return nil
}
// checkPairs returns the count of strings passed in, and an error if
// the count is not an even number.
func checkPairs(pairs ...string) (int, error) {
length := len(pairs)
if length%2 != 0 {
return length, fmt.Errorf(
"mux: number of parameters must be multiple of 2, got %v", pairs)
}
return length, nil
}
// mapFromPairsToString converts variadic string parameters to a
// string to string map.
func mapFromPairsToString(pairs ...string) (map[string]string, error) {
length, err := checkPairs(pairs...)
if err != nil {
return nil, err
}
m := make(map[string]string, length/2)
for i := 0; i < length; i += 2 {
m[pairs[i]] = pairs[i+1]
}
return m, nil
}
// mapFromPairsToRegex converts variadic string paramers to a
// string to regex map.
func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) {
length, err := checkPairs(pairs...)
if err != nil {
return nil, err
}
m := make(map[string]*regexp.Regexp, length/2)
for i := 0; i < length; i += 2 {
regex, err := regexp.Compile(pairs[i+1])
if err != nil {
return nil, err
}
m[pairs[i]] = regex
}
return m, nil
}
// matchInArray returns true if the given string value is in the array.
func matchInArray(arr []string, value string) bool {
for _, v := range arr {
if v == value {
return true
}
}
return false
}
// matchMapWithString returns true if the given key/value pairs exist in a given map.
func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, canonicalKey bool) bool {
for k, v := range toCheck {
// Check if key exists.
if canonicalKey {
k = http.CanonicalHeaderKey(k)
}
if values := toMatch[k]; values == nil {
return false
} else if v != "" {
// If value was defined as an empty string we only check that the
// key exists. Otherwise we also check for equality.
valueExists := false
for _, value := range values {
if v == value {
valueExists = true
break
}
}
if !valueExists {
return false
}
}
}
return true
}
// matchMapWithRegex returns true if the given key/value pairs exist in a given map compiled against
// the given regex
func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]string, canonicalKey bool) bool {
for k, v := range toCheck {
// Check if key exists.
if canonicalKey {
k = http.CanonicalHeaderKey(k)
}
if values := toMatch[k]; values == nil {
return false
} else if v != nil {
// If value was defined as an empty string we only check that the
// key exists. Otherwise we also check for equality.
valueExists := false
for _, value := range values {
if v.MatchString(value) {
valueExists = true
break
}
}
if !valueExists {
return false
}
}
}
return true
}
-1358
View File
File diff suppressed because it is too large Load Diff
-714
View File
@@ -1,714 +0,0 @@
// Old tests ported to Go1. This is a mess. Want to drop it one day.
// Copyright 2011 Gorilla Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mux
import (
"bytes"
"net/http"
"testing"
)
// ----------------------------------------------------------------------------
// ResponseRecorder
// ----------------------------------------------------------------------------
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// ResponseRecorder is an implementation of http.ResponseWriter that
// records its mutations for later inspection in tests.
type ResponseRecorder struct {
Code int // the HTTP response code from WriteHeader
HeaderMap http.Header // the HTTP response headers
Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
Flushed bool
}
// NewRecorder returns an initialized ResponseRecorder.
func NewRecorder() *ResponseRecorder {
return &ResponseRecorder{
HeaderMap: make(http.Header),
Body: new(bytes.Buffer),
}
}
// DefaultRemoteAddr is the default remote address to return in RemoteAddr if
// an explicit DefaultRemoteAddr isn't set on ResponseRecorder.
const DefaultRemoteAddr = "1.2.3.4"
// Header returns the response headers.
func (rw *ResponseRecorder) Header() http.Header {
return rw.HeaderMap
}
// Write always succeeds and writes to rw.Body, if not nil.
func (rw *ResponseRecorder) Write(buf []byte) (int, error) {
if rw.Body != nil {
rw.Body.Write(buf)
}
if rw.Code == 0 {
rw.Code = http.StatusOK
}
return len(buf), nil
}
// WriteHeader sets rw.Code.
func (rw *ResponseRecorder) WriteHeader(code int) {
rw.Code = code
}
// Flush sets rw.Flushed to true.
func (rw *ResponseRecorder) Flush() {
rw.Flushed = true
}
// ----------------------------------------------------------------------------
func TestRouteMatchers(t *testing.T) {
var scheme, host, path, query, method string
var headers map[string]string
var resultVars map[bool]map[string]string
router := NewRouter()
router.NewRoute().Host("{var1}.google.com").
Path("/{var2:[a-z]+}/{var3:[0-9]+}").
Queries("foo", "bar").
Methods("GET").
Schemes("https").
Headers("x-requested-with", "XMLHttpRequest")
router.NewRoute().Host("www.{var4}.com").
PathPrefix("/foo/{var5:[a-z]+}/{var6:[0-9]+}").
Queries("baz", "ding").
Methods("POST").
Schemes("http").
Headers("Content-Type", "application/json")
reset := func() {
// Everything match.
scheme = "https"
host = "www.google.com"
path = "/product/42"
query = "?foo=bar"
method = "GET"
headers = map[string]string{"X-Requested-With": "XMLHttpRequest"}
resultVars = map[bool]map[string]string{
true: {"var1": "www", "var2": "product", "var3": "42"},
false: {},
}
}
reset2 := func() {
// Everything match.
scheme = "http"
host = "www.google.com"
path = "/foo/product/42/path/that/is/ignored"
query = "?baz=ding"
method = "POST"
headers = map[string]string{"Content-Type": "application/json"}
resultVars = map[bool]map[string]string{
true: {"var4": "google", "var5": "product", "var6": "42"},
false: {},
}
}
match := func(shouldMatch bool) {
url := scheme + "://" + host + path + query
request, _ := http.NewRequest(method, url, nil)
for key, value := range headers {
request.Header.Add(key, value)
}
var routeMatch RouteMatch
matched := router.Match(request, &routeMatch)
if matched != shouldMatch {
// Need better messages. :)
if matched {
t.Errorf("Should match.")
} else {
t.Errorf("Should not match.")
}
}
if matched {
currentRoute := routeMatch.Route
if currentRoute == nil {
t.Errorf("Expected a current route.")
}
vars := routeMatch.Vars
expectedVars := resultVars[shouldMatch]
if len(vars) != len(expectedVars) {
t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars)
}
for name, value := range vars {
if expectedVars[name] != value {
t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars)
}
}
}
}
// 1st route --------------------------------------------------------------
// Everything match.
reset()
match(true)
// Scheme doesn't match.
reset()
scheme = "http"
match(false)
// Host doesn't match.
reset()
host = "www.mygoogle.com"
match(false)
// Path doesn't match.
reset()
path = "/product/notdigits"
match(false)
// Query doesn't match.
reset()
query = "?foo=baz"
match(false)
// Method doesn't match.
reset()
method = "POST"
match(false)
// Header doesn't match.
reset()
headers = map[string]string{}
match(false)
// Everything match, again.
reset()
match(true)
// 2nd route --------------------------------------------------------------
// Everything match.
reset2()
match(true)
// Scheme doesn't match.
reset2()
scheme = "https"
match(false)
// Host doesn't match.
reset2()
host = "sub.google.com"
match(false)
// Path doesn't match.
reset2()
path = "/bar/product/42"
match(false)
// Query doesn't match.
reset2()
query = "?foo=baz"
match(false)
// Method doesn't match.
reset2()
method = "GET"
match(false)
// Header doesn't match.
reset2()
headers = map[string]string{}
match(false)
// Everything match, again.
reset2()
match(true)
}
type headerMatcherTest struct {
matcher headerMatcher
headers map[string]string
result bool
}
var headerMatcherTests = []headerMatcherTest{
{
matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}),
headers: map[string]string{"X-Requested-With": "XMLHttpRequest"},
result: true,
},
{
matcher: headerMatcher(map[string]string{"x-requested-with": ""}),
headers: map[string]string{"X-Requested-With": "anything"},
result: true,
},
{
matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}),
headers: map[string]string{},
result: false,
},
}
type hostMatcherTest struct {
matcher *Route
url string
vars map[string]string
result bool
}
var hostMatcherTests = []hostMatcherTest{
{
matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"),
url: "http://abc.def.ghi/",
vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"},
result: true,
},
{
matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"),
url: "http://a.b.c/",
vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"},
result: false,
},
}
type methodMatcherTest struct {
matcher methodMatcher
method string
result bool
}
var methodMatcherTests = []methodMatcherTest{
{
matcher: methodMatcher([]string{"GET", "POST", "PUT"}),
method: "GET",
result: true,
},
{
matcher: methodMatcher([]string{"GET", "POST", "PUT"}),
method: "POST",
result: true,
},
{
matcher: methodMatcher([]string{"GET", "POST", "PUT"}),
method: "PUT",
result: true,
},
{
matcher: methodMatcher([]string{"GET", "POST", "PUT"}),
method: "DELETE",
result: false,
},
}
type pathMatcherTest struct {
matcher *Route
url string
vars map[string]string
result bool
}
var pathMatcherTests = []pathMatcherTest{
{
matcher: NewRouter().NewRoute().Path("/{foo:[0-9][0-9][0-9]}/{bar:[0-9][0-9][0-9]}/{baz:[0-9][0-9][0-9]}"),
url: "http://localhost:8080/123/456/789",
vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"},
result: true,
},
{
matcher: NewRouter().NewRoute().Path("/{foo:[0-9][0-9][0-9]}/{bar:[0-9][0-9][0-9]}/{baz:[0-9][0-9][0-9]}"),
url: "http://localhost:8080/1/2/3",
vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"},
result: false,
},
}
type schemeMatcherTest struct {
matcher schemeMatcher
url string
result bool
}
var schemeMatcherTests = []schemeMatcherTest{
{
matcher: schemeMatcher([]string{"http", "https"}),
url: "http://localhost:8080/",
result: true,
},
{
matcher: schemeMatcher([]string{"http", "https"}),
url: "https://localhost:8080/",
result: true,
},
{
matcher: schemeMatcher([]string{"https"}),
url: "http://localhost:8080/",
result: false,
},
{
matcher: schemeMatcher([]string{"http"}),
url: "https://localhost:8080/",
result: false,
},
}
type urlBuildingTest struct {
route *Route
vars []string
url string
}
var urlBuildingTests = []urlBuildingTest{
{
route: new(Route).Host("foo.domain.com"),
vars: []string{},
url: "http://foo.domain.com",
},
{
route: new(Route).Host("{subdomain}.domain.com"),
vars: []string{"subdomain", "bar"},
url: "http://bar.domain.com",
},
{
route: new(Route).Host("foo.domain.com").Path("/articles"),
vars: []string{},
url: "http://foo.domain.com/articles",
},
{
route: new(Route).Path("/articles"),
vars: []string{},
url: "/articles",
},
{
route: new(Route).Path("/articles/{category}/{id:[0-9]+}"),
vars: []string{"category", "technology", "id", "42"},
url: "/articles/technology/42",
},
{
route: new(Route).Host("{subdomain}.domain.com").Path("/articles/{category}/{id:[0-9]+}"),
vars: []string{"subdomain", "foo", "category", "technology", "id", "42"},
url: "http://foo.domain.com/articles/technology/42",
},
}
func TestHeaderMatcher(t *testing.T) {
for _, v := range headerMatcherTests {
request, _ := http.NewRequest("GET", "http://localhost:8080/", nil)
for key, value := range v.headers {
request.Header.Add(key, value)
}
var routeMatch RouteMatch
result := v.matcher.Match(request, &routeMatch)
if result != v.result {
if v.result {
t.Errorf("%#v: should match %v.", v.matcher, request.Header)
} else {
t.Errorf("%#v: should not match %v.", v.matcher, request.Header)
}
}
}
}
func TestHostMatcher(t *testing.T) {
for _, v := range hostMatcherTests {
request, _ := http.NewRequest("GET", v.url, nil)
var routeMatch RouteMatch
result := v.matcher.Match(request, &routeMatch)
vars := routeMatch.Vars
if result != v.result {
if v.result {
t.Errorf("%#v: should match %v.", v.matcher, v.url)
} else {
t.Errorf("%#v: should not match %v.", v.matcher, v.url)
}
}
if result {
if len(vars) != len(v.vars) {
t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars))
}
for name, value := range vars {
if v.vars[name] != value {
t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value)
}
}
} else {
if len(vars) != 0 {
t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars))
}
}
}
}
func TestMethodMatcher(t *testing.T) {
for _, v := range methodMatcherTests {
request, _ := http.NewRequest(v.method, "http://localhost:8080/", nil)
var routeMatch RouteMatch
result := v.matcher.Match(request, &routeMatch)
if result != v.result {
if v.result {
t.Errorf("%#v: should match %v.", v.matcher, v.method)
} else {
t.Errorf("%#v: should not match %v.", v.matcher, v.method)
}
}
}
}
func TestPathMatcher(t *testing.T) {
for _, v := range pathMatcherTests {
request, _ := http.NewRequest("GET", v.url, nil)
var routeMatch RouteMatch
result := v.matcher.Match(request, &routeMatch)
vars := routeMatch.Vars
if result != v.result {
if v.result {
t.Errorf("%#v: should match %v.", v.matcher, v.url)
} else {
t.Errorf("%#v: should not match %v.", v.matcher, v.url)
}
}
if result {
if len(vars) != len(v.vars) {
t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars))
}
for name, value := range vars {
if v.vars[name] != value {
t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value)
}
}
} else {
if len(vars) != 0 {
t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars))
}
}
}
}
func TestSchemeMatcher(t *testing.T) {
for _, v := range schemeMatcherTests {
request, _ := http.NewRequest("GET", v.url, nil)
var routeMatch RouteMatch
result := v.matcher.Match(request, &routeMatch)
if result != v.result {
if v.result {
t.Errorf("%#v: should match %v.", v.matcher, v.url)
} else {
t.Errorf("%#v: should not match %v.", v.matcher, v.url)
}
}
}
}
func TestUrlBuilding(t *testing.T) {
for _, v := range urlBuildingTests {
u, _ := v.route.URL(v.vars...)
url := u.String()
if url != v.url {
t.Errorf("expected %v, got %v", v.url, url)
/*
reversePath := ""
reverseHost := ""
if v.route.pathTemplate != nil {
reversePath = v.route.pathTemplate.Reverse
}
if v.route.hostTemplate != nil {
reverseHost = v.route.hostTemplate.Reverse
}
t.Errorf("%#v:\nexpected: %q\ngot: %q\nreverse path: %q\nreverse host: %q", v.route, v.url, url, reversePath, reverseHost)
*/
}
}
ArticleHandler := func(w http.ResponseWriter, r *http.Request) {
}
router := NewRouter()
router.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).Name("article")
url, _ := router.Get("article").URL("category", "technology", "id", "42")
expected := "/articles/technology/42"
if url.String() != expected {
t.Errorf("Expected %v, got %v", expected, url.String())
}
}
func TestMatchedRouteName(t *testing.T) {
routeName := "stock"
router := NewRouter()
route := router.NewRoute().Path("/products/").Name(routeName)
url := "http://www.example.com/products/"
request, _ := http.NewRequest("GET", url, nil)
var rv RouteMatch
ok := router.Match(request, &rv)
if !ok || rv.Route != route {
t.Errorf("Expected same route, got %+v.", rv.Route)
}
retName := rv.Route.GetName()
if retName != routeName {
t.Errorf("Expected %q, got %q.", routeName, retName)
}
}
func TestSubRouting(t *testing.T) {
// Example from docs.
router := NewRouter()
subrouter := router.NewRoute().Host("www.example.com").Subrouter()
route := subrouter.NewRoute().Path("/products/").Name("products")
url := "http://www.example.com/products/"
request, _ := http.NewRequest("GET", url, nil)
var rv RouteMatch
ok := router.Match(request, &rv)
if !ok || rv.Route != route {
t.Errorf("Expected same route, got %+v.", rv.Route)
}
u, _ := router.Get("products").URL()
builtUrl := u.String()
// Yay, subroute aware of the domain when building!
if builtUrl != url {
t.Errorf("Expected %q, got %q.", url, builtUrl)
}
}
func TestVariableNames(t *testing.T) {
route := new(Route).Host("{arg1}.domain.com").Path("/{arg1}/{arg2:[0-9]+}")
if route.err == nil {
t.Errorf("Expected error for duplicated variable names")
}
}
func TestRedirectSlash(t *testing.T) {
var route *Route
var routeMatch RouteMatch
r := NewRouter()
r.StrictSlash(false)
route = r.NewRoute()
if route.strictSlash != false {
t.Errorf("Expected false redirectSlash.")
}
r.StrictSlash(true)
route = r.NewRoute()
if route.strictSlash != true {
t.Errorf("Expected true redirectSlash.")
}
route = new(Route)
route.strictSlash = true
route.Path("/{arg1}/{arg2:[0-9]+}/")
request, _ := http.NewRequest("GET", "http://localhost/foo/123", nil)
routeMatch = RouteMatch{}
_ = route.Match(request, &routeMatch)
vars := routeMatch.Vars
if vars["arg1"] != "foo" {
t.Errorf("Expected foo.")
}
if vars["arg2"] != "123" {
t.Errorf("Expected 123.")
}
rsp := NewRecorder()
routeMatch.Handler.ServeHTTP(rsp, request)
if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123/" {
t.Errorf("Expected redirect header.")
}
route = new(Route)
route.strictSlash = true
route.Path("/{arg1}/{arg2:[0-9]+}")
request, _ = http.NewRequest("GET", "http://localhost/foo/123/", nil)
routeMatch = RouteMatch{}
_ = route.Match(request, &routeMatch)
vars = routeMatch.Vars
if vars["arg1"] != "foo" {
t.Errorf("Expected foo.")
}
if vars["arg2"] != "123" {
t.Errorf("Expected 123.")
}
rsp = NewRecorder()
routeMatch.Handler.ServeHTTP(rsp, request)
if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123" {
t.Errorf("Expected redirect header.")
}
}
// Test for the new regexp library, still not available in stable Go.
func TestNewRegexp(t *testing.T) {
var p *routeRegexp
var matches []string
tests := map[string]map[string][]string{
"/{foo:a{2}}": {
"/a": nil,
"/aa": {"aa"},
"/aaa": nil,
"/aaaa": nil,
},
"/{foo:a{2,}}": {
"/a": nil,
"/aa": {"aa"},
"/aaa": {"aaa"},
"/aaaa": {"aaaa"},
},
"/{foo:a{2,3}}": {
"/a": nil,
"/aa": {"aa"},
"/aaa": {"aaa"},
"/aaaa": nil,
},
"/{foo:[a-z]{3}}/{bar:[a-z]{2}}": {
"/a": nil,
"/ab": nil,
"/abc": nil,
"/abcd": nil,
"/abc/ab": {"abc", "ab"},
"/abc/abc": nil,
"/abcd/ab": nil,
},
`/{foo:\w{3,}}/{bar:\d{2,}}`: {
"/a": nil,
"/ab": nil,
"/abc": nil,
"/abc/1": nil,
"/abc/12": {"abc", "12"},
"/abcd/12": {"abcd", "12"},
"/abcd/123": {"abcd", "123"},
},
}
for pattern, paths := range tests {
p, _ = newRouteRegexp(pattern, false, false, false, false)
for path, result := range paths {
matches = p.regexp.FindStringSubmatch(path)
if result == nil {
if matches != nil {
t.Errorf("%v should not match %v.", pattern, path)
}
} else {
if len(matches) != len(result)+1 {
t.Errorf("Expected %v matches, got %v.", len(result)+1, len(matches))
} else {
for k, v := range result {
if matches[k+1] != v {
t.Errorf("Expected %v, got %v.", v, matches[k+1])
}
}
}
}
}
}
}
-317
View File
@@ -1,317 +0,0 @@
// Copyright 2012 The Gorilla Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mux
import (
"bytes"
"fmt"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
)
// newRouteRegexp parses a route template and returns a routeRegexp,
// used to match a host, a path or a query string.
//
// It will extract named variables, assemble a regexp to be matched, create
// a "reverse" template to build URLs and compile regexps to validate variable
// values used in URL building.
//
// Previously we accepted only Python-like identifiers for variable
// names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that
// name and pattern can't be empty, and names can't contain a colon.
func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash bool) (*routeRegexp, error) {
// Check if it is well-formed.
idxs, errBraces := braceIndices(tpl)
if errBraces != nil {
return nil, errBraces
}
// Backup the original.
template := tpl
// Now let's parse it.
defaultPattern := "[^/]+"
if matchQuery {
defaultPattern = "[^?&]*"
} else if matchHost {
defaultPattern = "[^.]+"
matchPrefix = false
}
// Only match strict slash if not matching
if matchPrefix || matchHost || matchQuery {
strictSlash = false
}
// Set a flag for strictSlash.
endSlash := false
if strictSlash && strings.HasSuffix(tpl, "/") {
tpl = tpl[:len(tpl)-1]
endSlash = true
}
varsN := make([]string, len(idxs)/2)
varsR := make([]*regexp.Regexp, len(idxs)/2)
pattern := bytes.NewBufferString("")
pattern.WriteByte('^')
reverse := bytes.NewBufferString("")
var end int
var err error
for i := 0; i < len(idxs); i += 2 {
// Set all values we are interested in.
raw := tpl[end:idxs[i]]
end = idxs[i+1]
parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2)
name := parts[0]
patt := defaultPattern
if len(parts) == 2 {
patt = parts[1]
}
// Name or pattern can't be empty.
if name == "" || patt == "" {
return nil, fmt.Errorf("mux: missing name or pattern in %q",
tpl[idxs[i]:end])
}
// Build the regexp pattern.
varIdx := i / 2
fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(varIdx), patt)
// Build the reverse template.
fmt.Fprintf(reverse, "%s%%s", raw)
// Append variable name and compiled pattern.
varsN[varIdx] = name
varsR[varIdx], err = regexp.Compile(fmt.Sprintf("^%s$", patt))
if err != nil {
return nil, err
}
}
// Add the remaining.
raw := tpl[end:]
pattern.WriteString(regexp.QuoteMeta(raw))
if strictSlash {
pattern.WriteString("[/]?")
}
if matchQuery {
// Add the default pattern if the query value is empty
if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" {
pattern.WriteString(defaultPattern)
}
}
if !matchPrefix {
pattern.WriteByte('$')
}
reverse.WriteString(raw)
if endSlash {
reverse.WriteByte('/')
}
// Compile full regexp.
reg, errCompile := regexp.Compile(pattern.String())
if errCompile != nil {
return nil, errCompile
}
// Done!
return &routeRegexp{
template: template,
matchHost: matchHost,
matchQuery: matchQuery,
strictSlash: strictSlash,
regexp: reg,
reverse: reverse.String(),
varsN: varsN,
varsR: varsR,
}, nil
}
// routeRegexp stores a regexp to match a host or path and information to
// collect and validate route variables.
type routeRegexp struct {
// The unmodified template.
template string
// True for host match, false for path or query string match.
matchHost bool
// True for query string match, false for path and host match.
matchQuery bool
// The strictSlash value defined on the route, but disabled if PathPrefix was used.
strictSlash bool
// Expanded regexp.
regexp *regexp.Regexp
// Reverse template.
reverse string
// Variable names.
varsN []string
// Variable regexps (validators).
varsR []*regexp.Regexp
}
// Match matches the regexp against the URL host or path.
func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
if !r.matchHost {
if r.matchQuery {
return r.matchQueryString(req)
} else {
return r.regexp.MatchString(req.URL.Path)
}
}
return r.regexp.MatchString(getHost(req))
}
// url builds a URL part using the given values.
func (r *routeRegexp) url(values map[string]string) (string, error) {
urlValues := make([]interface{}, len(r.varsN))
for k, v := range r.varsN {
value, ok := values[v]
if !ok {
return "", fmt.Errorf("mux: missing route variable %q", v)
}
urlValues[k] = value
}
rv := fmt.Sprintf(r.reverse, urlValues...)
if !r.regexp.MatchString(rv) {
// The URL is checked against the full regexp, instead of checking
// individual variables. This is faster but to provide a good error
// message, we check individual regexps if the URL doesn't match.
for k, v := range r.varsN {
if !r.varsR[k].MatchString(values[v]) {
return "", fmt.Errorf(
"mux: variable %q doesn't match, expected %q", values[v],
r.varsR[k].String())
}
}
}
return rv, nil
}
// getUrlQuery returns a single query parameter from a request URL.
// For a URL with foo=bar&baz=ding, we return only the relevant key
// value pair for the routeRegexp.
func (r *routeRegexp) getUrlQuery(req *http.Request) string {
if !r.matchQuery {
return ""
}
templateKey := strings.SplitN(r.template, "=", 2)[0]
for key, vals := range req.URL.Query() {
if key == templateKey && len(vals) > 0 {
return key + "=" + vals[0]
}
}
return ""
}
func (r *routeRegexp) matchQueryString(req *http.Request) bool {
return r.regexp.MatchString(r.getUrlQuery(req))
}
// braceIndices returns the first level curly brace indices from a string.
// It returns an error in case of unbalanced braces.
func braceIndices(s string) ([]int, error) {
var level, idx int
idxs := make([]int, 0)
for i := 0; i < len(s); i++ {
switch s[i] {
case '{':
if level++; level == 1 {
idx = i
}
case '}':
if level--; level == 0 {
idxs = append(idxs, idx, i+1)
} else if level < 0 {
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
}
}
}
if level != 0 {
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
}
return idxs, nil
}
// varGroupName builds a capturing group name for the indexed variable.
func varGroupName(idx int) string {
return "v" + strconv.Itoa(idx)
}
// ----------------------------------------------------------------------------
// routeRegexpGroup
// ----------------------------------------------------------------------------
// routeRegexpGroup groups the route matchers that carry variables.
type routeRegexpGroup struct {
host *routeRegexp
path *routeRegexp
queries []*routeRegexp
}
// setMatch extracts the variables from the URL once a route matches.
func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) {
// Store host variables.
if v.host != nil {
hostVars := v.host.regexp.FindStringSubmatch(getHost(req))
if hostVars != nil {
subexpNames := v.host.regexp.SubexpNames()
varName := 0
for i, name := range subexpNames[1:] {
if name != "" && name == varGroupName(varName) {
m.Vars[v.host.varsN[varName]] = hostVars[i+1]
varName++
}
}
}
}
// Store path variables.
if v.path != nil {
pathVars := v.path.regexp.FindStringSubmatch(req.URL.Path)
if pathVars != nil {
subexpNames := v.path.regexp.SubexpNames()
varName := 0
for i, name := range subexpNames[1:] {
if name != "" && name == varGroupName(varName) {
m.Vars[v.path.varsN[varName]] = pathVars[i+1]
varName++
}
}
// Check if we should redirect.
if v.path.strictSlash {
p1 := strings.HasSuffix(req.URL.Path, "/")
p2 := strings.HasSuffix(v.path.template, "/")
if p1 != p2 {
u, _ := url.Parse(req.URL.String())
if p1 {
u.Path = u.Path[:len(u.Path)-1]
} else {
u.Path += "/"
}
m.Handler = http.RedirectHandler(u.String(), 301)
}
}
}
}
// Store query string variables.
for _, q := range v.queries {
queryVars := q.regexp.FindStringSubmatch(q.getUrlQuery(req))
if queryVars != nil {
subexpNames := q.regexp.SubexpNames()
varName := 0
for i, name := range subexpNames[1:] {
if name != "" && name == varGroupName(varName) {
m.Vars[q.varsN[varName]] = queryVars[i+1]
varName++
}
}
}
}
}
// getHost tries its best to return the request host.
func getHost(r *http.Request) string {
if r.URL.IsAbs() {
return r.URL.Host
}
host := r.Host
// Slice off any port information.
if i := strings.Index(host, ":"); i != -1 {
host = host[:i]
}
return host
}
-595
View File
@@ -1,595 +0,0 @@
// Copyright 2012 The Gorilla Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mux
import (
"errors"
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
)
// Route stores information to match a request and build URLs.
type Route struct {
// Parent where the route was registered (a Router).
parent parentRoute
// Request handler for the route.
handler http.Handler
// List of matchers.
matchers []matcher
// Manager for the variables from host and path.
regexp *routeRegexpGroup
// If true, when the path pattern is "/path/", accessing "/path" will
// redirect to the former and vice versa.
strictSlash bool
// If true, this route never matches: it is only used to build URLs.
buildOnly bool
// The name used to build URLs.
name string
// Error resulted from building a route.
err error
buildVarsFunc BuildVarsFunc
}
// Match matches the route against the request.
func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
if r.buildOnly || r.err != nil {
return false
}
// Match everything.
for _, m := range r.matchers {
if matched := m.Match(req, match); !matched {
return false
}
}
// Yay, we have a match. Let's collect some info about it.
if match.Route == nil {
match.Route = r
}
if match.Handler == nil {
match.Handler = r.handler
}
if match.Vars == nil {
match.Vars = make(map[string]string)
}
// Set variables.
if r.regexp != nil {
r.regexp.setMatch(req, match, r)
}
return true
}
// ----------------------------------------------------------------------------
// Route attributes
// ----------------------------------------------------------------------------
// GetError returns an error resulted from building the route, if any.
func (r *Route) GetError() error {
return r.err
}
// BuildOnly sets the route to never match: it is only used to build URLs.
func (r *Route) BuildOnly() *Route {
r.buildOnly = true
return r
}
// Handler --------------------------------------------------------------------
// Handler sets a handler for the route.
func (r *Route) Handler(handler http.Handler) *Route {
if r.err == nil {
r.handler = handler
}
return r
}
// HandlerFunc sets a handler function for the route.
func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route {
return r.Handler(http.HandlerFunc(f))
}
// GetHandler returns the handler for the route, if any.
func (r *Route) GetHandler() http.Handler {
return r.handler
}
// Name -----------------------------------------------------------------------
// Name sets the name for the route, used to build URLs.
// If the name was registered already it will be overwritten.
func (r *Route) Name(name string) *Route {
if r.name != "" {
r.err = fmt.Errorf("mux: route already has name %q, can't set %q",
r.name, name)
}
if r.err == nil {
r.name = name
r.getNamedRoutes()[name] = r
}
return r
}
// GetName returns the name for the route, if any.
func (r *Route) GetName() string {
return r.name
}
// ----------------------------------------------------------------------------
// Matchers
// ----------------------------------------------------------------------------
// matcher types try to match a request.
type matcher interface {
Match(*http.Request, *RouteMatch) bool
}
// addMatcher adds a matcher to the route.
func (r *Route) addMatcher(m matcher) *Route {
if r.err == nil {
r.matchers = append(r.matchers, m)
}
return r
}
// addRegexpMatcher adds a host or path matcher and builder to a route.
func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery bool) error {
if r.err != nil {
return r.err
}
r.regexp = r.getRegexpGroup()
if !matchHost && !matchQuery {
if len(tpl) == 0 || tpl[0] != '/' {
return fmt.Errorf("mux: path must start with a slash, got %q", tpl)
}
if r.regexp.path != nil {
tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl
}
}
rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, matchQuery, r.strictSlash)
if err != nil {
return err
}
for _, q := range r.regexp.queries {
if err = uniqueVars(rr.varsN, q.varsN); err != nil {
return err
}
}
if matchHost {
if r.regexp.path != nil {
if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil {
return err
}
}
r.regexp.host = rr
} else {
if r.regexp.host != nil {
if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil {
return err
}
}
if matchQuery {
r.regexp.queries = append(r.regexp.queries, rr)
} else {
r.regexp.path = rr
}
}
r.addMatcher(rr)
return nil
}
// Headers --------------------------------------------------------------------
// headerMatcher matches the request against header values.
type headerMatcher map[string]string
func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool {
return matchMapWithString(m, r.Header, true)
}
// Headers adds a matcher for request header values.
// It accepts a sequence of key/value pairs to be matched. For example:
//
// r := mux.NewRouter()
// r.Headers("Content-Type", "application/json",
// "X-Requested-With", "XMLHttpRequest")
//
// The above route will only match if both request header values match.
// If the value is an empty string, it will match any value if the key is set.
func (r *Route) Headers(pairs ...string) *Route {
if r.err == nil {
var headers map[string]string
headers, r.err = mapFromPairsToString(pairs...)
return r.addMatcher(headerMatcher(headers))
}
return r
}
// headerRegexMatcher matches the request against the route given a regex for the header
type headerRegexMatcher map[string]*regexp.Regexp
func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool {
return matchMapWithRegex(m, r.Header, true)
}
// Regular expressions can be used with headers as well.
// It accepts a sequence of key/value pairs, where the value has regex support. For example
// r := mux.NewRouter()
// r.HeadersRegexp("Content-Type", "application/(text|json)",
// "X-Requested-With", "XMLHttpRequest")
//
// The above route will only match if both the request header matches both regular expressions.
// It the value is an empty string, it will match any value if the key is set.
func (r *Route) HeadersRegexp(pairs ...string) *Route {
if r.err == nil {
var headers map[string]*regexp.Regexp
headers, r.err = mapFromPairsToRegex(pairs...)
return r.addMatcher(headerRegexMatcher(headers))
}
return r
}
// Host -----------------------------------------------------------------------
// Host adds a matcher for the URL host.
// It accepts a template with zero or more URL variables enclosed by {}.
// Variables can define an optional regexp pattern to be matched:
//
// - {name} matches anything until the next dot.
//
// - {name:pattern} matches the given regexp pattern.
//
// For example:
//
// r := mux.NewRouter()
// r.Host("www.example.com")
// r.Host("{subdomain}.domain.com")
// r.Host("{subdomain:[a-z]+}.domain.com")
//
// Variable names must be unique in a given route. They can be retrieved
// calling mux.Vars(request).
func (r *Route) Host(tpl string) *Route {
r.err = r.addRegexpMatcher(tpl, true, false, false)
return r
}
// MatcherFunc ----------------------------------------------------------------
// MatcherFunc is the function signature used by custom matchers.
type MatcherFunc func(*http.Request, *RouteMatch) bool
func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool {
return m(r, match)
}
// MatcherFunc adds a custom function to be used as request matcher.
func (r *Route) MatcherFunc(f MatcherFunc) *Route {
return r.addMatcher(f)
}
// Methods --------------------------------------------------------------------
// methodMatcher matches the request against HTTP methods.
type methodMatcher []string
func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool {
return matchInArray(m, r.Method)
}
// Methods adds a matcher for HTTP methods.
// It accepts a sequence of one or more methods to be matched, e.g.:
// "GET", "POST", "PUT".
func (r *Route) Methods(methods ...string) *Route {
for k, v := range methods {
methods[k] = strings.ToUpper(v)
}
return r.addMatcher(methodMatcher(methods))
}
// Path -----------------------------------------------------------------------
// Path adds a matcher for the URL path.
// It accepts a template with zero or more URL variables enclosed by {}. The
// template must start with a "/".
// Variables can define an optional regexp pattern to be matched:
//
// - {name} matches anything until the next slash.
//
// - {name:pattern} matches the given regexp pattern.
//
// For example:
//
// r := mux.NewRouter()
// r.Path("/products/").Handler(ProductsHandler)
// r.Path("/products/{key}").Handler(ProductsHandler)
// r.Path("/articles/{category}/{id:[0-9]+}").
// Handler(ArticleHandler)
//
// Variable names must be unique in a given route. They can be retrieved
// calling mux.Vars(request).
func (r *Route) Path(tpl string) *Route {
r.err = r.addRegexpMatcher(tpl, false, false, false)
return r
}
// PathPrefix -----------------------------------------------------------------
// PathPrefix adds a matcher for the URL path prefix. This matches if the given
// template is a prefix of the full URL path. See Route.Path() for details on
// the tpl argument.
//
// Note that it does not treat slashes specially ("/foobar/" will be matched by
// the prefix "/foo") so you may want to use a trailing slash here.
//
// Also note that the setting of Router.StrictSlash() has no effect on routes
// with a PathPrefix matcher.
func (r *Route) PathPrefix(tpl string) *Route {
r.err = r.addRegexpMatcher(tpl, false, true, false)
return r
}
// Query ----------------------------------------------------------------------
// Queries adds a matcher for URL query values.
// It accepts a sequence of key/value pairs. Values may define variables.
// For example:
//
// r := mux.NewRouter()
// r.Queries("foo", "bar", "id", "{id:[0-9]+}")
//
// The above route will only match if the URL contains the defined queries
// values, e.g.: ?foo=bar&id=42.
//
// It the value is an empty string, it will match any value if the key is set.
//
// Variables can define an optional regexp pattern to be matched:
//
// - {name} matches anything until the next slash.
//
// - {name:pattern} matches the given regexp pattern.
func (r *Route) Queries(pairs ...string) *Route {
length := len(pairs)
if length%2 != 0 {
r.err = fmt.Errorf(
"mux: number of parameters must be multiple of 2, got %v", pairs)
return nil
}
for i := 0; i < length; i += 2 {
if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], false, false, true); r.err != nil {
return r
}
}
return r
}
// Schemes --------------------------------------------------------------------
// schemeMatcher matches the request against URL schemes.
type schemeMatcher []string
func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool {
return matchInArray(m, r.URL.Scheme)
}
// Schemes adds a matcher for URL schemes.
// It accepts a sequence of schemes to be matched, e.g.: "http", "https".
func (r *Route) Schemes(schemes ...string) *Route {
for k, v := range schemes {
schemes[k] = strings.ToLower(v)
}
return r.addMatcher(schemeMatcher(schemes))
}
// BuildVarsFunc --------------------------------------------------------------
// BuildVarsFunc is the function signature used by custom build variable
// functions (which can modify route variables before a route's URL is built).
type BuildVarsFunc func(map[string]string) map[string]string
// BuildVarsFunc adds a custom function to be used to modify build variables
// before a route's URL is built.
func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route {
r.buildVarsFunc = f
return r
}
// Subrouter ------------------------------------------------------------------
// Subrouter creates a subrouter for the route.
//
// It will test the inner routes only if the parent route matched. For example:
//
// r := mux.NewRouter()
// s := r.Host("www.example.com").Subrouter()
// s.HandleFunc("/products/", ProductsHandler)
// s.HandleFunc("/products/{key}", ProductHandler)
// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
//
// Here, the routes registered in the subrouter won't be tested if the host
// doesn't match.
func (r *Route) Subrouter() *Router {
router := &Router{parent: r, strictSlash: r.strictSlash}
r.addMatcher(router)
return router
}
// ----------------------------------------------------------------------------
// URL building
// ----------------------------------------------------------------------------
// URL builds a URL for the route.
//
// It accepts a sequence of key/value pairs for the route variables. For
// example, given this route:
//
// r := mux.NewRouter()
// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
// Name("article")
//
// ...a URL for it can be built using:
//
// url, err := r.Get("article").URL("category", "technology", "id", "42")
//
// ...which will return an url.URL with the following path:
//
// "/articles/technology/42"
//
// This also works for host variables:
//
// r := mux.NewRouter()
// r.Host("{subdomain}.domain.com").
// HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
// Name("article")
//
// // url.String() will be "http://news.domain.com/articles/technology/42"
// url, err := r.Get("article").URL("subdomain", "news",
// "category", "technology",
// "id", "42")
//
// All variables defined in the route are required, and their values must
// conform to the corresponding patterns.
func (r *Route) URL(pairs ...string) (*url.URL, error) {
if r.err != nil {
return nil, r.err
}
if r.regexp == nil {
return nil, errors.New("mux: route doesn't have a host or path")
}
values, err := r.prepareVars(pairs...)
if err != nil {
return nil, err
}
var scheme, host, path string
if r.regexp.host != nil {
// Set a default scheme.
scheme = "http"
if host, err = r.regexp.host.url(values); err != nil {
return nil, err
}
}
if r.regexp.path != nil {
if path, err = r.regexp.path.url(values); err != nil {
return nil, err
}
}
return &url.URL{
Scheme: scheme,
Host: host,
Path: path,
}, nil
}
// URLHost builds the host part of the URL for a route. See Route.URL().
//
// The route must have a host defined.
func (r *Route) URLHost(pairs ...string) (*url.URL, error) {
if r.err != nil {
return nil, r.err
}
if r.regexp == nil || r.regexp.host == nil {
return nil, errors.New("mux: route doesn't have a host")
}
values, err := r.prepareVars(pairs...)
if err != nil {
return nil, err
}
host, err := r.regexp.host.url(values)
if err != nil {
return nil, err
}
return &url.URL{
Scheme: "http",
Host: host,
}, nil
}
// URLPath builds the path part of the URL for a route. See Route.URL().
//
// The route must have a path defined.
func (r *Route) URLPath(pairs ...string) (*url.URL, error) {
if r.err != nil {
return nil, r.err
}
if r.regexp == nil || r.regexp.path == nil {
return nil, errors.New("mux: route doesn't have a path")
}
values, err := r.prepareVars(pairs...)
if err != nil {
return nil, err
}
path, err := r.regexp.path.url(values)
if err != nil {
return nil, err
}
return &url.URL{
Path: path,
}, nil
}
// prepareVars converts the route variable pairs into a map. If the route has a
// BuildVarsFunc, it is invoked.
func (r *Route) prepareVars(pairs ...string) (map[string]string, error) {
m, err := mapFromPairsToString(pairs...)
if err != nil {
return nil, err
}
return r.buildVars(m), nil
}
func (r *Route) buildVars(m map[string]string) map[string]string {
if r.parent != nil {
m = r.parent.buildVars(m)
}
if r.buildVarsFunc != nil {
m = r.buildVarsFunc(m)
}
return m
}
// ----------------------------------------------------------------------------
// parentRoute
// ----------------------------------------------------------------------------
// parentRoute allows routes to know about parent host and path definitions.
type parentRoute interface {
getNamedRoutes() map[string]*Route
getRegexpGroup() *routeRegexpGroup
buildVars(map[string]string) map[string]string
}
// getNamedRoutes returns the map where named routes are registered.
func (r *Route) getNamedRoutes() map[string]*Route {
if r.parent == nil {
// During tests router is not always set.
r.parent = NewRouter()
}
return r.parent.getNamedRoutes()
}
// getRegexpGroup returns regexp definitions from this route.
func (r *Route) getRegexpGroup() *routeRegexpGroup {
if r.regexp == nil {
if r.parent == nil {
// During tests router is not always set.
r.parent = NewRouter()
}
regexp := r.parent.getRegexpGroup()
if regexp == nil {
r.regexp = new(routeRegexpGroup)
} else {
// Copy.
r.regexp = &routeRegexpGroup{
host: regexp.host,
path: regexp.path,
queries: regexp.queries,
}
}
}
return r.regexp
}
-22
View File
@@ -1,22 +0,0 @@
# 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
-6
View File
@@ -1,6 +0,0 @@
language: go
go:
- 1.1
- 1.2
- tip
-8
View File
@@ -1,8 +0,0 @@
# This is the official list of Gorilla WebSocket authors for copyright
# purposes.
#
# Please keep the list sorted.
Gary Burd <[email protected]>
Joachim Bauch <[email protected]>
-22
View File
@@ -1,22 +0,0 @@
Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-61
View File
@@ -1,61 +0,0 @@
# Gorilla WebSocket
Gorilla WebSocket is a [Go](http://golang.org/) implementation of the
[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol.
### Documentation
* [API Reference](http://godoc.org/github.com/gorilla/websocket)
* [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat)
* [Command example](https://github.com/gorilla/websocket/tree/master/examples/command)
* [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo)
* [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch)
### Status
The Gorilla WebSocket package provides a complete and tested implementation of
the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. The
package API is stable.
### Installation
go get github.com/gorilla/websocket
### Protocol Compliance
The Gorilla WebSocket package passes the server tests in the [Autobahn Test
Suite](http://autobahn.ws/testsuite) using the application in the [examples/autobahn
subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn).
### Gorilla WebSocket compared with other packages
<table>
<tr>
<th></th>
<th><a href="http://godoc.org/github.com/gorilla/websocket">github.com/gorilla</a></th>
<th><a href="http://godoc.org/golang.org/x/net/websocket">golang.org/x/net</a></th>
</tr>
<tr>
<tr><td colspan="3"><a href="http://tools.ietf.org/html/rfc6455">RFC 6455</a> Features</td></tr>
<tr><td>Passes <a href="http://autobahn.ws/testsuite/">Autobahn Test Suite</a></td><td><a href="https://github.com/gorilla/websocket/tree/master/examples/autobahn">Yes</a></td><td>No</td></tr>
<tr><td>Receive <a href="https://tools.ietf.org/html/rfc6455#section-5.4">fragmented</a> message<td>Yes</td><td><a href="https://code.google.com/p/go/issues/detail?id=7632">No</a>, see note 1</td></tr>
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.1">close</a> message</td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td><a href="https://code.google.com/p/go/issues/detail?id=4588">No</a></td></tr>
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.2">pings</a> and receive <a href="https://tools.ietf.org/html/rfc6455#section-5.5.3">pongs</a></td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td>No</td></tr>
<tr><td>Get the <a href="https://tools.ietf.org/html/rfc6455#section-5.6">type</a> of a received data message</td><td>Yes</td><td>Yes, see note 2</td></tr>
<tr><td colspan="3">Other Features</tr></td>
<tr><td>Limit size of received message</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.SetReadLimit">Yes</a></td><td><a href="https://code.google.com/p/go/issues/detail?id=5082">No</a></td></tr>
<tr><td>Read message using io.Reader</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.NextReader">Yes</a></td><td>No, see note 3</td></tr>
<tr><td>Write message using io.WriteCloser</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.NextWriter">Yes</a></td><td>No, see note 3</td></tr>
</table>
Notes:
1. Large messages are fragmented in [Chrome's new WebSocket implementation](http://www.ietf.org/mail-archive/web/hybi/current/msg10503.html).
2. The application can get the type of a received data message by implementing
a [Codec marshal](http://godoc.org/golang.org/x/net/websocket#Codec.Marshal)
function.
3. The go.net io.Reader and io.Writer operate across WebSocket frame boundaries.
Read returns when the input buffer is full or a frame boundary is
encountered. Each call to Write sends a single frame message. The Gorilla
io.Reader and io.WriteCloser operate on a single WebSocket message.
-19
View File
@@ -1,19 +0,0 @@
// Copyright 2014 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"testing"
)
func BenchmarkMaskBytes(b *testing.B) {
var key [4]byte
data := make([]byte, 1024)
pos := 0
for i := 0; i < b.N; i++ {
pos = maskBytes(key, pos, data)
}
b.SetBytes(int64(len(data)))
}
-350
View File
@@ -1,350 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"bufio"
"bytes"
"crypto/tls"
"encoding/base64"
"errors"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
"time"
)
// ErrBadHandshake is returned when the server response to opening handshake is
// invalid.
var ErrBadHandshake = errors.New("websocket: bad handshake")
// NewClient creates a new client connection using the given net connection.
// The URL u specifies the host and request URI. Use requestHeader to specify
// the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies
// (Cookie). Use the response.Header to get the selected subprotocol
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
//
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
// non-nil *http.Response so that callers can handle redirects, authentication,
// etc.
//
// Deprecated: Use Dialer instead.
func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) {
d := Dialer{
ReadBufferSize: readBufSize,
WriteBufferSize: writeBufSize,
NetDial: func(net, addr string) (net.Conn, error) {
return netConn, nil
},
}
return d.Dial(u.String(), requestHeader)
}
// A Dialer contains options for connecting to WebSocket server.
type Dialer struct {
// NetDial specifies the dial function for creating TCP connections. If
// NetDial is nil, net.Dial is used.
NetDial func(network, addr string) (net.Conn, error)
// Proxy specifies a function to return a proxy for a given
// Request. If the function returns a non-nil error, the
// request is aborted with the provided error.
// If Proxy is nil or returns a nil *URL, no proxy is used.
Proxy func(*http.Request) (*url.URL, error)
// TLSClientConfig specifies the TLS configuration to use with tls.Client.
// If nil, the default configuration is used.
TLSClientConfig *tls.Config
// HandshakeTimeout specifies the duration for the handshake to complete.
HandshakeTimeout time.Duration
// Input and output buffer sizes. If the buffer size is zero, then a
// default value of 4096 is used.
ReadBufferSize, WriteBufferSize int
// Subprotocols specifies the client's requested subprotocols.
Subprotocols []string
}
var errMalformedURL = errors.New("malformed ws or wss URL")
// parseURL parses the URL.
//
// This function is a replacement for the standard library url.Parse function.
// In Go 1.4 and earlier, url.Parse loses information from the path.
func parseURL(s string) (*url.URL, error) {
// From the RFC:
//
// ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
// wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
var u url.URL
switch {
case strings.HasPrefix(s, "ws://"):
u.Scheme = "ws"
s = s[len("ws://"):]
case strings.HasPrefix(s, "wss://"):
u.Scheme = "wss"
s = s[len("wss://"):]
default:
return nil, errMalformedURL
}
if i := strings.Index(s, "?"); i >= 0 {
u.RawQuery = s[i+1:]
s = s[:i]
}
if i := strings.Index(s, "/"); i >= 0 {
u.Opaque = s[i:]
s = s[:i]
} else {
u.Opaque = "/"
}
u.Host = s
if strings.Contains(u.Host, "@") {
// Don't bother parsing user information because user information is
// not allowed in websocket URIs.
return nil, errMalformedURL
}
return &u, nil
}
func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) {
hostPort = u.Host
hostNoPort = u.Host
if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") {
hostNoPort = hostNoPort[:i]
} else {
switch u.Scheme {
case "wss":
hostPort += ":443"
case "https":
hostPort += ":443"
default:
hostPort += ":80"
}
}
return hostPort, hostNoPort
}
// DefaultDialer is a dialer with all fields set to the default zero values.
var DefaultDialer = &Dialer{
Proxy: http.ProxyFromEnvironment,
}
// Dial creates a new client connection. Use requestHeader to specify the
// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie).
// Use the response.Header to get the selected subprotocol
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
//
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
// non-nil *http.Response so that callers can handle redirects, authentication,
// etcetera. The response body may not contain the entire response and does not
// need to be closed by the application.
func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
if d == nil {
d = &Dialer{
Proxy: http.ProxyFromEnvironment,
}
}
challengeKey, err := generateChallengeKey()
if err != nil {
return nil, nil, err
}
u, err := parseURL(urlStr)
if err != nil {
return nil, nil, err
}
switch u.Scheme {
case "ws":
u.Scheme = "http"
case "wss":
u.Scheme = "https"
default:
return nil, nil, errMalformedURL
}
if u.User != nil {
// User name and password are not allowed in websocket URIs.
return nil, nil, errMalformedURL
}
req := &http.Request{
Method: "GET",
URL: u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
Host: u.Host,
}
// Set the request headers using the capitalization for names and values in
// RFC examples. Although the capitalization shouldn't matter, there are
// servers that depend on it. The Header.Set method is not used because the
// method canonicalizes the header names.
req.Header["Upgrade"] = []string{"websocket"}
req.Header["Connection"] = []string{"Upgrade"}
req.Header["Sec-WebSocket-Key"] = []string{challengeKey}
req.Header["Sec-WebSocket-Version"] = []string{"13"}
if len(d.Subprotocols) > 0 {
req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")}
}
for k, vs := range requestHeader {
switch {
case k == "Host":
if len(vs) > 0 {
req.Host = vs[0]
}
case k == "Upgrade" ||
k == "Connection" ||
k == "Sec-Websocket-Key" ||
k == "Sec-Websocket-Version" ||
(k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0):
return nil, nil, errors.New("websocket: duplicate header not allowed: " + k)
default:
req.Header[k] = vs
}
}
hostPort, hostNoPort := hostPortNoPort(u)
var proxyURL *url.URL
// Check wether the proxy method has been configured
if d.Proxy != nil {
proxyURL, err = d.Proxy(req)
}
if err != nil {
return nil, nil, err
}
var targetHostPort string
if proxyURL != nil {
targetHostPort, _ = hostPortNoPort(proxyURL)
} else {
targetHostPort = hostPort
}
var deadline time.Time
if d.HandshakeTimeout != 0 {
deadline = time.Now().Add(d.HandshakeTimeout)
}
netDial := d.NetDial
if netDial == nil {
netDialer := &net.Dialer{Deadline: deadline}
netDial = netDialer.Dial
}
netConn, err := netDial("tcp", targetHostPort)
if err != nil {
return nil, nil, err
}
defer func() {
if netConn != nil {
netConn.Close()
}
}()
if err := netConn.SetDeadline(deadline); err != nil {
return nil, nil, err
}
if proxyURL != nil {
connectHeader := make(http.Header)
if user := proxyURL.User; user != nil {
proxyUser := user.Username()
if proxyPassword, passwordSet := user.Password(); passwordSet {
credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword))
connectHeader.Set("Proxy-Authorization", "Basic "+credential)
}
}
connectReq := &http.Request{
Method: "CONNECT",
URL: &url.URL{Opaque: hostPort},
Host: hostPort,
Header: connectHeader,
}
connectReq.Write(netConn)
// Read response.
// Okay to use and discard buffered reader here, because
// TLS server will not speak until spoken to.
br := bufio.NewReader(netConn)
resp, err := http.ReadResponse(br, connectReq)
if err != nil {
return nil, nil, err
}
if resp.StatusCode != 200 {
f := strings.SplitN(resp.Status, " ", 2)
return nil, nil, errors.New(f[1])
}
}
if u.Scheme == "https" {
cfg := d.TLSClientConfig
if cfg == nil {
cfg = &tls.Config{ServerName: hostNoPort}
} else if cfg.ServerName == "" {
shallowCopy := *cfg
cfg = &shallowCopy
cfg.ServerName = hostNoPort
}
tlsConn := tls.Client(netConn, cfg)
netConn = tlsConn
if err := tlsConn.Handshake(); err != nil {
return nil, nil, err
}
if !cfg.InsecureSkipVerify {
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
return nil, nil, err
}
}
}
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize)
if err := req.Write(netConn); err != nil {
return nil, nil, err
}
resp, err := http.ReadResponse(conn.br, req)
if err != nil {
return nil, nil, err
}
if resp.StatusCode != 101 ||
!strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") ||
!strings.EqualFold(resp.Header.Get("Connection"), "upgrade") ||
resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) {
// Before closing the network connection on return from this
// function, slurp up some of the response to aid application
// debugging.
buf := make([]byte, 1024)
n, _ := io.ReadFull(resp.Body, buf)
resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n]))
return nil, resp, ErrBadHandshake
}
resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol")
netConn.SetDeadline(time.Time{})
netConn = nil // to avoid close in defer.
return conn, resp, nil
}
-451
View File
@@ -1,451 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"testing"
"time"
)
var cstUpgrader = Upgrader{
Subprotocols: []string{"p0", "p1"},
ReadBufferSize: 1024,
WriteBufferSize: 1024,
Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) {
http.Error(w, reason.Error(), status)
},
}
var cstDialer = Dialer{
Subprotocols: []string{"p1", "p2"},
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
type cstHandler struct{ *testing.T }
type cstServer struct {
*httptest.Server
URL string
}
const (
cstPath = "/a/b"
cstRawQuery = "x=y"
cstRequestURI = cstPath + "?" + cstRawQuery
)
func newServer(t *testing.T) *cstServer {
var s cstServer
s.Server = httptest.NewServer(cstHandler{t})
s.Server.URL += cstRequestURI
s.URL = makeWsProto(s.Server.URL)
return &s
}
func newTLSServer(t *testing.T) *cstServer {
var s cstServer
s.Server = httptest.NewTLSServer(cstHandler{t})
s.Server.URL += cstRequestURI
s.URL = makeWsProto(s.Server.URL)
return &s
}
func (t cstHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != cstPath {
t.Logf("path=%v, want %v", r.URL.Path, cstPath)
http.Error(w, "bad path", 400)
return
}
if r.URL.RawQuery != cstRawQuery {
t.Logf("query=%v, want %v", r.URL.RawQuery, cstRawQuery)
http.Error(w, "bad path", 400)
return
}
subprotos := Subprotocols(r)
if !reflect.DeepEqual(subprotos, cstDialer.Subprotocols) {
t.Logf("subprotols=%v, want %v", subprotos, cstDialer.Subprotocols)
http.Error(w, "bad protocol", 400)
return
}
ws, err := cstUpgrader.Upgrade(w, r, http.Header{"Set-Cookie": {"sessionID=1234"}})
if err != nil {
t.Logf("Upgrade: %v", err)
return
}
defer ws.Close()
if ws.Subprotocol() != "p1" {
t.Logf("Subprotocol() = %s, want p1", ws.Subprotocol())
ws.Close()
return
}
op, rd, err := ws.NextReader()
if err != nil {
t.Logf("NextReader: %v", err)
return
}
wr, err := ws.NextWriter(op)
if err != nil {
t.Logf("NextWriter: %v", err)
return
}
if _, err = io.Copy(wr, rd); err != nil {
t.Logf("NextWriter: %v", err)
return
}
if err := wr.Close(); err != nil {
t.Logf("Close: %v", err)
return
}
}
func makeWsProto(s string) string {
return "ws" + strings.TrimPrefix(s, "http")
}
func sendRecv(t *testing.T, ws *Conn) {
const message = "Hello World!"
if err := ws.SetWriteDeadline(time.Now().Add(time.Second)); err != nil {
t.Fatalf("SetWriteDeadline: %v", err)
}
if err := ws.WriteMessage(TextMessage, []byte(message)); err != nil {
t.Fatalf("WriteMessage: %v", err)
}
if err := ws.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
t.Fatalf("SetReadDeadline: %v", err)
}
_, p, err := ws.ReadMessage()
if err != nil {
t.Fatalf("ReadMessage: %v", err)
}
if string(p) != message {
t.Fatalf("message=%s, want %s", p, message)
}
}
func TestProxyDial(t *testing.T) {
s := newServer(t)
defer s.Close()
surl, _ := url.Parse(s.URL)
cstDialer.Proxy = http.ProxyURL(surl)
connect := false
origHandler := s.Server.Config.Handler
// Capture the request Host header.
s.Server.Config.Handler = http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
if r.Method == "CONNECT" {
connect = true
w.WriteHeader(200)
return
}
if !connect {
t.Log("connect not recieved")
http.Error(w, "connect not recieved", 405)
return
}
origHandler.ServeHTTP(w, r)
})
ws, _, err := cstDialer.Dial(s.URL, nil)
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer ws.Close()
sendRecv(t, ws)
cstDialer.Proxy = http.ProxyFromEnvironment
}
func TestProxyAuthorizationDial(t *testing.T) {
s := newServer(t)
defer s.Close()
surl, _ := url.Parse(s.URL)
surl.User = url.UserPassword("username", "password")
cstDialer.Proxy = http.ProxyURL(surl)
connect := false
origHandler := s.Server.Config.Handler
// Capture the request Host header.
s.Server.Config.Handler = http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
proxyAuth := r.Header.Get("Proxy-Authorization")
expectedProxyAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("username:password"))
if r.Method == "CONNECT" && proxyAuth == expectedProxyAuth {
connect = true
w.WriteHeader(200)
return
}
if !connect {
t.Log("connect with proxy authorization not recieved")
http.Error(w, "connect with proxy authorization not recieved", 405)
return
}
origHandler.ServeHTTP(w, r)
})
ws, _, err := cstDialer.Dial(s.URL, nil)
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer ws.Close()
sendRecv(t, ws)
cstDialer.Proxy = http.ProxyFromEnvironment
}
func TestDial(t *testing.T) {
s := newServer(t)
defer s.Close()
ws, _, err := cstDialer.Dial(s.URL, nil)
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer ws.Close()
sendRecv(t, ws)
}
func TestDialTLS(t *testing.T) {
s := newTLSServer(t)
defer s.Close()
certs := x509.NewCertPool()
for _, c := range s.TLS.Certificates {
roots, err := x509.ParseCertificates(c.Certificate[len(c.Certificate)-1])
if err != nil {
t.Fatalf("error parsing server's root cert: %v", err)
}
for _, root := range roots {
certs.AddCert(root)
}
}
u, _ := url.Parse(s.URL)
d := cstDialer
d.NetDial = func(network, addr string) (net.Conn, error) { return net.Dial(network, u.Host) }
d.TLSClientConfig = &tls.Config{RootCAs: certs}
ws, _, err := d.Dial("wss://example.com"+cstRequestURI, nil)
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer ws.Close()
sendRecv(t, ws)
}
func xTestDialTLSBadCert(t *testing.T) {
// This test is deactivated because of noisy logging from the net/http package.
s := newTLSServer(t)
defer s.Close()
ws, _, err := cstDialer.Dial(s.URL, nil)
if err == nil {
ws.Close()
t.Fatalf("Dial: nil")
}
}
func xTestDialTLSNoVerify(t *testing.T) {
s := newTLSServer(t)
defer s.Close()
d := cstDialer
d.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
ws, _, err := d.Dial(s.URL, nil)
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer ws.Close()
sendRecv(t, ws)
}
func TestDialTimeout(t *testing.T) {
s := newServer(t)
defer s.Close()
d := cstDialer
d.HandshakeTimeout = -1
ws, _, err := d.Dial(s.URL, nil)
if err == nil {
ws.Close()
t.Fatalf("Dial: nil")
}
}
func TestDialBadScheme(t *testing.T) {
s := newServer(t)
defer s.Close()
ws, _, err := cstDialer.Dial(s.Server.URL, nil)
if err == nil {
ws.Close()
t.Fatalf("Dial: nil")
}
}
func TestDialBadOrigin(t *testing.T) {
s := newServer(t)
defer s.Close()
ws, resp, err := cstDialer.Dial(s.URL, http.Header{"Origin": {"bad"}