Compare commits
90 Commits
black-box-test
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d7deb3edc3 | |||
| ea6e116836 | |||
| 3cb9eb27e1 | |||
| 0ff81caae4 | |||
| 0de46888eb | |||
| 05c088f158 | |||
| 3c51d0b614 | |||
| 1a9fb1a706 | |||
| d412759c16 | |||
| f4781ee483 | |||
| 964df8a5dd | |||
| 8da763ec2f | |||
| 4523549f71 | |||
| fe1eb8c5eb | |||
| 481311061f | |||
| b117386591 | |||
| 746b978e52 | |||
| da48528e51 | |||
| 065d6ab286 | |||
| 1d1ffa00b5 | |||
| 983f490737 | |||
| 5c9528bce4 | |||
| 8fb96f3cbf | |||
| 9d732fb01a | |||
| 0bd4fa02e9 | |||
| 4a305b32a6 | |||
| 4c5d5302ec | |||
| 994e8e00f1 | |||
| 9ad14daeeb | |||
| 0ac6c7a55c | |||
| 015cbdf07a | |||
| 431a19fe7b | |||
| 6225d8006f | |||
| 7a4568e92d | |||
| 6e46bcb2aa | |||
| 0c599ea5fb | |||
| 46ebd1b62b | |||
| 06449fdb58 | |||
| 6092cd85ca | |||
| 9c49ae16d3 | |||
| 7a794c97d7 | |||
| f0b3aadf74 | |||
| 2eac54a0d3 | |||
| a6f44d2175 | |||
| 6c479ced3c | |||
| 178bbc9b8e | |||
| 9f47b4fe2b | |||
| e9db8ea35e | |||
| 24425a2a9a | |||
| 952983a0f8 | |||
| de869b3e63 | |||
| abf9a980eb | |||
| e15a8a84e2 | |||
| 8c54491e27 | |||
| 03483592fc | |||
| edabc10008 | |||
| bfa96ebbfb | |||
| 5ce17c8856 | |||
| e71294de18 | |||
| a7c5813501 | |||
| 383c02de1c | |||
| 1bae7f13ad | |||
| f07549fb6a | |||
| c272361861 | |||
| 9dfc8c9cc0 | |||
| 6c55af2b5c | |||
| 679ff1e589 | |||
| cbdd4e428a | |||
| 805e9e3957 | |||
| 44bbc1b1ea | |||
| e02773bbeb | |||
| 5fa621d743 | |||
| 880d7bda2a | |||
| 7dad2de275 | |||
| 081611fd87 | |||
| 8810e523f0 | |||
| baf551ecdf | |||
| f3dba811b7 | |||
| 55a7a0b96c | |||
| cadee4e26d | |||
| 03e94887a7 | |||
| e93145efe7 | |||
| 051da185e5 | |||
| 51eacdcf51 | |||
| 412d8be451 | |||
| 68f2e15320 | |||
| a77fb791f7 | |||
| 8bf0786cb5 | |||
| ba3b699517 | |||
| 517462a7af |
+7
-1
@@ -38,7 +38,7 @@ _testmain.go
|
|||||||
|
|
||||||
# project files should be checked into the repository, unless a significant
|
# project files should be checked into the repository, unless a significant
|
||||||
# proportion of contributors will probably not be using SublimeText
|
# proportion of contributors will probably not be using SublimeText
|
||||||
# *.sublime-project
|
*.sublime-project
|
||||||
|
|
||||||
# sftp configuration file
|
# sftp configuration file
|
||||||
sftp-config.json
|
sftp-config.json
|
||||||
@@ -155,4 +155,10 @@ flymake*
|
|||||||
|
|
||||||
ignore_http/*
|
ignore_http/*
|
||||||
config.json
|
config.json
|
||||||
|
config.yml
|
||||||
|
*.pem
|
||||||
|
build
|
||||||
|
.vscode/*
|
||||||
|
debug
|
||||||
|
|
||||||
|
vendor/*
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
FROM golang:alpine as builder
|
||||||
|
RUN mkdir /build
|
||||||
|
ADD . /build/
|
||||||
|
WORKDIR /build
|
||||||
|
RUN apk add git gcc musl-dev
|
||||||
|
RUN go build -o ipe ./cmd
|
||||||
|
FROM alpine
|
||||||
|
USER root
|
||||||
|
RUN mkdir -p /config
|
||||||
|
RUN adduser -S -D -H -h /app appuser
|
||||||
|
COPY ./entrypoint.sh /app/entrypoint.sh
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
USER appuser
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /build/ipe /app/
|
||||||
|
COPY --from=builder /build/config-example.yml /app/config-example.yml
|
||||||
|
VOLUME /config
|
||||||
|
CMD ["/bin/sh", "/app/entrypoint.sh"]
|
||||||
|
EXPOSE 4343
|
||||||
|
EXPOSE 8080
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
default: debug
|
|
||||||
|
|
||||||
debug:
|
|
||||||
GO15VENDOREXPERIMENT=1 go install -ldflags "-w" github.com/dimiro1/ipe
|
|
||||||
|
|
||||||
run-debug: debug
|
|
||||||
${GOPATH}/bin/ipe --config ${GOPATH}/src/github.com/dimiro1/ipe/config.json -logtostderr=true -v=2
|
|
||||||
|
|
||||||
test:
|
|
||||||
GO15VENDOREXPERIMENT=1 go test `go list ./... | grep -v vendor`
|
|
||||||
|
|
||||||
dev-deps:
|
|
||||||
go get github.com/pusher/pusher-http-go
|
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
[](http://goreportcard.com/report/dimiro1/ipe)
|
[](https://goreportcard.com/report/github.com/dimiro1/ipe)
|
||||||
|
|
||||||
|
Try browsing [the code on Sourcegraph](https://sourcegraph.com/github.com/dimiro1/ipe)!
|
||||||
|
|
||||||
# IPÊ
|
# 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?
|
# Why I wrote this software?
|
||||||
|
|
||||||
1. I wanted to learn Go and I needed a non trivial application;
|
1. I wanted to learn Go and I needed a non trivial application;
|
||||||
2. I use Pusher in some projects;
|
2. I use Pusher in some projects;
|
||||||
3. I really like Pusher;
|
3. I really like Pusher;
|
||||||
|
4. I was using Pusher on some projects behind a firewall;
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
@@ -18,13 +21,17 @@ This software is written in Go - the WYSIWYG lang
|
|||||||
* Web Hooks;
|
* Web Hooks;
|
||||||
* Client events;
|
* Client events;
|
||||||
* Complete REST API;
|
* Complete REST API;
|
||||||
* Easy instalation;
|
* Easy installation;
|
||||||
* A single binary without dependencies;
|
* A single binary without dependencies;
|
||||||
* Easy configuration;
|
* Easy configuration;
|
||||||
* Protocol version 7;
|
* Protocol version 7;
|
||||||
* Multiple apps in the same instance;
|
* Multiple apps in the same instance;
|
||||||
* Drop in replacement for pusher server;
|
* Drop in replacement for pusher server;
|
||||||
|
|
||||||
|
# Download pre built binaries
|
||||||
|
|
||||||
|
You can download pre built binaries from the [releases tab](https://github.com/dimiro1/ipe/releases).
|
||||||
|
|
||||||
# Building
|
# Building
|
||||||
|
|
||||||
```console
|
```console
|
||||||
@@ -37,26 +44,40 @@ or simply
|
|||||||
$ go install github.com/dimiro1/ipe
|
$ 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?
|
# How to configure?
|
||||||
|
|
||||||
## The server
|
## The server
|
||||||
|
|
||||||
```json
|
```yaml
|
||||||
{
|
|
||||||
"Host": ":8080",
|
---
|
||||||
"Apps": [
|
host: ":8080"
|
||||||
{
|
profiling: false
|
||||||
"ApplicationDisabled": false,
|
ssl:
|
||||||
"Secret": "APP_SECRET",
|
enabled: false
|
||||||
"Key": "APP_KEY",
|
host: ":4343"
|
||||||
"Name": "APP_NAME",
|
key_file: "key.pem"
|
||||||
"AppID": "APP_ID",
|
cert_file: "cert.pem"
|
||||||
"UserEvents": true,
|
apps:
|
||||||
"WebHooks": true,
|
- name: "Sample Application"
|
||||||
"URLWebHook": "http://localhost:4567/php/hook.php"
|
enabled: true
|
||||||
}
|
only_ssl: false
|
||||||
]
|
key: "278d525bdf162c739803"
|
||||||
}
|
secret: "${APP_SECRET}" # Expand env vars
|
||||||
|
app_id: "1"
|
||||||
|
user_events: true
|
||||||
|
webhooks:
|
||||||
|
enabled: true
|
||||||
|
url: "http://127.0.0.1:5000/hook"
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Libraries
|
## Libraries
|
||||||
@@ -64,9 +85,11 @@ $ go install github.com/dimiro1/ipe
|
|||||||
### Client javascript library
|
### Client javascript library
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var pusher = new Pusher(APP_KEY, {
|
let pusher = new Pusher(APP_KEY, {
|
||||||
wsHost: 'localhost',
|
wsHost: 'localhost',
|
||||||
wsPort: 8080,
|
wsPort: 8080,
|
||||||
|
wssPort: 4433, // Required if encrypted is true
|
||||||
|
encrypted: false, // Optional. the application must use only SSL connections
|
||||||
enabledTransports: ["ws", "flash"],
|
enabledTransports: ["ws", "flash"],
|
||||||
disabledTransports: ["flash"]
|
disabledTransports: ["flash"]
|
||||||
});
|
});
|
||||||
@@ -87,10 +110,29 @@ PHP
|
|||||||
$pusher = new Pusher(APP_KEY, APP_SECRET, APP_ID, DEBUG, "http://localhost", "8080");
|
$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
|
NodeJS
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var pusher = new Pusher({
|
let pusher = new Pusher({
|
||||||
appId: APP_ID,
|
appId: APP_ID,
|
||||||
key: APP_KEY,
|
key: APP_KEY,
|
||||||
secret: APP_SECRET
|
secret: APP_SECRET
|
||||||
@@ -123,7 +165,7 @@ Feel free to fork this repo.
|
|||||||
|
|
||||||
# Pusher
|
# 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?
|
# Where this name came from?
|
||||||
|
|
||||||
@@ -137,7 +179,7 @@ Claudemiro Alves Feitosa Neto
|
|||||||
|
|
||||||
# LICENSE
|
# 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
|
Use of this source code is governed by a MIT-style
|
||||||
license that can be found in the LICENSE file.
|
license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
IPÊ
|
|
||||||
---
|
|
||||||
|
|
||||||
* TODO [11/14]
|
|
||||||
* [X] Autenticação API Rest
|
|
||||||
* [X] Autenticação Websockets
|
|
||||||
* [X] Ping e Pong
|
|
||||||
* [ ] Escrever testes automatizados
|
|
||||||
* [ ] 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
|
|
||||||
+193
-54
@@ -2,19 +2,130 @@
|
|||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package ipe
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
log "github.com/golang/glog"
|
log "github.com/golang/glog"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"ipe/events"
|
||||||
|
"ipe/storage"
|
||||||
|
"ipe/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// An event consists of a name and data (typically JSON) which may be sent to all subscribers to a particular channel or channels.
|
// // Maximum event size permitted 10 kB
|
||||||
|
// See: http://blogs.gnome.org/cneumair/2008/09/30/1-kb-1024-bytes-no-1-kb-1000-bytes/
|
||||||
|
const maxDataEventSize = 10 * 1000
|
||||||
|
|
||||||
|
// Prepare QueryString
|
||||||
|
func prepareQueryString(params url.Values) string {
|
||||||
|
var keys []string
|
||||||
|
|
||||||
|
for key := range params {
|
||||||
|
keys = append(keys, strings.ToLower(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
var pieces []string
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
pieces = append(pieces, key+"="+params.Get(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(pieces, "&")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication Authenticate pusher
|
||||||
|
// see: https://gist.github.com/mloughran/376898
|
||||||
|
//
|
||||||
|
// The signature is a HMAC SHA256 hex digest.
|
||||||
|
// This is generated by signing a string made up of the following components concatenated with newline characters \n.
|
||||||
|
//
|
||||||
|
// * The uppercase request method (e.g. POST)
|
||||||
|
// * The request path (e.g. /some/resource)
|
||||||
|
// * The query parameters sorted by key, with keys converted to lowercase, then joined as in the query string.
|
||||||
|
// Note that the string must not be url escaped (e.g. given the keys auth_key: foo, Name: Something else, you get auth_key=foo&name=Something else)
|
||||||
|
func Authentication(storage storage.Storage) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
pathVars = mux.Vars(r)
|
||||||
|
appID = pathVars["app_id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
app, err := storage.GetAppByAppID(appID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := r.URL.Query()
|
||||||
|
|
||||||
|
signature := query.Get("auth_signature")
|
||||||
|
query.Del("auth_signature")
|
||||||
|
|
||||||
|
queryString := prepareQueryString(query)
|
||||||
|
|
||||||
|
toSign := strings.ToUpper(r.Method) + "\n" + r.URL.Path + "\n" + queryString
|
||||||
|
|
||||||
|
if utils.HashMAC([]byte(toSign), []byte(app.Secret)) == signature {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
} else {
|
||||||
|
log.Error("Not authorized")
|
||||||
|
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAppDisabled Check if the application is disabled
|
||||||
|
func CheckAppDisabled(storage storage.Storage) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
pathVars = mux.Vars(r)
|
||||||
|
appID = pathVars["app_id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
currentApp, err := storage.GetAppByAppID(appID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !currentApp.Enabled {
|
||||||
|
http.Error(w, "Application disabled", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostEvents handle post events
|
||||||
|
type PostEvents struct{ storage storage.Storage }
|
||||||
|
|
||||||
|
// NewPostEvents return a new PostEvents handler
|
||||||
|
func NewPostEvents(storage storage.Storage) *PostEvents {
|
||||||
|
return &PostEvents{storage: storage}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP An event consists of a name and data (typically JSON) which may be sent to all subscribers to a particular channel or channels.
|
||||||
// This is conventionally known as triggering an event.
|
// 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.
|
// The body should contain a Hash of parameters encoded as JSON where data parameter itself is JSON encoded.
|
||||||
@@ -29,11 +140,13 @@ import (
|
|||||||
// Response is an empty JSON hash.
|
// Response is an empty JSON hash.
|
||||||
//
|
//
|
||||||
// POST /apps/{app_id}/events
|
// POST /apps/{app_id}/events
|
||||||
func postEvents(w http.ResponseWriter, r *http.Request) {
|
func (h *PostEvents) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
var (
|
||||||
appID := vars["app_id"]
|
pathVars = mux.Vars(r)
|
||||||
|
appID = pathVars["app_id"]
|
||||||
|
)
|
||||||
|
|
||||||
app, err := conf.GetAppByAppID(appID)
|
app, err := h.storage.GetAppByAppID(appID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
||||||
@@ -55,7 +168,7 @@ func postEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The event data should not be larger than 10KB.
|
// The event data should not be larger than 10KB.
|
||||||
if len(input.Data) > MAX_DATA_EVENT_SIZE {
|
if len(input.Data) > maxDataEventSize {
|
||||||
http.Error(w, "Request too large.", http.StatusRequestEntityTooLarge)
|
http.Error(w, "Request too large.", http.StatusRequestEntityTooLarge)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -68,15 +181,28 @@ func postEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
for _, c := range input.Channels {
|
for _, c := range input.Channels {
|
||||||
channel := app.FindOrCreateChannelByChannelID(c)
|
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.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.
|
// and optionally one or more attributes for each channel.
|
||||||
//
|
//
|
||||||
// Notes:
|
// Notes:
|
||||||
@@ -95,15 +221,15 @@ func postEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// GET /apps/{app_id}/channels
|
// GET /apps/{app_id}/channels
|
||||||
func getChannels(w http.ResponseWriter, r *http.Request) {
|
func (h *GetChannels) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
params := r.URL.Query()
|
var (
|
||||||
vars := mux.Vars(r)
|
pathVars = mux.Vars(r)
|
||||||
|
queryVars = r.URL.Query()
|
||||||
appID := vars["app_id"]
|
appID = pathVars["app_id"]
|
||||||
filter := params.Get("filter_by_prefix")
|
filter = queryVars.Get("filter_by_prefix")
|
||||||
info := params.Get("info")
|
info = queryVars.Get("info")
|
||||||
|
attributes = strings.Split(info, ",")
|
||||||
attributes := strings.Split(info, ",")
|
)
|
||||||
|
|
||||||
requestedUserCount := false
|
requestedUserCount := false
|
||||||
|
|
||||||
@@ -120,7 +246,7 @@ func getChannels(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app, err := conf.GetAppByAppID(appID)
|
app, err := h.storage.GetAppByAppID(appID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
||||||
@@ -132,30 +258,30 @@ func getChannels(w http.ResponseWriter, r *http.Request) {
|
|||||||
case "presence-":
|
case "presence-":
|
||||||
for _, c := range app.PresenceChannels() {
|
for _, c := range app.PresenceChannels() {
|
||||||
if requestedUserCount {
|
if requestedUserCount {
|
||||||
channels[c.ChannelID] = struct {
|
channels[c.ID] = struct {
|
||||||
UserCount int `json:"user_count"`
|
UserCount int `json:"user_count"`
|
||||||
}{
|
}{
|
||||||
c.TotalUsers(),
|
c.TotalUsers(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channels[c.ChannelID] = struct{}{}
|
channels[c.ID] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "public-":
|
case "public-":
|
||||||
for _, c := range app.PublicChannels() {
|
for _, c := range app.PublicChannels() {
|
||||||
channels[c.ChannelID] = struct{}{}
|
channels[c.ID] = struct{}{}
|
||||||
}
|
}
|
||||||
case "private-":
|
case "private-":
|
||||||
for _, c := range app.PrivateChannels() {
|
for _, c := range app.PrivateChannels() {
|
||||||
channels[c.ChannelID] = struct{}{}
|
channels[c.ID] = struct{}{}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
for _, c := range app.Channels {
|
for _, c := range app.Channels() {
|
||||||
channels[c.ChannelID] = struct{}{}
|
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 := make(map[string]interface{}, 1)
|
||||||
js["channels"] = channels
|
js["channels"] = channels
|
||||||
@@ -166,7 +292,15 @@ func getChannels(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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:
|
// Example:
|
||||||
// {
|
// {
|
||||||
@@ -176,30 +310,28 @@ func getChannels(w http.ResponseWriter, r *http.Request) {
|
|||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// GET /apps/{app_id}/channels/{channel_name}
|
// GET /apps/{app_id}/channels/{channel_name}
|
||||||
func getChannel(w http.ResponseWriter, r *http.Request) {
|
func (h *GetChannel) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
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, ",")
|
||||||
|
)
|
||||||
|
|
||||||
params := r.URL.Query()
|
app, err := h.storage.GetAppByAppID(appID)
|
||||||
vars := mux.Vars(r)
|
|
||||||
|
|
||||||
appID := vars["app_id"]
|
|
||||||
app, err := conf.GetAppByAppID(appID)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
channelName := vars["channel_name"]
|
|
||||||
|
|
||||||
// Channel name could not be empty
|
// Channel name could not be empty
|
||||||
if strings.TrimSpace(channelName) == "" {
|
if strings.TrimSpace(channelName) == "" {
|
||||||
http.Error(w, "Empty channel name", http.StatusBadRequest)
|
http.Error(w, "Empty channel name", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
info := params.Get("info")
|
|
||||||
attributes := strings.Split(info, ",")
|
|
||||||
|
|
||||||
// Attributes requested
|
// Attributes requested
|
||||||
requestedUserCount := false
|
requestedUserCount := false
|
||||||
requestedSubscriptionCount := false
|
requestedSubscriptionCount := false
|
||||||
@@ -247,15 +379,22 @@ func getChannel(w http.ResponseWriter, r *http.Request) {
|
|||||||
dtoChannel.SubscriptionCount = channel.TotalSubscriptions()
|
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 {
|
if err := json.NewEncoder(w).Encode(dtoChannel); err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
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:
|
// Example:
|
||||||
// {
|
// {
|
||||||
@@ -266,20 +405,21 @@ func getChannel(w http.ResponseWriter, r *http.Request) {
|
|||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// GET /apps/{app_id}/channels/{channel_name}/users
|
// GET /apps/{app_id}/channels/{channel_name}/users
|
||||||
func getChannelUsers(w http.ResponseWriter, r *http.Request) {
|
func (h *GetChannelUsers) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
var (
|
||||||
|
pathVars = mux.Vars(r)
|
||||||
|
appID = pathVars["app_id"]
|
||||||
|
channelName = pathVars["channel_name"]
|
||||||
|
)
|
||||||
|
|
||||||
appID := vars["app_id"]
|
isPresence := utils.IsPresenceChannel(channelName)
|
||||||
channelName := vars["channel_name"]
|
|
||||||
|
|
||||||
isPresence := strings.HasPrefix(channelName, "presence-")
|
|
||||||
|
|
||||||
if !isPresence {
|
if !isPresence {
|
||||||
http.Error(w, "This api endpoint is restricted to presence channels.", http.StatusBadRequest)
|
http.Error(w, "This api endpoint is restricted to presence channels.", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app, err := conf.GetAppByAppID(appID)
|
app, err := h.storage.GetAppByAppID(appID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
||||||
@@ -298,7 +438,7 @@ func getChannelUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var users []interface{}
|
var users []interface{}
|
||||||
|
|
||||||
for _, s := range channel.Subscriptions {
|
for _, s := range channel.Subscriptions() {
|
||||||
users = append(users, struct {
|
users = append(users, struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
}{s.ID})
|
}{s.ID})
|
||||||
@@ -306,8 +446,7 @@ func getChannelUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
result["users"] = users
|
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 {
|
if err := json.NewEncoder(w).Encode(result); err != nil {
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"ipe/app"
|
||||||
|
channel2 "ipe/channel"
|
||||||
|
"ipe/connection"
|
||||||
|
"ipe/mocks"
|
||||||
|
"ipe/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testApp *app.Application
|
||||||
|
database storage.Storage
|
||||||
|
id = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestApp() *app.Application {
|
||||||
|
a := app.NewApplication("Test", strconv.Itoa(id), "123", "123", false, false, true, false, "")
|
||||||
|
id++
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
testApp = newTestApp()
|
||||||
|
|
||||||
|
channel := channel2.New("presence-c1")
|
||||||
|
testApp.AddChannel(channel)
|
||||||
|
testApp.AddChannel(channel2.New("c2"))
|
||||||
|
testApp.AddChannel(channel2.New("private-c3"))
|
||||||
|
|
||||||
|
conn := connection.New("123.456", mocks.MockSocket{})
|
||||||
|
_ = testApp.Subscribe(channel, conn, "{}")
|
||||||
|
|
||||||
|
conn = connection.New("321.654", mocks.MockSocket{})
|
||||||
|
_ = testApp.Subscribe(channel, conn, "{}")
|
||||||
|
|
||||||
|
_storage := storage.NewInMemory()
|
||||||
|
_ = _storage.AddApp(testApp)
|
||||||
|
|
||||||
|
database = _storage
|
||||||
|
}
|
||||||
|
|
||||||
|
// All channels
|
||||||
|
func Test_getChannels_all(t *testing.T) {
|
||||||
|
appID := testApp.AppID
|
||||||
|
|
||||||
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels", appID), nil)
|
||||||
|
r = mux.SetURLVars(r, map[string]string{
|
||||||
|
"app_id": appID,
|
||||||
|
})
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := &GetChannels{database}
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
_ = json.Unmarshal(w.Body.Bytes(), &data)
|
||||||
|
|
||||||
|
channels := data["channels"].(map[string]interface{})
|
||||||
|
|
||||||
|
if len(channels) != 3 {
|
||||||
|
t.Errorf("len(%q) == %d, want %d", channels, len(channels), 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_getChannels_filter_by_presence_prefix(t *testing.T) {
|
||||||
|
appID := testApp.AppID
|
||||||
|
|
||||||
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-", appID), nil)
|
||||||
|
r = mux.SetURLVars(r, map[string]string{
|
||||||
|
"app_id": appID,
|
||||||
|
})
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := &GetChannels{database}
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
_ = json.Unmarshal(w.Body.Bytes(), &data)
|
||||||
|
|
||||||
|
channels := data["channels"].(map[string]interface{})
|
||||||
|
|
||||||
|
if len(channels) != 1 {
|
||||||
|
t.Errorf("len(%q) == %d, want %d", channels, len(channels), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only presence channels and user_count
|
||||||
|
func Test_getChannels_filter_by_presence_prefix_and_user_count(t *testing.T) {
|
||||||
|
appID := testApp.AppID
|
||||||
|
|
||||||
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-&info=user_count", appID), nil)
|
||||||
|
r = mux.SetURLVars(r, map[string]string{
|
||||||
|
"app_id": appID,
|
||||||
|
})
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := &GetChannels{database}
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
_ = json.Unmarshal(w.Body.Bytes(), &data)
|
||||||
|
|
||||||
|
channels := data["channels"].(map[string]interface{})
|
||||||
|
|
||||||
|
if len(channels) != 1 {
|
||||||
|
t.Errorf("len(%q) == %d, want %d", channels, len(channels), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
c, exists := channels["presence-c1"]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
t.Errorf("!exists == %t, want %t", !exists, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
_channel := c.(map[string]interface{})
|
||||||
|
|
||||||
|
if _channel["user_count"] != float64(1) {
|
||||||
|
t.Errorf("_channel['user_count'] == %f, want %d", _channel["user_count"], 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// User count only allowed in Presence channels
|
||||||
|
func Test_getChannels_filter_by_private_prefix_and_info_user_count(t *testing.T) {
|
||||||
|
appID := testApp.AppID
|
||||||
|
|
||||||
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-&info=user_count", appID), nil)
|
||||||
|
r = mux.SetURLVars(r, map[string]string{
|
||||||
|
"app_id": appID,
|
||||||
|
})
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := &GetChannels{database}
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_getChannels_filter_by_public_prefix(t *testing.T) {
|
||||||
|
appID := testApp.AppID
|
||||||
|
|
||||||
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=public-", appID), nil)
|
||||||
|
r = mux.SetURLVars(r, map[string]string{
|
||||||
|
"app_id": appID,
|
||||||
|
})
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := &GetChannels{database}
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
|
||||||
|
_ = json.Unmarshal(w.Body.Bytes(), &data)
|
||||||
|
|
||||||
|
channels := data["channels"].(map[string]interface{})
|
||||||
|
|
||||||
|
if len(channels) != 1 {
|
||||||
|
t.Errorf("len(%q) == %d, want %d", channels, len(channels), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, exists := channels["c2"]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
t.Errorf("!exists == %t, want %t", !exists, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_getChannels_filter_by_private_prefix(t *testing.T) {
|
||||||
|
appID := testApp.AppID
|
||||||
|
|
||||||
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-", appID), nil)
|
||||||
|
r = mux.SetURLVars(r, map[string]string{
|
||||||
|
"app_id": appID,
|
||||||
|
})
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := &GetChannels{database}
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
|
||||||
|
_ = json.Unmarshal(w.Body.Bytes(), &data)
|
||||||
|
|
||||||
|
channels := data["channels"].(map[string]interface{})
|
||||||
|
|
||||||
|
if len(channels) != 1 {
|
||||||
|
t.Errorf("len(%q) == %d, want %d", channels, len(channels), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, exists := channels["private-c3"]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
t.Errorf("!exists == %t, want %t", !exists, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
+314
@@ -0,0 +1,314 @@
|
|||||||
|
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"expvar"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
|
||||||
|
"ipe/channel"
|
||||||
|
"ipe/connection"
|
||||||
|
"ipe/events"
|
||||||
|
"ipe/subscription"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Application represents a Pusher application
|
||||||
|
type Application struct {
|
||||||
|
sync.RWMutex
|
||||||
|
|
||||||
|
Name string
|
||||||
|
AppID string
|
||||||
|
Key string
|
||||||
|
Secret string
|
||||||
|
OnlySSL bool
|
||||||
|
Enabled bool
|
||||||
|
UserEvents bool
|
||||||
|
WebHooks bool
|
||||||
|
URLWebHook string
|
||||||
|
|
||||||
|
channels map[string]*channel.Channel
|
||||||
|
connections map[string]*connection.Connection
|
||||||
|
|
||||||
|
Stats *expvar.Map `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApplication returns a new Application
|
||||||
|
func NewApplication(
|
||||||
|
name,
|
||||||
|
appID,
|
||||||
|
key,
|
||||||
|
secret string,
|
||||||
|
onlySSL,
|
||||||
|
enabled,
|
||||||
|
userEvents,
|
||||||
|
webHooks bool,
|
||||||
|
webHookURL string,
|
||||||
|
) *Application {
|
||||||
|
|
||||||
|
a := &Application{
|
||||||
|
Name: name,
|
||||||
|
AppID: appID,
|
||||||
|
Key: key,
|
||||||
|
Secret: secret,
|
||||||
|
OnlySSL: onlySSL,
|
||||||
|
Enabled: enabled,
|
||||||
|
UserEvents: userEvents,
|
||||||
|
WebHooks: webHooks,
|
||||||
|
URLWebHook: webHookURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
a.connections = make(map[string]*connection.Connection)
|
||||||
|
a.channels = make(map[string]*channel.Channel)
|
||||||
|
a.Stats = expvar.NewMap(fmt.Sprintf("%s (%s)", a.Name, a.AppID))
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channels returns the full list of channels
|
||||||
|
func (a *Application) Channels() []*channel.Channel {
|
||||||
|
a.RLock()
|
||||||
|
defer a.RUnlock()
|
||||||
|
|
||||||
|
var channels []*channel.Channel
|
||||||
|
|
||||||
|
for _, c := range a.channels {
|
||||||
|
channels = append(channels, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
// PresenceChannels Only Presence channels
|
||||||
|
func (a *Application) PresenceChannels() []*channel.Channel {
|
||||||
|
a.RLock()
|
||||||
|
defer a.RUnlock()
|
||||||
|
|
||||||
|
var channels []*channel.Channel
|
||||||
|
|
||||||
|
for _, c := range a.channels {
|
||||||
|
if c.IsPresence() {
|
||||||
|
channels = append(channels, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrivateChannels Only Private channels
|
||||||
|
func (a *Application) PrivateChannels() []*channel.Channel {
|
||||||
|
a.RLock()
|
||||||
|
defer a.RUnlock()
|
||||||
|
|
||||||
|
var channels []*channel.Channel
|
||||||
|
|
||||||
|
for _, c := range a.channels {
|
||||||
|
if c.IsPrivate() {
|
||||||
|
channels = append(channels, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicChannels Only Public channels
|
||||||
|
func (a *Application) PublicChannels() []*channel.Channel {
|
||||||
|
a.RLock()
|
||||||
|
defer a.RUnlock()
|
||||||
|
|
||||||
|
var channels []*channel.Channel
|
||||||
|
|
||||||
|
for _, c := range a.channels {
|
||||||
|
if c.IsPublic() {
|
||||||
|
channels = append(channels, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect Socket
|
||||||
|
func (a *Application) Disconnect(socketID string) {
|
||||||
|
log.Infof("disconnecting socket %+v", socketID)
|
||||||
|
|
||||||
|
conn, err := a.FindConnection(socketID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("socket not found, %+v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe from channels
|
||||||
|
for _, c := range a.channels {
|
||||||
|
if c.IsSubscribed(conn) {
|
||||||
|
if err := c.Unsubscribe(conn); err != nil {
|
||||||
|
log.Errorf("error while calling Channel.Unsubscribe, %+v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from Application
|
||||||
|
a.Lock()
|
||||||
|
_, exists := a.connections[conn.SocketID]
|
||||||
|
a.Unlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Lock()
|
||||||
|
delete(a.connections, conn.SocketID)
|
||||||
|
a.Unlock()
|
||||||
|
|
||||||
|
a.Stats.Add("TotalConnections", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect a new Subscriber
|
||||||
|
func (a *Application) Connect(conn *connection.Connection) {
|
||||||
|
log.Infof("adding a new Connection %s to Application %s", conn.SocketID, a.Name)
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
a.connections[conn.SocketID] = conn
|
||||||
|
|
||||||
|
a.Stats.Add("TotalConnections", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindConnection Find a Connection on this Application
|
||||||
|
func (a *Application) FindConnection(socketID string) (*connection.Connection, error) {
|
||||||
|
a.RLock()
|
||||||
|
defer a.RUnlock()
|
||||||
|
|
||||||
|
conn, exists := a.connections[socketID]
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("connection not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveChannel removes the Channel from Application
|
||||||
|
func (a *Application) RemoveChannel(c *channel.Channel) {
|
||||||
|
log.Infof("remove the Channel %s from Application %s", c.ID, a.Name)
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
delete(a.channels, c.ID)
|
||||||
|
|
||||||
|
if c.IsPresence() {
|
||||||
|
a.Stats.Add("TotalPresenceChannels", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsPrivate() {
|
||||||
|
a.Stats.Add("TotalPrivateChannels", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsPublic() {
|
||||||
|
a.Stats.Add("TotalPublicChannels", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Stats.Add("TotalChannels", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddChannel Add a new Channel to this APP
|
||||||
|
func (a *Application) AddChannel(c *channel.Channel) {
|
||||||
|
log.Infof("adding a new Channel %s to Application %s", c.ID, a.Name)
|
||||||
|
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
a.channels[c.ID] = c
|
||||||
|
|
||||||
|
if c.IsPresence() {
|
||||||
|
a.Stats.Add("TotalPresenceChannels", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsPrivate() {
|
||||||
|
a.Stats.Add("TotalPrivateChannels", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsPublic() {
|
||||||
|
a.Stats.Add("TotalPublicChannels", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Stats.Add("TotalChannels", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindOrCreateChannelByChannelID Returns a Channel from this Application
|
||||||
|
// If not found then the Channel is created and added to this Application
|
||||||
|
func (a *Application) FindOrCreateChannelByChannelID(n string) *channel.Channel {
|
||||||
|
c, err := a.FindChannelByChannelID(n)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c = channel.New(
|
||||||
|
n,
|
||||||
|
channel.WithChannelOccupiedListener(func(c *channel.Channel, s *subscription.Subscription) {
|
||||||
|
a.TriggerChannelOccupiedHook(c)
|
||||||
|
}),
|
||||||
|
channel.WithChannelVacatedListener(func(c *channel.Channel, s *subscription.Subscription) {
|
||||||
|
a.TriggerChannelVacatedHook(c)
|
||||||
|
}),
|
||||||
|
channel.WithMemberAddedListener(func(c *channel.Channel, s *subscription.Subscription) {
|
||||||
|
a.TriggerMemberAddedHook(c, s)
|
||||||
|
}),
|
||||||
|
channel.WithMemberRemovedListener(func(c *channel.Channel, s *subscription.Subscription) {
|
||||||
|
a.TriggerMemberRemovedHook(c, s)
|
||||||
|
}),
|
||||||
|
channel.WithClientEventListener(func(c *channel.Channel, s *subscription.Subscription, event string, data interface{}) {
|
||||||
|
a.TriggerClientEventHook(c, s, event, data)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
a.AddChannel(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindChannelByChannelID Find the Channel by Channel ID
|
||||||
|
func (a *Application) FindChannelByChannelID(n string) (*channel.Channel, error) {
|
||||||
|
a.RLock()
|
||||||
|
defer a.RUnlock()
|
||||||
|
|
||||||
|
c, exists := a.channels[n]
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("channel does not exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish an event into the channel
|
||||||
|
// skip the ignore connection
|
||||||
|
func (a *Application) Publish(c *channel.Channel, event events.Raw, ignore string) error {
|
||||||
|
a.Stats.Add("TotalUniqueMessages", 1)
|
||||||
|
|
||||||
|
return c.Publish(event, ignore)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe unsubscribe the given connection from the channel
|
||||||
|
// remove the channel from the application if it is empty
|
||||||
|
func (a *Application) Unsubscribe(c *channel.Channel, conn *connection.Connection) error {
|
||||||
|
err := c.Unsubscribe(conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.IsOccupied() {
|
||||||
|
a.RemoveChannel(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe the connection into the given channel
|
||||||
|
func (a *Application) Subscribe(c *channel.Channel, conn *connection.Connection, data string) error {
|
||||||
|
return c.Subscribe(conn, data)
|
||||||
|
}
|
||||||
+255
@@ -0,0 +1,255 @@
|
|||||||
|
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
channel2 "ipe/channel"
|
||||||
|
"ipe/connection"
|
||||||
|
"ipe/mocks"
|
||||||
|
)
|
||||||
|
|
||||||
|
var id = 0
|
||||||
|
|
||||||
|
func newTestApp() *Application {
|
||||||
|
a := NewApplication("Test", strconv.Itoa(id), "123", "123", false, false, true, false, "")
|
||||||
|
id++
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnect(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
app.Connect(connection.New("socketID", mocks.MockSocket{}))
|
||||||
|
|
||||||
|
if len(app.connections) != 1 {
|
||||||
|
t.Errorf("len(Application.connections) == %d, wants %d", len(app.connections), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisconnect(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
app.Connect(connection.New("socketID", mocks.MockSocket{}))
|
||||||
|
app.Disconnect("socketID")
|
||||||
|
|
||||||
|
if len(app.connections) != 0 {
|
||||||
|
t.Errorf("len(Application.connections) == %d, wants %d", len(app.connections), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindConnection(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
app.Connect(connection.New("socketID", mocks.MockSocket{}))
|
||||||
|
|
||||||
|
if _, err := app.FindConnection("socketID"); err != nil {
|
||||||
|
t.Errorf("Application.FindConnection('socketID') == _, %q, wants %v", err, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := app.FindConnection("NotFound"); err == nil {
|
||||||
|
t.Errorf("Application.FindConnection('socketID') == _, %q, wants !nil", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindChannelByChannelID(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
channel := channel2.New("ID")
|
||||||
|
app.AddChannel(channel)
|
||||||
|
|
||||||
|
if _, err := app.FindChannelByChannelID("ID"); err != nil {
|
||||||
|
t.Errorf("Application.FindChannelByChannelID('ID') == _, %q, wants %v", err, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindOrCreateChannelByChannelID(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
if len(app.channels) != 0 {
|
||||||
|
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.FindOrCreateChannelByChannelID("ID")
|
||||||
|
|
||||||
|
if len(app.channels) != 1 {
|
||||||
|
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveChannel(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
if len(app.channels) != 0 {
|
||||||
|
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
channel := channel2.New("ID")
|
||||||
|
app.AddChannel(channel)
|
||||||
|
|
||||||
|
if len(app.channels) != 1 {
|
||||||
|
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.RemoveChannel(channel)
|
||||||
|
|
||||||
|
if len(app.channels) != 0 {
|
||||||
|
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_add_channels(t *testing.T) {
|
||||||
|
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
// Public
|
||||||
|
|
||||||
|
if len(app.PublicChannels()) != 0 {
|
||||||
|
t.Errorf("len(Application.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.AddChannel(channel2.New("ID"))
|
||||||
|
|
||||||
|
if len(app.PublicChannels()) != 1 {
|
||||||
|
t.Errorf("len(Application.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Presence
|
||||||
|
|
||||||
|
if len(app.PresenceChannels()) != 0 {
|
||||||
|
t.Errorf("len(Application.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.AddChannel(channel2.New("presence-test"))
|
||||||
|
|
||||||
|
if len(app.PresenceChannels()) != 1 {
|
||||||
|
t.Errorf("len(Application.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private
|
||||||
|
|
||||||
|
if len(app.PrivateChannels()) != 0 {
|
||||||
|
t.Errorf("len(Application.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.AddChannel(channel2.New("private-test"))
|
||||||
|
|
||||||
|
if len(app.PrivateChannels()) != 1 {
|
||||||
|
t.Errorf("len(Application.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AllChannels(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
app.AddChannel(channel2.New("private-test"))
|
||||||
|
app.AddChannel(channel2.New("presence-test"))
|
||||||
|
app.AddChannel(channel2.New("test"))
|
||||||
|
|
||||||
|
if len(app.channels) != 3 {
|
||||||
|
t.Errorf("len(Application.channels) == %d, wants %d", len(app.channels), 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_New_Subscriber(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
if len(app.connections) != 0 {
|
||||||
|
t.Errorf("len(Application.connections) == %d, wants %d", len(app.connections), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := connection.New("1", mocks.MockSocket{})
|
||||||
|
app.Connect(conn)
|
||||||
|
|
||||||
|
if len(app.connections) != 1 {
|
||||||
|
t.Errorf("len(Application.connections) == %d, wants %d", len(app.connections), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_find_subscriber(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
conn := connection.New("1", mocks.MockSocket{})
|
||||||
|
app.Connect(conn)
|
||||||
|
|
||||||
|
conn, err := app.FindConnection("1")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if conn.SocketID != "1" {
|
||||||
|
t.Errorf("conn.SocketID == %s, wants %s", conn.SocketID, "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a wrong subscriber
|
||||||
|
|
||||||
|
conn, err = app.FindConnection("DoesNotExists")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("err == %q, wants !nil", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if conn != nil {
|
||||||
|
t.Errorf("conn == %q, wants nil", conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_find_or_create_channels(t *testing.T) {
|
||||||
|
app := newTestApp()
|
||||||
|
|
||||||
|
// Public
|
||||||
|
if len(app.PublicChannels()) != 0 {
|
||||||
|
t.Errorf("len(Application.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := app.FindOrCreateChannelByChannelID("id")
|
||||||
|
|
||||||
|
if len(app.PublicChannels()) != 1 {
|
||||||
|
t.Errorf("len(Application.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ID != "id" {
|
||||||
|
t.Errorf("c.id == %s, wants %s", c.ID, "id")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Presence
|
||||||
|
if len(app.PresenceChannels()) != 0 {
|
||||||
|
t.Errorf("len(Application.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
c = app.FindOrCreateChannelByChannelID("presence-test")
|
||||||
|
|
||||||
|
if len(app.PresenceChannels()) != 1 {
|
||||||
|
t.Errorf("len(Application.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ID != "presence-test" {
|
||||||
|
t.Errorf("c.id == %s, wants %s", c.ID, "presence-test")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private
|
||||||
|
if len(app.PrivateChannels()) != 0 {
|
||||||
|
t.Errorf("len(Application.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
c = app.FindOrCreateChannelByChannelID("private-test")
|
||||||
|
|
||||||
|
if len(app.PrivateChannels()) != 1 {
|
||||||
|
t.Errorf("len(Application.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ID != "private-test" {
|
||||||
|
t.Errorf("c.id == %s, wants %s", c.ID, "private-test")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+226
@@ -0,0 +1,226 @@
|
|||||||
|
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
|
||||||
|
"ipe/channel"
|
||||||
|
"ipe/subscription"
|
||||||
|
"ipe/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxTimeout = 3 * time.Second
|
||||||
|
|
||||||
|
// A webHook is sent as a HTTP POST request to the url which you specify.
|
||||||
|
// The POST request payload (body) contains a JSON document, and follows the following format:
|
||||||
|
// {
|
||||||
|
// "time_ms": 1327078148132
|
||||||
|
// "events": [
|
||||||
|
// { "name": "event_name", "some": "data" }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Security
|
||||||
|
// Encryption
|
||||||
|
//
|
||||||
|
// You may use a HTTP or a HTTPS url for WebHooks. In most cases HTTP is sufficient, but HTTPS can be useful if your data is sensitive or if you wish to protect against replay attacks for example.
|
||||||
|
// Authentication
|
||||||
|
//
|
||||||
|
// Since anyone could in principle send WebHooks to your application, it’s important to verify that these WebHooks originated from Pusher. Valid WebHooks will therefore contain these headers which contain a HMAC signature of the webHook payload (body):
|
||||||
|
//
|
||||||
|
// X-Pusher-Key: The App Key.
|
||||||
|
// X-Pusher-Signature: A HMAC SHA256 hex digest formed by signing the POST payload (body) with the token’s secret.
|
||||||
|
type webHook struct {
|
||||||
|
TimeMs int64 `json:"time_ms"`
|
||||||
|
Events []hookEvent `json:"events"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type hookEvent struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Event string `json:"event,omitempty"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
SocketID string `json:"socket_id,omitempty"`
|
||||||
|
UserID string `json:"user_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newChannelOcuppiedHook(channel *channel.Channel) hookEvent {
|
||||||
|
return hookEvent{Name: "channel_occupied", Channel: channel.ID}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newChannelVacatedHook(channel *channel.Channel) hookEvent {
|
||||||
|
return hookEvent{Name: "channel_vacated", Channel: channel.ID}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMemberAddedHook(channel *channel.Channel, s *subscription.Subscription) hookEvent {
|
||||||
|
return hookEvent{Name: "member_added", Channel: channel.ID, UserID: s.ID}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMemberRemovedHook(channel *channel.Channel, s *subscription.Subscription) hookEvent {
|
||||||
|
return hookEvent{Name: "member_removed", Channel: channel.ID, UserID: s.ID}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClientHook(channel *channel.Channel, s *subscription.Subscription, event string, data interface{}) hookEvent {
|
||||||
|
return hookEvent{Name: "client_event", Channel: channel.ID, Event: event, Data: data, SocketID: s.Connection.SocketID}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TriggerChannelOccupiedHook channel_occupied
|
||||||
|
// { "name": "channel_occupied", "channel": "test_channel" }
|
||||||
|
func (a *Application) TriggerChannelOccupiedHook(c *channel.Channel) {
|
||||||
|
event := newChannelOcuppiedHook(c)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := triggerHook(ctx, a, event); err != nil {
|
||||||
|
log.Errorf("triggering webhook %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TriggerChannelVacatedHook channel_vacated
|
||||||
|
// { "name": "channel_vacated", "channel": "test_channel" }
|
||||||
|
func (a *Application) TriggerChannelVacatedHook(c *channel.Channel) {
|
||||||
|
event := newChannelVacatedHook(c)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := triggerHook(ctx, a, event); err != nil {
|
||||||
|
log.Errorf("triggering webhook %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TriggerClientEventHook client_events
|
||||||
|
// {
|
||||||
|
// "name": "client_event",
|
||||||
|
// "channel": "name of the channel the event was published on",
|
||||||
|
// "event": "name of the event",
|
||||||
|
// "data": "data associated with the event",
|
||||||
|
// "socket_id": "socket_id of the sending socket",
|
||||||
|
// "user_id": "user_id associated with the sending socket" # Only for presence channels
|
||||||
|
// }
|
||||||
|
func (a *Application) TriggerClientEventHook(c *channel.Channel, s *subscription.Subscription, clientEvent string, data interface{}) {
|
||||||
|
event := newClientHook(c, s, clientEvent, data)
|
||||||
|
|
||||||
|
if c.IsPresence() {
|
||||||
|
event.UserID = s.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := triggerHook(ctx, a, event); err != nil {
|
||||||
|
log.Errorf("triggering webhook %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TriggerMemberAddedHook member_added
|
||||||
|
// {
|
||||||
|
// "name": "member_added",
|
||||||
|
// "channel": "presence-your_channel_name",
|
||||||
|
// "user_id": "a_user_id"
|
||||||
|
// }
|
||||||
|
func (a *Application) TriggerMemberAddedHook(c *channel.Channel, s *subscription.Subscription) {
|
||||||
|
event := newMemberAddedHook(c, s)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := triggerHook(ctx, a, event); err != nil {
|
||||||
|
log.Errorf("triggering webhook %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TriggerMemberRemovedHook member_removed
|
||||||
|
// {
|
||||||
|
// "name": "member_removed",
|
||||||
|
// "channel": "presence-your_channel_name",
|
||||||
|
// "user_id": "a_user_id"
|
||||||
|
// }
|
||||||
|
func (a *Application) TriggerMemberRemovedHook(c *channel.Channel, s *subscription.Subscription) {
|
||||||
|
event := newMemberRemovedHook(c, s)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := triggerHook(ctx, a, event); err != nil {
|
||||||
|
log.Errorf("triggering webhook %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func triggerHook(ctx context.Context, a *Application, event hookEvent) error {
|
||||||
|
if !a.WebHooks {
|
||||||
|
log.Infof("webhook are not enabled for app: %s", a.Name)
|
||||||
|
return fmt.Errorf("webhooks are not enabled for app: %s", a.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan bool)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Infof("Triggering %s event", event.Name)
|
||||||
|
|
||||||
|
hook := webHook{TimeMs: time.Now().Unix()}
|
||||||
|
|
||||||
|
hook.Events = append(hook.Events, event)
|
||||||
|
|
||||||
|
var js []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
js, err = json.Marshal(hook)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error decoding json: %+v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req *http.Request
|
||||||
|
|
||||||
|
req, err = http.NewRequest("POST", a.URLWebHook, bytes.NewReader(js))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error creating request: %+v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", "Ipe UA; (+https://github.com/dimiro1/ipe)")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("X-Pusher-Key", a.Key)
|
||||||
|
req.Header.Set("X-Pusher-Signature", utils.HashMAC(js, []byte(a.Secret)))
|
||||||
|
|
||||||
|
log.V(1).Infof("%+v", req.Header)
|
||||||
|
log.V(1).Infof("%+v", string(js))
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
|
||||||
|
// See: http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#close_http_resp_body
|
||||||
|
if resp != nil {
|
||||||
|
defer func() {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
log.Errorf("error closing response body %+v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error posting %s event: %+v", event.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Successfully terminated
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-done:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
|
||||||
|
"ipe/connection"
|
||||||
|
"ipe/events"
|
||||||
|
"ipe/subscription"
|
||||||
|
"ipe/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option constructor function for Channel
|
||||||
|
type Option func(*Channel)
|
||||||
|
|
||||||
|
// ListenerFunc listener function
|
||||||
|
type ListenerFunc func(*Channel, *subscription.Subscription)
|
||||||
|
|
||||||
|
// ClientEventListenerFunc listener for client events
|
||||||
|
type ClientEventListenerFunc func(*Channel, *subscription.Subscription, string, interface{})
|
||||||
|
|
||||||
|
// Channel represents an application channel
|
||||||
|
type Channel struct {
|
||||||
|
sync.RWMutex
|
||||||
|
|
||||||
|
ID string
|
||||||
|
subscriptions map[string]*subscription.Subscription
|
||||||
|
|
||||||
|
createdAt time.Time
|
||||||
|
|
||||||
|
memberAddedListeners []ListenerFunc
|
||||||
|
memberRemovedListeners []ListenerFunc
|
||||||
|
channelOccupiedListeners []ListenerFunc
|
||||||
|
channelVacatedListeners []ListenerFunc
|
||||||
|
clientEventListeners []ClientEventListenerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Create a new Channel
|
||||||
|
func New(channelID string, options ...Option) *Channel {
|
||||||
|
log.Infof("Creating a new Channel: %s", channelID)
|
||||||
|
|
||||||
|
c := &Channel{ID: channelID, createdAt: time.Now(), subscriptions: make(map[string]*subscription.Subscription)}
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
option(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMemberAddedListener appends the given ListenerFunc into the memberAddedListeners list
|
||||||
|
func WithMemberAddedListener(f ListenerFunc) func(*Channel) {
|
||||||
|
return func(c *Channel) {
|
||||||
|
c.memberAddedListeners = append(c.memberAddedListeners, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMemberRemovedListener appends the given ListenerFunc into the memberRemovedListeners list
|
||||||
|
func WithMemberRemovedListener(f ListenerFunc) func(*Channel) {
|
||||||
|
return func(c *Channel) {
|
||||||
|
c.memberRemovedListeners = append(c.memberRemovedListeners, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithChannelOccupiedListener appends the given ListenerFunc into the channelOccupiedListeners list
|
||||||
|
func WithChannelOccupiedListener(f ListenerFunc) func(*Channel) {
|
||||||
|
return func(c *Channel) {
|
||||||
|
c.channelOccupiedListeners = append(c.channelOccupiedListeners, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithChannelVacatedListener appends the given ListenerFunc into the channelVacatedListeners list
|
||||||
|
func WithChannelVacatedListener(f ListenerFunc) func(*Channel) {
|
||||||
|
return func(c *Channel) {
|
||||||
|
c.channelVacatedListeners = append(c.channelVacatedListeners, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithClientEventListener appends the given ListenerFunc into the clientEventListeners list
|
||||||
|
func WithClientEventListener(f ClientEventListenerFunc) func(*Channel) {
|
||||||
|
return func(c *Channel) {
|
||||||
|
c.clientEventListeners = append(c.clientEventListeners, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscriptions returns a slice of subscriptions
|
||||||
|
func (c *Channel) Subscriptions() []*subscription.Subscription {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
var subscriptions []*subscription.Subscription
|
||||||
|
|
||||||
|
for _, sub := range c.subscriptions {
|
||||||
|
subscriptions = append(subscriptions, sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
return subscriptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsOccupied Return true if the Channel has at least one subscriber
|
||||||
|
func (c *Channel) IsOccupied() bool {
|
||||||
|
return c.TotalSubscriptions() > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPresenceOrPrivate Check if the type of the Channel is presence or is private
|
||||||
|
func (c *Channel) IsPresenceOrPrivate() bool {
|
||||||
|
return c.IsPresence() || c.IsPrivate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPublic Check if the type of the Channel is public
|
||||||
|
func (c *Channel) IsPublic() bool {
|
||||||
|
return !c.IsPresenceOrPrivate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPresence Check if the type of the Channel is presence
|
||||||
|
func (c *Channel) IsPresence() bool {
|
||||||
|
return utils.IsPresenceChannel(c.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPrivate Check if the type of the Channel is private
|
||||||
|
func (c *Channel) IsPrivate() bool {
|
||||||
|
return utils.IsPrivateChannel(c.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TotalSubscriptions Get the total of subscribers
|
||||||
|
func (c *Channel) TotalSubscriptions() int {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
return len(c.subscriptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TotalUsers Get the total of users.
|
||||||
|
func (c *Channel) TotalUsers() int {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
total := make(map[string]int)
|
||||||
|
|
||||||
|
for _, s := range c.subscriptions {
|
||||||
|
total[s.ID]++
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe Add a new subscriber to the Channel
|
||||||
|
func (c *Channel) Subscribe(conn *connection.Connection, channelData string) error {
|
||||||
|
log.Infof("Subscribing %s to Channel %s", conn.SocketID, c.ID)
|
||||||
|
|
||||||
|
_subscription := subscription.New(conn, channelData)
|
||||||
|
c.Lock()
|
||||||
|
c.subscriptions[conn.SocketID] = _subscription
|
||||||
|
c.Unlock()
|
||||||
|
|
||||||
|
if c.IsPresence() {
|
||||||
|
// User Info Data
|
||||||
|
var info struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
UserInfo json.RawMessage `json:"user_info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("%+v", channelData)
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(channelData), &info); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
js, err := info.UserInfo.MarshalJSON()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Lock()
|
||||||
|
// Update the Subscription
|
||||||
|
_subscription.ID = info.UserID
|
||||||
|
_subscription.Data = string(js)
|
||||||
|
c.Unlock()
|
||||||
|
|
||||||
|
// Publish pusher_internal:member_added
|
||||||
|
c.PublishMemberAddedEvent(channelData, _subscription)
|
||||||
|
|
||||||
|
for _, hook := range c.memberAddedListeners {
|
||||||
|
hook(c, _subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pusher_internal:subscription_succeeded
|
||||||
|
data := make(map[string]events.SubscriptionSucceededPresenceData)
|
||||||
|
data["presence"] = events.NewSubscriptionSucceedPresenceData(c.subscriptions)
|
||||||
|
|
||||||
|
js, err = json.Marshal(data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.Publish(events.NewSubscriptionSucceeded(c.ID, string(js)))
|
||||||
|
} else {
|
||||||
|
conn.Publish(events.NewSubscriptionSucceeded(c.ID, "{}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.TotalSubscriptions() == 1 {
|
||||||
|
for _, hook := range c.channelOccupiedListeners {
|
||||||
|
hook(c, _subscription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSubscribed check if the user is subscribed
|
||||||
|
func (c *Channel) IsSubscribed(conn *connection.Connection) bool {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
_, exists := c.subscriptions[conn.SocketID]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe Remove the subscriber from the Channel
|
||||||
|
// It destroy the Channel if the channels does not have any subscribers.
|
||||||
|
func (c *Channel) Unsubscribe(conn *connection.Connection) error {
|
||||||
|
log.Infof("unsubscribe %s from Channel %s", conn.SocketID, c.ID)
|
||||||
|
|
||||||
|
c.RLock()
|
||||||
|
_subscription, exists := c.subscriptions[conn.SocketID]
|
||||||
|
c.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return errors.New("_subscription not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Lock()
|
||||||
|
delete(c.subscriptions, conn.SocketID)
|
||||||
|
c.Unlock()
|
||||||
|
|
||||||
|
if c.IsPresence() {
|
||||||
|
// Publish pusher_internal:member_removed
|
||||||
|
c.PublishMemberRemovedEvent(_subscription)
|
||||||
|
|
||||||
|
for _, hook := range c.memberRemovedListeners {
|
||||||
|
hook(c, _subscription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.IsOccupied() {
|
||||||
|
for _, hook := range c.channelVacatedListeners {
|
||||||
|
hook(c, _subscription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishMemberAddedEvent Publish a MemberAddedEvent to all subscriptions
|
||||||
|
func (c *Channel) PublishMemberAddedEvent(data string, subscription *subscription.Subscription) {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
for _, subs := range c.subscriptions {
|
||||||
|
if subs != subscription {
|
||||||
|
subs.Connection.Publish(events.NewMemberAdded(c.ID, data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishMemberRemovedEvent Publish a MemberRemovedEvent to all subscriptions
|
||||||
|
func (c *Channel) PublishMemberRemovedEvent(subscription *subscription.Subscription) {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
for _, subs := range c.subscriptions {
|
||||||
|
if subs != subscription {
|
||||||
|
subs.Connection.Publish(events.NewMemberRemoved(c.ID, subscription.ID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish messages to all Subscribers
|
||||||
|
// skip the ignore connection
|
||||||
|
func (c *Channel) Publish(event events.Raw, ignore string) error {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
b, err := event.Data.MarshalJSON()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var v interface{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b, &v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Publishing message %+v to Channel %s", v, c.ID)
|
||||||
|
|
||||||
|
for _, subs := range c.subscriptions {
|
||||||
|
if subs.Connection.SocketID != ignore {
|
||||||
|
subs.Connection.Publish(events.NewResponse(event.Event, event.Channel, v))
|
||||||
|
} else {
|
||||||
|
if utils.IsClientEvent(event.Event) {
|
||||||
|
for _, hook := range c.clientEventListeners {
|
||||||
|
hook(c, subs, event.Event, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ipe/connection"
|
||||||
|
"ipe/mocks"
|
||||||
|
"ipe/subscription"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsOccupied(t *testing.T) {
|
||||||
|
c := New("ID")
|
||||||
|
|
||||||
|
if c.IsOccupied() {
|
||||||
|
t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.subscriptions["ID"] = subscription.New(connection.New("ID", mocks.MockSocket{}), "")
|
||||||
|
|
||||||
|
if !c.IsOccupied() {
|
||||||
|
t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsPrivate(t *testing.T) {
|
||||||
|
c := New("private-Channel")
|
||||||
|
|
||||||
|
if !c.IsPrivate() {
|
||||||
|
t.Errorf("c.IsPrivate() == %t, wants %t", c.IsPrivate(), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsPresence(t *testing.T) {
|
||||||
|
c := New("presence-Channel")
|
||||||
|
|
||||||
|
if !c.IsPresence() {
|
||||||
|
t.Errorf("c.IsPresence() == %t, wants %t", c.IsPresence(), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsPublic(t *testing.T) {
|
||||||
|
c := New("Channel")
|
||||||
|
|
||||||
|
if !c.IsPublic() {
|
||||||
|
t.Errorf("c.IsPublic() == %t, wants %t", c.IsPublic(), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsPrivateOrPresence(t *testing.T) {
|
||||||
|
c := New("private-Channel")
|
||||||
|
|
||||||
|
if !c.IsPresenceOrPrivate() {
|
||||||
|
t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
c = New("presence-Channel")
|
||||||
|
|
||||||
|
if !c.IsPresenceOrPrivate() {
|
||||||
|
t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTotalSubscriptions(t *testing.T) {
|
||||||
|
c := New("ID")
|
||||||
|
|
||||||
|
if c.TotalSubscriptions() != len(c.subscriptions) {
|
||||||
|
t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.subscriptions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTotalUsers(t *testing.T) {
|
||||||
|
c := New("ID")
|
||||||
|
|
||||||
|
c.subscriptions["1"] = subscription.New(connection.New("ID", mocks.MockSocket{}), "")
|
||||||
|
c.subscriptions["2"] = subscription.New(connection.New("ID", mocks.MockSocket{}), "")
|
||||||
|
|
||||||
|
if c.TotalSubscriptions() != len(c.subscriptions) {
|
||||||
|
t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.subscriptions))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.TotalUsers() != 1 {
|
||||||
|
t.Errorf("c.TotalUsers() == %d, wants %d", c.TotalUsers(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsSubscribed(t *testing.T) {
|
||||||
|
c := New("ID")
|
||||||
|
conn := connection.New("ID", mocks.MockSocket{})
|
||||||
|
|
||||||
|
if c.IsSubscribed(conn) {
|
||||||
|
t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.subscriptions["ID"] = subscription.New(conn, "")
|
||||||
|
|
||||||
|
if !c.IsSubscribed(conn) {
|
||||||
|
t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"ipe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// These variables are generated by the linker
|
||||||
|
// please see the makefile for mor information.
|
||||||
|
var (
|
||||||
|
version = "version"
|
||||||
|
buildstamp = "buildstamp"
|
||||||
|
githash = "githash"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Main function, initialize the system
|
||||||
|
func main() {
|
||||||
|
var filename = flag.String("config", "config.yml", "Config file location")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
printBanner()
|
||||||
|
|
||||||
|
ipe.Start(*filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print a beautiful banner
|
||||||
|
func printBanner() {
|
||||||
|
fmt.Print("\033[31m")
|
||||||
|
fmt.Print(`
|
||||||
|
d8b
|
||||||
|
Y8P
|
||||||
|
|
||||||
|
888 88888b. .d88b.
|
||||||
|
888 888 "88b d8P Y8b
|
||||||
|
888 888 888 88888888
|
||||||
|
888 888 d88P Y8b.
|
||||||
|
888 88888P" "Y8888
|
||||||
|
888
|
||||||
|
888
|
||||||
|
888
|
||||||
|
`)
|
||||||
|
fmt.Println("\033[0m")
|
||||||
|
fmt.Println("\033[32mWelcome to Ipê - Yet another Pusher server clone (https://github.com/dimiro1/ipe)\033[0m")
|
||||||
|
fmt.Printf("\033[32mVersion %s+%s.git.%s\033[0m\n", version, buildstamp, githash)
|
||||||
|
fmt.Println("\033[33mBy: Claudemiro Alves Feitosa Neto <dimiro1@gmail.com>\033[0m")
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright 2014, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
// File config file
|
||||||
|
type File struct {
|
||||||
|
Host string `yaml:"host"` // The host, eg: :8080 will start on 0.0.0.0:8080
|
||||||
|
SSL SSL `yaml:"ssl"`
|
||||||
|
Profiling bool `yaml:"profiling"`
|
||||||
|
Apps []Application `yaml:"apps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSL related configuration options
|
||||||
|
type SSL struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
KeyFile string `yaml:"key_file"`
|
||||||
|
CertFile string `yaml:"cert_file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application related configuration options
|
||||||
|
type Application struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
AppID string `yaml:"app_id"`
|
||||||
|
Key string `yaml:"key"`
|
||||||
|
Secret string `yaml:"secret"`
|
||||||
|
OnlySSL bool `yaml:"only_ssl"`
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
UserEvents bool `yaml:"user_events"`
|
||||||
|
WebHooks Webhooks `yaml:"webhooks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webhooks related configuration options
|
||||||
|
type Webhooks struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
URL string `yaml:"url"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package connection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Socket interface to write to the client
|
||||||
|
type Socket interface {
|
||||||
|
WriteJSON(interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection An user connection
|
||||||
|
type Connection struct {
|
||||||
|
sync.Mutex
|
||||||
|
|
||||||
|
SocketID string
|
||||||
|
Socket Socket
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Create a new Subscriber
|
||||||
|
func New(socketID string, s Socket) *Connection {
|
||||||
|
log.Infof("Creating a new Subscriber %+v", socketID)
|
||||||
|
|
||||||
|
return &Connection{SocketID: socketID, Socket: s, CreatedAt: time.Now()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish the message to websocket attached to this client
|
||||||
|
func (conn *Connection) Publish(m interface{}) {
|
||||||
|
conn.Lock()
|
||||||
|
defer conn.Unlock()
|
||||||
|
|
||||||
|
if err := conn.Socket.WriteJSON(m); err != nil {
|
||||||
|
log.Errorf("error writing json into Socket, %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,29 +2,28 @@
|
|||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package ipe
|
package connection
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"ipe/mocks"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewConnection(t *testing.T) {
|
func TestNewConnection(t *testing.T) {
|
||||||
expectedSocketID := "socketID"
|
expectedSocketID := "socketID"
|
||||||
expectedSocket := &websocket.Conn{}
|
expectedSocket := mocks.MockSocket{}
|
||||||
|
|
||||||
c := newConnection(expectedSocketID, expectedSocket)
|
c := New(expectedSocketID, expectedSocket)
|
||||||
|
|
||||||
if c.SocketID != expectedSocketID {
|
if c.SocketID != expectedSocketID {
|
||||||
t.Errorf("Expected: %s but got %s", expectedSocketID, c.SocketID)
|
t.Errorf("c.SocketID == %s, wants %s", c.SocketID, expectedSocketID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Socket != expectedSocket {
|
if c.Socket != expectedSocket {
|
||||||
t.Errorf("Expected: %+v but got %+v", expectedSocket, c.Socket)
|
t.Errorf("c.Socket == %v, wants %v", c.Socket, expectedSocket)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.CreatedAt.IsZero() {
|
if c.CreatedAt.IsZero() {
|
||||||
t.Errorf("Expected %s to not be zero", c.CreatedAt)
|
t.Errorf("c.createdAt.IsZero() == %t, wants %t", c.CreatedAt.IsZero(), false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
if [ -z "$(ls -A /config)" ]; then
|
||||||
|
cp /app/config-example.yml /config/config.yml
|
||||||
|
fi
|
||||||
|
|
||||||
|
/app/ipe --config=/config/config.yml
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
|
||||||
|
"ipe/subscription"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SubscribeData data for Subscribe event
|
||||||
|
type SubscribeData struct {
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Auth string `json:"auth,omitempty"`
|
||||||
|
ChannelData string `json:"channel_data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe event
|
||||||
|
// {
|
||||||
|
// "event": "pusher:subscribe",
|
||||||
|
// "data": {
|
||||||
|
// "channel": "the channel",
|
||||||
|
// "auth": "the auth",
|
||||||
|
// "channelData": "extra data"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
type Subscribe struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Data SubscribeData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSubscribe Create a new subscribe event with the specified channel and data
|
||||||
|
func NewSubscribe(channel, auth, channelData string) Subscribe {
|
||||||
|
data := SubscribeData{Channel: channel, Auth: auth, ChannelData: channelData}
|
||||||
|
return Subscribe{Event: "pusher:subscribe", Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsubscribeData event data
|
||||||
|
type UnsubscribeData struct {
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe event
|
||||||
|
// {
|
||||||
|
// "event": "pusher:unsubscribe",
|
||||||
|
// "data": {
|
||||||
|
// "channel": "The channel"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
type Unsubscribe struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Data UnsubscribeData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUnsubscribe Create a new unsubscribe event for the specified channel
|
||||||
|
func NewUnsubscribe(channel string) Unsubscribe {
|
||||||
|
data := UnsubscribeData{Channel: channel}
|
||||||
|
return Unsubscribe{Event: "pusher:unsubscribe", Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriptionSucceeded event
|
||||||
|
// {
|
||||||
|
// "event": "pusher_internal:subscription_succeeded",
|
||||||
|
// "channel": "the channel"
|
||||||
|
// }
|
||||||
|
type SubscriptionSucceeded struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSubscriptionSucceeded Create a new subscription succeed event for the specified channel
|
||||||
|
func NewSubscriptionSucceeded(channel, data string) SubscriptionSucceeded {
|
||||||
|
return SubscriptionSucceeded{Event: "pusher_internal:subscription_succeeded", Channel: channel, Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriptionSucceededPresenceData Data Subscription Succeed
|
||||||
|
// "{
|
||||||
|
// \"presence\": {
|
||||||
|
// \"ids\": [\"11814b369700141b222a3f3791cec2d9\",\"71dd6a29da2a4833336d2a964becf820\"],
|
||||||
|
// \"hash\": {
|
||||||
|
// \"11814b369700141b222a3f3791cec2d9\": {
|
||||||
|
// \"name\":\"Phil Leggetter\",
|
||||||
|
// \"twitter\": \"@leggetter\"
|
||||||
|
// },
|
||||||
|
// \"71dd6a29da2a4833336d2a964becf820\": {
|
||||||
|
// \"name\":\"Max Williams\",
|
||||||
|
// \"twitter\": \"@maxthelion\"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// \"count\": 2
|
||||||
|
// }
|
||||||
|
// }"
|
||||||
|
type SubscriptionSucceededPresenceData struct {
|
||||||
|
Ids []string `json:"ids"`
|
||||||
|
Hash map[string]interface{} `json:"hash"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSubscriptionSucceedPresenceData returns new SubscriptionSucceededPresenceData
|
||||||
|
func NewSubscriptionSucceedPresenceData(subscriptions map[string]*subscription.Subscription) SubscriptionSucceededPresenceData {
|
||||||
|
event := SubscriptionSucceededPresenceData{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ids []string
|
||||||
|
hash = make(map[string]interface{}, len(subscriptions))
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, s := range subscriptions {
|
||||||
|
// Do you have any other idea?
|
||||||
|
var js interface{}
|
||||||
|
if err := json.Unmarshal([]byte(s.Data), &js); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
hash[s.ID] = js
|
||||||
|
ids = append(ids, s.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
event.Ids = ids
|
||||||
|
event.Hash = hash
|
||||||
|
event.Count = len(subscriptions)
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pong event
|
||||||
|
// {
|
||||||
|
// "event": "pusher:pong",
|
||||||
|
// "data": {}
|
||||||
|
// }
|
||||||
|
type Pong struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPong Create a new pong event
|
||||||
|
func NewPong() Pong {
|
||||||
|
return Pong{Event: "pusher:pong", Data: "{}"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping event
|
||||||
|
// {
|
||||||
|
// "event": "pusher:ping",
|
||||||
|
// "data": {}
|
||||||
|
// }
|
||||||
|
type Ping struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPing Create a new ping event
|
||||||
|
func NewPing() Ping {
|
||||||
|
return Ping{Event: "pusher:ping", Data: "{}"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error event
|
||||||
|
// {
|
||||||
|
// "event": "pusher:error",
|
||||||
|
// "data": {
|
||||||
|
// "message": "A Message",
|
||||||
|
// "code": 4000
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
type Error struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewError Create a new error event
|
||||||
|
// Pusher protocol is very strange in some parts
|
||||||
|
// It send null in some errors.
|
||||||
|
func NewError(code int, message string) Error {
|
||||||
|
var data = struct {
|
||||||
|
Code *int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}{
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
if code == 0 {
|
||||||
|
data.Code = nil
|
||||||
|
} else {
|
||||||
|
data.Code = &code
|
||||||
|
}
|
||||||
|
|
||||||
|
return Error{Event: "pusher:error", Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectionEstablished event
|
||||||
|
// {
|
||||||
|
// "event" : "pusher:connection_established",
|
||||||
|
// "data" : {
|
||||||
|
// "socket_id" : "123456",
|
||||||
|
// "activity_timeout" : 120
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
type ConnectionEstablished struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConnectionEstablished Create a new connection established event using the specified socketId
|
||||||
|
func NewConnectionEstablished(socketID string) ConnectionEstablished {
|
||||||
|
b, err := json.Marshal(struct {
|
||||||
|
SocketID string `json:"socket_id"`
|
||||||
|
ActivityTimeout int `json:"activity_timeout"`
|
||||||
|
}{
|
||||||
|
SocketID: socketID, ActivityTimeout: 120,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic("events: Could not Marshal json ConnectionEstablishedEvent")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConnectionEstablished{Event: "pusher:connection_established", Data: string(b)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemberAdded event
|
||||||
|
// {
|
||||||
|
// "event": "pusher_internal:member_added",
|
||||||
|
// "channel": "presence-example-channel",
|
||||||
|
// "data": String
|
||||||
|
// }
|
||||||
|
type MemberAdded struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMemberAdded creates a new MemberAdded event
|
||||||
|
func NewMemberAdded(channel, data string) MemberAdded {
|
||||||
|
return MemberAdded{Event: "pusher_internal:member_added", Channel: channel, Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemberRemoved event
|
||||||
|
// {
|
||||||
|
// "event": "pusher_internal:member_removed",
|
||||||
|
// "channel": "presence-example-channel",
|
||||||
|
// "data": String
|
||||||
|
// }
|
||||||
|
type MemberRemoved struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMemberRemoved returns a new MemberRemoved event
|
||||||
|
func NewMemberRemoved(channel string, userID string) MemberRemoved {
|
||||||
|
data, err := json.Marshal(struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
}{
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MemberRemoved{Event: "pusher_internal:member_removed", Channel: channel, Data: string(data)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw event, usually used for client events
|
||||||
|
// {
|
||||||
|
// "event": "client-?",
|
||||||
|
// "channel": "The channel",
|
||||||
|
// "data": {}
|
||||||
|
// }
|
||||||
|
type Raw struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response event
|
||||||
|
type Response struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewResponse The response event that is broadcasted to the client sockets
|
||||||
|
func NewResponse(name, channel string, data interface{}) Response {
|
||||||
|
return Response{Event: name, Channel: channel, Data: data}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_newErrorEvent_with_invalid_code(t *testing.T) {
|
||||||
|
event := NewError(0, "The error message")
|
||||||
|
|
||||||
|
data, _ := json.Marshal(event)
|
||||||
|
expected := `{"event":"pusher:error","data":{"code":null,"message":"The error message"}}`
|
||||||
|
|
||||||
|
if bytes.Compare(data, []byte(expected)) != 0 {
|
||||||
|
t.Errorf("%s != %s", string(data), expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_newErrorEvent_with_valid_code(t *testing.T) {
|
||||||
|
event := NewError(4007, "Unsupported protocol version")
|
||||||
|
|
||||||
|
data, _ := json.Marshal(event)
|
||||||
|
expected := `{"event":"pusher:error","data":{"code":4007,"message":"Unsupported protocol version"}}`
|
||||||
|
|
||||||
|
if bytes.Compare(data, []byte(expected)) != 0 {
|
||||||
|
t.Errorf("%s != %s", string(data), expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
client: go run client.go
|
client: go run client.go
|
||||||
server: go run ../main.go -config ./config.json -logtostderr
|
server: go run ../cmd/main.go -config ./functional-config.yml -alsologtostderr
|
||||||
+12
-13
@@ -1,24 +1,23 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Pusher Spec</title>
|
<title>Pusher Spec</title>
|
||||||
<link href="https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.css" rel="stylesheet" />
|
<link href="//cdnjs.cloudflare.com/ajax/libs/mocha/5.2.0/mocha.min.css" rel="stylesheet"/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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="//cdnjs.cloudflare.com/ajax/libs/mocha/5.2.0/mocha.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.3.4/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/chai/3.4.1/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>mocha.setup('bdd')</script>
|
||||||
<script src="test.pusher.js"></script>
|
<script src="test.pusher.js"></script>
|
||||||
<script>
|
<script>
|
||||||
mocha.checkLeaks();
|
mocha.checkLeaks();
|
||||||
mocha.globals(['jQuery', 'Pusher']);
|
mocha.globals(['Pusher']);
|
||||||
mocha.run();
|
mocha.run();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+43
-6
@@ -1,9 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
|
||||||
"github.com/pusher/pusher-http-go"
|
"github.com/pusher/pusher-http-go"
|
||||||
)
|
)
|
||||||
@@ -20,6 +23,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func pusherPresenceAuth(res http.ResponseWriter, req *http.Request) {
|
func pusherPresenceAuth(res http.ResponseWriter, req *http.Request) {
|
||||||
|
log.Println("Presence Request")
|
||||||
presenceData := pusher.MemberData{
|
presenceData := pusher.MemberData{
|
||||||
UserId: "1",
|
UserId: "1",
|
||||||
UserInfo: map[string]string{},
|
UserInfo: map[string]string{},
|
||||||
@@ -32,30 +36,63 @@ func pusherPresenceAuth(res http.ResponseWriter, req *http.Request) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(res, string(response))
|
_, _ = fmt.Fprint(res, string(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
func pusherPrivateAuth(res http.ResponseWriter, req *http.Request) {
|
func pusherPrivateAuth(res http.ResponseWriter, req *http.Request) {
|
||||||
params, _ := ioutil.ReadAll(req.Body)
|
params, _ := ioutil.ReadAll(req.Body)
|
||||||
response, err := client.AuthenticatePrivateChannel(params)
|
response, err := client.AuthenticatePrivateChannel(params)
|
||||||
|
|
||||||
|
log.Printf("Private Request %s", params)
|
||||||
|
log.Printf("Auth %s", response)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(res, string(response))
|
_, _ = fmt.Fprint(res, string(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
func triggerMessage(res http.ResponseWriter, req *http.Request) {
|
func triggerMessage(res http.ResponseWriter, _ *http.Request) {
|
||||||
client.Trigger("private-messages", "messages", "The message from server")
|
_, 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() {
|
func main() {
|
||||||
http.HandleFunc("/pusher/presence/auth", pusherPresenceAuth)
|
http.HandleFunc("/pusher/presence/auth", pusherPresenceAuth)
|
||||||
http.HandleFunc("/pusher/private/auth", pusherPrivateAuth)
|
http.HandleFunc("/pusher/private/auth", pusherPrivateAuth)
|
||||||
http.HandleFunc("/trigger", triggerMessage)
|
http.HandleFunc("/trigger", triggerMessage)
|
||||||
|
http.HandleFunc("/hook", hookcallback)
|
||||||
http.Handle("/", http.FileServer(http.Dir("./")))
|
http.Handle("/", http.FileServer(http.Dir("./")))
|
||||||
http.ListenAndServe(":5000", nil)
|
_ = http.ListenAndServe(":5000", nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
host: ':8080'
|
||||||
|
profiling: false
|
||||||
|
ssl:
|
||||||
|
enabled: false
|
||||||
|
host: ':4343'
|
||||||
|
key_file: 'key.pem'
|
||||||
|
cert_file: 'cert.pem'
|
||||||
|
apps:
|
||||||
|
- name: 'Sample Application'
|
||||||
|
enabled: true
|
||||||
|
only_ssl: false
|
||||||
|
key: '278d525bdf162c739803'
|
||||||
|
secret: '7ad3753142a6693b25b9'
|
||||||
|
app_id: '1'
|
||||||
|
user_events: true
|
||||||
|
webhooks:
|
||||||
|
enabled: true # Default is false
|
||||||
|
url: 'http://127.0.0.1:5000/hook'
|
||||||
+127
-104
@@ -1,135 +1,158 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
var assert = chai.assert;
|
let assert = chai.assert;
|
||||||
|
|
||||||
var APP_KEY = "278d525bdf162c739803";
|
let APP_KEY = "278d525bdf162c739803";
|
||||||
var HOST = "localhost";
|
let HOST = "localhost";
|
||||||
var PORT = 8080;
|
let PORT = 8080;
|
||||||
var AUTH = "http://localhost:5000/pusher/private/auth"
|
let AUTH = "http://localhost:5000/pusher/private/auth";
|
||||||
var AUTH_PRESENCE = "http://localhost:5000/pusher/presence/auth"
|
let AUTH_PRESENCE = "http://localhost:5000/pusher/presence/auth";
|
||||||
|
|
||||||
Pusher.log = function(msg) {
|
Pusher.log = function (msg) {
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
function getPusher(auth) {
|
function getPusher(auth) {
|
||||||
return new Pusher(APP_KEY, {
|
return new Pusher(APP_KEY, {
|
||||||
wsHost: HOST,
|
wsHost: HOST,
|
||||||
wsPort: PORT,
|
wsPort: PORT,
|
||||||
authEndpoint: auth,
|
authEndpoint: auth,
|
||||||
enabledTransports: ["ws"],
|
enabledTransports: ["ws"],
|
||||||
disabledTransports: ["flash"]
|
disabledTransports: ["flash"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Pusher", function() {
|
describe("Pusher", function () {
|
||||||
|
|
||||||
describe("connection", function() {
|
describe("connection", function () {
|
||||||
it("should connect sucessfully with correct config", function(done) {
|
it("should connect sucessfully with correct config", function (done) {
|
||||||
var pusher = getPusher(AUTH);
|
let pusher = getPusher(AUTH);
|
||||||
|
|
||||||
pusher.connection.bind('connected', function() {
|
pusher.connection.bind('connected', function () {
|
||||||
assert.ok(true, "Connected");
|
assert.ok(true, "Connected");
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not connect without the correct config", function(done) {
|
it("should not connect without the correct config", function (done) {
|
||||||
var pusher = new Pusher("INVALID_APP_KEY", {
|
let pusher = new Pusher("INVALID_APP_KEY", {
|
||||||
wsHost: HOST,
|
wsHost: HOST,
|
||||||
wsPort: PORT,
|
wsPort: PORT,
|
||||||
enabledTransports: ["ws"],
|
enabledTransports: ["ws"],
|
||||||
disabledTransports: ["flash"]
|
disabledTransports: ["flash"]
|
||||||
});
|
});
|
||||||
|
|
||||||
pusher.connection.bind('disconnected', function() {
|
pusher.connection.bind('disconnected', function () {
|
||||||
assert.ok(true, "Not Connected");
|
assert.ok(true, "Not Connected");
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}); // connection
|
}); // connection
|
||||||
|
|
||||||
describe("subscription", function() {
|
describe("subscription", function () {
|
||||||
it("should subscribe to a public channel", function(done) {
|
it("should subscribe to a public channel", function (done) {
|
||||||
var pusher = getPusher(AUTH);
|
let pusher = getPusher(AUTH);
|
||||||
|
|
||||||
var channel = pusher.subscribe('public-channel');
|
let channel = pusher.subscribe('public-channel');
|
||||||
channel.bind("pusher:subscription_succeeded", function(data) {
|
channel.bind("pusher:subscription_succeeded", function () {
|
||||||
assert.ok(true, "Connected to the channel");
|
assert.ok(true, "Connected to the channel");
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should subscribe to a private channel", function(done) {
|
it("should subscribe to a private channel", function (done) {
|
||||||
var pusher = getPusher(AUTH);
|
let pusher = getPusher(AUTH);
|
||||||
|
|
||||||
var channel = pusher.subscribe('private-channel');
|
let channel = pusher.subscribe('private-channel');
|
||||||
channel.bind("pusher:subscription_succeeded", function(data) {
|
channel.bind("pusher:subscription_succeeded", function () {
|
||||||
assert.ok(true, "Connected to the channel");
|
assert.ok(true, "Connected to the channel");
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should subscribe to a presence channel", function(done) {
|
it("should subscribe to a presence channel", function (done) {
|
||||||
var pusher = getPusher(AUTH_PRESENCE);
|
let pusher = getPusher(AUTH_PRESENCE);
|
||||||
|
|
||||||
var channel = pusher.subscribe('presence-channel');
|
let channel = pusher.subscribe('presence-channel');
|
||||||
channel.bind("pusher:subscription_succeeded", function(data) {
|
channel.bind("pusher:subscription_succeeded", function () {
|
||||||
assert.ok(true, "Connected to the channel");
|
assert.ok(true, "Connected to the channel");
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}); // subscription
|
}); // subscription
|
||||||
|
|
||||||
describe("events", function() {
|
describe("hooks", function () {
|
||||||
it('should not allowed client events on public channels', function(done) {
|
it('should receive hook', function (done) {
|
||||||
var pusher = getPusher(AUTH);
|
let pusher = getPusher(AUTH);
|
||||||
var channel = pusher.subscribe('public-channel');
|
let channel = pusher.subscribe('private-webhook');
|
||||||
|
|
||||||
channel.bind("pusher:subscription_succeeded", function(data) {
|
channel.bind("pusher:subscription_succeeded", function () {
|
||||||
channel.trigger("client-message", "The message");
|
console.log("subscribed");
|
||||||
});
|
});
|
||||||
|
|
||||||
pusher.bind("pusher:error", function(data) {
|
channel.bind("channel_occupied", function (data) {
|
||||||
assert.ok(true, "Expected error");
|
assert.equal(data, "The Webhoook from server");
|
||||||
done();
|
pusher.unsubscribe('private-webhook');
|
||||||
});
|
done();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}); // hooks
|
||||||
|
|
||||||
it('should allow client events on private channels', function(done) {
|
describe("events", function () {
|
||||||
var pusher_a = getPusher(AUTH);
|
it('should not allowed client events on public channels', function (done) {
|
||||||
var pusher_b = getPusher(AUTH);
|
let pusher = getPusher(AUTH);
|
||||||
|
let channel = pusher.subscribe('public-channel');
|
||||||
|
|
||||||
var channel_a = pusher_a.subscribe('private-channel');
|
channel.bind("pusher:subscription_succeeded", function () {
|
||||||
var channel_b = pusher_b.subscribe('private-channel');
|
channel.trigger("client-message", "The message");
|
||||||
|
});
|
||||||
|
|
||||||
channel_a.bind("pusher:subscription_succeeded", function() {
|
pusher.bind("pusher:error", function () {
|
||||||
channel_a.trigger("client-message", "The message");
|
assert.ok(true, "Expected error");
|
||||||
});
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
channel_b.bind("client-message", function(data) {
|
it('should allow client events on private channels', function (done) {
|
||||||
assert.equal(data, "The message");
|
let pusher_a = getPusher(AUTH);
|
||||||
done();
|
let pusher_b = getPusher(AUTH);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should publish event on private channel', function(done) {
|
let channel_a = pusher_a.subscribe('private-channel');
|
||||||
var pusher_a = getPusher(AUTH);
|
let channel_b = pusher_b.subscribe('private-channel');
|
||||||
var pusher_b = getPusher(AUTH);
|
|
||||||
|
|
||||||
var channel_a = pusher_a.subscribe('private-messages');
|
channel_a.bind("pusher:subscription_succeeded", function () {
|
||||||
var channel_b = pusher_b.subscribe('private-messages');
|
channel_a.trigger("client-message", "The message");
|
||||||
|
});
|
||||||
|
|
||||||
channel_a.bind("pusher:subscription_succeeded", function() {
|
channel_b.bind("client-message", function (data) {
|
||||||
var xhttp = new XMLHttpRequest();
|
assert.equal(data, "The message");
|
||||||
xhttp.open("GET", "/trigger", true);
|
done();
|
||||||
xhttp.send();
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
channel_b.bind("messages", function(data) {
|
it('should publish event on private channel', function (done) {
|
||||||
assert.equal(data, "The message from server");
|
let pusher_a = getPusher(AUTH);
|
||||||
done();
|
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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
module ipe
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
|
||||||
|
github.com/gorilla/context v1.1.1 // indirect
|
||||||
|
github.com/gorilla/handlers v1.4.0
|
||||||
|
github.com/gorilla/mux v1.6.2
|
||||||
|
github.com/gorilla/websocket v1.4.0
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/pusher/pusher-http-go v1.3.0
|
||||||
|
github.com/stretchr/testify v1.2.2 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.2.1
|
||||||
|
)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
|
github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA=
|
||||||
|
github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||||
|
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
|
||||||
|
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pusher/pusher-http-go v1.3.0 h1:dWrjIsNheCUEo6YE9qlS8pIl3XzJa5yM8VlBu/LUl3M=
|
||||||
|
github.com/pusher/pusher-http-go v1.3.0/go.mod h1:XAv1fxRmVTI++2xsfofDhg7whapsLRG/gH/DXbF3a18=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 h1:kkXA53yGe04D0adEYJwEVQjeBppL01Exg+fnMjfUraU=
|
||||||
|
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Ipe
|
||||||
|
After=syslog.target network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=ipe
|
||||||
|
StandardOutput=syslog
|
||||||
|
StandardError=syslog
|
||||||
|
SyslogIdentifier=ipe
|
||||||
|
ExecStart=/path/to/ipe -logtostderr -config="path_to_config.json"
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,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))
|
||||||
|
}
|
||||||
-220
@@ -1,220 +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 `json:"-"`
|
|
||||||
Connections map[string]*connection `json:"-"`
|
|
||||||
|
|
||||||
Stats *expvar.Map `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alloc memory for Connections and Channels
|
|
||||||
func (a *app) Init() {
|
|
||||||
a.Connections = make(map[string]*connection)
|
|
||||||
a.Channels = make(map[string]*channel)
|
|
||||||
a.Stats = expvar.NewMap(fmt.Sprintf("%s (%s)", a.Name, a.AppID))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
-253
@@ -1,253 +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 newApp() *app {
|
|
||||||
|
|
||||||
a := app{Name: "Test", AppID: strconv.Itoa(id), Key: "123", Secret: "123", OnlySSL: false, ApplicationDisabled: false, UserEvents: true}
|
|
||||||
a.Init()
|
|
||||||
|
|
||||||
id++
|
|
||||||
return &a
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConnect(t *testing.T) {
|
|
||||||
app := newApp()
|
|
||||||
|
|
||||||
app.Connect(newConnection("socketID", nil))
|
|
||||||
|
|
||||||
if len(app.Connections) != 1 {
|
|
||||||
t.Errorf("Connections must be 1, but was %d", len(app.Connections))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDisconnect(t *testing.T) {
|
|
||||||
app := newApp()
|
|
||||||
|
|
||||||
app.Connect(newConnection("socketID", nil))
|
|
||||||
app.Disconnect("socketID")
|
|
||||||
|
|
||||||
if len(app.Connections) != 0 {
|
|
||||||
t.Errorf("Connections must be 0, but was %d", len(app.Connections))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFindConnection(t *testing.T) {
|
|
||||||
app := newApp()
|
|
||||||
|
|
||||||
app.Connect(newConnection("socketID", nil))
|
|
||||||
|
|
||||||
if _, err := app.FindConnection("socketID"); err != nil {
|
|
||||||
t.Error("Must find Connection")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := app.FindConnection("NotFound"); err == nil {
|
|
||||||
t.Error("Must not found Connection")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFindChannelByChannelID(t *testing.T) {
|
|
||||||
app := newApp()
|
|
||||||
|
|
||||||
channel := newChannel("ID")
|
|
||||||
app.AddChannel(channel)
|
|
||||||
|
|
||||||
if _, err := app.FindChannelByChannelID("ID"); err != nil {
|
|
||||||
t.Error("Channel not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFindOrCreateChannelByChannelID(t *testing.T) {
|
|
||||||
app := newApp()
|
|
||||||
|
|
||||||
if len(app.Channels) != 0 {
|
|
||||||
t.Error("Length of channels must be 0 before test")
|
|
||||||
}
|
|
||||||
|
|
||||||
app.FindOrCreateChannelByChannelID("ID")
|
|
||||||
|
|
||||||
if len(app.Channels) != 1 {
|
|
||||||
t.Error("Length of channels must be 1 after test")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemoveChannel(t *testing.T) {
|
|
||||||
app := newApp()
|
|
||||||
|
|
||||||
if len(app.Channels) != 0 {
|
|
||||||
t.Error("Length of channels must be 0 before test")
|
|
||||||
}
|
|
||||||
|
|
||||||
channel := newChannel("ID")
|
|
||||||
app.AddChannel(channel)
|
|
||||||
|
|
||||||
if len(app.Channels) != 1 {
|
|
||||||
t.Error("Length of channels after insert must be 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
app.RemoveChannel(channel)
|
|
||||||
|
|
||||||
if len(app.Channels) != 0 {
|
|
||||||
t.Error("Length of channels must be 0 after remove")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_add_channels(t *testing.T) {
|
|
||||||
|
|
||||||
app := newApp()
|
|
||||||
|
|
||||||
// Public
|
|
||||||
|
|
||||||
if len(app.PublicChannels()) != 0 {
|
|
||||||
t.Error("Length of public channels must be 0 before test")
|
|
||||||
}
|
|
||||||
|
|
||||||
app.AddChannel(newChannel("ID"))
|
|
||||||
|
|
||||||
if len(app.PublicChannels()) != 1 {
|
|
||||||
t.Error("Length os public channels after insert must be 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Presence
|
|
||||||
|
|
||||||
if len(app.PresenceChannels()) != 0 {
|
|
||||||
t.Error("Length of presence channels must be 0 before test")
|
|
||||||
}
|
|
||||||
|
|
||||||
app.AddChannel(newChannel("presence-test"))
|
|
||||||
|
|
||||||
if len(app.PresenceChannels()) != 1 {
|
|
||||||
t.Error("Length os presence channels after insert must be 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private
|
|
||||||
|
|
||||||
if len(app.PrivateChannels()) != 0 {
|
|
||||||
t.Error("Length of private channels must be 0 before test")
|
|
||||||
}
|
|
||||||
|
|
||||||
app.AddChannel(newChannel("private-test"))
|
|
||||||
|
|
||||||
if len(app.PrivateChannels()) != 1 {
|
|
||||||
t.Error("Length os private channels after insert must be 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_AllChannels(t *testing.T) {
|
|
||||||
app := newApp()
|
|
||||||
app.AddChannel(newChannel("private-test"))
|
|
||||||
app.AddChannel(newChannel("presence-test"))
|
|
||||||
app.AddChannel(newChannel("test"))
|
|
||||||
|
|
||||||
if len(app.Channels) != 3 {
|
|
||||||
t.Error("Must have 3 channels")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_New_Subscriber(t *testing.T) {
|
|
||||||
app := newApp()
|
|
||||||
|
|
||||||
if len(app.Connections) != 0 {
|
|
||||||
t.Error("Length of subscribers before test must be 0")
|
|
||||||
}
|
|
||||||
|
|
||||||
conn := newConnection("1", nil)
|
|
||||||
app.Connect(conn)
|
|
||||||
|
|
||||||
if len(app.Connections) != 1 {
|
|
||||||
t.Error("Length os subscribers after test must be 1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_find_subscriber(t *testing.T) {
|
|
||||||
app := newApp()
|
|
||||||
conn := newConnection("1", nil)
|
|
||||||
app.Connect(conn)
|
|
||||||
|
|
||||||
conn, err := app.FindConnection("1")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conn.SocketID != "1" {
|
|
||||||
t.Error("Wrong subscriber.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a wrong subscriber
|
|
||||||
|
|
||||||
conn, err = app.FindConnection("DoesNotExists")
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Opps, Must be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if conn != nil {
|
|
||||||
t.Error("Opps, Must be nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_find_or_create_channels(t *testing.T) {
|
|
||||||
app := newApp()
|
|
||||||
|
|
||||||
// Public
|
|
||||||
if len(app.PublicChannels()) != 0 {
|
|
||||||
t.Error("Length of public channels must be 0 before test")
|
|
||||||
}
|
|
||||||
|
|
||||||
c := app.FindOrCreateChannelByChannelID("id")
|
|
||||||
|
|
||||||
if len(app.PublicChannels()) != 1 {
|
|
||||||
t.Error("Length os public channels after insert must be 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.ChannelID != "id" {
|
|
||||||
t.Error("Opps wrong channel")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Presence
|
|
||||||
if len(app.PresenceChannels()) != 0 {
|
|
||||||
t.Error("Length of presence channels must be 0 before test")
|
|
||||||
}
|
|
||||||
|
|
||||||
c = app.FindOrCreateChannelByChannelID("presence-test")
|
|
||||||
|
|
||||||
if len(app.PresenceChannels()) != 1 {
|
|
||||||
t.Error("Length os presence channels after insert must be 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.ChannelID != "presence-test" {
|
|
||||||
t.Error("Opps wrong channel")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private
|
|
||||||
if len(app.PrivateChannels()) != 0 {
|
|
||||||
t.Error("Length of private channels must be 0 before test")
|
|
||||||
}
|
|
||||||
|
|
||||||
c = app.FindOrCreateChannelByChannelID("private-test")
|
|
||||||
|
|
||||||
if len(app.PrivateChannels()) != 1 {
|
|
||||||
t.Error("Length os private channels after insert must be 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.ChannelID != "private-test" {
|
|
||||||
t.Error("Opps wrong channel")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
-77
@@ -1,77 +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 (
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
log "github.com/golang/glog"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
|
|
||||||
"github.com/dimiro1/ipe/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Prepare Querystring
|
|
||||||
func prepareQueryString(params url.Values) string {
|
|
||||||
var keys []string
|
|
||||||
|
|
||||||
for key := range params {
|
|
||||||
keys = append(keys, strings.ToLower(key))
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
var pieces []string
|
|
||||||
|
|
||||||
for _, key := range keys {
|
|
||||||
pieces = append(pieces, key+"="+params.Get(key))
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(pieces, "&")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authenticate pusher
|
|
||||||
// see: https://gist.github.com/mloughran/376898
|
|
||||||
//
|
|
||||||
// The signature is a HMAC SHA256 hex digest.
|
|
||||||
// This is generated by signing a string made up of the following components concatenated with newline characters \n.
|
|
||||||
//
|
|
||||||
// * The uppercase request method (e.g. POST)
|
|
||||||
// * The request path (e.g. /some/resource)
|
|
||||||
// * The query parameters sorted by key, with keys converted to lowercase, then joined as in the query string.
|
|
||||||
// Note that the string must not be url escaped (e.g. given the keys auth_key: foo, Name: Something else, you get auth_key=foo&name=Something else)
|
|
||||||
func restAuthenticationHandler(h http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
appID := vars["app_id"]
|
|
||||||
|
|
||||||
app, err := conf.GetAppByAppID(appID)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
params := r.URL.Query()
|
|
||||||
|
|
||||||
signature := params.Get("auth_signature")
|
|
||||||
params.Del("auth_signature")
|
|
||||||
|
|
||||||
queryString := prepareQueryString(params)
|
|
||||||
|
|
||||||
toSign := strings.ToUpper(r.Method) + "\n" + r.URL.Path + "\n" + queryString
|
|
||||||
|
|
||||||
if utils.HashMAC([]byte(toSign), []byte(app.Secret)) == signature {
|
|
||||||
h.ServeHTTP(w, r)
|
|
||||||
} else {
|
|
||||||
log.Error("Not authorized")
|
|
||||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
-224
@@ -1,224 +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"
|
|
||||||
|
|
||||||
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 strings.HasPrefix(c.ChannelID, "presence-")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the type of the channel is private
|
|
||||||
func (c *channel) IsPrivate() bool {
|
|
||||||
return strings.HasPrefix(c.ChannelID, "private-")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,98 +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 "testing"
|
|
||||||
|
|
||||||
func TestIsOccupied(t *testing.T) {
|
|
||||||
c := newChannel("ID")
|
|
||||||
|
|
||||||
if c.IsOccupied() {
|
|
||||||
t.Error("Channels must be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Subscriptions["ID"] = newSubscription(newConnection("ID", nil), "")
|
|
||||||
|
|
||||||
if !c.IsOccupied() {
|
|
||||||
t.Error("Channels must be empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsPrivate(t *testing.T) {
|
|
||||||
c := newChannel("private-channel")
|
|
||||||
|
|
||||||
if !c.IsPrivate() {
|
|
||||||
t.Error("The Channel must be private")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsPresence(t *testing.T) {
|
|
||||||
c := newChannel("presence-channel")
|
|
||||||
|
|
||||||
if !c.IsPresence() {
|
|
||||||
t.Error("The Channel must be presence")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsPublic(t *testing.T) {
|
|
||||||
c := newChannel("channel")
|
|
||||||
|
|
||||||
if !c.IsPublic() {
|
|
||||||
t.Error("The Channel must be public")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsPrivateOrPresence(t *testing.T) {
|
|
||||||
c := newChannel("private-channel")
|
|
||||||
|
|
||||||
if !c.IsPresenceOrPrivate() {
|
|
||||||
t.Error("The Channel must be private or presence")
|
|
||||||
}
|
|
||||||
|
|
||||||
c = newChannel("presence-channel")
|
|
||||||
|
|
||||||
if !c.IsPresenceOrPrivate() {
|
|
||||||
t.Error("The Channel must be private or presence")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTotalSubscriptions(t *testing.T) {
|
|
||||||
c := newChannel("ID")
|
|
||||||
|
|
||||||
if c.TotalSubscriptions() != len(c.Subscriptions) {
|
|
||||||
t.Error("TotalSubscriptions must be equal to len of total subscriptions")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTotalUsers(t *testing.T) {
|
|
||||||
c := newChannel("ID")
|
|
||||||
|
|
||||||
c.Subscriptions["1"] = newSubscription(newConnection("ID", nil), "")
|
|
||||||
c.Subscriptions["2"] = newSubscription(newConnection("ID", nil), "")
|
|
||||||
|
|
||||||
if c.TotalSubscriptions() != len(c.Subscriptions) {
|
|
||||||
t.Error("TotalSubscriptions must be equal to len of total subscriptions")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.TotalUsers() != 1 {
|
|
||||||
t.Error("TotalUsers must be equal to 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsSubscribed(t *testing.T) {
|
|
||||||
c := newChannel("ID")
|
|
||||||
conn := newConnection("ID", nil)
|
|
||||||
|
|
||||||
if c.IsSubscribed(conn) {
|
|
||||||
t.Error("Must not be subscribed")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Subscriptions["ID"] = newSubscription(conn, "")
|
|
||||||
|
|
||||||
if !c.IsSubscribed(conn) {
|
|
||||||
t.Error("Must be subscribed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"Host": ":8080",
|
|
||||||
"Apps": [
|
|
||||||
{
|
|
||||||
"ApplicationDisabled": false,
|
|
||||||
"Secret": "7ad3753142a6693b25b9",
|
|
||||||
"Key": "278d525bdf162c739803",
|
|
||||||
"Name": "App 1",
|
|
||||||
"AppID": "321",
|
|
||||||
"UserEvents": true,
|
|
||||||
"WebHooks": true,
|
|
||||||
"URLWebHook": "http://127.0.0.1:4567/php/hook.php"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ApplicationDisabled": false,
|
|
||||||
"Secret": "d6824d2fa32888931504",
|
|
||||||
"Key": "c8b30f611ffb13202976",
|
|
||||||
"Name": "App 2",
|
|
||||||
"AppID": "123",
|
|
||||||
"UserEvents": true,
|
|
||||||
"WebHooks": false,
|
|
||||||
"URLWebHook": "http://127.0.0.1:4567/php/hook.php"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,49 +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"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The config file
|
|
||||||
type configFile struct {
|
|
||||||
Host string // The host, eg: :8080 will start on 0.0.0.0:8080
|
|
||||||
User string
|
|
||||||
Password string
|
|
||||||
Apps []*app
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize Apps
|
|
||||||
func (c *configFile) Init() {
|
|
||||||
for _, app := range c.Apps {
|
|
||||||
app.Init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *configFile) WasProvidedUserAndPassword() bool {
|
|
||||||
return len(strings.TrimSpace(c.User)) > 0 && len(strings.TrimSpace(c.Password)) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns an App with by appID
|
|
||||||
func (c *configFile) GetAppByAppID(appID string) (*app, error) {
|
|
||||||
for _, a := range c.Apps {
|
|
||||||
if a.AppID == appID {
|
|
||||||
return a, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &app{}, errors.New("App not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns an App with by key
|
|
||||||
func (c *configFile) GetAppByKey(key string) (*app, error) {
|
|
||||||
for _, a := range c.Apps {
|
|
||||||
if a.Key == key {
|
|
||||||
return a, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &app{}, errors.New("App not found")
|
|
||||||
}
|
|
||||||
@@ -1,35 +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"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
// An User Connection
|
|
||||||
type connection struct {
|
|
||||||
SocketID string
|
|
||||||
Socket *websocket.Conn
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new Subscriber
|
|
||||||
func newConnection(socketID string, s *websocket.Conn) *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{}) {
|
|
||||||
go func() {
|
|
||||||
if err := conn.Socket.WriteJSON(m); err != nil {
|
|
||||||
log.Errorf("Error publishing message to connection %+v, %s", conn, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
@@ -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.
|
|
||||||
APPLICATION_ONLY_ACCEPTS_SSL = 4000
|
|
||||||
APPLICATION_DOES_NOT_EXISTS = 4001
|
|
||||||
APPLICATION_DISABLED = 4003
|
|
||||||
APPLICATION_IS_OVER_CONNECTION_QUOTA = 4004 // Not Implemented
|
|
||||||
PATH_NOT_FOUND = 4005 // Not Implemented
|
|
||||||
INVALID_VERSION_STRING_FORMAT = 4006
|
|
||||||
UNSUPPORTED_PROTOCOL_VERSION = 4007
|
|
||||||
NO_PROTOCOL_VERSION_SUPPLIED = 4008
|
|
||||||
|
|
||||||
// 4100 - 4199
|
|
||||||
// Indicates an error resulting in the connection being closed by Pusher,
|
|
||||||
// and the client may reconnect after 1s or more
|
|
||||||
OVER_CAPACITY = 4100 // Not Implemented
|
|
||||||
|
|
||||||
// 4200 - 4299
|
|
||||||
// Indicate an error resulting in the connection being closed by Pusher,
|
|
||||||
// and the client my reconnect immediately
|
|
||||||
GENERIC_RECONNECT_IMMEDIATELY = 4200
|
|
||||||
PONG_REPLY_NOT_RECEIVED = 4201 // Ping was sent to the client, but no reply was received; Not Implemented
|
|
||||||
CLOSED_AFTER_INACTIVITY = 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
|
|
||||||
CLIENT_REJECTED_DUE_TO_RATE_LIMIT = 4301 // Not Implemented
|
|
||||||
|
|
||||||
// Pusher send null, This app use this error code to send the null value
|
|
||||||
// see ErrorEvent
|
|
||||||
GENERIC_ERROR = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
// Only this version is supported
|
|
||||||
const SUPPORTED_PROTOCOL_VERSION = 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 MAX_DATA_EVENT_SIZE = 10 * 1000
|
|
||||||
-118
@@ -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: UNSUPPORTED_PROTOCOL_VERSION, Msg: "Unsupported protocol version"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The application does not exists
|
|
||||||
// See the configuration file
|
|
||||||
type applicationDoesNotExistsError struct {
|
|
||||||
baseWebsocketError
|
|
||||||
}
|
|
||||||
|
|
||||||
func newApplicationDoesNotExistsError() applicationDoesNotExistsError {
|
|
||||||
return applicationDoesNotExistsError{
|
|
||||||
baseWebsocketError{Code: APPLICATION_DOES_NOT_EXISTS, 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: NO_PROTOCOL_VERSION_SUPPLIED, 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: APPLICATION_DISABLED, Msg: "Application disabled"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the application only accepts SSL connections
|
|
||||||
type applicationOnlyAccepsSSLError struct {
|
|
||||||
baseWebsocketError
|
|
||||||
}
|
|
||||||
|
|
||||||
func newApplicationOnlyAccepsSSLError() applicationOnlyAccepsSSLError {
|
|
||||||
return applicationOnlyAccepsSSLError{
|
|
||||||
baseWebsocketError{Code: APPLICATION_ONLY_ACCEPTS_SSL, 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: INVALID_VERSION_STRING_FORMAT, 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: GENERIC_RECONNECT_IMMEDIATELY, 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: GENERIC_ERROR, Msg: msg},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-278
@@ -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 == GENERIC_ERROR {
|
|
||||||
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}
|
|
||||||
}
|
|
||||||
@@ -1,34 +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 (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check if the application is disabled
|
|
||||||
func restCheckAppDisabledHandler(h http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
appID := vars["app_id"]
|
|
||||||
|
|
||||||
currentApp, err := conf.GetAppByAppID(appID)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if currentApp.ApplicationDisabled {
|
|
||||||
http.Error(w, "Application disabled", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
-39
@@ -1,39 +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"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Conf holds the global configuration state
|
|
||||||
var conf configFile
|
|
||||||
|
|
||||||
// Start Parse the configuration file and starts the ipe server
|
|
||||||
func Start(configfile string) error {
|
|
||||||
rand.Seed(time.Now().Unix())
|
|
||||||
file, err := ioutil.ReadFile(configfile)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(file, &conf); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.Init()
|
|
||||||
router := newRouter()
|
|
||||||
|
|
||||||
if err := http.ListenAndServe(conf.Host, router); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,32 +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 (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
// newRouter is a function that returns a new configured Router
|
|
||||||
// It add the necessary middlewares
|
|
||||||
func newRouter() *mux.Router {
|
|
||||||
router := mux.NewRouter().StrictSlash(true)
|
|
||||||
|
|
||||||
for _, route := range routes {
|
|
||||||
var handler http.Handler
|
|
||||||
|
|
||||||
handler = route.HandlerFunc
|
|
||||||
|
|
||||||
if route.RequiresRestAuth {
|
|
||||||
handler = restAuthenticationHandler(handler)
|
|
||||||
handler = restCheckAppDisabledHandler(handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
router.Methods(route.Method).Path(route.Pattern).Name(route.Name).Handler(handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
return router
|
|
||||||
}
|
|
||||||
@@ -1,56 +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 (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A route
|
|
||||||
type route struct {
|
|
||||||
Name string
|
|
||||||
Method string
|
|
||||||
Pattern string
|
|
||||||
HandlerFunc http.HandlerFunc
|
|
||||||
RequiresRestAuth bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var routes = []route{
|
|
||||||
{
|
|
||||||
"PostEvents",
|
|
||||||
"POST",
|
|
||||||
"/apps/{app_id}/events",
|
|
||||||
postEvents,
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"GetChannels",
|
|
||||||
"GET",
|
|
||||||
"/apps/{app_id}/channels",
|
|
||||||
getChannels,
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"GetChannel",
|
|
||||||
"GET",
|
|
||||||
"/apps/{app_id}/channels/{channel_name}",
|
|
||||||
getChannel,
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"GetChannelUsers",
|
|
||||||
"GET",
|
|
||||||
"/apps/{app_id}/channels/{channel_name}/users",
|
|
||||||
getChannelUsers,
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Websocket",
|
|
||||||
"GET",
|
|
||||||
"/app/{key}",
|
|
||||||
wsHandler,
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -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}
|
|
||||||
}
|
|
||||||
-164
@@ -1,164 +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 (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/dimiro1/ipe/utils"
|
|
||||||
log "github.com/golang/glog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A WebHook is sent as a HTTP POST request to the url which you specify.
|
|
||||||
// The POST request payload (body) contains a JSON document, and follows the following format:
|
|
||||||
// {
|
|
||||||
// "time_ms": 1327078148132
|
|
||||||
// "events": [
|
|
||||||
// { "name": "event_name", "some": "data" }
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Security
|
|
||||||
// Encryption
|
|
||||||
//
|
|
||||||
// You may use a HTTP or a HTTPS url for WebHooks. In most cases HTTP is sufficient, but HTTPS can be useful if your data is sensitive or if you wish to protect against replay attacks for example.
|
|
||||||
// Authentication
|
|
||||||
//
|
|
||||||
// Since anyone could in principle send WebHooks to your application, it’s important to verify that these WebHooks originated from Pusher. Valid WebHooks will therefore contain these headers which contain a HMAC signature of the WebHook payload (body):
|
|
||||||
//
|
|
||||||
// X-Pusher-Key: The App Key.
|
|
||||||
// X-Pusher-Signature: A HMAC SHA256 hex digest formed by signing the POST payload (body) with the token’s secret.
|
|
||||||
type webHook struct {
|
|
||||||
TimeMs int64 `json:"time_ms"`
|
|
||||||
Events []hookEvent `json:"events"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type hookEvent struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Channel string `json:"channel"`
|
|
||||||
Event string `json:"event,omitempty"`
|
|
||||||
Data interface{} `json:"data,omitempty"`
|
|
||||||
SocketID string `json:"socket_id,omitempty"`
|
|
||||||
UserID string `json:"user_id,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func newChannelOcuppiedHook(channel *channel) hookEvent {
|
|
||||||
return hookEvent{Name: "channel_occupied", Channel: channel.ChannelID}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newChannelVacatedHook(channel *channel) hookEvent {
|
|
||||||
return hookEvent{Name: "channel_vacated", Channel: channel.ChannelID}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMemberAddedHook(channel *channel, s *subscription) hookEvent {
|
|
||||||
return hookEvent{Name: "member_added", Channel: channel.ChannelID, UserID: s.ID}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMemberRemovedHook(channel *channel, s *subscription) hookEvent {
|
|
||||||
return hookEvent{Name: "member_removed", Channel: channel.ChannelID, UserID: s.ID}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newClientHook(channel *channel, s *subscription, event string, data interface{}) hookEvent {
|
|
||||||
return hookEvent{Name: "client_event", Channel: channel.ChannelID, Event: event, Data: data, SocketID: s.Connection.SocketID}
|
|
||||||
}
|
|
||||||
|
|
||||||
// channel_occupied
|
|
||||||
// { "name": "channel_occupied", "channel": "test_channel" }
|
|
||||||
func (a *app) TriggerChannelOccupiedHook(c *channel) {
|
|
||||||
event := newChannelOcuppiedHook(c)
|
|
||||||
triggerHook(event.Name, a, c, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// channel_vacated
|
|
||||||
// { "name": "channel_vacated", "channel": "test_channel" }
|
|
||||||
func (a *app) TriggerChannelVacatedHook(c *channel) {
|
|
||||||
event := newChannelVacatedHook(c)
|
|
||||||
triggerHook(event.Name, a, c, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// {
|
|
||||||
// "name": "client_event",
|
|
||||||
// "channel": "name of the channel the event was published on",
|
|
||||||
// "event": "name of the event",
|
|
||||||
// "data": "data associated with the event",
|
|
||||||
// "socket_id": "socket_id of the sending socket",
|
|
||||||
// "user_id": "user_id associated with the sending socket" # Only for presence channels
|
|
||||||
// }
|
|
||||||
func (a *app) TriggerClientEventHook(c *channel, s *subscription, clientEvent string, data interface{}) {
|
|
||||||
event := newClientHook(c, s, clientEvent, data)
|
|
||||||
|
|
||||||
if c.IsPresence() {
|
|
||||||
event.UserID = s.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerHook(event.Name, a, c, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// {
|
|
||||||
// "name": "member_added",
|
|
||||||
// "channel": "presence-your_channel_name",
|
|
||||||
// "user_id": "a_user_id"
|
|
||||||
// }
|
|
||||||
func (a *app) TriggerMemberAddedHook(c *channel, s *subscription) {
|
|
||||||
event := newMemberAddedHook(c, s)
|
|
||||||
triggerHook(event.Name, a, c, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// {
|
|
||||||
// "name": "member_removed",
|
|
||||||
// "channel": "presence-your_channel_name",
|
|
||||||
// "user_id": "a_user_id"
|
|
||||||
// }
|
|
||||||
func (a *app) TriggerMemberRemovedHook(c *channel, s *subscription) {
|
|
||||||
event := newMemberRemovedHook(c, s)
|
|
||||||
triggerHook(event.Name, a, c, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
func triggerHook(name string, a *app, c *channel, event hookEvent) {
|
|
||||||
if !a.WebHooks {
|
|
||||||
log.Infof("Webhooks are not enabled for app: %s", a.Name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
log.Infof("Triggering %s event", name)
|
|
||||||
|
|
||||||
hook := webHook{TimeMs: time.Now().Unix()}
|
|
||||||
|
|
||||||
hook.Events = append(hook.Events, event)
|
|
||||||
|
|
||||||
var js []byte
|
|
||||||
var err error
|
|
||||||
|
|
||||||
js, err = json.Marshal(hook)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error decoding json: %+v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req *http.Request
|
|
||||||
|
|
||||||
req, err = http.NewRequest("POST", a.URLWebHook, bytes.NewReader(js))
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error creating request: %+v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("X-Pusher-Key", a.Key)
|
|
||||||
req.Header.Set("X-Pusher-Signature", utils.HashMAC(js, []byte(a.Secret)))
|
|
||||||
|
|
||||||
log.V(1).Infof("%+v", req.Header)
|
|
||||||
log.V(1).Infof("%+v", string(js))
|
|
||||||
|
|
||||||
if _, err := http.DefaultClient.Do(req); err != nil {
|
|
||||||
log.Errorf("Error posting %s event: %+v", name, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
log "github.com/golang/glog"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"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 != SUPPORTED_PROTOCOL_VERSION:
|
|
||||||
return newUnsupportedProtocolVersionError()
|
|
||||||
case app.ApplicationDisabled:
|
|
||||||
return newApplicationDisabledError()
|
|
||||||
case r.TLS != nil:
|
|
||||||
if app.OnlySSL {
|
|
||||||
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 := strings.HasPrefix(channelName, "presence-")
|
|
||||||
isPrivate := strings.HasPrefix(channelName, "private-")
|
|
||||||
|
|
||||||
if isPresence || isPrivate {
|
|
||||||
toSign := []string{connection.SocketID, channelName}
|
|
||||||
|
|
||||||
if isPresence {
|
|
||||||
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(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
|
|
||||||
}
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
appKey := vars["key"]
|
|
||||||
|
|
||||||
app, err := conf.GetAppByKey(appKey)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
emitWSError(newApplicationDoesNotExistsError(), conn)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionID := utils.GenerateSessionID()
|
|
||||||
|
|
||||||
if err := onOpen(conn, w, r, sessionID, app); err != nil {
|
|
||||||
emitWSError(err, conn)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
onMessage(conn, w, r, sessionID, app)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit an Websocket ErrorEvent
|
|
||||||
func emitWSError(err websocketError, conn *websocket.Conn) {
|
|
||||||
|
|
||||||
event := newErrorEvent(err.GetCode(), err.GetMsg())
|
|
||||||
|
|
||||||
if err := conn.WriteJSON(event); err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +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 main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/dimiro1/ipe/ipe"
|
|
||||||
log "github.com/golang/glog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Main function, initialze the system
|
|
||||||
func main() {
|
|
||||||
var filename = flag.String("config", "config.json", "Config file location")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
printBanner()
|
|
||||||
|
|
||||||
if err := ipe.Start(*filename); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print a beautifull banner
|
|
||||||
func printBanner() {
|
|
||||||
fmt.Print("\033[36m")
|
|
||||||
fmt.Print(`
|
|
||||||
██╗██████╗ ███████╗
|
|
||||||
██║██╔══██╗██╔════╝
|
|
||||||
██║██████╔╝█████╗
|
|
||||||
██║██╔═══╝ ██╔══╝
|
|
||||||
██║██║ ███████╗
|
|
||||||
╚═╝╚═╝ ╚══════╝`)
|
|
||||||
fmt.Println("\033[0m")
|
|
||||||
fmt.Println("\033[32mWelcome to Ipê - Yet another Pusher server clone\033[0m")
|
|
||||||
fmt.Println("\033[33mBy: Claudemiro Alves Feitosa Neto <dimiro1@gmail.com>\033[0m")
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"ipe/app"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Storage represents a app database
|
||||||
|
// For now it there is only one memory database implementation
|
||||||
|
// but in the future I can write a sql implementation
|
||||||
|
type Storage interface {
|
||||||
|
GetAppByAppID(appID string) (*app.Application, error)
|
||||||
|
GetAppByKey(key string) (*app.Application, error)
|
||||||
|
AddApp(application *app.Application) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// InMemory in memory implementation of Storage
|
||||||
|
type InMemory struct {
|
||||||
|
sync.RWMutex
|
||||||
|
Apps []*app.Application
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInMemory returns an InMemory storage
|
||||||
|
func NewInMemory() Storage {
|
||||||
|
return &InMemory{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddApp adds app into memory
|
||||||
|
func (db *InMemory) AddApp(application *app.Application) error {
|
||||||
|
db.Lock()
|
||||||
|
defer db.Unlock()
|
||||||
|
|
||||||
|
db.Apps = append(db.Apps, application)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppByAppID returns an App with by appID
|
||||||
|
func (db *InMemory) GetAppByAppID(appID string) (*app.Application, error) {
|
||||||
|
db.RLock()
|
||||||
|
defer db.RUnlock()
|
||||||
|
|
||||||
|
for _, a := range db.Apps {
|
||||||
|
if a.AppID == appID {
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("app not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppByKey returns an App with by key
|
||||||
|
func (db *InMemory) GetAppByKey(key string) (*app.Application, error) {
|
||||||
|
db.RLock()
|
||||||
|
defer db.RUnlock()
|
||||||
|
|
||||||
|
for _, a := range db.Apps {
|
||||||
|
if a.Key == key {
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("app not found")
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ipe/app"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Benchmark_memdb_GetAppByAppID(b *testing.B) {
|
||||||
|
storage := NewInMemory()
|
||||||
|
_ = storage.AddApp(&app.Application{AppID: "123456", Name: "Example"})
|
||||||
|
_ = storage.AddApp(&app.Application{AppID: "654321", Name: "Example2"})
|
||||||
|
_ = storage.AddApp(&app.Application{AppID: "678901", Name: "Example3"})
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = storage.GetAppByAppID("123456")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_db_GetAppByAppID(t *testing.T) {
|
||||||
|
_app := &app.Application{AppID: "123456", Name: "Example"}
|
||||||
|
|
||||||
|
storage := NewInMemory()
|
||||||
|
_ = storage.AddApp(_app)
|
||||||
|
|
||||||
|
a, err := storage.GetAppByAppID("123456")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetAppByAppID(%q) == %+v, want %+v", "123456", a, _app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_db_GetAppByAppID__error(t *testing.T) {
|
||||||
|
_app := &app.Application{AppID: "123456", Name: "Example"}
|
||||||
|
|
||||||
|
storage := NewInMemory()
|
||||||
|
_ = storage.AddApp(_app)
|
||||||
|
|
||||||
|
a, err := storage.GetAppByAppID("not-found")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("GetAppByAppID(%q) == %+v, want %+v", "123456", a, _app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_db_GetAppByKey(t *testing.T) {
|
||||||
|
_app := &app.Application{AppID: "123456", Name: "Example", Key: "654321"}
|
||||||
|
|
||||||
|
storage := NewInMemory()
|
||||||
|
_ = storage.AddApp(_app)
|
||||||
|
|
||||||
|
a, err := storage.GetAppByKey("654321")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetAppByKey(%q) == %+v, want %+v", "654321", a, _app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_db_GetAppByKey__error(t *testing.T) {
|
||||||
|
_app := &app.Application{AppID: "123456", Name: "Example", Key: "654321"}
|
||||||
|
|
||||||
|
storage := NewInMemory()
|
||||||
|
_ = storage.AddApp(_app)
|
||||||
|
|
||||||
|
a, err := storage.GetAppByKey("not-found")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("GetAppByKey(%q) == %+v, want %+v", "not-found", a, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
}
|
||||||
+20
-10
@@ -12,8 +12,11 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var channelValidationRegex = regexp.MustCompile("^[A-Za-z0-9_\\-=@,.;]+$")
|
||||||
|
|
||||||
// HashMAC Calculates the MAC signing with the given key and returns the hexadecimal encoded Result
|
// HashMAC Calculates the MAC signing with the given key and returns the hexadecimal encoded Result
|
||||||
func HashMAC(message, key []byte) string {
|
func HashMAC(message, key []byte) string {
|
||||||
mac := hmac.New(sha256.New, key)
|
mac := hmac.New(sha256.New, key)
|
||||||
@@ -25,18 +28,25 @@ func HashMAC(message, key []byte) string {
|
|||||||
|
|
||||||
// GenerateSessionID Generate a new random Hash
|
// GenerateSessionID Generate a new random Hash
|
||||||
func GenerateSessionID() string {
|
func GenerateSessionID() string {
|
||||||
MAX := math.MaxInt64
|
return fmt.Sprintf("%d.%d", rand.Intn(math.MaxInt32), rand.Intn(math.MaxInt32))
|
||||||
|
|
||||||
return fmt.Sprintf("%d.%d", rand.Intn(MAX), rand.Intn(MAX))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsChannelNameValid Verify if the channel name is valid
|
// IsChannelNameValid Verify if the channel name is valid
|
||||||
func IsChannelNameValid(channelName string) bool {
|
func IsChannelNameValid(channelName string) bool {
|
||||||
matched, err := regexp.MatchString("^[A-Za-z0-9_\\-=@,.;]+$", channelName)
|
return channelValidationRegex.MatchString(channelName)
|
||||||
|
}
|
||||||
if err == nil && matched {
|
|
||||||
return true
|
// IsPrivateChannel Verify if the channel name represents a private channel
|
||||||
}
|
func IsPrivateChannel(channelName string) bool {
|
||||||
|
return strings.HasPrefix(channelName, "private-")
|
||||||
return false
|
}
|
||||||
|
|
||||||
|
// 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-")
|
||||||
}
|
}
|
||||||
|
|||||||
+99
-8
@@ -9,6 +9,18 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func BenchmarkGenerateSession(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
GenerateSessionID()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkIsChannelNameValid(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
IsChannelNameValid("hello-world")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGenerateSession(t *testing.T) {
|
func TestGenerateSession(t *testing.T) {
|
||||||
sessionID := GenerateSessionID()
|
sessionID := GenerateSessionID()
|
||||||
|
|
||||||
@@ -18,19 +30,98 @@ func TestGenerateSession(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIsValidChannelName(t *testing.T) {
|
func TestIsValidChannelName(t *testing.T) {
|
||||||
if IsChannelNameValid("#@#hhh**sasas") {
|
name := "#@#hhh**sasas"
|
||||||
t.Errorf("Invalid Channel Name")
|
ok := IsChannelNameValid(name)
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !IsChannelNameValid("private-hello") {
|
name = "private-hello"
|
||||||
t.Errorf("Must be Valid Channel Name")
|
ok = IsChannelNameValid(name)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !IsChannelNameValid("presence-hello") {
|
name = "presence-hello"
|
||||||
t.Errorf("Must be Valid Channel Name")
|
ok = IsChannelNameValid(name)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !IsChannelNameValid("public") {
|
name = "public"
|
||||||
t.Errorf("Must be Valid Channel Name")
|
ok = IsChannelNameValid(name)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsPrivateChannel_valid(t *testing.T) {
|
||||||
|
name := "private-hello"
|
||||||
|
ok := IsPrivateChannel(name)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("IsPrivateChannel(%s) == %t, wants %t", name, ok, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsPrivateChannel_invalid(t *testing.T) {
|
||||||
|
name := "hello"
|
||||||
|
ok := IsPrivateChannel(name)
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Errorf("IsPrivateChannel(%s) == %t, wants %t", name, ok, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsIsPresenceChannel_valid(t *testing.T) {
|
||||||
|
name := "presence-hello"
|
||||||
|
ok := IsPresenceChannel(name)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("IsPresenceChannel(%s) == %t, wants %t", name, ok, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsPresenceChannel_invalid(t *testing.T) {
|
||||||
|
name := "hello"
|
||||||
|
ok := IsPresenceChannel(name)
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Errorf("IsPresenceChannel(%s) == %t, wants %t", name, ok, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsClientEvent_valid(t *testing.T) {
|
||||||
|
name := "client-hello"
|
||||||
|
ok := IsClientEvent(name)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("IsClientEvent(%s) == %t, wants %t", name, ok, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsClientEvent_invalid(t *testing.T) {
|
||||||
|
name := "hello"
|
||||||
|
ok := IsClientEvent(name)
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Errorf("IsClientEvent(%s) == %t, wants %t", name, ok, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashMAC(t *testing.T) {
|
||||||
|
message := []byte("hello world")
|
||||||
|
key := []byte("my super secret key")
|
||||||
|
digest := HashMAC(message, key)
|
||||||
|
|
||||||
|
// See: http://www.freeformatter.com/hmac-generator.html
|
||||||
|
expected := "0811b8affc185a01e1a65b80089ebb1f7f68d287fc3b64581da9ec99136ad1db"
|
||||||
|
|
||||||
|
if digest != expected {
|
||||||
|
t.Errorf("HashMAC(%s, %q) == %s, wants %s", message, key, digest, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-191
@@ -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
@@ -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
|
|
||||||
http://code.google.com/p/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 golang-nuts@googlegroups.com.
|
|
||||||
-1177
File diff suppressed because it is too large
Load Diff
-124
@@ -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
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
language: go
|
|
||||||
|
|
||||||
go:
|
|
||||||
- 1.0
|
|
||||||
- 1.1
|
|
||||||
- 1.2
|
|
||||||
- 1.3
|
|
||||||
- 1.4
|
|
||||||
- tip
|
|
||||||
-27
@@ -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
@@ -1,7 +0,0 @@
|
|||||||
context
|
|
||||||
=======
|
|
||||||
[](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
@@ -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
@@ -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
@@ -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
|
|
||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
language: go
|
|
||||||
|
|
||||||
go:
|
|
||||||
- 1.0
|
|
||||||
- 1.1
|
|
||||||
- 1.2
|
|
||||||
- tip
|
|
||||||
-27
@@ -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
@@ -1,7 +0,0 @@
|
|||||||
mux
|
|
||||||
===
|
|
||||||
[](https://travis-ci.org/gorilla/mux)
|
|
||||||
|
|
||||||
gorilla/mux is a powerful URL router and dispatcher.
|
|
||||||
|
|
||||||
Read the full documentation here: http://www.gorillatoolkit.org/pkg/mux
|
|
||||||
-21
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-199
@@ -1,199 +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.domain.com".
|
|
||||||
r.Host("www.domain.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.domain.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.domain.com". Create a route for that host and get a "subrouter"
|
|
||||||
from it:
|
|
||||||
|
|
||||||
r := mux.NewRouter()
|
|
||||||
s := r.Host("www.domain.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.domain.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.
|
|
||||||
|
|
||||||
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
|
|
||||||
-366
@@ -1,366 +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 (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 droping 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 = r.NotFoundHandler
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// 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.
|
|
||||||
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{}) {
|
|
||||||
context.Set(r, varsKey, val)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setCurrentRoute(r *http.Request, val interface{}) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// mapFromPairs converts variadic string parameters to a string map.
|
|
||||||
func mapFromPairs(pairs ...string) (map[string]string, error) {
|
|
||||||
length := len(pairs)
|
|
||||||
if length%2 != 0 {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"mux: number of parameters must be multiple of 2, got %v", pairs)
|
|
||||||
}
|
|
||||||
m := make(map[string]string, length/2)
|
|
||||||
for i := 0; i < length; i += 2 {
|
|
||||||
m[pairs[i]] = pairs[i+1]
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchMap returns true if the given key/value pairs exist in a given map.
|
|
||||||
func matchMap(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
|
|
||||||
}
|
|
||||||
-1012
File diff suppressed because it is too large
Load Diff
-714
@@ -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.domain.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.domain.com").Subrouter()
|
|
||||||
route := subrouter.NewRoute().Path("/products/").Name("products")
|
|
||||||
|
|
||||||
url := "http://www.domain.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])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-272
@@ -1,272 +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"
|
|
||||||
"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 = "[^?&]+"
|
|
||||||
matchPrefix = true
|
|
||||||
} 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("")
|
|
||||||
if !matchQuery {
|
|
||||||
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.
|
|
||||||
fmt.Fprintf(pattern, "%s(%s)", regexp.QuoteMeta(raw), patt)
|
|
||||||
// Build the reverse template.
|
|
||||||
fmt.Fprintf(reverse, "%s%%s", raw)
|
|
||||||
// Append variable name and compiled pattern.
|
|
||||||
varsN[i/2] = name
|
|
||||||
varsR[i/2], 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 !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.regexp.MatchString(req.URL.RawQuery)
|
|
||||||
} 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// 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 {
|
|
||||||
for k, v := range v.host.varsN {
|
|
||||||
m.Vars[v] = hostVars[k+1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Store path variables.
|
|
||||||
if v.path != nil {
|
|
||||||
pathVars := v.path.regexp.FindStringSubmatch(req.URL.Path)
|
|
||||||
if pathVars != nil {
|
|
||||||
for k, v := range v.path.varsN {
|
|
||||||
m.Vars[v] = pathVars[k+1]
|
|
||||||
}
|
|
||||||
// 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.
|
|
||||||
rawQuery := req.URL.RawQuery
|
|
||||||
for _, q := range v.queries {
|
|
||||||
queryVars := q.regexp.FindStringSubmatch(rawQuery)
|
|
||||||
if queryVars != nil {
|
|
||||||
for k, v := range q.varsN {
|
|
||||||
m.Vars[v] = queryVars[k+1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
}
|
|
||||||
-571
@@ -1,571 +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"
|
|
||||||
"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 matchMap(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.
|
|
||||||
//
|
|
||||||
// It 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 = mapFromPairs(pairs...)
|
|
||||||
return r.addMatcher(headerMatcher(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.domain.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, true, 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.domain.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 := mapFromPairs(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
@@ -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
@@ -1,6 +0,0 @@
|
|||||||
language: go
|
|
||||||
|
|
||||||
go:
|
|
||||||
- 1.1
|
|
||||||
- 1.2
|
|
||||||
- tip
|
|
||||||
-8
@@ -1,8 +0,0 @@
|
|||||||
# This is the official list of Gorilla WebSocket authors for copyright
|
|
||||||
# purposes.
|
|
||||||
#
|
|
||||||
# Please keep the list sorted.
|
|
||||||
|
|
||||||
Gary Burd <gary@beagledreams.com>
|
|
||||||
Joachim Bauch <mail@joachim-bauch.de>
|
|
||||||
|
|
||||||
-22
@@ -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.
|
|
||||||
-59
@@ -1,59 +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)
|
|
||||||
* [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
@@ -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)))
|
|
||||||
}
|
|
||||||
-269
@@ -1,269 +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 (
|
|
||||||
"bytes"
|
|
||||||
"crypto/tls"
|
|
||||||
"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.
|
|
||||||
func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) {
|
|
||||||
challengeKey, err := generateChallengeKey()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
acceptKey := computeAcceptKey(challengeKey)
|
|
||||||
|
|
||||||
c = newConn(netConn, false, readBufSize, writeBufSize)
|
|
||||||
p := c.writeBuf[:0]
|
|
||||||
p = append(p, "GET "...)
|
|
||||||
p = append(p, u.RequestURI()...)
|
|
||||||
p = append(p, " HTTP/1.1\r\nHost: "...)
|
|
||||||
p = append(p, u.Host...)
|
|
||||||
// "Upgrade" is capitalized for servers that do not use case insensitive
|
|
||||||
// comparisons on header tokens.
|
|
||||||
p = append(p, "\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: "...)
|
|
||||||
p = append(p, challengeKey...)
|
|
||||||
p = append(p, "\r\n"...)
|
|
||||||
for k, vs := range requestHeader {
|
|
||||||
for _, v := range vs {
|
|
||||||
p = append(p, k...)
|
|
||||||
p = append(p, ": "...)
|
|
||||||
p = append(p, v...)
|
|
||||||
p = append(p, "\r\n"...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p = append(p, "\r\n"...)
|
|
||||||
|
|
||||||
if _, err := netConn.Write(p); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := http.ReadResponse(c.br, &http.Request{Method: "GET", URL: u})
|
|
||||||
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") != acceptKey {
|
|
||||||
return nil, resp, ErrBadHandshake
|
|
||||||
}
|
|
||||||
c.subprotocol = resp.Header.Get("Sec-Websocket-Protocol")
|
|
||||||
return c, resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
// 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. The url.Parse function is not used here because
|
|
||||||
// url.Parse mangles 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 ]
|
|
||||||
//
|
|
||||||
// We don't use the net/url parser here because the dialer interface does
|
|
||||||
// not provide a way for applications to work around percent deocding in
|
|
||||||
// the net/url parser.
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
u.Host = s
|
|
||||||
u.Opaque = "/"
|
|
||||||
if i := strings.Index(s, "/"); i >= 0 {
|
|
||||||
u.Host = s[:i]
|
|
||||||
u.Opaque = s[i:]
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(u.Host, "@") {
|
|
||||||
// WebSocket URIs do not contain user information.
|
|
||||||
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 {
|
|
||||||
if u.Scheme == "wss" {
|
|
||||||
hostPort += ":443"
|
|
||||||
} else {
|
|
||||||
hostPort += ":80"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hostPort, hostNoPort
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultDialer is a dialer with all fields set to the default zero values.
|
|
||||||
var DefaultDialer *Dialer
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
u, err := parseURL(urlStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hostPort, hostNoPort := hostPortNoPort(u)
|
|
||||||
|
|
||||||
if d == nil {
|
|
||||||
d = &Dialer{}
|
|
||||||
}
|
|
||||||
|
|
||||||
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", hostPort)
|
|
||||||
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 u.Scheme == "wss" {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(d.Subprotocols) > 0 {
|
|
||||||
h := http.Header{}
|
|
||||||
for k, v := range requestHeader {
|
|
||||||
h[k] = v
|
|
||||||
}
|
|
||||||
h.Set("Sec-Websocket-Protocol", strings.Join(d.Subprotocols, ", "))
|
|
||||||
requestHeader = h
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(requestHeader["Host"]) > 0 {
|
|
||||||
// This can be used to supply a Host: header which is different from
|
|
||||||
// the dial address.
|
|
||||||
u.Host = requestHeader.Get("Host")
|
|
||||||
|
|
||||||
// Drop "Host" header
|
|
||||||
h := http.Header{}
|
|
||||||
for k, v := range requestHeader {
|
|
||||||
if k == "Host" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
h[k] = v
|
|
||||||
}
|
|
||||||
requestHeader = h
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, resp, err := NewClient(netConn, u, requestHeader, d.ReadBufferSize, d.WriteBufferSize)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if err == ErrBadHandshake {
|
|
||||||
// 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, err
|
|
||||||
}
|
|
||||||
|
|
||||||
netConn.SetDeadline(time.Time{})
|
|
||||||
netConn = nil // to avoid close in defer.
|
|
||||||
return conn, resp, nil
|
|
||||||
}
|
|
||||||
-323
@@ -1,323 +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"
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
|
|
||||||
func newServer(t *testing.T) *cstServer {
|
|
||||||
var s cstServer
|
|
||||||
s.Server = httptest.NewServer(cstHandler{t})
|
|
||||||
s.URL = makeWsProto(s.Server.URL)
|
|
||||||
return &s
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTLSServer(t *testing.T) *cstServer {
|
|
||||||
var s cstServer
|
|
||||||
s.Server = httptest.NewTLSServer(cstHandler{t})
|
|
||||||
s.URL = makeWsProto(s.Server.URL)
|
|
||||||
return &s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t cstHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "GET" {
|
|
||||||
t.Logf("method %s not allowed", r.Method)
|
|
||||||
http.Error(w, "method not allowed", 405)
|
|
||||||
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 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/", 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"}})
|
|
||||||
if err == nil {
|
|
||||||
ws.Close()
|
|
||||||
t.Fatalf("Dial: nil")
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatalf("resp=nil, err=%v", err)
|
|
||||||
}
|
|
||||||
if resp.StatusCode != http.StatusForbidden {
|
|
||||||
t.Fatalf("status=%d, want %d", resp.StatusCode, http.StatusForbidden)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandshake(t *testing.T) {
|
|
||||||
s := newServer(t)
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
ws, resp, err := cstDialer.Dial(s.URL, http.Header{"Origin": {s.URL}})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Dial: %v", err)
|
|
||||||
}
|
|
||||||
defer ws.Close()
|
|
||||||
|
|
||||||
var sessionID string
|
|
||||||
for _, c := range resp.Cookies() {
|
|
||||||
if c.Name == "sessionID" {
|
|
||||||
sessionID = c.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sessionID != "1234" {
|
|
||||||
t.Error("Set-Cookie not received from the server.")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ws.Subprotocol() != "p1" {
|
|
||||||
t.Errorf("ws.Subprotocol() = %s, want p1", ws.Subprotocol())
|
|
||||||
}
|
|
||||||
sendRecv(t, ws)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRespOnBadHandshake(t *testing.T) {
|
|
||||||
const expectedStatus = http.StatusGone
|
|
||||||
const expectedBody = "This is the response body."
|
|
||||||
|
|
||||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(expectedStatus)
|
|
||||||
io.WriteString(w, expectedBody)
|
|
||||||
}))
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
ws, resp, err := cstDialer.Dial(makeWsProto(s.URL), nil)
|
|
||||||
if err == nil {
|
|
||||||
ws.Close()
|
|
||||||
t.Fatalf("Dial: nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatalf("resp=nil, err=%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != expectedStatus {
|
|
||||||
t.Errorf("resp.StatusCode=%d, want %d", resp.StatusCode, expectedStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ReadFull(resp.Body) returned error %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(p) != expectedBody {
|
|
||||||
t.Errorf("resp.Body=%s, want %s", p, expectedBody)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the Host header is specified in `Dial()`, the server must receive it as
|
|
||||||
// the `Host:` header.
|
|
||||||
func TestHostHeader(t *testing.T) {
|
|
||||||
s := newServer(t)
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
specifiedHost := make(chan string, 1)
|
|
||||||
origHandler := s.Server.Config.Handler
|
|
||||||
|
|
||||||
// Capture the request Host header.
|
|
||||||
s.Server.Config.Handler = http.HandlerFunc(
|
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
specifiedHost <- r.Host
|
|
||||||
origHandler.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
|
|
||||||
ws, resp, err := cstDialer.Dial(s.URL, http.Header{"Host": {"testhost"}})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Dial: %v", err)
|
|
||||||
}
|
|
||||||
defer ws.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
|
||||||
t.Fatalf("resp.StatusCode = %v, want http.StatusSwitchingProtocols", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if gotHost := <-specifiedHost; gotHost != "testhost" {
|
|
||||||
t.Fatalf("gotHost = %q, want \"testhost\"", gotHost)
|
|
||||||
}
|
|
||||||
|
|
||||||
sendRecv(t, ws)
|
|
||||||
}
|
|
||||||
-64
@@ -1,64 +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 (
|
|
||||||
"net/url"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var parseURLTests = []struct {
|
|
||||||
s string
|
|
||||||
u *url.URL
|
|
||||||
}{
|
|
||||||
{"ws://example.com/", &url.URL{Scheme: "ws", Host: "example.com", Opaque: "/"}},
|
|
||||||
{"ws://example.com", &url.URL{Scheme: "ws", Host: "example.com", Opaque: "/"}},
|
|
||||||
{"ws://example.com:7777/", &url.URL{Scheme: "ws", Host: "example.com:7777", Opaque: "/"}},
|
|
||||||
{"wss://example.com/", &url.URL{Scheme: "wss", Host: "example.com", Opaque: "/"}},
|
|
||||||
{"wss://example.com/a/b", &url.URL{Scheme: "wss", Host: "example.com", Opaque: "/a/b"}},
|
|
||||||
{"ss://example.com/a/b", nil},
|
|
||||||
{"ws://webmaster@example.com/", nil},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseURL(t *testing.T) {
|
|
||||||
for _, tt := range parseURLTests {
|
|
||||||
u, err := parseURL(tt.s)
|
|
||||||
if tt.u != nil && err != nil {
|
|
||||||
t.Errorf("parseURL(%q) returned error %v", tt.s, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if tt.u == nil && err == nil {
|
|
||||||
t.Errorf("parseURL(%q) did not return error", tt.s)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(u, tt.u) {
|
|
||||||
t.Errorf("parseURL(%q) returned %v, want %v", tt.s, u, tt.u)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostPortNoPortTests = []struct {
|
|
||||||
u *url.URL
|
|
||||||
hostPort, hostNoPort string
|
|
||||||
}{
|
|
||||||
{&url.URL{Scheme: "ws", Host: "example.com"}, "example.com:80", "example.com"},
|
|
||||||
{&url.URL{Scheme: "wss", Host: "example.com"}, "example.com:443", "example.com"},
|
|
||||||
{&url.URL{Scheme: "ws", Host: "example.com:7777"}, "example.com:7777", "example.com"},
|
|
||||||
{&url.URL{Scheme: "wss", Host: "example.com:7777"}, "example.com:7777", "example.com"},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHostPortNoPort(t *testing.T) {
|
|
||||||
for _, tt := range hostPortNoPortTests {
|
|
||||||
hostPort, hostNoPort := hostPortNoPort(tt.u)
|
|
||||||
if hostPort != tt.hostPort {
|
|
||||||
t.Errorf("hostPortNoPort(%v) returned hostPort %q, want %q", tt.u, hostPort, tt.hostPort)
|
|
||||||
}
|
|
||||||
if hostNoPort != tt.hostNoPort {
|
|
||||||
t.Errorf("hostPortNoPort(%v) returned hostNoPort %q, want %q", tt.u, hostNoPort, tt.hostNoPort)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-825
@@ -1,825 +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"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"math/rand"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxFrameHeaderSize = 2 + 8 + 4 // Fixed header + length + mask
|
|
||||||
maxControlFramePayloadSize = 125
|
|
||||||
finalBit = 1 << 7
|
|
||||||
maskBit = 1 << 7
|
|
||||||
writeWait = time.Second
|
|
||||||
|
|
||||||
defaultReadBufferSize = 4096
|
|
||||||
defaultWriteBufferSize = 4096
|
|
||||||
|
|
||||||
continuationFrame = 0
|
|
||||||
noFrame = -1
|
|
||||||
)
|
|
||||||
|
|
||||||
// Close codes defined in RFC 6455, section 11.7.
|
|
||||||
const (
|
|
||||||
CloseNormalClosure = 1000
|
|
||||||
CloseGoingAway = 1001
|
|
||||||
CloseProtocolError = 1002
|
|
||||||
CloseUnsupportedData = 1003
|
|
||||||
CloseNoStatusReceived = 1005
|
|
||||||
CloseAbnormalClosure = 1006
|
|
||||||
CloseInvalidFramePayloadData = 1007
|
|
||||||
ClosePolicyViolation = 1008
|
|
||||||
CloseMessageTooBig = 1009
|
|
||||||
CloseMandatoryExtension = 1010
|
|
||||||
CloseInternalServerErr = 1011
|
|
||||||
CloseTLSHandshake = 1015
|
|
||||||
)
|
|
||||||
|
|
||||||
// The message types are defined in RFC 6455, section 11.8.
|
|
||||||
const (
|
|
||||||
// TextMessage denotes a text data message. The text message payload is
|
|
||||||
// interpreted as UTF-8 encoded text data.
|
|
||||||
TextMessage = 1
|
|
||||||
|
|
||||||
// BinaryMessage denotes a binary data message.
|
|
||||||
BinaryMessage = 2
|
|
||||||
|
|
||||||
// CloseMessage denotes a close control message. The optional message
|
|
||||||
// payload contains a numeric code and text. Use the FormatCloseMessage
|
|
||||||
// function to format a close message payload.
|
|
||||||
CloseMessage = 8
|
|
||||||
|
|
||||||
// PingMessage denotes a ping control message. The optional message payload
|
|
||||||
// is UTF-8 encoded text.
|
|
||||||
PingMessage = 9
|
|
||||||
|
|
||||||
// PongMessage denotes a ping control message. The optional message payload
|
|
||||||
// is UTF-8 encoded text.
|
|
||||||
PongMessage = 10
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrCloseSent is returned when the application writes a message to the
|
|
||||||
// connection after sending a close message.
|
|
||||||
var ErrCloseSent = errors.New("websocket: close sent")
|
|
||||||
|
|
||||||
// ErrReadLimit is returned when reading a message that is larger than the
|
|
||||||
// read limit set for the connection.
|
|
||||||
var ErrReadLimit = errors.New("websocket: read limit exceeded")
|
|
||||||
|
|
||||||
// netError satisfies the net Error interface.
|
|
||||||
type netError struct {
|
|
||||||
msg string
|
|
||||||
temporary bool
|
|
||||||
timeout bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *netError) Error() string { return e.msg }
|
|
||||||
func (e *netError) Temporary() bool { return e.temporary }
|
|
||||||
func (e *netError) Timeout() bool { return e.timeout }
|
|
||||||
|
|
||||||
// closeError represents close frame.
|
|
||||||
type closeError struct {
|
|
||||||
code int
|
|
||||||
text string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *closeError) Error() string {
|
|
||||||
return "websocket: close " + strconv.Itoa(e.code) + " " + e.text
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
errWriteTimeout = &netError{msg: "websocket: write timeout", timeout: true}
|
|
||||||
errUnexpectedEOF = &closeError{code: CloseAbnormalClosure, text: io.ErrUnexpectedEOF.Error()}
|
|
||||||
errBadWriteOpCode = errors.New("websocket: bad write message type")
|
|
||||||
errWriteClosed = errors.New("websocket: write closed")
|
|
||||||
errInvalidControlFrame = errors.New("websocket: invalid control frame")
|
|
||||||
)
|
|
||||||
|
|
||||||
func hideTempErr(err error) error {
|
|
||||||
if e, ok := err.(net.Error); ok && e.Temporary() {
|
|
||||||
err = &netError{msg: e.Error(), timeout: e.Timeout()}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func isControl(frameType int) bool {
|
|
||||||
return frameType == CloseMessage || frameType == PingMessage || frameType == PongMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
func isData(frameType int) bool {
|
|
||||||
return frameType == TextMessage || frameType == BinaryMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
func maskBytes(key [4]byte, pos int, b []byte) int {
|
|
||||||
for i := range b {
|
|
||||||
b[i] ^= key[pos&3]
|
|
||||||
pos++
|
|
||||||
}
|
|
||||||
return pos & 3
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMaskKey() [4]byte {
|
|
||||||
n := rand.Uint32()
|
|
||||||
return [4]byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conn represents a WebSocket connection.
|
|
||||||
type Conn struct {
|
|
||||||
conn net.Conn
|
|
||||||
isServer bool
|
|
||||||
subprotocol string
|
|
||||||
|
|
||||||
// Write fields
|
|
||||||
mu chan bool // used as mutex to protect write to conn and closeSent
|
|
||||||
closeSent bool // true if close message was sent
|
|
||||||
|
|
||||||
// Message writer fields.
|
|
||||||
writeErr error
|
|
||||||
writeBuf []byte // frame is constructed in this buffer.
|
|
||||||
writePos int // end of data in writeBuf.
|
|
||||||
writeFrameType int // type of the current frame.
|
|
||||||
writeSeq int // incremented to invalidate message writers.
|
|
||||||
writeDeadline time.Time
|
|
||||||
|
|
||||||
// Read fields
|
|
||||||
readErr error
|
|
||||||
br *bufio.Reader
|
|
||||||
readRemaining int64 // bytes remaining in current frame.
|
|
||||||
readFinal bool // true the current message has more frames.
|
|
||||||
readSeq int // incremented to invalidate message readers.
|
|
||||||
readLength int64 // Message size.
|
|
||||||
readLimit int64 // Maximum message size.
|
|
||||||
readMaskPos int
|
|
||||||
readMaskKey [4]byte
|
|
||||||
handlePong func(string) error
|
|
||||||
handlePing func(string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn {
|
|
||||||
mu := make(chan bool, 1)
|
|
||||||
mu <- true
|
|
||||||
|
|
||||||
if readBufferSize == 0 {
|
|
||||||
readBufferSize = defaultReadBufferSize
|
|
||||||
}
|
|
||||||
if writeBufferSize == 0 {
|
|
||||||
writeBufferSize = defaultWriteBufferSize
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &Conn{
|
|
||||||
isServer: isServer,
|
|
||||||
br: bufio.NewReaderSize(conn, readBufferSize),
|
|
||||||
conn: conn,
|
|
||||||
mu: mu,
|
|
||||||
readFinal: true,
|
|
||||||
writeBuf: make([]byte, writeBufferSize+maxFrameHeaderSize),
|
|
||||||
writeFrameType: noFrame,
|
|
||||||
writePos: maxFrameHeaderSize,
|
|
||||||
}
|
|
||||||
c.SetPingHandler(nil)
|
|
||||||
c.SetPongHandler(nil)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subprotocol returns the negotiated protocol for the connection.
|
|
||||||
func (c *Conn) Subprotocol() string {
|
|
||||||
return c.subprotocol
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the underlying network connection without sending or waiting for a close frame.
|
|
||||||
func (c *Conn) Close() error {
|
|
||||||
return c.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// LocalAddr returns the local network address.
|
|
||||||
func (c *Conn) LocalAddr() net.Addr {
|
|
||||||
return c.conn.LocalAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoteAddr returns the remote network address.
|
|
||||||
func (c *Conn) RemoteAddr() net.Addr {
|
|
||||||
return c.conn.RemoteAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write methods
|
|
||||||
|
|
||||||
func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error {
|
|
||||||
<-c.mu
|
|
||||||
defer func() { c.mu <- true }()
|
|
||||||
|
|
||||||
if c.closeSent {
|
|
||||||
return ErrCloseSent
|
|
||||||
} else if frameType == CloseMessage {
|
|
||||||
c.closeSent = true
|
|
||||||
}
|
|
||||||
|
|
||||||
c.conn.SetWriteDeadline(deadline)
|
|
||||||
for _, buf := range bufs {
|
|
||||||
if len(buf) > 0 {
|
|
||||||
n, err := c.conn.Write(buf)
|
|
||||||
if n != len(buf) {
|
|
||||||
// Close on partial write.
|
|
||||||
c.conn.Close()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteControl writes a control message with the given deadline. The allowed
|
|
||||||
// message types are CloseMessage, PingMessage and PongMessage.
|
|
||||||
func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) error {
|
|
||||||
if !isControl(messageType) {
|
|
||||||
return errBadWriteOpCode
|
|
||||||
}
|
|
||||||
if len(data) > maxControlFramePayloadSize {
|
|
||||||
return errInvalidControlFrame
|
|
||||||
}
|
|
||||||
|
|
||||||
b0 := byte(messageType) | finalBit
|
|
||||||
b1 := byte(len(data))
|
|
||||||
if !c.isServer {
|
|
||||||
b1 |= maskBit
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, 0, maxFrameHeaderSize+maxControlFramePayloadSize)
|
|
||||||
buf = append(buf, b0, b1)
|
|
||||||
|
|
||||||
if c.isServer {
|
|
||||||
buf = append(buf, data...)
|
|
||||||
} else {
|
|
||||||
key := newMaskKey()
|
|
||||||
buf = append(buf, key[:]...)
|
|
||||||
buf = append(buf, data...)
|
|
||||||
maskBytes(key, 0, buf[6:])
|
|
||||||
}
|
|
||||||
|
|
||||||
d := time.Hour * 1000
|
|
||||||
if !deadline.IsZero() {
|
|
||||||
d = deadline.Sub(time.Now())
|
|
||||||
if d < 0 {
|
|
||||||
return errWriteTimeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
timer := time.NewTimer(d)
|
|
||||||
select {
|
|
||||||
case <-c.mu:
|
|
||||||
timer.Stop()
|
|
||||||
case <-timer.C:
|
|
||||||
return errWriteTimeout
|
|
||||||
}
|
|
||||||
defer func() { c.mu <- true }()
|
|
||||||
|
|
||||||
if c.closeSent {
|
|
||||||
return ErrCloseSent
|
|
||||||
} else if messageType == CloseMessage {
|
|
||||||
c.closeSent = true
|
|
||||||
}
|
|
||||||
|
|
||||||
c.conn.SetWriteDeadline(deadline)
|
|
||||||
n, err := c.conn.Write(buf)
|
|
||||||
if n != 0 && n != len(buf) {
|
|
||||||
c.conn.Close()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// NextWriter returns a writer for the next message to send. The writer's
|
|
||||||
// Close method flushes the complete message to the network.
|
|
||||||
//
|
|
||||||
// There can be at most one open writer on a connection. NextWriter closes the
|
|
||||||
// previous writer if the application has not already done so.
|
|
||||||
//
|
|
||||||
// The NextWriter method and the writers returned from the method cannot be
|
|
||||||
// accessed by more than one goroutine at a time.
|
|
||||||
func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) {
|
|
||||||
if c.writeErr != nil {
|
|
||||||
return nil, c.writeErr
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.writeFrameType != noFrame {
|
|
||||||
if err := c.flushFrame(true, nil); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isControl(messageType) && !isData(messageType) {
|
|
||||||
return nil, errBadWriteOpCode
|
|
||||||
}
|
|
||||||
|
|
||||||
c.writeFrameType = messageType
|
|
||||||
return messageWriter{c, c.writeSeq}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) flushFrame(final bool, extra []byte) error {
|
|
||||||
length := c.writePos - maxFrameHeaderSize + len(extra)
|
|
||||||
|
|
||||||
// Check for invalid control frames.
|
|
||||||
if isControl(c.writeFrameType) &&
|
|
||||||
(!final || length > maxControlFramePayloadSize) {
|
|
||||||
c.writeSeq++
|
|
||||||
c.writeFrameType = noFrame
|
|
||||||
c.writePos = maxFrameHeaderSize
|
|
||||||
return errInvalidControlFrame
|
|
||||||
}
|
|
||||||
|
|
||||||
b0 := byte(c.writeFrameType)
|
|
||||||
if final {
|
|
||||||
b0 |= finalBit
|
|
||||||
}
|
|
||||||
b1 := byte(0)
|
|
||||||
if !c.isServer {
|
|
||||||
b1 |= maskBit
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assume that the frame starts at beginning of c.writeBuf.
|
|
||||||
framePos := 0
|
|
||||||
if c.isServer {
|
|
||||||
// Adjust up if mask not included in the header.
|
|
||||||
framePos = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case length >= 65536:
|
|
||||||
c.writeBuf[framePos] = b0
|
|
||||||
c.writeBuf[framePos+1] = b1 | 127
|
|
||||||
binary.BigEndian.PutUint64(c.writeBuf[framePos+2:], uint64(length))
|
|
||||||
case length > 125:
|
|
||||||
framePos += 6
|
|
||||||
c.writeBuf[framePos] = b0
|
|
||||||
c.writeBuf[framePos+1] = b1 | 126
|
|
||||||
binary.BigEndian.PutUint16(c.writeBuf[framePos+2:], uint16(length))
|
|
||||||
default:
|
|
||||||
framePos += 8
|
|
||||||
c.writeBuf[framePos] = b0
|
|
||||||
c.writeBuf[framePos+1] = b1 | byte(length)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !c.isServer {
|
|
||||||
key := newMaskKey()
|
|
||||||
copy(c.writeBuf[maxFrameHeaderSize-4:], key[:])
|
|
||||||
maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:c.writePos])
|
|
||||||
if len(extra) > 0 {
|
|
||||||
c.writeErr = errors.New("websocket: internal error, extra used in client mode")
|
|
||||||
return c.writeErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the buffers to the connection.
|
|
||||||
c.writeErr = c.write(c.writeFrameType, c.writeDeadline, c.writeBuf[framePos:c.writePos], extra)
|
|
||||||
|
|
||||||
// Setup for next frame.
|
|
||||||
c.writePos = maxFrameHeaderSize
|
|
||||||
c.writeFrameType = continuationFrame
|
|
||||||
if final {
|
|
||||||
c.writeSeq++
|
|
||||||
c.writeFrameType = noFrame
|
|
||||||
}
|
|
||||||
return c.writeErr
|
|
||||||
}
|
|
||||||
|
|
||||||
type messageWriter struct {
|
|
||||||
c *Conn
|
|
||||||
seq int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w messageWriter) err() error {
|
|
||||||
c := w.c
|
|
||||||
if c.writeSeq != w.seq {
|
|
||||||
return errWriteClosed
|
|
||||||
}
|
|
||||||
if c.writeErr != nil {
|
|
||||||
return c.writeErr
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w messageWriter) ncopy(max int) (int, error) {
|
|
||||||
n := len(w.c.writeBuf) - w.c.writePos
|
|
||||||
if n <= 0 {
|
|
||||||
if err := w.c.flushFrame(false, nil); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
n = len(w.c.writeBuf) - w.c.writePos
|
|
||||||
}
|
|
||||||
if n > max {
|
|
||||||
n = max
|
|
||||||
}
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w messageWriter) write(final bool, p []byte) (int, error) {
|
|
||||||
if err := w.err(); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p) > 2*len(w.c.writeBuf) && w.c.isServer {
|
|
||||||
// Don't buffer large messages.
|
|
||||||
err := w.c.flushFrame(final, p)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
nn := len(p)
|
|
||||||
for len(p) > 0 {
|
|
||||||
n, err := w.ncopy(len(p))
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
copy(w.c.writeBuf[w.c.writePos:], p[:n])
|
|
||||||
w.c.writePos += n
|
|
||||||
p = p[n:]
|
|
||||||
}
|
|
||||||
return nn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w messageWriter) Write(p []byte) (int, error) {
|
|
||||||
return w.write(false, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w messageWriter) WriteString(p string) (int, error) {
|
|
||||||
if err := w.err(); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
nn := len(p)
|
|
||||||
for len(p) > 0 {
|
|
||||||
n, err := w.ncopy(len(p))
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
copy(w.c.writeBuf[w.c.writePos:], p[:n])
|
|
||||||
w.c.writePos += n
|
|
||||||
p = p[n:]
|
|
||||||
}
|
|
||||||
return nn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w messageWriter) ReadFrom(r io.Reader) (nn int64, err error) {
|
|
||||||
if err := w.err(); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
if w.c.writePos == len(w.c.writeBuf) {
|
|
||||||
err = w.c.flushFrame(false, nil)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var n int
|
|
||||||
n, err = r.Read(w.c.writeBuf[w.c.writePos:])
|
|
||||||
w.c.writePos += n
|
|
||||||
nn += int64(n)
|
|
||||||
if err != nil {
|
|
||||||
if err == io.EOF {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nn, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w messageWriter) Close() error {
|
|
||||||
if err := w.err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return w.c.flushFrame(true, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteMessage is a helper method for getting a writer using NextWriter,
|
|
||||||
// writing the message and closing the writer.
|
|
||||||
func (c *Conn) WriteMessage(messageType int, data []byte) error {
|
|
||||||
wr, err := c.NextWriter(messageType)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
w := wr.(messageWriter)
|
|
||||||
if _, err := w.write(true, data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if c.writeSeq == w.seq {
|
|
||||||
if err := c.flushFrame(true, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetWriteDeadline sets the write deadline on the underlying network
|
|
||||||
// connection. After a write has timed out, the websocket state is corrupt and
|
|
||||||
// all future writes will return an error. A zero value for t means writes will
|
|
||||||
// not time out.
|
|
||||||
func (c *Conn) SetWriteDeadline(t time.Time) error {
|
|
||||||
c.writeDeadline = t
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read methods
|
|
||||||
|
|
||||||
// readFull is like io.ReadFull except that io.EOF is never returned.
|
|
||||||
func (c *Conn) readFull(p []byte) (err error) {
|
|
||||||
var n int
|
|
||||||
for n < len(p) && err == nil {
|
|
||||||
var nn int
|
|
||||||
nn, err = c.br.Read(p[n:])
|
|
||||||
n += nn
|
|
||||||
}
|
|
||||||
if n == len(p) {
|
|
||||||
err = nil
|
|
||||||
} else if err == io.EOF {
|
|
||||||
err = errUnexpectedEOF
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) advanceFrame() (int, error) {
|
|
||||||
|
|
||||||
// 1. Skip remainder of previous frame.
|
|
||||||
|
|
||||||
if c.readRemaining > 0 {
|
|
||||||
if _, err := io.CopyN(ioutil.Discard, c.br, c.readRemaining); err != nil {
|
|
||||||
return noFrame, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Read and parse first two bytes of frame header.
|
|
||||||
|
|
||||||
var b [8]byte
|
|
||||||
if err := c.readFull(b[:2]); err != nil {
|
|
||||||
return noFrame, err
|
|
||||||
}
|
|
||||||
|
|
||||||
final := b[0]&finalBit != 0
|
|
||||||
frameType := int(b[0] & 0xf)
|
|
||||||
reserved := int((b[0] >> 4) & 0x7)
|
|
||||||
mask := b[1]&maskBit != 0
|
|
||||||
c.readRemaining = int64(b[1] & 0x7f)
|
|
||||||
|
|
||||||
if reserved != 0 {
|
|
||||||
return noFrame, c.handleProtocolError("unexpected reserved bits " + strconv.Itoa(reserved))
|
|
||||||
}
|
|
||||||
|
|
||||||
switch frameType {
|
|
||||||
case CloseMessage, PingMessage, PongMessage:
|
|
||||||
if c.readRemaining > maxControlFramePayloadSize {
|
|
||||||
return noFrame, c.handleProtocolError("control frame length > 125")
|
|
||||||
}
|
|
||||||
if !final {
|
|
||||||
return noFrame, c.handleProtocolError("control frame not final")
|
|
||||||
}
|
|
||||||
case TextMessage, BinaryMessage:
|
|
||||||
if !c.readFinal {
|
|
||||||
return noFrame, c.handleProtocolError("message start before final message frame")
|
|
||||||
}
|
|
||||||
c.readFinal = final
|
|
||||||
case continuationFrame:
|
|
||||||
if c.readFinal {
|
|
||||||
return noFrame, c.handleProtocolError("continuation after final message frame")
|
|
||||||
}
|
|
||||||
c.readFinal = final
|
|
||||||
default:
|
|
||||||
return noFrame, c.handleProtocolError("unknown opcode " + strconv.Itoa(frameType))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Read and parse frame length.
|
|
||||||
|
|
||||||
switch c.readRemaining {
|
|
||||||
case 126:
|
|
||||||
if err := c.readFull(b[:2]); err != nil {
|
|
||||||
return noFrame, err
|
|
||||||
}
|
|
||||||
c.readRemaining = int64(binary.BigEndian.Uint16(b[:2]))
|
|
||||||
case 127:
|
|
||||||
if err := c.readFull(b[:8]); err != nil {
|
|
||||||
return noFrame, err
|
|
||||||
}
|
|
||||||
c.readRemaining = int64(binary.BigEndian.Uint64(b[:8]))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Handle frame masking.
|
|
||||||
|
|
||||||
if mask != c.isServer {
|
|
||||||
return noFrame, c.handleProtocolError("incorrect mask flag")
|
|
||||||
}
|
|
||||||
|
|
||||||
if mask {
|
|
||||||
c.readMaskPos = 0
|
|
||||||
if err := c.readFull(c.readMaskKey[:]); err != nil {
|
|
||||||
return noFrame, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. For text and binary messages, enforce read limit and return.
|
|
||||||
|
|
||||||
if frameType == continuationFrame || frameType == TextMessage || frameType == BinaryMessage {
|
|
||||||
|
|
||||||
c.readLength += c.readRemaining
|
|
||||||
if c.readLimit > 0 && c.readLength > c.readLimit {
|
|
||||||
c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait))
|
|
||||||
return noFrame, ErrReadLimit
|
|
||||||
}
|
|
||||||
|
|
||||||
return frameType, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Read control frame payload.
|
|
||||||
|
|
||||||
var payload []byte
|
|
||||||
if c.readRemaining > 0 {
|
|
||||||
payload = make([]byte, c.readRemaining)
|
|
||||||
c.readRemaining = 0
|
|
||||||
if err := c.readFull(payload); err != nil {
|
|
||||||
return noFrame, err
|
|
||||||
}
|
|
||||||
if c.isServer {
|
|
||||||
maskBytes(c.readMaskKey, 0, payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. Process control frame payload.
|
|
||||||
|
|
||||||
switch frameType {
|
|
||||||
case PongMessage:
|
|
||||||
if err := c.handlePong(string(payload)); err != nil {
|
|
||||||
return noFrame, err
|
|
||||||
}
|
|
||||||
case PingMessage:
|
|
||||||
if err := c.handlePing(string(payload)); err != nil {
|
|
||||||
return noFrame, err
|
|
||||||
}
|
|
||||||
case CloseMessage:
|
|
||||||
c.WriteControl(CloseMessage, []byte{}, time.Now().Add(writeWait))
|
|
||||||
closeCode := CloseNoStatusReceived
|
|
||||||
closeText := ""
|
|
||||||
if len(payload) >= 2 {
|
|
||||||
closeCode = int(binary.BigEndian.Uint16(payload))
|
|
||||||
closeText = string(payload[2:])
|
|
||||||
}
|
|
||||||
switch closeCode {
|
|
||||||
case CloseNormalClosure, CloseGoingAway:
|
|
||||||
return noFrame, io.EOF
|
|
||||||
default:
|
|
||||||
return noFrame, &closeError{code: closeCode, text: closeText}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return frameType, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) handleProtocolError(message string) error {
|
|
||||||
c.WriteControl(CloseMessage, FormatCloseMessage(CloseProtocolError, message), time.Now().Add(writeWait))
|
|
||||||
return errors.New("websocket: " + message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NextReader returns the next data message received from the peer. The
|
|
||||||
// returned messageType is either TextMessage or BinaryMessage.
|
|
||||||
//
|
|
||||||
// There can be at most one open reader on a connection. NextReader discards
|
|
||||||
// the previous message if the application has not already consumed it.
|
|
||||||
//
|
|
||||||
// The NextReader method and the readers returned from the method cannot be
|
|
||||||
// accessed by more than one goroutine at a time.
|
|
||||||
func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {
|
|
||||||
|
|
||||||
c.readSeq++
|
|
||||||
c.readLength = 0
|
|
||||||
|
|
||||||
for c.readErr == nil {
|
|
||||||
frameType, err := c.advanceFrame()
|
|
||||||
if err != nil {
|
|
||||||
c.readErr = hideTempErr(err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if frameType == TextMessage || frameType == BinaryMessage {
|
|
||||||
return frameType, messageReader{c, c.readSeq}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return noFrame, nil, c.readErr
|
|
||||||
}
|
|
||||||
|
|
||||||
type messageReader struct {
|
|
||||||
c *Conn
|
|
||||||
seq int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r messageReader) Read(b []byte) (int, error) {
|
|
||||||
|
|
||||||
if r.seq != r.c.readSeq {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
for r.c.readErr == nil {
|
|
||||||
|
|
||||||
if r.c.readRemaining > 0 {
|
|
||||||
if int64(len(b)) > r.c.readRemaining {
|
|
||||||
b = b[:r.c.readRemaining]
|
|
||||||
}
|
|
||||||
n, err := r.c.br.Read(b)
|
|
||||||
r.c.readErr = hideTempErr(err)
|
|
||||||
if r.c.isServer {
|
|
||||||
r.c.readMaskPos = maskBytes(r.c.readMaskKey, r.c.readMaskPos, b[:n])
|
|
||||||
}
|
|
||||||
r.c.readRemaining -= int64(n)
|
|
||||||
return n, r.c.readErr
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.c.readFinal {
|
|
||||||
r.c.readSeq++
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
frameType, err := r.c.advanceFrame()
|
|
||||||
switch {
|
|
||||||
case err != nil:
|
|
||||||
r.c.readErr = hideTempErr(err)
|
|
||||||
case frameType == TextMessage || frameType == BinaryMessage:
|
|
||||||
r.c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := r.c.readErr
|
|
||||||
if err == io.EOF && r.seq == r.c.readSeq {
|
|
||||||
err = errUnexpectedEOF
|
|
||||||
}
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadMessage is a helper method for getting a reader using NextReader and
|
|
||||||
// reading from that reader to a buffer.
|
|
||||||
func (c *Conn) ReadMessage() (messageType int, p []byte, err error) {
|
|
||||||
var r io.Reader
|
|
||||||
messageType, r, err = c.NextReader()
|
|
||||||
if err != nil {
|
|
||||||
return messageType, nil, err
|
|
||||||
}
|
|
||||||
p, err = ioutil.ReadAll(r)
|
|
||||||
return messageType, p, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetReadDeadline sets the read deadline on the underlying network connection.
|
|
||||||
// After a read has timed out, the websocket connection state is corrupt and
|
|
||||||
// all future reads will return an error. A zero value for t means reads will
|
|
||||||
// not time out.
|
|
||||||
func (c *Conn) SetReadDeadline(t time.Time) error {
|
|
||||||
return c.conn.SetReadDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetReadLimit sets the maximum size for a message read from the peer. If a
|
|
||||||
// message exceeds the limit, the connection sends a close frame to the peer
|
|
||||||
// and returns ErrReadLimit to the application.
|
|
||||||
func (c *Conn) SetReadLimit(limit int64) {
|
|
||||||
c.readLimit = limit
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPingHandler sets the handler for ping messages received from the peer.
|
|
||||||
// The default ping handler sends a pong to the peer.
|
|
||||||
func (c *Conn) SetPingHandler(h func(string) error) {
|
|
||||||
if h == nil {
|
|
||||||
h = func(message string) error {
|
|
||||||
c.WriteControl(PongMessage, []byte(message), time.Now().Add(writeWait))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.handlePing = h
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPongHandler sets the handler for pong messages received from the peer.
|
|
||||||
// The default pong handler does nothing.
|
|
||||||
func (c *Conn) SetPongHandler(h func(string) error) {
|
|
||||||
if h == nil {
|
|
||||||
h = func(string) error { return nil }
|
|
||||||
}
|
|
||||||
c.handlePong = h
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnderlyingConn returns the internal net.Conn. This can be used to further
|
|
||||||
// modifications to connection specific flags.
|
|
||||||
func (c *Conn) UnderlyingConn() net.Conn {
|
|
||||||
return c.conn
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatCloseMessage formats closeCode and text as a WebSocket close message.
|
|
||||||
func FormatCloseMessage(closeCode int, text string) []byte {
|
|
||||||
buf := make([]byte, 2+len(text))
|
|
||||||
binary.BigEndian.PutUint16(buf, uint16(closeCode))
|
|
||||||
copy(buf[2:], text)
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
-238
@@ -1,238 +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 (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
"testing/iotest"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ net.Error = errWriteTimeout
|
|
||||||
|
|
||||||
type fakeNetConn struct {
|
|
||||||
io.Reader
|
|
||||||
io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c fakeNetConn) Close() error { return nil }
|
|
||||||
func (c fakeNetConn) LocalAddr() net.Addr { return nil }
|
|
||||||
func (c fakeNetConn) RemoteAddr() net.Addr { return nil }
|
|
||||||
func (c fakeNetConn) SetDeadline(t time.Time) error { return nil }
|
|
||||||
func (c fakeNetConn) SetReadDeadline(t time.Time) error { return nil }
|
|
||||||
func (c fakeNetConn) SetWriteDeadline(t time.Time) error { return nil }
|
|
||||||
|
|
||||||
func TestFraming(t *testing.T) {
|
|
||||||
frameSizes := []int{0, 1, 2, 124, 125, 126, 127, 128, 129, 65534, 65535, 65536, 65537}
|
|
||||||
var readChunkers = []struct {
|
|
||||||
name string
|
|
||||||
f func(io.Reader) io.Reader
|
|
||||||
}{
|
|
||||||
{"half", iotest.HalfReader},
|
|
||||||
{"one", iotest.OneByteReader},
|
|
||||||
{"asis", func(r io.Reader) io.Reader { return r }},
|
|
||||||
}
|
|
||||||
|
|
||||||
writeBuf := make([]byte, 65537)
|
|
||||||
for i := range writeBuf {
|
|
||||||
writeBuf[i] = byte(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, isServer := range []bool{true, false} {
|
|
||||||
for _, chunker := range readChunkers {
|
|
||||||
|
|
||||||
var connBuf bytes.Buffer
|
|
||||||
wc := newConn(fakeNetConn{Reader: nil, Writer: &connBuf}, isServer, 1024, 1024)
|
|
||||||
rc := newConn(fakeNetConn{Reader: chunker.f(&connBuf), Writer: nil}, !isServer, 1024, 1024)
|
|
||||||
|
|
||||||
for _, n := range frameSizes {
|
|
||||||
for _, iocopy := range []bool{true, false} {
|
|
||||||
name := fmt.Sprintf("s:%v, r:%s, n:%d c:%v", isServer, chunker.name, n, iocopy)
|
|
||||||
|
|
||||||
w, err := wc.NextWriter(TextMessage)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%s: wc.NextWriter() returned %v", name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var nn int
|
|
||||||
if iocopy {
|
|
||||||
var n64 int64
|
|
||||||
n64, err = io.Copy(w, bytes.NewReader(writeBuf[:n]))
|
|
||||||
nn = int(n64)
|
|
||||||
} else {
|
|
||||||
nn, err = w.Write(writeBuf[:n])
|
|
||||||
}
|
|
||||||
if err != nil || nn != n {
|
|
||||||
t.Errorf("%s: w.Write(writeBuf[:n]) returned %d, %v", name, nn, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err = w.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%s: w.Close() returned %v", name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
opCode, r, err := rc.NextReader()
|
|
||||||
if err != nil || opCode != TextMessage {
|
|
||||||
t.Errorf("%s: NextReader() returned %d, r, %v", name, opCode, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rbuf, err := ioutil.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%s: ReadFull() returned rbuf, %v", name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(rbuf) != n {
|
|
||||||
t.Errorf("%s: len(rbuf) is %d, want %d", name, len(rbuf), n)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, b := range rbuf {
|
|
||||||
if byte(i) != b {
|
|
||||||
t.Errorf("%s: bad byte at offset %d", name, i)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestControl(t *testing.T) {
|
|
||||||
const message = "this is a ping/pong messsage"
|
|
||||||
for _, isServer := range []bool{true, false} {
|
|
||||||
for _, isWriteControl := range []bool{true, false} {
|
|
||||||
name := fmt.Sprintf("s:%v, wc:%v", isServer, isWriteControl)
|
|
||||||
var connBuf bytes.Buffer
|
|
||||||
wc := newConn(fakeNetConn{Reader: nil, Writer: &connBuf}, isServer, 1024, 1024)
|
|
||||||
rc := newConn(fakeNetConn{Reader: &connBuf, Writer: nil}, !isServer, 1024, 1024)
|
|
||||||
if isWriteControl {
|
|
||||||
wc.WriteControl(PongMessage, []byte(message), time.Now().Add(time.Second))
|
|
||||||
} else {
|
|
||||||
w, err := wc.NextWriter(PongMessage)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%s: wc.NextWriter() returned %v", name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, err := w.Write([]byte(message)); err != nil {
|
|
||||||
t.Errorf("%s: w.Write() returned %v", name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := w.Close(); err != nil {
|
|
||||||
t.Errorf("%s: w.Close() returned %v", name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var actualMessage string
|
|
||||||
rc.SetPongHandler(func(s string) error { actualMessage = s; return nil })
|
|
||||||
rc.NextReader()
|
|
||||||
if actualMessage != message {
|
|
||||||
t.Errorf("%s: pong=%q, want %q", name, actualMessage, message)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCloseBeforeFinalFrame(t *testing.T) {
|
|
||||||
const bufSize = 512
|
|
||||||
|
|
||||||
var b1, b2 bytes.Buffer
|
|
||||||
wc := newConn(fakeNetConn{Reader: nil, Writer: &b1}, false, 1024, bufSize)
|
|
||||||
rc := newConn(fakeNetConn{Reader: &b1, Writer: &b2}, true, 1024, 1024)
|
|
||||||
|
|
||||||
w, _ := wc.NextWriter(BinaryMessage)
|
|
||||||
w.Write(make([]byte, bufSize+bufSize/2))
|
|
||||||
wc.WriteControl(CloseMessage, FormatCloseMessage(CloseNormalClosure, ""), time.Now().Add(10*time.Second))
|
|
||||||
w.Close()
|
|
||||||
|
|
||||||
op, r, err := rc.NextReader()
|
|
||||||
if op != BinaryMessage || err != nil {
|
|
||||||
t.Fatalf("NextReader() returned %d, %v", op, err)
|
|
||||||
}
|
|
||||||
_, err = io.Copy(ioutil.Discard, r)
|
|
||||||
if err != errUnexpectedEOF {
|
|
||||||
t.Fatalf("io.Copy() returned %v, want %v", err, errUnexpectedEOF)
|
|
||||||
}
|
|
||||||
_, _, err = rc.NextReader()
|
|
||||||
if err != io.EOF {
|
|
||||||
t.Fatalf("NextReader() returned %v, want %v", err, io.EOF)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEOFBeforeFinalFrame(t *testing.T) {
|
|
||||||
const bufSize = 512
|
|
||||||
|
|
||||||
var b1, b2 bytes.Buffer
|
|
||||||
wc := newConn(fakeNetConn{Reader: nil, Writer: &b1}, false, 1024, bufSize)
|
|
||||||
rc := newConn(fakeNetConn{Reader: &b1, Writer: &b2}, true, 1024, 1024)
|
|
||||||
|
|
||||||
w, _ := wc.NextWriter(BinaryMessage)
|
|
||||||
w.Write(make([]byte, bufSize+bufSize/2))
|
|
||||||
|
|
||||||
op, r, err := rc.NextReader()
|
|
||||||
if op != BinaryMessage || err != nil {
|
|
||||||
t.Fatalf("NextReader() returned %d, %v", op, err)
|
|
||||||
}
|
|
||||||
_, err = io.Copy(ioutil.Discard, r)
|
|
||||||
if err != errUnexpectedEOF {
|
|
||||||
t.Fatalf("io.Copy() returned %v, want %v", err, errUnexpectedEOF)
|
|
||||||
}
|
|
||||||
_, _, err = rc.NextReader()
|
|
||||||
if err != errUnexpectedEOF {
|
|
||||||
t.Fatalf("NextReader() returned %v, want %v", err, errUnexpectedEOF)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReadLimit(t *testing.T) {
|
|
||||||
|
|
||||||
const readLimit = 512
|
|
||||||
message := make([]byte, readLimit+1)
|
|
||||||
|
|
||||||
var b1, b2 bytes.Buffer
|
|
||||||
wc := newConn(fakeNetConn{Reader: nil, Writer: &b1}, false, 1024, readLimit-2)
|
|
||||||
rc := newConn(fakeNetConn{Reader: &b1, Writer: &b2}, true, 1024, 1024)
|
|
||||||
rc.SetReadLimit(readLimit)
|
|
||||||
|
|
||||||
// Send message at the limit with interleaved pong.
|
|
||||||
w, _ := wc.NextWriter(BinaryMessage)
|
|
||||||
w.Write(message[:readLimit-1])
|
|
||||||
wc.WriteControl(PongMessage, []byte("this is a pong"), time.Now().Add(10*time.Second))
|
|
||||||
w.Write(message[:1])
|
|
||||||
w.Close()
|
|
||||||
|
|
||||||
// Send message larger than the limit.
|
|
||||||
wc.WriteMessage(BinaryMessage, message[:readLimit+1])
|
|
||||||
|
|
||||||
op, _, err := rc.NextReader()
|
|
||||||
if op != BinaryMessage || err != nil {
|
|
||||||
t.Fatalf("1: NextReader() returned %d, %v", op, err)
|
|
||||||
}
|
|
||||||
op, r, err := rc.NextReader()
|
|
||||||
if op != BinaryMessage || err != nil {
|
|
||||||
t.Fatalf("2: NextReader() returned %d, %v", op, err)
|
|
||||||
}
|
|
||||||
_, err = io.Copy(ioutil.Discard, r)
|
|
||||||
if err != ErrReadLimit {
|
|
||||||
t.Fatalf("io.Copy() returned %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnderlyingConn(t *testing.T) {
|
|
||||||
var b1, b2 bytes.Buffer
|
|
||||||
fc := fakeNetConn{Reader: &b1, Writer: &b2}
|
|
||||||
c := newConn(fc, true, 1024, 1024)
|
|
||||||
ul := c.UnderlyingConn()
|
|
||||||
if ul != fc {
|
|
||||||
t.Fatalf("Underlying conn is not what it should be.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-148
@@ -1,148 +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 implements the WebSocket protocol defined in RFC 6455.
|
|
||||||
//
|
|
||||||
// Overview
|
|
||||||
//
|
|
||||||
// The Conn type represents a WebSocket connection. A server application uses
|
|
||||||
// the Upgrade function from an Upgrader object with a HTTP request handler
|
|
||||||
// to get a pointer to a Conn:
|
|
||||||
//
|
|
||||||
// var upgrader = websocket.Upgrader{
|
|
||||||
// ReadBufferSize: 1024,
|
|
||||||
// WriteBufferSize: 1024,
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// func handler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// conn, err := upgrader.Upgrade(w, r, nil)
|
|
||||||
// if err != nil {
|
|
||||||
// log.Println(err)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// ... Use conn to send and receive messages.
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Call the connection's WriteMessage and ReadMessage methods to send and
|
|
||||||
// receive messages as a slice of bytes. This snippet of code shows how to echo
|
|
||||||
// messages using these methods:
|
|
||||||
//
|
|
||||||
// for {
|
|
||||||
// messageType, p, err := conn.ReadMessage()
|
|
||||||
// if err != nil {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// if err = conn.WriteMessage(messageType, p); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// In above snippet of code, p is a []byte and messageType is an int with value
|
|
||||||
// websocket.BinaryMessage or websocket.TextMessage.
|
|
||||||
//
|
|
||||||
// An application can also send and receive messages using the io.WriteCloser
|
|
||||||
// and io.Reader interfaces. To send a message, call the connection NextWriter
|
|
||||||
// method to get an io.WriteCloser, write the message to the writer and close
|
|
||||||
// the writer when done. To receive a message, call the connection NextReader
|
|
||||||
// method to get an io.Reader and read until io.EOF is returned. This snippet
|
|
||||||
// snippet shows how to echo messages using the NextWriter and NextReader
|
|
||||||
// methods:
|
|
||||||
//
|
|
||||||
// for {
|
|
||||||
// messageType, r, err := conn.NextReader()
|
|
||||||
// if err != nil {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// w, err := conn.NextWriter(messageType)
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// if _, err := io.Copy(w, r); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// if err := w.Close(); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Data Messages
|
|
||||||
//
|
|
||||||
// The WebSocket protocol distinguishes between text and binary data messages.
|
|
||||||
// Text messages are interpreted as UTF-8 encoded text. The interpretation of
|
|
||||||
// binary messages is left to the application.
|
|
||||||
//
|
|
||||||
// This package uses the TextMessage and BinaryMessage integer constants to
|
|
||||||
// identify the two data message types. The ReadMessage and NextReader methods
|
|
||||||
// return the type of the received message. The messageType argument to the
|
|
||||||
// WriteMessage and NextWriter methods specifies the type of a sent message.
|
|
||||||
//
|
|
||||||
// It is the application's responsibility to ensure that text messages are
|
|
||||||
// valid UTF-8 encoded text.
|
|
||||||
//
|
|
||||||
// Control Messages
|
|
||||||
//
|
|
||||||
// The WebSocket protocol defines three types of control messages: close, ping
|
|
||||||
// and pong. Call the connection WriteControl, WriteMessage or NextWriter
|
|
||||||
// methods to send a control message to the peer.
|
|
||||||
//
|
|
||||||
// Connections handle received ping and pong messages by invoking a callback
|
|
||||||
// function set with SetPingHandler and SetPongHandler methods. These callback
|
|
||||||
// functions can be invoked from the ReadMessage method, the NextReader method
|
|
||||||
// or from a call to the data message reader returned from NextReader.
|
|
||||||
//
|
|
||||||
// Connections handle received close messages by returning an error from the
|
|
||||||
// ReadMessage method, the NextReader method or from a call to the data message
|
|
||||||
// reader returned from NextReader.
|
|
||||||
//
|
|
||||||
// Concurrency
|
|
||||||
//
|
|
||||||
// Connections do not support concurrent calls to the write methods
|
|
||||||
// (NextWriter, SetWriteDeadline, WriteMessage) or concurrent calls to the read
|
|
||||||
// methods methods (NextReader, SetReadDeadline, ReadMessage). Connections do
|
|
||||||
// support a concurrent reader and writer.
|
|
||||||
//
|
|
||||||
// The Close and WriteControl methods can be called concurrently with all other
|
|
||||||
// methods.
|
|
||||||
//
|
|
||||||
// Read is Required
|
|
||||||
//
|
|
||||||
// The application must read the connection to process ping and close messages
|
|
||||||
// sent from the peer. If the application is not otherwise interested in
|
|
||||||
// messages from the peer, then the application should start a goroutine to read
|
|
||||||
// and discard messages from the peer. A simple example is:
|
|
||||||
//
|
|
||||||
// func readLoop(c *websocket.Conn) {
|
|
||||||
// for {
|
|
||||||
// if _, _, err := c.NextReader(); err != nil {
|
|
||||||
// c.Close()
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Origin Considerations
|
|
||||||
//
|
|
||||||
// Web browsers allow Javascript applications to open a WebSocket connection to
|
|
||||||
// any host. It's up to the server to enforce an origin policy using the Origin
|
|
||||||
// request header sent by the browser.
|
|
||||||
//
|
|
||||||
// The Upgrader calls the function specified in the CheckOrigin field to check
|
|
||||||
// the origin. If the CheckOrigin function returns false, then the Upgrade
|
|
||||||
// method fails the WebSocket handshake with HTTP status 403.
|
|
||||||
//
|
|
||||||
// If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail
|
|
||||||
// the handshake if the Origin request header is present and not equal to the
|
|
||||||
// Host request header.
|
|
||||||
//
|
|
||||||
// An application can allow connections from any origin by specifying a
|
|
||||||
// function that always returns true:
|
|
||||||
//
|
|
||||||
// var upgrader = websocket.Upgrader{
|
|
||||||
// CheckOrigin: func(r *http.Request) bool { return true },
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// The deprecated Upgrade function does not enforce an origin policy. It's the
|
|
||||||
// application's responsibility to check the Origin header before calling
|
|
||||||
// Upgrade.
|
|
||||||
package websocket
|
|
||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
# Test Server
|
|
||||||
|
|
||||||
This package contains a server for the [Autobahn WebSockets Test Suite](http://autobahn.ws/testsuite).
|
|
||||||
|
|
||||||
To test the server, run
|
|
||||||
|
|
||||||
go run server.go
|
|
||||||
|
|
||||||
and start the client test driver
|
|
||||||
|
|
||||||
wstest -m fuzzingclient -s fuzzingclient.json
|
|
||||||
|
|
||||||
When the client completes, it writes a report to reports/clients/index.html.
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
|
|
||||||
{
|
|
||||||
"options": {"failByDrop": false},
|
|
||||||
"outdir": "./reports/clients",
|
|
||||||
"servers": [
|
|
||||||
{"agent": "ReadAllWriteMessage", "url": "ws://localhost:9000/m", "options": {"version": 18}},
|
|
||||||
{"agent": "ReadAllWrite", "url": "ws://localhost:9000/r", "options": {"version": 18}},
|
|
||||||
{"agent": "CopyFull", "url": "ws://localhost:9000/f", "options": {"version": 18}},
|
|
||||||
{"agent": "CopyWriterOnly", "url": "ws://localhost:9000/c", "options": {"version": 18}}
|
|
||||||
],
|
|
||||||
"cases": ["*"],
|
|
||||||
"exclude-cases": [],
|
|
||||||
"exclude-agent-cases": {}
|
|
||||||
}
|
|
||||||
-246
@@ -1,246 +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.
|
|
||||||
|
|
||||||
// Command server is a test server for the Autobahn WebSockets Test Suite.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"flag"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{
|
|
||||||
ReadBufferSize: 4096,
|
|
||||||
WriteBufferSize: 4096,
|
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// echoCopy echoes messages from the client using io.Copy.
|
|
||||||
func echoCopy(w http.ResponseWriter, r *http.Request, writerOnly bool) {
|
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Upgrade:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
for {
|
|
||||||
mt, r, err := conn.NextReader()
|
|
||||||
if err != nil {
|
|
||||||
if err != io.EOF {
|
|
||||||
log.Println("NextReader:", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if mt == websocket.TextMessage {
|
|
||||||
r = &validator{r: r}
|
|
||||||
}
|
|
||||||
w, err := conn.NextWriter(mt)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("NextWriter:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if mt == websocket.TextMessage {
|
|
||||||
r = &validator{r: r}
|
|
||||||
}
|
|
||||||
if writerOnly {
|
|
||||||
_, err = io.Copy(struct{ io.Writer }{w}, r)
|
|
||||||
} else {
|
|
||||||
_, err = io.Copy(w, r)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if err == errInvalidUTF8 {
|
|
||||||
conn.WriteControl(websocket.CloseMessage,
|
|
||||||
websocket.FormatCloseMessage(websocket.CloseInvalidFramePayloadData, ""),
|
|
||||||
time.Time{})
|
|
||||||
}
|
|
||||||
log.Println("Copy:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = w.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Close:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func echoCopyWriterOnly(w http.ResponseWriter, r *http.Request) {
|
|
||||||
echoCopy(w, r, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func echoCopyFull(w http.ResponseWriter, r *http.Request) {
|
|
||||||
echoCopy(w, r, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// echoReadAll echoes messages from the client by reading the entire message
|
|
||||||
// with ioutil.ReadAll.
|
|
||||||
func echoReadAll(w http.ResponseWriter, r *http.Request, writeMessage bool) {
|
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Upgrade:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
for {
|
|
||||||
mt, b, err := conn.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
if err != io.EOF {
|
|
||||||
log.Println("NextReader:", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if mt == websocket.TextMessage {
|
|
||||||
if !utf8.Valid(b) {
|
|
||||||
conn.WriteControl(websocket.CloseMessage,
|
|
||||||
websocket.FormatCloseMessage(websocket.CloseInvalidFramePayloadData, ""),
|
|
||||||
time.Time{})
|
|
||||||
log.Println("ReadAll: invalid utf8")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if writeMessage {
|
|
||||||
err = conn.WriteMessage(mt, b)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("WriteMessage:", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
w, err := conn.NextWriter(mt)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("NextWriter:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, err := w.Write(b); err != nil {
|
|
||||||
log.Println("Writer:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := w.Close(); err != nil {
|
|
||||||
log.Println("Close:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func echoReadAllWriter(w http.ResponseWriter, r *http.Request) {
|
|
||||||
echoReadAll(w, r, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func echoReadAllWriteMessage(w http.ResponseWriter, r *http.Request) {
|
|
||||||
echoReadAll(w, r, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.URL.Path != "/" {
|
|
||||||
http.Error(w, "Not found.", 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != "GET" {
|
|
||||||
http.Error(w, "Method not allowed", 405)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
io.WriteString(w, "<html><body>Echo Server</body></html>")
|
|
||||||
}
|
|
||||||
|
|
||||||
var addr = flag.String("addr", ":9000", "http service address")
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
http.HandleFunc("/", serveHome)
|
|
||||||
http.HandleFunc("/c", echoCopyWriterOnly)
|
|
||||||
http.HandleFunc("/f", echoCopyFull)
|
|
||||||
http.HandleFunc("/r", echoReadAllWriter)
|
|
||||||
http.HandleFunc("/m", echoReadAllWriteMessage)
|
|
||||||
err := http.ListenAndServe(*addr, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("ListenAndServe: ", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type validator struct {
|
|
||||||
state int
|
|
||||||
x rune
|
|
||||||
r io.Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
var errInvalidUTF8 = errors.New("invalid utf8")
|
|
||||||
|
|
||||||
func (r *validator) Read(p []byte) (int, error) {
|
|
||||||
n, err := r.r.Read(p)
|
|
||||||
state := r.state
|
|
||||||
x := r.x
|
|
||||||
for _, b := range p[:n] {
|
|
||||||
state, x = decode(state, x, b)
|
|
||||||
if state == utf8Reject {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r.state = state
|
|
||||||
r.x = x
|
|
||||||
if state == utf8Reject || (err == io.EOF && state != utf8Accept) {
|
|
||||||
return n, errInvalidUTF8
|
|
||||||
}
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UTF-8 decoder from http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
|
|
||||||
//
|
|
||||||
// Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to
|
|
||||||
// deal in the Software without restriction, including without limitation the
|
|
||||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
||||||
// sell copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
||||||
// IN THE SOFTWARE.
|
|
||||||
var utf8d = [...]byte{
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00..1f
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20..3f
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40..5f
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60..7f
|
|
||||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // 80..9f
|
|
||||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // a0..bf
|
|
||||||
8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // c0..df
|
|
||||||
0xa, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, // e0..ef
|
|
||||||
0xb, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, // f0..ff
|
|
||||||
0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, // s0..s0
|
|
||||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, // s1..s2
|
|
||||||
1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, // s3..s4
|
|
||||||
1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, // s5..s6
|
|
||||||
1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // s7..s8
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
utf8Accept = 0
|
|
||||||
utf8Reject = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
func decode(state int, x rune, b byte) (int, rune) {
|
|
||||||
t := utf8d[b]
|
|
||||||
if state != utf8Accept {
|
|
||||||
x = rune(b&0x3f) | (x << 6)
|
|
||||||
} else {
|
|
||||||
x = rune((0xff >> t) & b)
|
|
||||||
}
|
|
||||||
state = int(utf8d[256+state*16+int(t)])
|
|
||||||
return state, x
|
|
||||||
}
|
|
||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
# Chat Example
|
|
||||||
|
|
||||||
This application shows how to use use the
|
|
||||||
[websocket](https://github.com/gorilla/websocket) package and
|
|
||||||
[jQuery](http://jquery.com) to implement a simple web chat application.
|
|
||||||
|
|
||||||
## Running the example
|
|
||||||
|
|
||||||
The example requires a working Go development environment. The [Getting
|
|
||||||
Started](http://golang.org/doc/install) page describes how to install the
|
|
||||||
development environment.
|
|
||||||
|
|
||||||
Once you have Go up and running, you can download, build and run the example
|
|
||||||
using the following commands.
|
|
||||||
|
|
||||||
$ go get github.com/gorilla/websocket
|
|
||||||
$ cd `go list -f '{{.Dir}}' github.com/gorilla/websocket/examples/chat`
|
|
||||||
$ go run *.go
|
|
||||||
|
|
||||||
-106
@@ -1,106 +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 main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Time allowed to write a message to the peer.
|
|
||||||
writeWait = 10 * time.Second
|
|
||||||
|
|
||||||
// Time allowed to read the next pong message from the peer.
|
|
||||||
pongWait = 60 * time.Second
|
|
||||||
|
|
||||||
// Send pings to peer with this period. Must be less than pongWait.
|
|
||||||
pingPeriod = (pongWait * 9) / 10
|
|
||||||
|
|
||||||
// Maximum message size allowed from peer.
|
|
||||||
maxMessageSize = 512
|
|
||||||
)
|
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{
|
|
||||||
ReadBufferSize: 1024,
|
|
||||||
WriteBufferSize: 1024,
|
|
||||||
}
|
|
||||||
|
|
||||||
// connection is an middleman between the websocket connection and the hub.
|
|
||||||
type connection struct {
|
|
||||||
// The websocket connection.
|
|
||||||
ws *websocket.Conn
|
|
||||||
|
|
||||||
// Buffered channel of outbound messages.
|
|
||||||
send chan []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// readPump pumps messages from the websocket connection to the hub.
|
|
||||||
func (c *connection) readPump() {
|
|
||||||
defer func() {
|
|
||||||
h.unregister <- c
|
|
||||||
c.ws.Close()
|
|
||||||
}()
|
|
||||||
c.ws.SetReadLimit(maxMessageSize)
|
|
||||||
c.ws.SetReadDeadline(time.Now().Add(pongWait))
|
|
||||||
c.ws.SetPongHandler(func(string) error { c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
|
||||||
for {
|
|
||||||
_, message, err := c.ws.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
h.broadcast <- message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// write writes a message with the given message type and payload.
|
|
||||||
func (c *connection) write(mt int, payload []byte) error {
|
|
||||||
c.ws.SetWriteDeadline(time.Now().Add(writeWait))
|
|
||||||
return c.ws.WriteMessage(mt, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
// writePump pumps messages from the hub to the websocket connection.
|
|
||||||
func (c *connection) writePump() {
|
|
||||||
ticker := time.NewTicker(pingPeriod)
|
|
||||||
defer func() {
|
|
||||||
ticker.Stop()
|
|
||||||
c.ws.Close()
|
|
||||||
}()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case message, ok := <-c.send:
|
|
||||||
if !ok {
|
|
||||||
c.write(websocket.CloseMessage, []byte{})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := c.write(websocket.TextMessage, message); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-ticker.C:
|
|
||||||
if err := c.write(websocket.PingMessage, []byte{}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// serverWs handles websocket requests from the peer.
|
|
||||||
func serveWs(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "GET" {
|
|
||||||
http.Error(w, "Method not allowed", 405)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ws, err := upgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c := &connection{send: make(chan []byte, 256), ws: ws}
|
|
||||||
h.register <- c
|
|
||||||
go c.writePump()
|
|
||||||
c.readPump()
|
|
||||||
}
|
|
||||||
-92
@@ -1,92 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>Chat Example</title>
|
|
||||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
|
|
||||||
<script type="text/javascript">
|
|
||||||
$(function() {
|
|
||||||
|
|
||||||
var conn;
|
|
||||||
var msg = $("#msg");
|
|
||||||
var log = $("#log");
|
|
||||||
|
|
||||||
function appendLog(msg) {
|
|
||||||
var d = log[0]
|
|
||||||
var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight;
|
|
||||||
msg.appendTo(log)
|
|
||||||
if (doScroll) {
|
|
||||||
d.scrollTop = d.scrollHeight - d.clientHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#form").submit(function() {
|
|
||||||
if (!conn) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!msg.val()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
conn.send(msg.val());
|
|
||||||
msg.val("");
|
|
||||||
return false
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window["WebSocket"]) {
|
|
||||||
conn = new WebSocket("ws://{{$}}/ws");
|
|
||||||
conn.onclose = function(evt) {
|
|
||||||
appendLog($("<div><b>Connection closed.</b></div>"))
|
|
||||||
}
|
|
||||||
conn.onmessage = function(evt) {
|
|
||||||
appendLog($("<div/>").text(evt.data))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
appendLog($("<div><b>Your browser does not support WebSockets.</b></div>"))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style type="text/css">
|
|
||||||
html {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
#log {
|
|
||||||
background: white;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0.5em 0.5em 0.5em 0.5em;
|
|
||||||
position: absolute;
|
|
||||||
top: 0.5em;
|
|
||||||
left: 0.5em;
|
|
||||||
right: 0.5em;
|
|
||||||
bottom: 3em;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#form {
|
|
||||||
padding: 0 0.5em 0 0.5em;
|
|
||||||
margin: 0;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1em;
|
|
||||||
left: 0px;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="log"></div>
|
|
||||||
<form id="form">
|
|
||||||
<input type="submit" value="Send" />
|
|
||||||
<input type="text" id="msg" size="64"/>
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
-51
@@ -1,51 +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 main
|
|
||||||
|
|
||||||
// hub maintains the set of active connections and broadcasts messages to the
|
|
||||||
// connections.
|
|
||||||
type hub struct {
|
|
||||||
// Registered connections.
|
|
||||||
connections map[*connection]bool
|
|
||||||
|
|
||||||
// Inbound messages from the connections.
|
|
||||||
broadcast chan []byte
|
|
||||||
|
|
||||||
// Register requests from the connections.
|
|
||||||
register chan *connection
|
|
||||||
|
|
||||||
// Unregister requests from connections.
|
|
||||||
unregister chan *connection
|
|
||||||
}
|
|
||||||
|
|
||||||
var h = hub{
|
|
||||||
broadcast: make(chan []byte),
|
|
||||||
register: make(chan *connection),
|
|
||||||
unregister: make(chan *connection),
|
|
||||||
connections: make(map[*connection]bool),
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *hub) run() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case c := <-h.register:
|
|
||||||
h.connections[c] = true
|
|
||||||
case c := <-h.unregister:
|
|
||||||
if _, ok := h.connections[c]; ok {
|
|
||||||
delete(h.connections, c)
|
|
||||||
close(c.send)
|
|
||||||
}
|
|
||||||
case m := <-h.broadcast:
|
|
||||||
for c := range h.connections {
|
|
||||||
select {
|
|
||||||
case c.send <- m:
|
|
||||||
default:
|
|
||||||
close(c.send)
|
|
||||||
delete(h.connections, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-39
@@ -1,39 +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 main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"text/template"
|
|
||||||
)
|
|
||||||
|
|
||||||
var addr = flag.String("addr", ":8080", "http service address")
|
|
||||||
var homeTempl = template.Must(template.ParseFiles("home.html"))
|
|
||||||
|
|
||||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.URL.Path != "/" {
|
|
||||||
http.Error(w, "Not found", 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != "GET" {
|
|
||||||
http.Error(w, "Method not allowed", 405)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
homeTempl.Execute(w, r.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
go h.run()
|
|
||||||
http.HandleFunc("/", serveHome)
|
|
||||||
http.HandleFunc("/ws", serveWs)
|
|
||||||
err := http.ListenAndServe(*addr, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("ListenAndServe: ", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
# File Watch example.
|
|
||||||
|
|
||||||
This example sends a file to the browser client for display whenever the file is modified.
|
|
||||||
|
|
||||||
$ go get github.com/gorilla/websocket
|
|
||||||
$ cd `go list -f '{{.Dir}}' github.com/gorilla/websocket/examples/filewatch`
|
|
||||||
$ go run main.go <name of file to watch>
|
|
||||||
# Open http://localhost:8080/ .
|
|
||||||
# Modify the file to see it update in the browser.
|
|
||||||
-193
@@ -1,193 +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 main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"text/template"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Time allowed to write the file to the client.
|
|
||||||
writeWait = 10 * time.Second
|
|
||||||
|
|
||||||
// Time allowed to read the next pong message from the client.
|
|
||||||
pongWait = 60 * time.Second
|
|
||||||
|
|
||||||
// Send pings to client with this period. Must be less than pongWait.
|
|
||||||
pingPeriod = (pongWait * 9) / 10
|
|
||||||
|
|
||||||
// Poll file for changes with this period.
|
|
||||||
filePeriod = 10 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
addr = flag.String("addr", ":8080", "http service address")
|
|
||||||
homeTempl = template.Must(template.New("").Parse(homeHTML))
|
|
||||||
filename string
|
|
||||||
upgrader = websocket.Upgrader{
|
|
||||||
ReadBufferSize: 1024,
|
|
||||||
WriteBufferSize: 1024,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func readFileIfModified(lastMod time.Time) ([]byte, time.Time, error) {
|
|
||||||
fi, err := os.Stat(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, lastMod, err
|
|
||||||
}
|
|
||||||
if !fi.ModTime().After(lastMod) {
|
|
||||||
return nil, lastMod, nil
|
|
||||||
}
|
|
||||||
p, err := ioutil.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fi.ModTime(), err
|
|
||||||
}
|
|
||||||
return p, fi.ModTime(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func reader(ws *websocket.Conn) {
|
|
||||||
defer ws.Close()
|
|
||||||
ws.SetReadLimit(512)
|
|
||||||
ws.SetReadDeadline(time.Now().Add(pongWait))
|
|
||||||
ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
|
||||||
for {
|
|
||||||
_, _, err := ws.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func writer(ws *websocket.Conn, lastMod time.Time) {
|
|
||||||
lastError := ""
|
|
||||||
pingTicker := time.NewTicker(pingPeriod)
|
|
||||||
fileTicker := time.NewTicker(filePeriod)
|
|
||||||
defer func() {
|
|
||||||
pingTicker.Stop()
|
|
||||||
fileTicker.Stop()
|
|
||||||
ws.Close()
|
|
||||||
}()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-fileTicker.C:
|
|
||||||
var p []byte
|
|
||||||
var err error
|
|
||||||
|
|
||||||
p, lastMod, err = readFileIfModified(lastMod)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if s := err.Error(); s != lastError {
|
|
||||||
lastError = s
|
|
||||||
p = []byte(lastError)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lastError = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if p != nil {
|
|
||||||
ws.SetWriteDeadline(time.Now().Add(writeWait))
|
|
||||||
if err := ws.WriteMessage(websocket.TextMessage, p); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case <-pingTicker.C:
|
|
||||||
ws.SetWriteDeadline(time.Now().Add(writeWait))
|
|
||||||
if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveWs(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ws, err := upgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(websocket.HandshakeError); !ok {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastMod time.Time
|
|
||||||
if n, err := strconv.ParseInt(r.FormValue("lastMod"), 16, 64); err != nil {
|
|
||||||
lastMod = time.Unix(0, n)
|
|
||||||
}
|
|
||||||
|
|
||||||
go writer(ws, lastMod)
|
|
||||||
reader(ws)
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.URL.Path != "/" {
|
|
||||||
http.Error(w, "Not found", 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != "GET" {
|
|
||||||
http.Error(w, "Method not allowed", 405)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
p, lastMod, err := readFileIfModified(time.Time{})
|
|
||||||
if err != nil {
|
|
||||||
p = []byte(err.Error())
|
|
||||||
lastMod = time.Unix(0, 0)
|
|
||||||
}
|
|
||||||
var v = struct {
|
|
||||||
Host string
|
|
||||||
Data string
|
|
||||||
LastMod string
|
|
||||||
}{
|
|
||||||
r.Host,
|
|
||||||
string(p),
|
|
||||||
strconv.FormatInt(lastMod.UnixNano(), 16),
|
|
||||||
}
|
|
||||||
homeTempl.Execute(w, &v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
if flag.NArg() != 1 {
|
|
||||||
log.Fatal("filename not specified")
|
|
||||||
}
|
|
||||||
filename = flag.Args()[0]
|
|
||||||
http.HandleFunc("/", serveHome)
|
|
||||||
http.HandleFunc("/ws", serveWs)
|
|
||||||
if err := http.ListenAndServe(*addr, nil); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const homeHTML = `<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>WebSocket Example</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<pre id="fileData">{{.Data}}</pre>
|
|
||||||
<script type="text/javascript">
|
|
||||||
(function() {
|
|
||||||
var data = document.getElementById("fileData");
|
|
||||||
var conn = new WebSocket("ws://{{.Host}}/ws?lastMod={{.LastMod}}");
|
|
||||||
conn.onclose = function(evt) {
|
|
||||||
data.textContent = 'Connection closed';
|
|
||||||
}
|
|
||||||
conn.onmessage = function(evt) {
|
|
||||||
console.log('file updated');
|
|
||||||
data.textContent = evt.data;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`
|
|
||||||
-57
@@ -1,57 +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 (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// WriteJSON is deprecated, use c.WriteJSON instead.
|
|
||||||
func WriteJSON(c *Conn, v interface{}) error {
|
|
||||||
return c.WriteJSON(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteJSON writes the JSON encoding of v to the connection.
|
|
||||||
//
|
|
||||||
// See the documentation for encoding/json Marshal for details about the
|
|
||||||
// conversion of Go values to JSON.
|
|
||||||
func (c *Conn) WriteJSON(v interface{}) error {
|
|
||||||
w, err := c.NextWriter(TextMessage)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err1 := json.NewEncoder(w).Encode(v)
|
|
||||||
err2 := w.Close()
|
|
||||||
if err1 != nil {
|
|
||||||
return err1
|
|
||||||
}
|
|
||||||
return err2
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadJSON is deprecated, use c.ReadJSON instead.
|
|
||||||
func ReadJSON(c *Conn, v interface{}) error {
|
|
||||||
return c.ReadJSON(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadJSON reads the next JSON-encoded message from the connection and stores
|
|
||||||
// it in the value pointed to by v.
|
|
||||||
//
|
|
||||||
// See the documentation for the encoding/json Unmarshal function for details
|
|
||||||
// about the conversion of JSON to a Go value.
|
|
||||||
func (c *Conn) ReadJSON(v interface{}) error {
|
|
||||||
_, r, err := c.NextReader()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = json.NewDecoder(r).Decode(v)
|
|
||||||
if err == io.EOF {
|
|
||||||
// Decode returns io.EOF when the message is empty or all whitespace.
|
|
||||||
// Convert to io.ErrUnexpectedEOF so that application can distinguish
|
|
||||||
// between an error reading the JSON value and the connection closing.
|
|
||||||
err = io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
-119
@@ -1,119 +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 (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestJSON(t *testing.T) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
c := fakeNetConn{&buf, &buf}
|
|
||||||
wc := newConn(c, true, 1024, 1024)
|
|
||||||
rc := newConn(c, false, 1024, 1024)
|
|
||||||
|
|
||||||
var actual, expect struct {
|
|
||||||
A int
|
|
||||||
B string
|
|
||||||
}
|
|
||||||
expect.A = 1
|
|
||||||
expect.B = "hello"
|
|
||||||
|
|
||||||
if err := wc.WriteJSON(&expect); err != nil {
|
|
||||||
t.Fatal("write", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := rc.ReadJSON(&actual); err != nil {
|
|
||||||
t.Fatal("read", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(&actual, &expect) {
|
|
||||||
t.Fatal("equal", actual, expect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPartialJsonRead(t *testing.T) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
c := fakeNetConn{&buf, &buf}
|
|
||||||
wc := newConn(c, true, 1024, 1024)
|
|
||||||
rc := newConn(c, false, 1024, 1024)
|
|
||||||
|
|
||||||
var v struct {
|
|
||||||
A int
|
|
||||||
B string
|
|
||||||
}
|
|
||||||
v.A = 1
|
|
||||||
v.B = "hello"
|
|
||||||
|
|
||||||
messageCount := 0
|
|
||||||
|
|
||||||
// Partial JSON values.
|
|
||||||
|
|
||||||
data, err := json.Marshal(v)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for i := len(data) - 1; i >= 0; i-- {
|
|
||||||
if err := wc.WriteMessage(TextMessage, data[:i]); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
messageCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whitespace.
|
|
||||||
|
|
||||||
if err := wc.WriteMessage(TextMessage, []byte(" ")); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
messageCount++
|
|
||||||
|
|
||||||
// Close.
|
|
||||||
|
|
||||||
if err := wc.WriteMessage(CloseMessage, FormatCloseMessage(CloseNormalClosure, "")); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < messageCount; i++ {
|
|
||||||
err := rc.ReadJSON(&v)
|
|
||||||
if err != io.ErrUnexpectedEOF {
|
|
||||||
t.Error("read", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = rc.ReadJSON(&v)
|
|
||||||
if err != io.EOF {
|
|
||||||
t.Error("final", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeprecatedJSON(t *testing.T) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
c := fakeNetConn{&buf, &buf}
|
|
||||||
wc := newConn(c, true, 1024, 1024)
|
|
||||||
rc := newConn(c, false, 1024, 1024)
|
|
||||||
|
|
||||||
var actual, expect struct {
|
|
||||||
A int
|
|
||||||
B string
|
|
||||||
}
|
|
||||||
expect.A = 1
|
|
||||||
expect.B = "hello"
|
|
||||||
|
|
||||||
if err := WriteJSON(wc, &expect); err != nil {
|
|
||||||
t.Fatal("write", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ReadJSON(rc, &actual); err != nil {
|
|
||||||
t.Fatal("read", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(&actual, &expect) {
|
|
||||||
t.Fatal("equal", actual, expect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-247
@@ -1,247 +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"
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HandshakeError describes an error with the handshake from the peer.
|
|
||||||
type HandshakeError struct {
|
|
||||||
message string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e HandshakeError) Error() string { return e.message }
|
|
||||||
|
|
||||||
// Upgrader specifies parameters for upgrading an HTTP connection to a
|
|
||||||
// WebSocket connection.
|
|
||||||
type Upgrader struct {
|
|
||||||
// HandshakeTimeout specifies the duration for the handshake to complete.
|
|
||||||
HandshakeTimeout time.Duration
|
|
||||||
|
|
||||||
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer
|
|
||||||
// size is zero, then a default value of 4096 is used. The I/O buffer sizes
|
|
||||||
// do not limit the size of the messages that can be sent or received.
|
|
||||||
ReadBufferSize, WriteBufferSize int
|
|
||||||
|
|
||||||
// Subprotocols specifies the server's supported protocols in order of
|
|
||||||
// preference. If this field is set, then the Upgrade method negotiates a
|
|
||||||
// subprotocol by selecting the first match in this list with a protocol
|
|
||||||
// requested by the client.
|
|
||||||
Subprotocols []string
|
|
||||||
|
|
||||||
// Error specifies the function for generating HTTP error responses. If Error
|
|
||||||
// is nil, then http.Error is used to generate the HTTP response.
|
|
||||||
Error func(w http.ResponseWriter, r *http.Request, status int, reason error)
|
|
||||||
|
|
||||||
// CheckOrigin returns true if the request Origin header is acceptable. If
|
|
||||||
// CheckOrigin is nil, the host in the Origin header must not be set or
|
|
||||||
// must match the host of the request.
|
|
||||||
CheckOrigin func(r *http.Request) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status int, reason string) (*Conn, error) {
|
|
||||||
err := HandshakeError{reason}
|
|
||||||
if u.Error != nil {
|
|
||||||
u.Error(w, r, status, err)
|
|
||||||
} else {
|
|
||||||
http.Error(w, http.StatusText(status), status)
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkSameOrigin returns true if the origin is not set or is equal to the request host.
|
|
||||||
func checkSameOrigin(r *http.Request) bool {
|
|
||||||
origin := r.Header["Origin"]
|
|
||||||
if len(origin) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
u, err := url.Parse(origin[0])
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return u.Host == r.Host
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string {
|
|
||||||
if u.Subprotocols != nil {
|
|
||||||
clientProtocols := Subprotocols(r)
|
|
||||||
for _, serverProtocol := range u.Subprotocols {
|
|
||||||
for _, clientProtocol := range clientProtocols {
|
|
||||||
if clientProtocol == serverProtocol {
|
|
||||||
return clientProtocol
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if responseHeader != nil {
|
|
||||||
return responseHeader.Get("Sec-Websocket-Protocol")
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upgrade upgrades the HTTP server connection to the WebSocket protocol.
|
|
||||||
//
|
|
||||||
// The responseHeader is included in the response to the client's upgrade
|
|
||||||
// request. Use the responseHeader to specify cookies (Set-Cookie) and the
|
|
||||||
// application negotiated subprotocol (Sec-Websocket-Protocol).
|
|
||||||
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {
|
|
||||||
if values := r.Header["Sec-Websocket-Version"]; len(values) == 0 || values[0] != "13" {
|
|
||||||
return u.returnError(w, r, http.StatusBadRequest, "websocket: version != 13")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tokenListContainsValue(r.Header, "Connection", "upgrade") {
|
|
||||||
return u.returnError(w, r, http.StatusBadRequest, "websocket: could not find connection header with token 'upgrade'")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tokenListContainsValue(r.Header, "Upgrade", "websocket") {
|
|
||||||
return u.returnError(w, r, http.StatusBadRequest, "websocket: could not find upgrade header with token 'websocket'")
|
|
||||||
}
|
|
||||||
|
|
||||||
checkOrigin := u.CheckOrigin
|
|
||||||
if checkOrigin == nil {
|
|
||||||
checkOrigin = checkSameOrigin
|
|
||||||
}
|
|
||||||
if !checkOrigin(r) {
|
|
||||||
return u.returnError(w, r, http.StatusForbidden, "websocket: origin not allowed")
|
|
||||||
}
|
|
||||||
|
|
||||||
challengeKey := r.Header.Get("Sec-Websocket-Key")
|
|
||||||
if challengeKey == "" {
|
|
||||||
return u.returnError(w, r, http.StatusBadRequest, "websocket: key missing or blank")
|
|
||||||
}
|
|
||||||
|
|
||||||
subprotocol := u.selectSubprotocol(r, responseHeader)
|
|
||||||
|
|
||||||
var (
|
|
||||||
netConn net.Conn
|
|
||||||
br *bufio.Reader
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
h, ok := w.(http.Hijacker)
|
|
||||||
if !ok {
|
|
||||||
return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker")
|
|
||||||
}
|
|
||||||
var rw *bufio.ReadWriter
|
|
||||||
netConn, rw, err = h.Hijack()
|
|
||||||
if err != nil {
|
|
||||||
return u.returnError(w, r, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
br = rw.Reader
|
|
||||||
|
|
||||||
if br.Buffered() > 0 {
|
|
||||||
netConn.Close()
|
|
||||||
return nil, errors.New("websocket: client sent data before handshake is complete")
|
|
||||||
}
|
|
||||||
|
|
||||||
c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize)
|
|
||||||
c.subprotocol = subprotocol
|
|
||||||
|
|
||||||
p := c.writeBuf[:0]
|
|
||||||
p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
|
|
||||||
p = append(p, computeAcceptKey(challengeKey)...)
|
|
||||||
p = append(p, "\r\n"...)
|
|
||||||
if c.subprotocol != "" {
|
|
||||||
p = append(p, "Sec-Websocket-Protocol: "...)
|
|
||||||
p = append(p, c.subprotocol...)
|
|
||||||
p = append(p, "\r\n"...)
|
|
||||||
}
|
|
||||||
for k, vs := range responseHeader {
|
|
||||||
if k == "Sec-Websocket-Protocol" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, v := range vs {
|
|
||||||
p = append(p, k...)
|
|
||||||
p = append(p, ": "...)
|
|
||||||
for i := 0; i < len(v); i++ {
|
|
||||||
b := v[i]
|
|
||||||
if b <= 31 {
|
|
||||||
// prevent response splitting.
|
|
||||||
b = ' '
|
|
||||||
}
|
|
||||||
p = append(p, b)
|
|
||||||
}
|
|
||||||
p = append(p, "\r\n"...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p = append(p, "\r\n"...)
|
|
||||||
|
|
||||||
// Clear deadlines set by HTTP server.
|
|
||||||
netConn.SetDeadline(time.Time{})
|
|
||||||
|
|
||||||
if u.HandshakeTimeout > 0 {
|
|
||||||
netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout))
|
|
||||||
}
|
|
||||||
if _, err = netConn.Write(p); err != nil {
|
|
||||||
netConn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if u.HandshakeTimeout > 0 {
|
|
||||||
netConn.SetWriteDeadline(time.Time{})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upgrade upgrades the HTTP server connection to the WebSocket protocol.
|
|
||||||
//
|
|
||||||
// This function is deprecated, use websocket.Upgrader instead.
|
|
||||||
//
|
|
||||||
// The application is responsible for checking the request origin before
|
|
||||||
// calling Upgrade. An example implementation of the same origin policy is:
|
|
||||||
//
|
|
||||||
// if req.Header.Get("Origin") != "http://"+req.Host {
|
|
||||||
// http.Error(w, "Origin not allowed", 403)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// If the endpoint supports subprotocols, then the application is responsible
|
|
||||||
// for negotiating the protocol used on the connection. Use the Subprotocols()
|
|
||||||
// function to get the subprotocols requested by the client. Use the
|
|
||||||
// Sec-Websocket-Protocol response header to specify the subprotocol selected
|
|
||||||
// by the application.
|
|
||||||
//
|
|
||||||
// The responseHeader is included in the response to the client's upgrade
|
|
||||||
// request. Use the responseHeader to specify cookies (Set-Cookie) and the
|
|
||||||
// negotiated subprotocol (Sec-Websocket-Protocol).
|
|
||||||
//
|
|
||||||
// The connection buffers IO to the underlying network connection. The
|
|
||||||
// readBufSize and writeBufSize parameters specify the size of the buffers to
|
|
||||||
// use. Messages can be larger than the buffers.
|
|
||||||
//
|
|
||||||
// If the request is not a valid WebSocket handshake, then Upgrade returns an
|
|
||||||
// error of type HandshakeError. Applications should handle this error by
|
|
||||||
// replying to the client with an HTTP error response.
|
|
||||||
func Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header, readBufSize, writeBufSize int) (*Conn, error) {
|
|
||||||
u := Upgrader{ReadBufferSize: readBufSize, WriteBufferSize: writeBufSize}
|
|
||||||
u.Error = func(w http.ResponseWriter, r *http.Request, status int, reason error) {
|
|
||||||
// don't return errors to maintain backwards compatibility
|
|
||||||
}
|
|
||||||
u.CheckOrigin = func(r *http.Request) bool {
|
|
||||||
// allow all connections by default
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return u.Upgrade(w, r, responseHeader)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subprotocols returns the subprotocols requested by the client in the
|
|
||||||
// Sec-Websocket-Protocol header.
|
|
||||||
func Subprotocols(r *http.Request) []string {
|
|
||||||
h := strings.TrimSpace(r.Header.Get("Sec-Websocket-Protocol"))
|
|
||||||
if h == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
protocols := strings.Split(h, ",")
|
|
||||||
for i := range protocols {
|
|
||||||
protocols[i] = strings.TrimSpace(protocols[i])
|
|
||||||
}
|
|
||||||
return protocols
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user