Compare commits
73
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
722e0ad3e3 | ||
|
|
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 |
+6
-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,9 @@ flymake*
|
|||||||
|
|
||||||
ignore_http/*
|
ignore_http/*
|
||||||
config.json
|
config.json
|
||||||
|
*.pem
|
||||||
|
build
|
||||||
|
.vscode/*
|
||||||
|
debug
|
||||||
|
|
||||||
|
vendor/*
|
||||||
@@ -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,4 +1,4 @@
|
|||||||
[](http://goreportcard.com/report/dimiro1/ipe)
|
[](https://goreportcard.com/report/github.com/dimiro1/ipe)
|
||||||
|
|
||||||
# IPÊ
|
# IPÊ
|
||||||
|
|
||||||
@@ -18,13 +18,19 @@ 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).
|
||||||
|
|
||||||
|
I do not have a Windows machine, so I can only distribute binaries for amd64 linux and amd64 darwin.
|
||||||
|
|
||||||
# Building
|
# Building
|
||||||
|
|
||||||
```console
|
```console
|
||||||
@@ -41,22 +47,29 @@ $ go install github.com/dimiro1/ipe
|
|||||||
|
|
||||||
## The server
|
## The server
|
||||||
|
|
||||||
```json
|
```javascript
|
||||||
{
|
{
|
||||||
"Host": ":8080",
|
"Host": ":8080", // Required
|
||||||
"Apps": [
|
"SSL": false, // Not Required, default is false
|
||||||
|
"Profiling": false, // Mount pprof at /debug. Not Required, default is false
|
||||||
|
"SSLHost": ":4433", // Required if SSL is true
|
||||||
|
"SSLKeyFile": "A key.pem file", // Required if SSL is true
|
||||||
|
"SSLCertFile": "A cert.pem file", // Required if SSL is true
|
||||||
|
"Apps": [ // Required, A Json arrays with multiple apps
|
||||||
{
|
{
|
||||||
"ApplicationDisabled": false,
|
"ApplicationDisabled": false, // Required but can be false
|
||||||
"Secret": "APP_SECRET",
|
"Secret": "A really secret random string", // Required
|
||||||
"Key": "APP_KEY",
|
"Key": "A random Key string", // Required
|
||||||
"Name": "APP_NAME",
|
"OnlySSL": false, // Required but can be false
|
||||||
"AppID": "APP_ID",
|
"Name": "The app name", // Required
|
||||||
"UserEvents": true,
|
"AppID": "The app ID", // Required
|
||||||
"WebHooks": true,
|
"UserEvents": true, // Required but can be false
|
||||||
"URLWebHook": "http://localhost:4567/php/hook.php"
|
"WebHooks": true, // Required but can be false
|
||||||
|
"URLWebHook": "Some URL to send webhooks" // Required if WebHooks is true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Libraries
|
## Libraries
|
||||||
@@ -67,6 +80,8 @@ $ go install github.com/dimiro1/ipe
|
|||||||
var pusher = new Pusher(APP_KEY, {
|
var 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"]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
# Use of this source code is governed by a MIT-style
|
||||||
|
# license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
require 'rake/clean'
|
||||||
|
|
||||||
|
VERSION = 'v1.3.0-SNAPSHOT'
|
||||||
|
GITHASH = `git rev-parse --short HEAD`
|
||||||
|
DATE = Time.now.strftime '%Y%m%d%H%M%S'
|
||||||
|
|
||||||
|
CLOBBER.include 'build'
|
||||||
|
|
||||||
|
task :default => [:'run-debug']
|
||||||
|
|
||||||
|
desc 'Build a debug version'
|
||||||
|
task :debug do
|
||||||
|
sh "GO15VENDOREXPERIMENT=1 go install -ldflags '-w -X main.version=DEBUG -X main.buildstamp=DEBUG -X main.githash=DEBUG' github.com/dimiro1/ipe"
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Build and run debug version'
|
||||||
|
task :'run-debug' => :debug do
|
||||||
|
sh '$GOPATH/bin/ipe --config $GOPATH/src/github.com/dimiro1/ipe/config.json -logtostderr=true -v=2'
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Run test suite'
|
||||||
|
task :test do
|
||||||
|
sh 'GO15VENDOREXPERIMENT=1 go test . `glide nv`'
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Download the dependencies'
|
||||||
|
task :'deps' do
|
||||||
|
sh 'glide install -v -s'
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Generate distributions'
|
||||||
|
task :distribute => [:linux, :darwin]
|
||||||
|
|
||||||
|
desc 'Generate a linux distribution'
|
||||||
|
task :linux do
|
||||||
|
Rake::Task['build'].invoke 'linux'
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Generate a darwin distribution'
|
||||||
|
task :darwin do
|
||||||
|
Rake::Task['build'].invoke 'darwin'
|
||||||
|
end
|
||||||
|
|
||||||
|
task :build, [:os] do |t, args|
|
||||||
|
t.reenable
|
||||||
|
os = args[:os]
|
||||||
|
|
||||||
|
sh "mkdir -p build/#{os}"
|
||||||
|
sh "GO15VENDOREXPERIMENT=1 GOOS=#{os} GOARCH=amd64 go build -ldflags '-X main.version=#{VERSION} -X main.buildstamp=#{DATE} -X main.githash=#{GITHASH}' -o build/#{os}/ipe github.com/dimiro1/ipe"
|
||||||
|
sh "cp ipe/config-example.json build/#{os}/config.json"
|
||||||
|
sh "cp LICENSE build/#{os}/"
|
||||||
|
sh "cp README.md build/#{os}/"
|
||||||
|
sh "tar -C build/#{os} -czf build/ipe_#{VERSION}_#{os}_amd64.tar.gz ."
|
||||||
|
end
|
||||||
@@ -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
|
|
||||||
+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 ../main.go -config ./functional-config.json -alsologtostderr
|
||||||
+12
-12
@@ -1,24 +1,24 @@
|
|||||||
<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="https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.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="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.3.4/mocha.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.3.4/mocha.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.4.1/chai.min.js"></script>
|
<script src="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/3.2/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(['jQuery', 'Pusher']);
|
||||||
mocha.run();
|
mocha.run();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/pusher/pusher-http-go"
|
"github.com/pusher/pusher-http-go"
|
||||||
@@ -20,6 +21,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,24 +34,27 @@ 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")
|
client.Trigger("private-messages", "messages", "The message from server")
|
||||||
|
|
||||||
fmt.Fprintf(res, "OK")
|
fmt.Fprint(res, "OK")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"Host": ":8080",
|
||||||
|
"SSL": false,
|
||||||
|
"Profiling": true,
|
||||||
|
"SSLHost": ":8090",
|
||||||
|
"SSLKeyFile": "key.pem",
|
||||||
|
"SSLCertFile": "cert.pem",
|
||||||
|
"Apps": [
|
||||||
|
{
|
||||||
|
"ApplicationDisabled": false,
|
||||||
|
"OnlySSL": false,
|
||||||
|
"Secret": "7ad3753142a6693b25b9",
|
||||||
|
"Key": "278d525bdf162c739803",
|
||||||
|
"Name": "App for Functional Test",
|
||||||
|
"AppID": "1",
|
||||||
|
"UserEvents": true,
|
||||||
|
"WebHooks": false,
|
||||||
|
"URLWebHook": "http://127.0.0.1:4567/php/hook.php"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -17,7 +17,8 @@ function getPusher(auth) {
|
|||||||
wsPort: PORT,
|
wsPort: PORT,
|
||||||
authEndpoint: auth,
|
authEndpoint: auth,
|
||||||
enabledTransports: ["ws"],
|
enabledTransports: ["ws"],
|
||||||
disabledTransports: ["flash"]
|
disabledTransports: ["flash"],
|
||||||
|
cluster: "hello", // Should be ignored
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+12
@@ -0,0 +1,12 @@
|
|||||||
|
hash: ee2de935e70f2a39cbfd2dd16add0736c698b32f5c02409d0c25844d883c91a1
|
||||||
|
updated: 2016-08-22T22:55:07.279113421-03:00
|
||||||
|
imports:
|
||||||
|
- name: github.com/golang/glog
|
||||||
|
version: 23def4e6c14b4da8ac2ed8007337bc5eb5007998
|
||||||
|
- name: github.com/gorilla/websocket
|
||||||
|
version: a69d25be2fe2923a97c2af6849b2f52426f68fc0
|
||||||
|
- name: github.com/pressly/chi
|
||||||
|
version: 12aad88c7d86de2affe686f855b6ed94a07cba9c
|
||||||
|
subpackages:
|
||||||
|
- middleware
|
||||||
|
testImports: []
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package: github.com/dimiro1/ipe
|
||||||
|
import:
|
||||||
|
- package: github.com/golang/glog
|
||||||
|
- package: github.com/gorilla/websocket
|
||||||
|
- package: github.com/pressly/chi
|
||||||
|
excludeDirs:
|
||||||
|
- functional
|
||||||
@@ -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
|
||||||
+19
-5
@@ -27,17 +27,31 @@ type app struct {
|
|||||||
WebHooks bool
|
WebHooks bool
|
||||||
URLWebHook string
|
URLWebHook string
|
||||||
|
|
||||||
Channels map[string]*channel `json:"-"`
|
Channels map[string]*channel
|
||||||
Connections map[string]*connection `json:"-"`
|
Connections map[string]*connection
|
||||||
|
|
||||||
Stats *expvar.Map `json:"-"`
|
Stats *expvar.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alloc memory for Connections and Channels
|
func newApp(name, appID, key, secret string, onlySSL, disabled, userEvents, webHooks bool, webHookURL string) *app {
|
||||||
func (a *app) Init() {
|
|
||||||
|
a := &app{
|
||||||
|
Name: name,
|
||||||
|
AppID: appID,
|
||||||
|
Key: key,
|
||||||
|
Secret: secret,
|
||||||
|
OnlySSL: onlySSL,
|
||||||
|
ApplicationDisabled: disabled,
|
||||||
|
UserEvents: userEvents,
|
||||||
|
WebHooks: webHooks,
|
||||||
|
URLWebHook: webHookURL,
|
||||||
|
}
|
||||||
|
|
||||||
a.Connections = make(map[string]*connection)
|
a.Connections = make(map[string]*connection)
|
||||||
a.Channels = make(map[string]*channel)
|
a.Channels = make(map[string]*channel)
|
||||||
a.Stats = expvar.NewMap(fmt.Sprintf("%s (%s)", a.Name, a.AppID))
|
a.Stats = expvar.NewMap(fmt.Sprintf("%s (%s)", a.Name, a.AppID))
|
||||||
|
|
||||||
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only Presence channels
|
// Only Presence channels
|
||||||
|
|||||||
+51
-52
@@ -11,172 +11,171 @@ import (
|
|||||||
|
|
||||||
var id = 0
|
var id = 0
|
||||||
|
|
||||||
func newApp() *app {
|
func newTestApp() *app {
|
||||||
|
|
||||||
a := app{Name: "Test", AppID: strconv.Itoa(id), Key: "123", Secret: "123", OnlySSL: false, ApplicationDisabled: false, UserEvents: true}
|
|
||||||
a.Init()
|
|
||||||
|
|
||||||
|
a := newApp("Test", strconv.Itoa(id), "123", "123", false, false, true, false, "")
|
||||||
id++
|
id++
|
||||||
return &a
|
|
||||||
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConnect(t *testing.T) {
|
func TestConnect(t *testing.T) {
|
||||||
app := newApp()
|
app := newTestApp()
|
||||||
|
|
||||||
app.Connect(newConnection("socketID", nil))
|
app.Connect(newConnection("socketID", mockSocket{}))
|
||||||
|
|
||||||
if len(app.Connections) != 1 {
|
if len(app.Connections) != 1 {
|
||||||
t.Errorf("Connections must be 1, but was %d", len(app.Connections))
|
t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDisconnect(t *testing.T) {
|
func TestDisconnect(t *testing.T) {
|
||||||
app := newApp()
|
app := newTestApp()
|
||||||
|
|
||||||
app.Connect(newConnection("socketID", nil))
|
app.Connect(newConnection("socketID", mockSocket{}))
|
||||||
app.Disconnect("socketID")
|
app.Disconnect("socketID")
|
||||||
|
|
||||||
if len(app.Connections) != 0 {
|
if len(app.Connections) != 0 {
|
||||||
t.Errorf("Connections must be 0, but was %d", len(app.Connections))
|
t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindConnection(t *testing.T) {
|
func TestFindConnection(t *testing.T) {
|
||||||
app := newApp()
|
app := newTestApp()
|
||||||
|
|
||||||
app.Connect(newConnection("socketID", nil))
|
app.Connect(newConnection("socketID", mockSocket{}))
|
||||||
|
|
||||||
if _, err := app.FindConnection("socketID"); err != nil {
|
if _, err := app.FindConnection("socketID"); err != nil {
|
||||||
t.Error("Must find Connection")
|
t.Errorf("app.FindConnection('socketID') == _, %q, wants %v", err, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := app.FindConnection("NotFound"); err == nil {
|
if _, err := app.FindConnection("NotFound"); err == nil {
|
||||||
t.Error("Must not found Connection")
|
t.Errorf("app.FindConnection('socketID') == _, %q, wants !nil", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindChannelByChannelID(t *testing.T) {
|
func TestFindChannelByChannelID(t *testing.T) {
|
||||||
app := newApp()
|
app := newTestApp()
|
||||||
|
|
||||||
channel := newChannel("ID")
|
channel := newChannel("ID")
|
||||||
app.AddChannel(channel)
|
app.AddChannel(channel)
|
||||||
|
|
||||||
if _, err := app.FindChannelByChannelID("ID"); err != nil {
|
if _, err := app.FindChannelByChannelID("ID"); err != nil {
|
||||||
t.Error("Channel not found")
|
t.Errorf("app.FindChannelByChannelID('ID') == _, %q, wants %v", err, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindOrCreateChannelByChannelID(t *testing.T) {
|
func TestFindOrCreateChannelByChannelID(t *testing.T) {
|
||||||
app := newApp()
|
app := newTestApp()
|
||||||
|
|
||||||
if len(app.Channels) != 0 {
|
if len(app.Channels) != 0 {
|
||||||
t.Error("Length of channels must be 0 before test")
|
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.FindOrCreateChannelByChannelID("ID")
|
app.FindOrCreateChannelByChannelID("ID")
|
||||||
|
|
||||||
if len(app.Channels) != 1 {
|
if len(app.Channels) != 1 {
|
||||||
t.Error("Length of channels must be 1 after test")
|
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRemoveChannel(t *testing.T) {
|
func TestRemoveChannel(t *testing.T) {
|
||||||
app := newApp()
|
app := newTestApp()
|
||||||
|
|
||||||
if len(app.Channels) != 0 {
|
if len(app.Channels) != 0 {
|
||||||
t.Error("Length of channels must be 0 before test")
|
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
channel := newChannel("ID")
|
channel := newChannel("ID")
|
||||||
app.AddChannel(channel)
|
app.AddChannel(channel)
|
||||||
|
|
||||||
if len(app.Channels) != 1 {
|
if len(app.Channels) != 1 {
|
||||||
t.Error("Length of channels after insert must be 1")
|
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.RemoveChannel(channel)
|
app.RemoveChannel(channel)
|
||||||
|
|
||||||
if len(app.Channels) != 0 {
|
if len(app.Channels) != 0 {
|
||||||
t.Error("Length of channels must be 0 after remove")
|
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_add_channels(t *testing.T) {
|
func Test_add_channels(t *testing.T) {
|
||||||
|
|
||||||
app := newApp()
|
app := newTestApp()
|
||||||
|
|
||||||
// Public
|
// Public
|
||||||
|
|
||||||
if len(app.PublicChannels()) != 0 {
|
if len(app.PublicChannels()) != 0 {
|
||||||
t.Error("Length of public channels must be 0 before test")
|
t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.AddChannel(newChannel("ID"))
|
app.AddChannel(newChannel("ID"))
|
||||||
|
|
||||||
if len(app.PublicChannels()) != 1 {
|
if len(app.PublicChannels()) != 1 {
|
||||||
t.Error("Length os public channels after insert must be 1")
|
t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Presence
|
// Presence
|
||||||
|
|
||||||
if len(app.PresenceChannels()) != 0 {
|
if len(app.PresenceChannels()) != 0 {
|
||||||
t.Error("Length of presence channels must be 0 before test")
|
t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.AddChannel(newChannel("presence-test"))
|
app.AddChannel(newChannel("presence-test"))
|
||||||
|
|
||||||
if len(app.PresenceChannels()) != 1 {
|
if len(app.PresenceChannels()) != 1 {
|
||||||
t.Error("Length os presence channels after insert must be 1")
|
t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private
|
// Private
|
||||||
|
|
||||||
if len(app.PrivateChannels()) != 0 {
|
if len(app.PrivateChannels()) != 0 {
|
||||||
t.Error("Length of private channels must be 0 before test")
|
t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.AddChannel(newChannel("private-test"))
|
app.AddChannel(newChannel("private-test"))
|
||||||
|
|
||||||
if len(app.PrivateChannels()) != 1 {
|
if len(app.PrivateChannels()) != 1 {
|
||||||
t.Error("Length os private channels after insert must be 1")
|
t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_AllChannels(t *testing.T) {
|
func Test_AllChannels(t *testing.T) {
|
||||||
app := newApp()
|
app := newTestApp()
|
||||||
app.AddChannel(newChannel("private-test"))
|
app.AddChannel(newChannel("private-test"))
|
||||||
app.AddChannel(newChannel("presence-test"))
|
app.AddChannel(newChannel("presence-test"))
|
||||||
app.AddChannel(newChannel("test"))
|
app.AddChannel(newChannel("test"))
|
||||||
|
|
||||||
if len(app.Channels) != 3 {
|
if len(app.Channels) != 3 {
|
||||||
t.Error("Must have 3 channels")
|
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_New_Subscriber(t *testing.T) {
|
func Test_New_Subscriber(t *testing.T) {
|
||||||
app := newApp()
|
app := newTestApp()
|
||||||
|
|
||||||
if len(app.Connections) != 0 {
|
if len(app.Connections) != 0 {
|
||||||
t.Error("Length of subscribers before test must be 0")
|
t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
conn := newConnection("1", nil)
|
conn := newConnection("1", mockSocket{})
|
||||||
app.Connect(conn)
|
app.Connect(conn)
|
||||||
|
|
||||||
if len(app.Connections) != 1 {
|
if len(app.Connections) != 1 {
|
||||||
t.Error("Length os subscribers after test must be 1")
|
t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_find_subscriber(t *testing.T) {
|
func Test_find_subscriber(t *testing.T) {
|
||||||
app := newApp()
|
app := newTestApp()
|
||||||
conn := newConnection("1", nil)
|
conn := newConnection("1", mockSocket{})
|
||||||
app.Connect(conn)
|
app.Connect(conn)
|
||||||
|
|
||||||
conn, err := app.FindConnection("1")
|
conn, err := app.FindConnection("1")
|
||||||
@@ -186,7 +185,7 @@ func Test_find_subscriber(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if conn.SocketID != "1" {
|
if conn.SocketID != "1" {
|
||||||
t.Error("Wrong subscriber.")
|
t.Errorf("conn.SocketID == %s, wants %s", conn.SocketID, "1")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find a wrong subscriber
|
// Find a wrong subscriber
|
||||||
@@ -194,60 +193,60 @@ func Test_find_subscriber(t *testing.T) {
|
|||||||
conn, err = app.FindConnection("DoesNotExists")
|
conn, err = app.FindConnection("DoesNotExists")
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("Opps, Must be nil")
|
t.Errorf("err == %q, wants !nil", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if conn != nil {
|
if conn != nil {
|
||||||
t.Error("Opps, Must be nil")
|
t.Errorf("conn == %q, wants nil", conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_find_or_create_channels(t *testing.T) {
|
func Test_find_or_create_channels(t *testing.T) {
|
||||||
app := newApp()
|
app := newTestApp()
|
||||||
|
|
||||||
// Public
|
// Public
|
||||||
if len(app.PublicChannels()) != 0 {
|
if len(app.PublicChannels()) != 0 {
|
||||||
t.Error("Length of public channels must be 0 before test")
|
t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
c := app.FindOrCreateChannelByChannelID("id")
|
c := app.FindOrCreateChannelByChannelID("id")
|
||||||
|
|
||||||
if len(app.PublicChannels()) != 1 {
|
if len(app.PublicChannels()) != 1 {
|
||||||
t.Error("Length os public channels after insert must be 1")
|
t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.ChannelID != "id" {
|
if c.ChannelID != "id" {
|
||||||
t.Error("Opps wrong channel")
|
t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "id")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Presence
|
// Presence
|
||||||
if len(app.PresenceChannels()) != 0 {
|
if len(app.PresenceChannels()) != 0 {
|
||||||
t.Error("Length of presence channels must be 0 before test")
|
t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
c = app.FindOrCreateChannelByChannelID("presence-test")
|
c = app.FindOrCreateChannelByChannelID("presence-test")
|
||||||
|
|
||||||
if len(app.PresenceChannels()) != 1 {
|
if len(app.PresenceChannels()) != 1 {
|
||||||
t.Error("Length os presence channels after insert must be 1")
|
t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.ChannelID != "presence-test" {
|
if c.ChannelID != "presence-test" {
|
||||||
t.Error("Opps wrong channel")
|
t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "presence-test")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private
|
// Private
|
||||||
if len(app.PrivateChannels()) != 0 {
|
if len(app.PrivateChannels()) != 0 {
|
||||||
t.Error("Length of private channels must be 0 before test")
|
t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
c = app.FindOrCreateChannelByChannelID("private-test")
|
c = app.FindOrCreateChannelByChannelID("private-test")
|
||||||
|
|
||||||
if len(app.PrivateChannels()) != 1 {
|
if len(app.PrivateChannels()) != 1 {
|
||||||
t.Error("Length os private channels after insert must be 1")
|
t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.ChannelID != "private-test" {
|
if c.ChannelID != "private-test" {
|
||||||
t.Error("Opps wrong channel")
|
t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "private-test")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
-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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
+47
-47
@@ -7,10 +7,10 @@ package ipe
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dimiro1/ipe/utils"
|
||||||
log "github.com/golang/glog"
|
log "github.com/golang/glog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,12 +40,12 @@ func (c *channel) IsPublic() bool {
|
|||||||
|
|
||||||
// Check if the type of the channel is presence
|
// Check if the type of the channel is presence
|
||||||
func (c *channel) IsPresence() bool {
|
func (c *channel) IsPresence() bool {
|
||||||
return strings.HasPrefix(c.ChannelID, "presence-")
|
return utils.IsPresenceChannel(c.ChannelID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the type of the channel is private
|
// Check if the type of the channel is private
|
||||||
func (c *channel) IsPrivate() bool {
|
func (c *channel) IsPrivate() bool {
|
||||||
return strings.HasPrefix(c.ChannelID, "private-")
|
return utils.IsPrivateChannel(c.ChannelID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the total of subscribers
|
// Get the total of subscribers
|
||||||
@@ -74,51 +74,51 @@ func (c *channel) Subscribe(a *app, conn *connection, channelData string) error
|
|||||||
subscription := newSubscription(conn, channelData)
|
subscription := newSubscription(conn, channelData)
|
||||||
c.Subscriptions[conn.SocketID] = subscription
|
c.Subscriptions[conn.SocketID] = subscription
|
||||||
|
|
||||||
if c.IsPresence() {
|
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, "{}"))
|
conn.Publish(newSubscriptionSucceededEvent(c.ChannelID, "{}"))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
// 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)))
|
||||||
|
|
||||||
// WebHook
|
// WebHook
|
||||||
if c.TotalSubscriptions() == 1 {
|
if c.TotalSubscriptions() == 1 {
|
||||||
@@ -214,7 +214,7 @@ func (c *channel) Publish(a *app, event rawEvent, ignore string) error {
|
|||||||
subs.Connection.Publish(newResponseEvent(event.Event, event.Channel, v))
|
subs.Connection.Publish(newResponseEvent(event.Event, event.Channel, v))
|
||||||
} else {
|
} else {
|
||||||
// Webhook
|
// Webhook
|
||||||
if strings.HasPrefix(event.Event, "client-") {
|
if utils.IsClientEvent(event.Event) {
|
||||||
a.TriggerClientEventHook(c, subs, event.Event, v)
|
a.TriggerClientEventHook(c, subs, event.Event, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-16
@@ -10,13 +10,13 @@ func TestIsOccupied(t *testing.T) {
|
|||||||
c := newChannel("ID")
|
c := newChannel("ID")
|
||||||
|
|
||||||
if c.IsOccupied() {
|
if c.IsOccupied() {
|
||||||
t.Error("Channels must be empty")
|
t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Subscriptions["ID"] = newSubscription(newConnection("ID", nil), "")
|
c.Subscriptions["ID"] = newSubscription(newConnection("ID", mockSocket{}), "")
|
||||||
|
|
||||||
if !c.IsOccupied() {
|
if !c.IsOccupied() {
|
||||||
t.Error("Channels must be empty")
|
t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ func TestIsPrivate(t *testing.T) {
|
|||||||
c := newChannel("private-channel")
|
c := newChannel("private-channel")
|
||||||
|
|
||||||
if !c.IsPrivate() {
|
if !c.IsPrivate() {
|
||||||
t.Error("The Channel must be private")
|
t.Errorf("c.IsPrivate() == %t, wants %t", c.IsPrivate(), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ func TestIsPresence(t *testing.T) {
|
|||||||
c := newChannel("presence-channel")
|
c := newChannel("presence-channel")
|
||||||
|
|
||||||
if !c.IsPresence() {
|
if !c.IsPresence() {
|
||||||
t.Error("The Channel must be presence")
|
t.Errorf("c.IsPresence() == %t, wants %t", c.IsPresence(), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ func TestIsPublic(t *testing.T) {
|
|||||||
c := newChannel("channel")
|
c := newChannel("channel")
|
||||||
|
|
||||||
if !c.IsPublic() {
|
if !c.IsPublic() {
|
||||||
t.Error("The Channel must be public")
|
t.Errorf("c.IsPublic() == %t, wants %t", c.IsPublic(), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,13 +48,13 @@ func TestIsPrivateOrPresence(t *testing.T) {
|
|||||||
c := newChannel("private-channel")
|
c := newChannel("private-channel")
|
||||||
|
|
||||||
if !c.IsPresenceOrPrivate() {
|
if !c.IsPresenceOrPrivate() {
|
||||||
t.Error("The Channel must be private or presence")
|
t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
c = newChannel("presence-channel")
|
c = newChannel("presence-channel")
|
||||||
|
|
||||||
if !c.IsPresenceOrPrivate() {
|
if !c.IsPresenceOrPrivate() {
|
||||||
t.Error("The Channel must be private or presence")
|
t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,37 +62,37 @@ func TestTotalSubscriptions(t *testing.T) {
|
|||||||
c := newChannel("ID")
|
c := newChannel("ID")
|
||||||
|
|
||||||
if c.TotalSubscriptions() != len(c.Subscriptions) {
|
if c.TotalSubscriptions() != len(c.Subscriptions) {
|
||||||
t.Error("TotalSubscriptions must be equal to len of total subscriptions")
|
t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.Subscriptions))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTotalUsers(t *testing.T) {
|
func TestTotalUsers(t *testing.T) {
|
||||||
c := newChannel("ID")
|
c := newChannel("ID")
|
||||||
|
|
||||||
c.Subscriptions["1"] = newSubscription(newConnection("ID", nil), "")
|
c.Subscriptions["1"] = newSubscription(newConnection("ID", mockSocket{}), "")
|
||||||
c.Subscriptions["2"] = newSubscription(newConnection("ID", nil), "")
|
c.Subscriptions["2"] = newSubscription(newConnection("ID", mockSocket{}), "")
|
||||||
|
|
||||||
if c.TotalSubscriptions() != len(c.Subscriptions) {
|
if c.TotalSubscriptions() != len(c.Subscriptions) {
|
||||||
t.Error("TotalSubscriptions must be equal to len of total subscriptions")
|
t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.Subscriptions))
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.TotalUsers() != 1 {
|
if c.TotalUsers() != 1 {
|
||||||
t.Error("TotalUsers must be equal to 1")
|
t.Errorf("c.TotalUsers() == %d, wants %d", c.TotalUsers(), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsSubscribed(t *testing.T) {
|
func TestIsSubscribed(t *testing.T) {
|
||||||
c := newChannel("ID")
|
c := newChannel("ID")
|
||||||
conn := newConnection("ID", nil)
|
conn := newConnection("ID", mockSocket{})
|
||||||
|
|
||||||
if c.IsSubscribed(conn) {
|
if c.IsSubscribed(conn) {
|
||||||
t.Error("Must not be subscribed")
|
t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Subscriptions["ID"] = newSubscription(conn, "")
|
c.Subscriptions["ID"] = newSubscription(conn, "")
|
||||||
|
|
||||||
if !c.IsSubscribed(conn) {
|
if !c.IsSubscribed(conn) {
|
||||||
t.Error("Must be subscribed")
|
t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-23
@@ -1,25 +1,21 @@
|
|||||||
{
|
{
|
||||||
"Host": ":8080",
|
"Host": ":8080",
|
||||||
"Apps": [
|
"SSL": false,
|
||||||
{
|
"Profiling": false,
|
||||||
"ApplicationDisabled": false,
|
"SSLHost": ":4433",
|
||||||
"Secret": "7ad3753142a6693b25b9",
|
"SSLKeyFile": "A key.pem file",
|
||||||
"Key": "278d525bdf162c739803",
|
"SSLCertFile": "A cert.pem file",
|
||||||
"Name": "App 1",
|
"Apps": [
|
||||||
"AppID": "321",
|
{
|
||||||
"UserEvents": true,
|
"ApplicationDisabled": false,
|
||||||
"WebHooks": true,
|
"Secret": "A really secret random string",
|
||||||
"URLWebHook": "http://127.0.0.1:4567/php/hook.php"
|
"Key": "A random Key string",
|
||||||
},
|
"OnlySSL": false,
|
||||||
{
|
"Name": "The app name",
|
||||||
"ApplicationDisabled": false,
|
"AppID": "The app ID",
|
||||||
"Secret": "d6824d2fa32888931504",
|
"UserEvents": true,
|
||||||
"Key": "c8b30f611ffb13202976",
|
"WebHooks": true,
|
||||||
"Name": "App 2",
|
"URLWebHook": "Some URL to send webhooks"
|
||||||
"AppID": "123",
|
}
|
||||||
"UserEvents": true,
|
]
|
||||||
"WebHooks": false,
|
|
||||||
"URLWebHook": "http://127.0.0.1:4567/php/hook.php"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-37
@@ -1,49 +1,44 @@
|
|||||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
// Copyright 2014, 2016 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.
|
||||||
|
|
||||||
package ipe
|
package ipe
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The config file
|
// The config file
|
||||||
type configFile struct {
|
type configFile struct {
|
||||||
Host string // The host, eg: :8080 will start on 0.0.0.0:8080
|
Host string // The host, eg: :8080 will start on 0.0.0.0:8080
|
||||||
User string
|
User string
|
||||||
Password string
|
SSL bool
|
||||||
Apps []*app
|
Profiling bool
|
||||||
|
SSLHost string
|
||||||
|
SSLKeyFile string
|
||||||
|
SSLCertFile string
|
||||||
|
|
||||||
|
Apps []configApp
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Apps
|
type configApp struct {
|
||||||
func (c *configFile) Init() {
|
Name string
|
||||||
for _, app := range c.Apps {
|
AppID string
|
||||||
app.Init()
|
Key string
|
||||||
}
|
Secret string
|
||||||
|
OnlySSL bool
|
||||||
|
ApplicationDisabled bool
|
||||||
|
UserEvents bool
|
||||||
|
WebHooks bool
|
||||||
|
URLWebHook string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *configFile) WasProvidedUserAndPassword() bool {
|
func newAppFromConfig(a configApp) *app {
|
||||||
return len(strings.TrimSpace(c.User)) > 0 && len(strings.TrimSpace(c.Password)) > 0
|
return newApp(
|
||||||
}
|
a.Name,
|
||||||
|
a.AppID,
|
||||||
// Returns an App with by appID
|
a.Key,
|
||||||
func (c *configFile) GetAppByAppID(appID string) (*app, error) {
|
a.Secret,
|
||||||
for _, a := range c.Apps {
|
a.OnlySSL,
|
||||||
if a.AppID == appID {
|
a.ApplicationDisabled,
|
||||||
return a, nil
|
a.UserEvents,
|
||||||
}
|
a.WebHooks,
|
||||||
}
|
a.URLWebHook,
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-8
@@ -8,18 +8,22 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/golang/glog"
|
log "github.com/golang/glog"
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// socket interface to write to the client
|
||||||
|
type socket interface {
|
||||||
|
WriteJSON(interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
// An User Connection
|
// An User Connection
|
||||||
type connection struct {
|
type connection struct {
|
||||||
SocketID string
|
SocketID string
|
||||||
Socket *websocket.Conn
|
Socket socket
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new Subscriber
|
// Create a new Subscriber
|
||||||
func newConnection(socketID string, s *websocket.Conn) *connection {
|
func newConnection(socketID string, s socket) *connection {
|
||||||
log.Infof("Creating a new Subscriber %+v", socketID)
|
log.Infof("Creating a new Subscriber %+v", socketID)
|
||||||
|
|
||||||
return &connection{SocketID: socketID, Socket: s, CreatedAt: time.Now()}
|
return &connection{SocketID: socketID, Socket: s, CreatedAt: time.Now()}
|
||||||
@@ -27,9 +31,5 @@ func newConnection(socketID string, s *websocket.Conn) *connection {
|
|||||||
|
|
||||||
// Publish the message to websocket atached to this client
|
// Publish the message to websocket atached to this client
|
||||||
func (conn *connection) Publish(m interface{}) {
|
func (conn *connection) Publish(m interface{}) {
|
||||||
go func() {
|
conn.Socket.WriteJSON(m)
|
||||||
if err := conn.Socket.WriteJSON(m); err != nil {
|
|
||||||
log.Errorf("Error publishing message to connection %+v, %s", conn, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-8
@@ -4,27 +4,31 @@
|
|||||||
|
|
||||||
package ipe
|
package ipe
|
||||||
|
|
||||||
import (
|
import "testing"
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
// mockSocket is a mock implementation of socket
|
||||||
)
|
// used in the test suite
|
||||||
|
type mockSocket struct{}
|
||||||
|
|
||||||
|
func (s mockSocket) WriteJSON(i interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewConnection(t *testing.T) {
|
func TestNewConnection(t *testing.T) {
|
||||||
expectedSocketID := "socketID"
|
expectedSocketID := "socketID"
|
||||||
expectedSocket := &websocket.Conn{}
|
expectedSocket := mockSocket{}
|
||||||
|
|
||||||
c := newConnection(expectedSocketID, expectedSocket)
|
c := newConnection(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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-37
@@ -4,44 +4,9 @@
|
|||||||
|
|
||||||
package ipe
|
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
|
// Only this version is supported
|
||||||
const SUPPORTED_PROTOCOL_VERSION = 7
|
const supportedProtocolVersion = 7
|
||||||
|
|
||||||
// // Maximun event size permitted 10 kB
|
// // Maximun event size permitted 10 kB
|
||||||
// See: http://blogs.gnome.org/cneumair/2008/09/30/1-kb-1024-bytes-no-1-kb-1000-bytes/
|
// 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
|
const maxDataEventSize = 10 * 1000
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package ipe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// db represents a app database
|
||||||
|
// For now it there is only one memory database implementation
|
||||||
|
// but in the future I can write a sql implementation
|
||||||
|
type db interface {
|
||||||
|
GetAppByAppID(appID string) (*app, error)
|
||||||
|
GetAppByKey(key string) (*app, error)
|
||||||
|
AddApp(*app) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type memdb struct {
|
||||||
|
sync.Mutex
|
||||||
|
Apps []*app
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMemdb() db {
|
||||||
|
return &memdb{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *memdb) AddApp(a *app) error {
|
||||||
|
db.Lock()
|
||||||
|
db.Apps = append(db.Apps, a)
|
||||||
|
db.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppByAppID returns an App with by appID
|
||||||
|
func (db *memdb) GetAppByAppID(appID string) (*app, error) {
|
||||||
|
for _, a := range db.Apps {
|
||||||
|
if a.AppID == appID {
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("App not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppByKey returns an App with by key
|
||||||
|
func (db *memdb) GetAppByKey(key string) (*app, error) {
|
||||||
|
for _, a := range db.Apps {
|
||||||
|
if a.Key == key {
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("App not found")
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package ipe
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func Benchmark_memdb_GetAppByAppID(b *testing.B) {
|
||||||
|
db := newMemdb()
|
||||||
|
db.AddApp(&app{AppID: "123456", Name: "Example"})
|
||||||
|
db.AddApp(&app{AppID: "654321", Name: "Example2"})
|
||||||
|
db.AddApp(&app{AppID: "678901", Name: "Example3"})
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
db.GetAppByAppID("123456")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_db_GetAppByAppID(t *testing.T) {
|
||||||
|
app := &app{AppID: "123456", Name: "Example"}
|
||||||
|
|
||||||
|
db := newMemdb()
|
||||||
|
db.AddApp(app)
|
||||||
|
|
||||||
|
a, err := db.GetAppByAppID("123456")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetAppByAppID(%q) == %q, want %q", "123456", a, app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_db_GetAppByAppID__error(t *testing.T) {
|
||||||
|
app := &app{AppID: "123456", Name: "Example"}
|
||||||
|
|
||||||
|
db := newMemdb()
|
||||||
|
db.AddApp(app)
|
||||||
|
|
||||||
|
a, err := db.GetAppByAppID("not-found")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("GetAppByAppID(%q) == %q, want %q", "123456", a, app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_db_GetAppByKey(t *testing.T) {
|
||||||
|
app := &app{AppID: "123456", Name: "Example", Key: "654321"}
|
||||||
|
|
||||||
|
db := newMemdb()
|
||||||
|
db.AddApp(app)
|
||||||
|
|
||||||
|
a, err := db.GetAppByKey("654321")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetAppByKey(%q) == %q, want %q", "654321", a, app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_db_GetAppByKey__error(t *testing.T) {
|
||||||
|
app := &app{AppID: "123456", Name: "Example", Key: "654321"}
|
||||||
|
|
||||||
|
db := newMemdb()
|
||||||
|
db.AddApp(app)
|
||||||
|
|
||||||
|
a, err := db.GetAppByKey("not-found")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("GetAppByKey(%q) == %q, want %v", "not-found", a, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
-88
@@ -4,115 +4,61 @@
|
|||||||
|
|
||||||
package ipe
|
package ipe
|
||||||
|
|
||||||
// Base interface
|
import "fmt"
|
||||||
type websocketError interface {
|
|
||||||
GetCode() int
|
|
||||||
GetMsg() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base struct
|
// Base struct
|
||||||
type baseWebsocketError struct {
|
type websocketError struct {
|
||||||
Code int
|
Code *int
|
||||||
Msg string
|
Msg string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e baseWebsocketError) GetCode() int {
|
func (e websocketError) GetCode() *int {
|
||||||
return e.Code
|
return e.Code
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e baseWebsocketError) GetMsg() string {
|
func (e websocketError) GetMsg() string {
|
||||||
return e.Msg
|
return e.Msg
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unsupprted protocol version
|
func (e websocketError) Error() string {
|
||||||
type unsupportedProtocolVersionError struct {
|
return fmt.Sprintf("%d: %s", e.Code, e.Msg)
|
||||||
baseWebsocketError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUnsupportedProtocolVersionError() unsupportedProtocolVersionError {
|
func newWebsocketError(code int, msg string) websocketError {
|
||||||
return unsupportedProtocolVersionError{
|
return websocketError{Code: &code, Msg: msg}
|
||||||
baseWebsocketError{Code: UNSUPPORTED_PROTOCOL_VERSION, Msg: "Unsupported protocol version"},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The application does not exists
|
var (
|
||||||
// See the configuration file
|
// Unsupprted protocol version
|
||||||
type applicationDoesNotExistsError struct {
|
unsupportedProtocolVersionError = newWebsocketError(4007, "Unsupported protocol version")
|
||||||
baseWebsocketError
|
|
||||||
}
|
|
||||||
|
|
||||||
func newApplicationDoesNotExistsError() applicationDoesNotExistsError {
|
// The application does not exists
|
||||||
return applicationDoesNotExistsError{
|
// See the configuration file
|
||||||
baseWebsocketError{Code: APPLICATION_DOES_NOT_EXISTS, Msg: "Could not found an app with the given key"},
|
applicationDoesNotExistsError = newWebsocketError(4001, "Could not found an app with the given key")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The user did not send the protocol version
|
// The user did not send the protocol version
|
||||||
type noProtocolVersionSuppliedError struct {
|
noProtocolVersionSuppliedError = newWebsocketError(4008, "No protocol version supplied")
|
||||||
baseWebsocketError
|
|
||||||
}
|
|
||||||
|
|
||||||
func newNoProtocolVersionSuppliedError() noProtocolVersionSuppliedError {
|
// When the application is disabled.
|
||||||
return noProtocolVersionSuppliedError{
|
// See the configuration file
|
||||||
baseWebsocketError{Code: NO_PROTOCOL_VERSION_SUPPLIED, Msg: "No protocol version supplied"},
|
applicationDisabledError = newWebsocketError(4003, "Application disabled")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the application is disabled.
|
// When the application only accepts SSL connections
|
||||||
// See the configuration file
|
applicationOnlyAccepsSSLError = newWebsocketError(4000, "Application only accepts SSL connections, reconnect using wss://")
|
||||||
type applicationDisabledError struct {
|
|
||||||
baseWebsocketError
|
|
||||||
}
|
|
||||||
|
|
||||||
func newApplicationDisabledError() noProtocolVersionSuppliedError {
|
// When the user send an invalid version
|
||||||
return noProtocolVersionSuppliedError{
|
invalidVersionStringFormatError = newWebsocketError(4006, "Invalid version string format")
|
||||||
baseWebsocketError{Code: APPLICATION_DISABLED, Msg: "Application disabled"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the application only accepts SSL connections
|
// Used when the error was internal
|
||||||
type applicationOnlyAccepsSSLError struct {
|
// * Decoding json
|
||||||
baseWebsocketError
|
// * Writing to output
|
||||||
}
|
genericReconnectImmediatelyError = newWebsocketError(4200, "Generic reconnect immediately")
|
||||||
|
|
||||||
func newApplicationOnlyAccepsSSLError() applicationOnlyAccepsSSLError {
|
// When pusher wants to send an Generic error, it only send the message, the code become nil
|
||||||
return applicationOnlyAccepsSSLError{
|
// Currently I do not know how to send nil, so I send GENERIC_ERROR
|
||||||
baseWebsocketError{Code: APPLICATION_ONLY_ACCEPTS_SSL, Msg: "Application only accepts SSL connections, reconnect using wss://"},
|
genericError = newWebsocketError(0, "Generic Error")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the user send an invalid version
|
disabledClientEventsError = websocketError{Msg: "To send client events, you must enable this feature in the Settings."}
|
||||||
type invalidVersionStringFormatError struct {
|
|
||||||
baseWebsocketError
|
|
||||||
}
|
|
||||||
|
|
||||||
func newInvalidVersionStringFormatError() invalidVersionStringFormatError {
|
couldNotFoundChannelError = websocketError{Msg: "Could not find a channel with the given id"}
|
||||||
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},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+8
-20
@@ -160,29 +160,17 @@ type errorEvent struct {
|
|||||||
// Create a new error event
|
// Create a new error event
|
||||||
// Pusher protocol is very strange in some parts
|
// Pusher protocol is very strange in some parts
|
||||||
// It send null in some errors.
|
// 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 {
|
||||||
func newErrorEvent(code int, message string) errorEvent {
|
|
||||||
var data interface{}
|
|
||||||
|
|
||||||
if code == GENERIC_ERROR {
|
type dataErrorEvent struct {
|
||||||
data = struct {
|
Code *int `json:"code"`
|
||||||
Code *int `json:"code"`
|
Message string `json:"message"`
|
||||||
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}
|
return errorEvent{Event: "pusher:error", Data: dataErrorEvent{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package ipe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_newErrorEvent_with_invalid_code(t *testing.T) {
|
||||||
|
event := newErrorEvent(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 := newErrorEvent(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,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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
+122
-28
@@ -8,13 +8,106 @@ 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/pressly/chi"
|
||||||
|
|
||||||
|
"github.com/dimiro1/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.
|
// 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 authenticationHandler(DB db) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
appID := chi.URLParam(r, "app_id")
|
||||||
|
|
||||||
|
app, err := DB.GetAppByAppID(appID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := r.URL.Query()
|
||||||
|
|
||||||
|
signature := query.Get("auth_signature")
|
||||||
|
query.Del("auth_signature")
|
||||||
|
|
||||||
|
queryString := prepareQueryString(query)
|
||||||
|
|
||||||
|
toSign := strings.ToUpper(r.Method) + "\n" + r.URL.Path + "\n" + queryString
|
||||||
|
|
||||||
|
if utils.HashMAC([]byte(toSign), []byte(app.Secret)) == signature {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
} else {
|
||||||
|
log.Error("Not authorized")
|
||||||
|
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the application is disabled
|
||||||
|
func checkAppDisabled(DB db) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
appID := chi.URLParam(r, "app_id")
|
||||||
|
|
||||||
|
currentApp, err := DB.GetAppByAppID(appID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentApp.ApplicationDisabled {
|
||||||
|
http.Error(w, "Application disabled", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type postEventsHandler struct{ DB db }
|
||||||
|
|
||||||
|
// ServeHTTPC An event consists of a name and data (typically JSON) which may be sent to all subscribers to a particular channel or channels.
|
||||||
// This is conventionally known as triggering an event.
|
// 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 +122,10 @@ 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 *postEventsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
appID := chi.URLParam(r, "app_id")
|
||||||
appID := vars["app_id"]
|
|
||||||
|
|
||||||
app, err := conf.GetAppByAppID(appID)
|
app, err := h.DB.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 +147,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
|
||||||
}
|
}
|
||||||
@@ -76,6 +168,8 @@ func postEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte("{}"))
|
w.Write([]byte("{}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type getChannelsHandler struct{ DB db }
|
||||||
|
|
||||||
// Allows fetching a hash of occupied channels (optionally filtered by prefix),
|
// 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.
|
||||||
//
|
//
|
||||||
@@ -95,13 +189,12 @@ 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 *getChannelsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
params := r.URL.Query()
|
query := r.URL.Query()
|
||||||
vars := mux.Vars(r)
|
|
||||||
|
|
||||||
appID := vars["app_id"]
|
appID := chi.URLParam(r, "app_id")
|
||||||
filter := params.Get("filter_by_prefix")
|
filter := query.Get("filter_by_prefix")
|
||||||
info := params.Get("info")
|
info := query.Get("info")
|
||||||
|
|
||||||
attributes := strings.Split(info, ",")
|
attributes := strings.Split(info, ",")
|
||||||
|
|
||||||
@@ -120,7 +213,7 @@ func getChannels(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app, err := conf.GetAppByAppID(appID)
|
app, err := h.DB.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)
|
||||||
@@ -166,6 +259,8 @@ func getChannels(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type getChannelHandler struct{ DB db }
|
||||||
|
|
||||||
// Fetch info for one channel
|
// Fetch info for one channel
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
@@ -176,20 +271,19 @@ 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 *getChannelHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||||
|
|
||||||
params := r.URL.Query()
|
query := r.URL.Query()
|
||||||
vars := mux.Vars(r)
|
|
||||||
|
|
||||||
appID := vars["app_id"]
|
appID := chi.URLParam(r, "app_id")
|
||||||
app, err := conf.GetAppByAppID(appID)
|
app, err := h.DB.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"]
|
channelName := chi.URLParam(r, "channel_name")
|
||||||
|
|
||||||
// Channel name could not be empty
|
// Channel name could not be empty
|
||||||
if strings.TrimSpace(channelName) == "" {
|
if strings.TrimSpace(channelName) == "" {
|
||||||
@@ -197,7 +291,7 @@ func getChannel(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
info := params.Get("info")
|
info := query.Get("info")
|
||||||
attributes := strings.Split(info, ",")
|
attributes := strings.Split(info, ",")
|
||||||
|
|
||||||
// Attributes requested
|
// Attributes requested
|
||||||
@@ -255,6 +349,8 @@ func getChannel(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type getChannelUsersHandler struct{ DB db }
|
||||||
|
|
||||||
// Allowed only for presence-channels
|
// Allowed only for presence-channels
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
@@ -266,20 +362,18 @@ 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 *getChannelUsersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
appID := chi.URLParam(r, "app_id")
|
||||||
|
channelName := chi.URLParam(r, "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.DB.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)
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
package ipe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pressly/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testApp *app
|
||||||
|
database db
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
testApp = newTestApp()
|
||||||
|
|
||||||
|
channel := newChannel("presence-c1")
|
||||||
|
testApp.AddChannel(channel)
|
||||||
|
testApp.AddChannel(newChannel("c2"))
|
||||||
|
testApp.AddChannel(newChannel("private-c3"))
|
||||||
|
|
||||||
|
conn := newConnection("123.456", mockSocket{})
|
||||||
|
testApp.Subscribe(channel, conn, "{}")
|
||||||
|
|
||||||
|
conn = newConnection("321.654", mockSocket{})
|
||||||
|
testApp.Subscribe(channel, conn, "{}")
|
||||||
|
|
||||||
|
db := newMemdb()
|
||||||
|
db.AddApp(testApp)
|
||||||
|
|
||||||
|
database = db
|
||||||
|
}
|
||||||
|
|
||||||
|
// All Channels
|
||||||
|
func Test_getChannels_all(t *testing.T) {
|
||||||
|
appID := testApp.AppID
|
||||||
|
|
||||||
|
rctx := chi.NewRouteContext()
|
||||||
|
rctx.URLParams.Add("app_id", appID)
|
||||||
|
|
||||||
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels", appID), nil)
|
||||||
|
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := &getChannelsHandler{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
|
||||||
|
|
||||||
|
rctx := chi.NewRouteContext()
|
||||||
|
rctx.URLParams.Add("app_id", appID)
|
||||||
|
|
||||||
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-", appID), nil)
|
||||||
|
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := &getChannelsHandler{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
|
||||||
|
|
||||||
|
rctx := chi.NewRouteContext()
|
||||||
|
rctx.URLParams.Add("app_id", appID)
|
||||||
|
|
||||||
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-&info=user_count", appID), nil)
|
||||||
|
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := &getChannelsHandler{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 alowed in Presence channels
|
||||||
|
func Test_getChannels_filter_by_private_prefix_and_info_user_count(t *testing.T) {
|
||||||
|
appID := testApp.AppID
|
||||||
|
|
||||||
|
rctx := chi.NewRouteContext()
|
||||||
|
rctx.URLParams.Add("app_id", appID)
|
||||||
|
|
||||||
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-&info=user_count", appID), nil)
|
||||||
|
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := &getChannelsHandler{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
|
||||||
|
|
||||||
|
rctx := chi.NewRouteContext()
|
||||||
|
rctx.URLParams.Add("app_id", appID)
|
||||||
|
|
||||||
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=public-", appID), nil)
|
||||||
|
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := &getChannelsHandler{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
|
||||||
|
|
||||||
|
rctx := chi.NewRouteContext()
|
||||||
|
rctx.URLParams.Add("app_id", appID)
|
||||||
|
|
||||||
|
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-", appID), nil)
|
||||||
|
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := &getChannelsHandler{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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
-15
@@ -6,34 +6,70 @@ package ipe
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
"github.com/pressly/chi"
|
||||||
|
"github.com/pressly/chi/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Conf holds the global configuration state
|
|
||||||
var conf configFile
|
|
||||||
|
|
||||||
// Start Parse the configuration file and starts the ipe server
|
// Start Parse the configuration file and starts the ipe server
|
||||||
func Start(configfile string) error {
|
// It Panic if could not start the HTTP or HTTPS server
|
||||||
|
func Start(filename string) {
|
||||||
|
var conf configFile
|
||||||
|
|
||||||
rand.Seed(time.Now().Unix())
|
rand.Seed(time.Now().Unix())
|
||||||
file, err := ioutil.ReadFile(configfile)
|
file, err := os.Open(filename)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
log.Error(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(file, &conf); err != nil {
|
defer file.Close()
|
||||||
return err
|
|
||||||
|
// Reading config
|
||||||
|
if err := json.NewDecoder(file).Decode(&conf); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conf.Init()
|
// Using a in memory database
|
||||||
router := newRouter()
|
db := newMemdb()
|
||||||
|
|
||||||
if err := http.ListenAndServe(conf.Host, router); err != nil {
|
// Adding applications
|
||||||
return err
|
for _, a := range conf.Apps {
|
||||||
|
db.AddApp(newAppFromConfig(a))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
r := chi.NewRouter()
|
||||||
|
r.Use(middleware.Recoverer)
|
||||||
|
|
||||||
|
r.Get("/app/:key", (&websocketHandler{db}).ServeHTTP)
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Use(checkAppDisabled(db))
|
||||||
|
r.Use(authenticationHandler(db))
|
||||||
|
|
||||||
|
r.Post("/apps/:app_id/events", (&postEventsHandler{db}).ServeHTTP)
|
||||||
|
r.Get("/apps/:app_id/channels", (&getChannelsHandler{db}).ServeHTTP)
|
||||||
|
r.Get("/apps/:app_id/channels/:channel_name", (&getChannelHandler{db}).ServeHTTP)
|
||||||
|
r.Get("/apps/:app_id/channels/:channel_name/users", (&getChannelUsersHandler{db}).ServeHTTP)
|
||||||
|
})
|
||||||
|
|
||||||
|
if conf.Profiling {
|
||||||
|
r.Mount("/debug", middleware.Profiler())
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.SSL {
|
||||||
|
go func() {
|
||||||
|
log.Infof("Starting HTTPS service on %s ...", conf.SSLHost)
|
||||||
|
log.Fatal(http.ListenAndServeTLS(conf.SSLHost, conf.SSLCertFile, conf.SSLKeyFile, r))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Starting HTTP service on %s ...", conf.Host)
|
||||||
|
log.Fatal(http.ListenAndServe(conf.Host, r))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
+50
-10
@@ -10,10 +10,15 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/dimiro1/ipe/utils"
|
"github.com/dimiro1/ipe/utils"
|
||||||
log "github.com/golang/glog"
|
log "github.com/golang/glog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const maxTimeout = 3 * time.Second
|
||||||
|
|
||||||
// A WebHook is sent as a HTTP POST request to the url which you specify.
|
// 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:
|
// The POST request payload (body) contains a JSON document, and follows the following format:
|
||||||
// {
|
// {
|
||||||
@@ -71,14 +76,19 @@ func newClientHook(channel *channel, s *subscription, event string, data interfa
|
|||||||
// { "name": "channel_occupied", "channel": "test_channel" }
|
// { "name": "channel_occupied", "channel": "test_channel" }
|
||||||
func (a *app) TriggerChannelOccupiedHook(c *channel) {
|
func (a *app) TriggerChannelOccupiedHook(c *channel) {
|
||||||
event := newChannelOcuppiedHook(c)
|
event := newChannelOcuppiedHook(c)
|
||||||
triggerHook(event.Name, a, c, event)
|
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
triggerHook(ctx, a, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
// channel_vacated
|
// channel_vacated
|
||||||
// { "name": "channel_vacated", "channel": "test_channel" }
|
// { "name": "channel_vacated", "channel": "test_channel" }
|
||||||
func (a *app) TriggerChannelVacatedHook(c *channel) {
|
func (a *app) TriggerChannelVacatedHook(c *channel) {
|
||||||
event := newChannelVacatedHook(c)
|
event := newChannelVacatedHook(c)
|
||||||
triggerHook(event.Name, a, c, event)
|
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||||
|
defer cancel()
|
||||||
|
triggerHook(ctx, a, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
// {
|
// {
|
||||||
@@ -96,7 +106,9 @@ func (a *app) TriggerClientEventHook(c *channel, s *subscription, clientEvent st
|
|||||||
event.UserID = s.ID
|
event.UserID = s.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerHook(event.Name, a, c, event)
|
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||||
|
defer cancel()
|
||||||
|
triggerHook(ctx, a, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
// {
|
// {
|
||||||
@@ -106,7 +118,9 @@ func (a *app) TriggerClientEventHook(c *channel, s *subscription, clientEvent st
|
|||||||
// }
|
// }
|
||||||
func (a *app) TriggerMemberAddedHook(c *channel, s *subscription) {
|
func (a *app) TriggerMemberAddedHook(c *channel, s *subscription) {
|
||||||
event := newMemberAddedHook(c, s)
|
event := newMemberAddedHook(c, s)
|
||||||
triggerHook(event.Name, a, c, event)
|
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||||
|
defer cancel()
|
||||||
|
triggerHook(ctx, a, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
// {
|
// {
|
||||||
@@ -116,17 +130,22 @@ func (a *app) TriggerMemberAddedHook(c *channel, s *subscription) {
|
|||||||
// }
|
// }
|
||||||
func (a *app) TriggerMemberRemovedHook(c *channel, s *subscription) {
|
func (a *app) TriggerMemberRemovedHook(c *channel, s *subscription) {
|
||||||
event := newMemberRemovedHook(c, s)
|
event := newMemberRemovedHook(c, s)
|
||||||
triggerHook(event.Name, a, c, event)
|
ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
|
||||||
|
defer cancel()
|
||||||
|
triggerHook(ctx, a, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
func triggerHook(name string, a *app, c *channel, event hookEvent) {
|
func triggerHook(ctx context.Context, a *app, event hookEvent) error {
|
||||||
if !a.WebHooks {
|
if !a.WebHooks {
|
||||||
log.Infof("Webhooks are not enabled for app: %s", a.Name)
|
log.Infof("Webhooks are not enabled for app: %s", a.Name)
|
||||||
return
|
return fmt.Errorf("Webhooks are not enabled for app: %s", a.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var done chan bool
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
log.Infof("Triggering %s event", name)
|
log.Infof("Triggering %s event", event.Name)
|
||||||
|
|
||||||
hook := webHook{TimeMs: time.Now().Unix()}
|
hook := webHook{TimeMs: time.Now().Unix()}
|
||||||
|
|
||||||
@@ -145,11 +164,15 @@ func triggerHook(name string, a *app, c *channel, event hookEvent) {
|
|||||||
var req *http.Request
|
var req *http.Request
|
||||||
|
|
||||||
req, err = http.NewRequest("POST", a.URLWebHook, bytes.NewReader(js))
|
req, err = http.NewRequest("POST", a.URLWebHook, bytes.NewReader(js))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Error creating request: %+v", err)
|
log.Errorf("Error creating request: %+v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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("Content-Type", "application/json")
|
||||||
req.Header.Set("X-Pusher-Key", a.Key)
|
req.Header.Set("X-Pusher-Key", a.Key)
|
||||||
req.Header.Set("X-Pusher-Signature", utils.HashMAC(js, []byte(a.Secret)))
|
req.Header.Set("X-Pusher-Signature", utils.HashMAC(js, []byte(a.Secret)))
|
||||||
@@ -157,8 +180,25 @@ func triggerHook(name string, a *app, c *channel, event hookEvent) {
|
|||||||
log.V(1).Infof("%+v", req.Header)
|
log.V(1).Infof("%+v", req.Header)
|
||||||
log.V(1).Infof("%+v", string(js))
|
log.V(1).Infof("%+v", string(js))
|
||||||
|
|
||||||
if _, err := http.DefaultClient.Do(req); err != nil {
|
resp, err := http.DefaultClient.Do(req)
|
||||||
log.Errorf("Error posting %s event: %+v", name, err)
|
|
||||||
|
// See: http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#close_http_resp_body
|
||||||
|
if resp != nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error posting %s event: %+v", event.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Successfully terminated
|
||||||
|
done <- true
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-done:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package ipe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/pressly/chi"
|
||||||
|
|
||||||
|
"github.com/dimiro1/ipe/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var upgrader = websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
CheckOrigin: func(_ *http.Request) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessages(conn *websocket.Conn, sessionID string, app *app) {
|
||||||
|
var event struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, message, err := conn.ReadMessage()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
handleError(conn, sessionID, app, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(message, &event); err != nil {
|
||||||
|
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("websockets: Handling %s event", event.Event)
|
||||||
|
|
||||||
|
switch event.Event {
|
||||||
|
case "pusher:ping":
|
||||||
|
onPing(conn)
|
||||||
|
case "pusher:subscribe":
|
||||||
|
onSubscribe(conn, sessionID, app, message)
|
||||||
|
case "pusher:unsubscribe":
|
||||||
|
onUnsubscribe(conn, sessionID, app, message)
|
||||||
|
default:
|
||||||
|
if utils.IsClientEvent(event.Event) {
|
||||||
|
onClientEvent(conn, sessionID, app, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError(conn *websocket.Conn, sessionID string, app *app, err error) {
|
||||||
|
log.Errorf("%+v", err)
|
||||||
|
if err == io.EOF {
|
||||||
|
onClose(sessionID, app)
|
||||||
|
} else if _, ok := err.(*websocket.CloseError); ok {
|
||||||
|
onClose(sessionID, app)
|
||||||
|
} else {
|
||||||
|
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func onOpen(conn *websocket.Conn, r *http.Request, sessionID string, app *app) error {
|
||||||
|
params := r.URL.Query()
|
||||||
|
p := params.Get("protocol")
|
||||||
|
|
||||||
|
protocol, err := strconv.Atoi(p)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return newInvalidVersionStringFormatError()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.TrimSpace(p) == "":
|
||||||
|
return newNoProtocolVersionSuppliedError()
|
||||||
|
case protocol != supportedProtocolVersion:
|
||||||
|
return unsupportedProtocolVersionError
|
||||||
|
case app.ApplicationDisabled:
|
||||||
|
return newApplicationDisabledError()
|
||||||
|
case app.OnlySSL:
|
||||||
|
if r.TLS == nil {
|
||||||
|
return newApplicationOnlyAccepsSSLError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the new Subscriber
|
||||||
|
connection := newConnection(sessionID, conn)
|
||||||
|
app.Connect(connection)
|
||||||
|
|
||||||
|
// Everything went fine. Huhu.
|
||||||
|
if err := conn.WriteJSON(newConnectionEstablishedEvent(connection.SocketID)); err != nil {
|
||||||
|
return newGenericReconnectImmediatelyError()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func onClose(sessionID string, app *app) {
|
||||||
|
app.Disconnect(sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func onPing(conn *websocket.Conn) {
|
||||||
|
if err := conn.WriteJSON(newPongEvent()); err != nil {
|
||||||
|
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func onClientEvent(conn *websocket.Conn, sessionID string, app *app, message []byte) {
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Publish(channel, clientEvent, sessionID); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func onUnsubscribe(conn *websocket.Conn, sessionID string, app *app, message []byte) {
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func onSubscribe(conn *websocket.Conn, sessionID string, app *app, message []byte) {
|
||||||
|
subscribeEvent := subscribeEvent{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(message, &subscribeEvent); err != nil {
|
||||||
|
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
connection, err := app.FindConnection(sessionID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channelName := strings.TrimSpace(subscribeEvent.Data.Channel)
|
||||||
|
|
||||||
|
if !utils.IsChannelNameValid(channelName) {
|
||||||
|
emitWSError(newGenericError("This channel name is not valid"), conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isPresence := utils.IsPresenceChannel(channelName)
|
||||||
|
isPrivate := utils.IsPrivateChannel(channelName)
|
||||||
|
|
||||||
|
if isPresence || isPrivate {
|
||||||
|
toSign := []string{connection.SocketID, channelName}
|
||||||
|
|
||||||
|
if isPresence || len(subscribeEvent.Data.ChannelData) > 0 {
|
||||||
|
toSign = append(toSign, subscribeEvent.Data.ChannelData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validateAuthKey(subscribeEvent.Data.Auth, toSign, app) {
|
||||||
|
emitWSError(newGenericError(fmt.Sprintf("Auth value for subscription to %s is invalid", channelName)), conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channel := app.FindOrCreateChannelByChannelID(channelName)
|
||||||
|
log.Info(subscribeEvent.Data.ChannelData)
|
||||||
|
|
||||||
|
if err := app.Subscribe(channel, connection, subscribeEvent.Data.ChannelData); err != nil {
|
||||||
|
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAuthKey(givenAuthKey string, toSign []string, app *app) bool {
|
||||||
|
expectedAuthKey := fmt.Sprintf("%s:%s", app.Key, utils.HashMAC([]byte(strings.Join(toSign, ":")), []byte(app.Secret)))
|
||||||
|
return givenAuthKey == expectedAuthKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit an Websocket ErrorEvent
|
||||||
|
func emitWSError(err error, conn *websocket.Conn) {
|
||||||
|
e, ok := err.(websocketError)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event := newErrorEvent(e.GetCode(), e.GetMsg())
|
||||||
|
|
||||||
|
if err := conn.WriteJSON(event); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type websocketHandler struct {
|
||||||
|
DB db
|
||||||
|
}
|
||||||
|
|
||||||
|
// Websocket GET /app/{key}
|
||||||
|
func (h *websocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
defer func() {
|
||||||
|
if conn != nil {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appKey := chi.URLParam(r, "key")
|
||||||
|
|
||||||
|
app, err := h.DB.GetAppByKey(appKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
emitWSError(newApplicationDoesNotExistsError(), conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionID := utils.GenerateSessionID()
|
||||||
|
|
||||||
|
if err := onOpen(conn, r, sessionID, app); err != nil {
|
||||||
|
emitWSError(err, conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessages(conn, sessionID, app)
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,32 +9,44 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/dimiro1/ipe/ipe"
|
"github.com/dimiro1/ipe/ipe"
|
||||||
log "github.com/golang/glog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main function, initialze the system
|
// 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() {
|
func main() {
|
||||||
var filename = flag.String("config", "config.json", "Config file location")
|
var filename = flag.String("config", "config.json", "Config file location")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
printBanner()
|
printBanner()
|
||||||
|
|
||||||
if err := ipe.Start(*filename); err != nil {
|
ipe.Start(*filename)
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print a beautifull banner
|
// Print a beautiful banner
|
||||||
func printBanner() {
|
func printBanner() {
|
||||||
fmt.Print("\033[36m")
|
fmt.Print("\033[31m")
|
||||||
fmt.Print(`
|
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[0m")
|
||||||
fmt.Println("\033[32mWelcome to Ipê - Yet another Pusher server clone\033[0m")
|
fmt.Println("\033[32mWelcome to Ipê - Yet another Pusher server clone (https://github.com/dimiro1/ipe)\033[0m")
|
||||||
|
fmt.Printf("\033[32mVersion %s+%s.git.%s\033[0m\n", version, buildstamp, githash)
|
||||||
fmt.Println("\033[33mBy: Claudemiro Alves Feitosa Neto <[email protected]>\033[0m")
|
fmt.Println("\033[33mBy: Claudemiro Alves Feitosa Neto <[email protected]>\033[0m")
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-8
@@ -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.MaxInt64), rand.Intn(math.MaxInt64))
|
||||||
|
|
||||||
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 {
|
// IsPrivateChannel Verify if the channel name represents a private channel
|
||||||
return true
|
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 [email protected].
|
|
||||||
-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 <[email protected]>
|
|
||||||
Joachim Bauch <[email protected]>
|
|
||||||
|
|
||||||
-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://[email protected]/", 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 <[email protected]>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to
|
|
||||||
// deal in the Software without restriction, including without limitation the
|
|
||||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
||||||
// sell copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
||||||
// IN THE SOFTWARE.
|
|
||||||
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
|
|
||||||
}
|
|
||||||
-33
@@ -1,33 +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 (
|
|
||||||
"net/http"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var subprotocolTests = []struct {
|
|
||||||
h string
|
|
||||||
protocols []string
|
|
||||||
}{
|
|
||||||
{"", nil},
|
|
||||||
{"foo", []string{"foo"}},
|
|
||||||
{"foo,bar", []string{"foo", "bar"}},
|
|
||||||
{"foo, bar", []string{"foo", "bar"}},
|
|
||||||
{" foo, bar", []string{"foo", "bar"}},
|
|
||||||
{" foo, bar ", []string{"foo", "bar"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubprotocols(t *testing.T) {
|
|
||||||
for _, st := range subprotocolTests {
|
|
||||||
r := http.Request{Header: http.Header{"Sec-Websocket-Protocol": {st.h}}}
|
|
||||||
protocols := Subprotocols(&r)
|
|
||||||
if !reflect.DeepEqual(st.protocols, protocols) {
|
|
||||||
t.Errorf("SubProtocols(%q) returned %#v, want %#v", st.h, protocols, st.protocols)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-44
@@ -1,44 +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/rand"
|
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/base64"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// tokenListContainsValue returns true if the 1#token header with the given
|
|
||||||
// name contains token.
|
|
||||||
func tokenListContainsValue(header http.Header, name string, value string) bool {
|
|
||||||
for _, v := range header[name] {
|
|
||||||
for _, s := range strings.Split(v, ",") {
|
|
||||||
if strings.EqualFold(value, strings.TrimSpace(s)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
|
|
||||||
|
|
||||||
func computeAcceptKey(challengeKey string) string {
|
|
||||||
h := sha1.New()
|
|
||||||
h.Write([]byte(challengeKey))
|
|
||||||
h.Write(keyGUID)
|
|
||||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateChallengeKey() (string, error) {
|
|
||||||
p := make([]byte, 16)
|
|
||||||
if _, err := io.ReadFull(rand.Reader, p); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return base64.StdEncoding.EncodeToString(p), nil
|
|
||||||
}
|
|
||||||
-34
@@ -1,34 +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/http"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var tokenListContainsValueTests = []struct {
|
|
||||||
value string
|
|
||||||
ok bool
|
|
||||||
}{
|
|
||||||
{"WebSocket", true},
|
|
||||||
{"WEBSOCKET", true},
|
|
||||||
{"websocket", true},
|
|
||||||
{"websockets", false},
|
|
||||||
{"x websocket", false},
|
|
||||||
{"websocket x", false},
|
|
||||||
{"other,websocket,more", true},
|
|
||||||
{"other, websocket, more", true},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTokenListContainsValue(t *testing.T) {
|
|
||||||
for _, tt := range tokenListContainsValueTests {
|
|
||||||
h := http.Header{"Upgrade": {tt.value}}
|
|
||||||
ok := tokenListContainsValue(h, "Upgrade", "websocket")
|
|
||||||
if ok != tt.ok {
|
|
||||||
t.Errorf("tokenListContainsValue(h, n, %q) = %v, want %v", tt.value, ok, tt.ok)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user