Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
+4
-1
@@ -155,4 +155,7 @@ flymake*
|
||||
|
||||
ignore_http/*
|
||||
config.json
|
||||
|
||||
*.pem
|
||||
build
|
||||
.vscode/*
|
||||
debug
|
||||
@@ -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
|
||||
@@ -25,6 +25,12 @@ This software is written in Go - the WYSIWYG lang
|
||||
* Multiple apps in the same instance;
|
||||
* Drop in replacement for pusher server;
|
||||
|
||||
# Download pre built binaries
|
||||
|
||||
You can download pre built binaries from the [releases tab](https://github.com/dimiro1/ipe/releases).
|
||||
|
||||
I do not have a Windows machine, so I can only distribute binaries for amd64 linux and amd64 darwin.
|
||||
|
||||
# Building
|
||||
|
||||
```console
|
||||
@@ -41,22 +47,28 @@ $ go install github.com/dimiro1/ipe
|
||||
|
||||
## The server
|
||||
|
||||
```json
|
||||
```javascript
|
||||
{
|
||||
"Host": ":8080",
|
||||
"Apps": [
|
||||
"Host": ":8080", // Required
|
||||
"SSL": false, // Required but can be false
|
||||
"SSLHost": ":4433", // Required if SSL is true
|
||||
"SSLKeyFile": "A key.pem file", // Required if SSL is true
|
||||
"SSLCertFile": "A cert.pem file", // Required if SSL is true
|
||||
"Apps": [ // Required, A Json arrays with multiple apps
|
||||
{
|
||||
"ApplicationDisabled": false,
|
||||
"Secret": "APP_SECRET",
|
||||
"Key": "APP_KEY",
|
||||
"Name": "APP_NAME",
|
||||
"AppID": "APP_ID",
|
||||
"UserEvents": true,
|
||||
"WebHooks": true,
|
||||
"URLWebHook": "http://localhost:4567/php/hook.php"
|
||||
"ApplicationDisabled": false, // Required but can be false
|
||||
"Secret": "A really secret random string", // Required
|
||||
"Key": "A random Key string", // Required
|
||||
"OnlySSL": false, // Required but can be false
|
||||
"Name": "The app name", // Required
|
||||
"AppID": "The app ID", // Required
|
||||
"UserEvents": true, // Required but can be false
|
||||
"WebHooks": true, // Required but can be false
|
||||
"URLWebHook": "Some URL to send webhooks" // Required if WebHooks is true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Libraries
|
||||
@@ -67,6 +79,8 @@ $ go install github.com/dimiro1/ipe
|
||||
var pusher = new Pusher(APP_KEY, {
|
||||
wsHost: 'localhost',
|
||||
wsPort: 8080,
|
||||
wssPort: 4433, // Required if encrypted is true
|
||||
encrypted: false, // Optional. the application must use only SSL connections
|
||||
enabledTransports: ["ws", "flash"],
|
||||
disabledTransports: ["flash"]
|
||||
});
|
||||
|
||||
@@ -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.1.0'
|
||||
GITHASH = `git rev-parse --short HEAD`
|
||||
DATE = Time.now.strftime '%Y%m%d%H%M%S'
|
||||
|
||||
CLOBBER.include 'build'
|
||||
|
||||
task :default => [:'run-debug']
|
||||
|
||||
desc 'Build a debug version'
|
||||
task :debug do
|
||||
sh "GO15VENDOREXPERIMENT=1 go install -ldflags '-w -X main.version=DEBUG -X main.buildstamp=DEBUG -X main.githash=DEBUG' github.com/dimiro1/ipe"
|
||||
end
|
||||
|
||||
desc 'Build and run debug version'
|
||||
task :'run-debug' => :debug do
|
||||
sh '$GOPATH/bin/ipe --config $GOPATH/src/github.com/dimiro1/ipe/config.json -logtostderr=true -v=2'
|
||||
end
|
||||
|
||||
desc 'Run test suite'
|
||||
task :test do
|
||||
sh 'GO15VENDOREXPERIMENT=1 go test . `glide nv`'
|
||||
end
|
||||
|
||||
desc 'Download the development dependencies'
|
||||
task :'dev-deps' do
|
||||
sh 'go get github.com/pusher/pusher-http-go'
|
||||
end
|
||||
|
||||
desc 'Generate distributions'
|
||||
task :distribute => [:linux, :darwin]
|
||||
|
||||
desc 'Generate a linux distribution'
|
||||
task :linux do
|
||||
Rake::Task['build'].invoke 'linux'
|
||||
end
|
||||
|
||||
desc 'Generate a darwin distribution'
|
||||
task :darwin do
|
||||
Rake::Task['build'].invoke 'darwin'
|
||||
end
|
||||
|
||||
task :build, [:os] do |t, args|
|
||||
t.reenable
|
||||
os = args[:os]
|
||||
|
||||
sh "mkdir -p build/#{os}"
|
||||
sh "GO15VENDOREXPERIMENT=1 GOOS=#{os} GOARCH=amd64 go build -ldflags '-X main.version=#{VERSION} -X main.buildstamp=#{DATE} -X main.githash=#{GITHASH}' -o build/#{os}/ipe github.com/dimiro1/ipe"
|
||||
sh "cp ipe/config-example.json build/#{os}/config.json"
|
||||
sh "cp LICENSE build/#{os}/"
|
||||
sh "cp README.md build/#{os}/"
|
||||
sh "tar -C build/#{os} -czf build/ipe_#{VERSION}_#{os}_amd64.tar.gz ."
|
||||
end
|
||||
@@ -1,12 +1,12 @@
|
||||
IPÊ
|
||||
---
|
||||
|
||||
* TODO [11/14]
|
||||
* TODO [12/14]
|
||||
* [X] Autenticação API Rest
|
||||
* [X] Autenticação Websockets
|
||||
* [X] Ping e Pong
|
||||
* [ ] Escrever testes automatizados
|
||||
* [ ] SSL
|
||||
* [X] SSL
|
||||
* [X] Expvar - Canais, inscritos
|
||||
* [X] Otimizações [3/3]
|
||||
* [X] Refatorar partes do código, remover repetições
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
client: go run client.go
|
||||
server: go run ../main.go -config ./config.json -logtostderr
|
||||
server: go run ../main.go -config ./functional-config.json -alsologtostderr
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"Host": ":8080",
|
||||
"Encrypted": false,
|
||||
"SSLHost": ":8090",
|
||||
"SSLKeyFile": "key.pem",
|
||||
"SSLCertFile": "cert.pem",
|
||||
"Apps": [
|
||||
{
|
||||
"ApplicationDisabled": false,
|
||||
"OnlySSL": false,
|
||||
"Secret": "7ad3753142a6693b25b9",
|
||||
"Key": "278d525bdf162c739803",
|
||||
"Name": "App for Functional Test",
|
||||
"AppID": "1",
|
||||
"UserEvents": true,
|
||||
"WebHooks": false,
|
||||
"URLWebHook": "http://127.0.0.1:4567/php/hook.php"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -17,7 +17,8 @@ function getPusher(auth) {
|
||||
wsPort: PORT,
|
||||
authEndpoint: auth,
|
||||
enabledTransports: ["ws"],
|
||||
disabledTransports: ["flash"]
|
||||
disabledTransports: ["flash"],
|
||||
cluster: "hello", // Should be ignored
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
hash: bfb508bdf4f85c71c7eef4bb9431148ee373dfaf31d001d050d933f80ac4288e
|
||||
updated: 2016-02-21T20:13:57.03748112-03:00
|
||||
imports:
|
||||
- name: github.com/golang/glog
|
||||
version: 23def4e6c14b4da8ac2ed8007337bc5eb5007998
|
||||
- name: github.com/gorilla/context
|
||||
version: 1c83b3eabd45b6d76072b66b746c20815fb2872d
|
||||
- name: github.com/gorilla/mux
|
||||
version: 26a6070f849969ba72b72256e9f14cf519751690
|
||||
- name: github.com/gorilla/websocket
|
||||
version: 5c91b59efa232fa9a87b705d54101832c498a172
|
||||
- name: github.com/pusher/pusher-http-go
|
||||
version: 8d4ffe157699620440932e4d03253a22533f2e43
|
||||
devImports: []
|
||||
@@ -0,0 +1,6 @@
|
||||
package: github.com/dimiro1/ipe
|
||||
import:
|
||||
- package: github.com/golang/glog
|
||||
- package: github.com/gorilla/mux
|
||||
- package: github.com/gorilla/websocket
|
||||
- package: github.com/pusher/pusher-http-go
|
||||
@@ -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
|
||||
URLWebHook string
|
||||
|
||||
Channels map[string]*channel `json:"-"`
|
||||
Connections map[string]*connection `json:"-"`
|
||||
Channels map[string]*channel
|
||||
Connections map[string]*connection
|
||||
|
||||
Stats *expvar.Map `json:"-"`
|
||||
Stats *expvar.Map
|
||||
}
|
||||
|
||||
// Alloc memory for Connections and Channels
|
||||
func (a *app) Init() {
|
||||
func newApp(name, appID, key, secret string, onlySSL, disabled, userEvents, webHooks bool, webHookURL string) *app {
|
||||
|
||||
a := &app{
|
||||
Name: name,
|
||||
AppID: appID,
|
||||
Key: key,
|
||||
Secret: secret,
|
||||
OnlySSL: onlySSL,
|
||||
ApplicationDisabled: disabled,
|
||||
UserEvents: userEvents,
|
||||
WebHooks: webHooks,
|
||||
URLWebHook: webHookURL,
|
||||
}
|
||||
|
||||
a.Connections = make(map[string]*connection)
|
||||
a.Channels = make(map[string]*channel)
|
||||
a.Stats = expvar.NewMap(fmt.Sprintf("%s (%s)", a.Name, a.AppID))
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// Only Presence channels
|
||||
|
||||
+51
-52
@@ -11,172 +11,171 @@ import (
|
||||
|
||||
var id = 0
|
||||
|
||||
func newApp() *app {
|
||||
|
||||
a := app{Name: "Test", AppID: strconv.Itoa(id), Key: "123", Secret: "123", OnlySSL: false, ApplicationDisabled: false, UserEvents: true}
|
||||
a.Init()
|
||||
func newTestApp() *app {
|
||||
|
||||
a := newApp("Test", strconv.Itoa(id), "123", "123", false, false, true, false, "")
|
||||
id++
|
||||
return &a
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func TestConnect(t *testing.T) {
|
||||
app := newApp()
|
||||
app := newTestApp()
|
||||
|
||||
app.Connect(newConnection("socketID", nil))
|
||||
app.Connect(newConnection("socketID", mockSocket{}))
|
||||
|
||||
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) {
|
||||
app := newApp()
|
||||
app := newTestApp()
|
||||
|
||||
app.Connect(newConnection("socketID", nil))
|
||||
app.Connect(newConnection("socketID", mockSocket{}))
|
||||
app.Disconnect("socketID")
|
||||
|
||||
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) {
|
||||
app := newApp()
|
||||
app := newTestApp()
|
||||
|
||||
app.Connect(newConnection("socketID", nil))
|
||||
app.Connect(newConnection("socketID", mockSocket{}))
|
||||
|
||||
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 {
|
||||
t.Error("Must not found Connection")
|
||||
t.Errorf("app.FindConnection('socketID') == _, %q, wants !nil", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFindChannelByChannelID(t *testing.T) {
|
||||
app := newApp()
|
||||
app := newTestApp()
|
||||
|
||||
channel := newChannel("ID")
|
||||
app.AddChannel(channel)
|
||||
|
||||
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) {
|
||||
app := newApp()
|
||||
app := newTestApp()
|
||||
|
||||
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")
|
||||
|
||||
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) {
|
||||
app := newApp()
|
||||
app := newTestApp()
|
||||
|
||||
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")
|
||||
app.AddChannel(channel)
|
||||
|
||||
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)
|
||||
|
||||
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) {
|
||||
|
||||
app := newApp()
|
||||
app := newTestApp()
|
||||
|
||||
// Public
|
||||
|
||||
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"))
|
||||
|
||||
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
|
||||
|
||||
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"))
|
||||
|
||||
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
|
||||
|
||||
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"))
|
||||
|
||||
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) {
|
||||
app := newApp()
|
||||
app := newTestApp()
|
||||
app.AddChannel(newChannel("private-test"))
|
||||
app.AddChannel(newChannel("presence-test"))
|
||||
app.AddChannel(newChannel("test"))
|
||||
|
||||
if len(app.Channels) != 3 {
|
||||
t.Error("Must have 3 channels")
|
||||
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 3)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_New_Subscriber(t *testing.T) {
|
||||
app := newApp()
|
||||
app := newTestApp()
|
||||
|
||||
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)
|
||||
|
||||
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) {
|
||||
app := newApp()
|
||||
conn := newConnection("1", nil)
|
||||
app := newTestApp()
|
||||
conn := newConnection("1", mockSocket{})
|
||||
app.Connect(conn)
|
||||
|
||||
conn, err := app.FindConnection("1")
|
||||
@@ -186,7 +185,7 @@ func Test_find_subscriber(t *testing.T) {
|
||||
}
|
||||
|
||||
if conn.SocketID != "1" {
|
||||
t.Error("Wrong subscriber.")
|
||||
t.Errorf("conn.SocketID == %s, wants %s", conn.SocketID, "1")
|
||||
}
|
||||
|
||||
// Find a wrong subscriber
|
||||
@@ -194,60 +193,60 @@ func Test_find_subscriber(t *testing.T) {
|
||||
conn, err = app.FindConnection("DoesNotExists")
|
||||
|
||||
if err == nil {
|
||||
t.Error("Opps, Must be nil")
|
||||
t.Errorf("err == %q, wants !nil", err)
|
||||
}
|
||||
|
||||
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) {
|
||||
app := newApp()
|
||||
app := newTestApp()
|
||||
|
||||
// Public
|
||||
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")
|
||||
|
||||
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" {
|
||||
t.Error("Opps wrong channel")
|
||||
t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "id")
|
||||
}
|
||||
|
||||
// Presence
|
||||
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")
|
||||
|
||||
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" {
|
||||
t.Error("Opps wrong channel")
|
||||
t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "presence-test")
|
||||
}
|
||||
|
||||
// Private
|
||||
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")
|
||||
|
||||
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" {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
+3
-2
@@ -11,6 +11,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dimiro1/ipe/utils"
|
||||
log "github.com/golang/glog"
|
||||
)
|
||||
|
||||
@@ -40,12 +41,12 @@ func (c *channel) IsPublic() bool {
|
||||
|
||||
// Check if the type of the channel is presence
|
||||
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
|
||||
func (c *channel) IsPrivate() bool {
|
||||
return strings.HasPrefix(c.ChannelID, "private-")
|
||||
return utils.IsPrivateChannel(c.ChannelID)
|
||||
}
|
||||
|
||||
// Get the total of subscribers
|
||||
|
||||
+16
-16
@@ -10,13 +10,13 @@ func TestIsOccupied(t *testing.T) {
|
||||
c := newChannel("ID")
|
||||
|
||||
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() {
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
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) {
|
||||
c := newChannel("ID")
|
||||
|
||||
c.Subscriptions["1"] = newSubscription(newConnection("ID", nil), "")
|
||||
c.Subscriptions["2"] = newSubscription(newConnection("ID", nil), "")
|
||||
c.Subscriptions["1"] = newSubscription(newConnection("ID", mockSocket{}), "")
|
||||
c.Subscriptions["2"] = newSubscription(newConnection("ID", mockSocket{}), "")
|
||||
|
||||
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 {
|
||||
t.Error("TotalUsers must be equal to 1")
|
||||
t.Errorf("c.TotalUsers() == %d, wants %d", c.TotalUsers(), 1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIsSubscribed(t *testing.T) {
|
||||
c := newChannel("ID")
|
||||
conn := newConnection("ID", nil)
|
||||
conn := newConnection("ID", mockSocket{})
|
||||
|
||||
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, "")
|
||||
|
||||
if !c.IsSubscribed(conn) {
|
||||
t.Error("Must be subscribed")
|
||||
t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), true)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-15
@@ -1,25 +1,20 @@
|
||||
{
|
||||
"Host": ":8080",
|
||||
"SSL": false,
|
||||
"SSLHost": ":4433",
|
||||
"SSLKeyFile": "A key.pem file",
|
||||
"SSLCertFile": "A cert.pem file",
|
||||
"Apps": [
|
||||
{
|
||||
"ApplicationDisabled": false,
|
||||
"Secret": "7ad3753142a6693b25b9",
|
||||
"Key": "278d525bdf162c739803",
|
||||
"Name": "App 1",
|
||||
"AppID": "321",
|
||||
"Secret": "A really secret random string",
|
||||
"Key": "A random Key string",
|
||||
"OnlySSL": false,
|
||||
"Name": "The app name",
|
||||
"AppID": "The app ID",
|
||||
"UserEvents": true,
|
||||
"WebHooks": true,
|
||||
"URLWebHook": "http://127.0.0.1:4567/php/hook.php"
|
||||
},
|
||||
{
|
||||
"ApplicationDisabled": false,
|
||||
"Secret": "d6824d2fa32888931504",
|
||||
"Key": "c8b30f611ffb13202976",
|
||||
"Name": "App 2",
|
||||
"AppID": "123",
|
||||
"UserEvents": true,
|
||||
"WebHooks": false,
|
||||
"URLWebHook": "http://127.0.0.1:4567/php/hook.php"
|
||||
"URLWebHook": "Some URL to send webhooks"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+31
-37
@@ -1,49 +1,43 @@
|
||||
// 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
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The config file
|
||||
type configFile struct {
|
||||
Host string // The host, eg: :8080 will start on 0.0.0.0:8080
|
||||
User string
|
||||
Password string
|
||||
Apps []*app
|
||||
Host string // The host, eg: :8080 will start on 0.0.0.0:8080
|
||||
User string
|
||||
SSL bool
|
||||
SSLHost string
|
||||
SSLKeyFile string
|
||||
SSLCertFile string
|
||||
|
||||
Apps []configApp
|
||||
}
|
||||
|
||||
// Initialize Apps
|
||||
func (c *configFile) Init() {
|
||||
for _, app := range c.Apps {
|
||||
app.Init()
|
||||
}
|
||||
type configApp struct {
|
||||
Name string
|
||||
AppID string
|
||||
Key string
|
||||
Secret string
|
||||
OnlySSL bool
|
||||
ApplicationDisabled bool
|
||||
UserEvents bool
|
||||
WebHooks bool
|
||||
URLWebHook string
|
||||
}
|
||||
|
||||
func (c *configFile) WasProvidedUserAndPassword() bool {
|
||||
return len(strings.TrimSpace(c.User)) > 0 && len(strings.TrimSpace(c.Password)) > 0
|
||||
}
|
||||
|
||||
// Returns an App with by appID
|
||||
func (c *configFile) GetAppByAppID(appID string) (*app, error) {
|
||||
for _, a := range c.Apps {
|
||||
if a.AppID == appID {
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
return &app{}, errors.New("App not found")
|
||||
}
|
||||
|
||||
// Returns an App with by key
|
||||
func (c *configFile) GetAppByKey(key string) (*app, error) {
|
||||
for _, a := range c.Apps {
|
||||
if a.Key == key {
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
return &app{}, errors.New("App not found")
|
||||
func newAppFromConfig(a configApp) *app {
|
||||
return newApp(
|
||||
a.Name,
|
||||
a.AppID,
|
||||
a.Key,
|
||||
a.Secret,
|
||||
a.OnlySSL,
|
||||
a.ApplicationDisabled,
|
||||
a.UserEvents,
|
||||
a.WebHooks,
|
||||
a.URLWebHook,
|
||||
)
|
||||
}
|
||||
|
||||
+16
-8
@@ -8,18 +8,30 @@ import (
|
||||
"time"
|
||||
|
||||
log "github.com/golang/glog"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// socket interface to write to the client
|
||||
type socket interface {
|
||||
WriteJSON(interface{}) error
|
||||
}
|
||||
|
||||
// mockSocket is a mock implementation of socket
|
||||
// used in the test suite
|
||||
type mockSocket struct{}
|
||||
|
||||
func (s mockSocket) WriteJSON(i interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// An User Connection
|
||||
type connection struct {
|
||||
SocketID string
|
||||
Socket *websocket.Conn
|
||||
Socket socket
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
return &connection{SocketID: socketID, Socket: s, CreatedAt: time.Now()}
|
||||
@@ -27,9 +39,5 @@ func newConnection(socketID string, s *websocket.Conn) *connection {
|
||||
|
||||
// Publish the message to websocket atached to this client
|
||||
func (conn *connection) Publish(m interface{}) {
|
||||
go func() {
|
||||
if err := conn.Socket.WriteJSON(m); err != nil {
|
||||
log.Errorf("Error publishing message to connection %+v, %s", conn, err)
|
||||
}
|
||||
}()
|
||||
conn.Socket.WriteJSON(m)
|
||||
}
|
||||
|
||||
@@ -4,27 +4,23 @@
|
||||
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
import "testing"
|
||||
|
||||
func TestNewConnection(t *testing.T) {
|
||||
expectedSocketID := "socketID"
|
||||
expectedSocket := &websocket.Conn{}
|
||||
expectedSocket := mockSocket{}
|
||||
|
||||
c := newConnection(expectedSocketID, expectedSocket)
|
||||
|
||||
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 {
|
||||
t.Errorf("Expected: %+v but got %+v", expectedSocket, c.Socket)
|
||||
t.Errorf("c.Socket == %v, wants %v", c.Socket, expectedSocket)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
+16
-16
@@ -9,39 +9,39 @@ 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
|
||||
applicationOnlyAcceptsSSL = 4000
|
||||
applicationDoesNotExists = 4001
|
||||
applicationDisabled = 4003
|
||||
applicationIsOverConnectionQuota = 4004 // Not Implemented
|
||||
pathNotFound = 4005 // Not Implemented
|
||||
invalidVersionStringFormat = 4006
|
||||
unsupportedProtocolVersion = 4007
|
||||
noProtocolVersionSupplied = 4008
|
||||
|
||||
// 4100 - 4199
|
||||
// Indicates an error resulting in the connection being closed by Pusher,
|
||||
// and the client may reconnect after 1s or more
|
||||
OVER_CAPACITY = 4100 // Not Implemented
|
||||
overCapacity = 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
|
||||
genericReconnectImmediately = 4200
|
||||
pongReplyNotReceived = 4201 // Ping was sent to the client, but no reply was received; Not Implemented
|
||||
closedAfterInactivity = 4202 // Client has been inactive for a long time (24 hours) and client does not suppot ping.; Not Implemented
|
||||
|
||||
// 4300 - 4399
|
||||
// Any other type of error
|
||||
CLIENT_REJECTED_DUE_TO_RATE_LIMIT = 4301 // Not Implemented
|
||||
clientRejectedDueToRateLimit = 4301 // Not Implemented
|
||||
|
||||
// Pusher send null, This app use this error code to send the null value
|
||||
// see ErrorEvent
|
||||
GENERIC_ERROR = 0
|
||||
otherError = 0
|
||||
)
|
||||
|
||||
// Only this version is supported
|
||||
const SUPPORTED_PROTOCOL_VERSION = 7
|
||||
const supportedProtocolVersion = 7
|
||||
|
||||
// // Maximun event size permitted 10 kB
|
||||
// See: http://blogs.gnome.org/cneumair/2008/09/30/1-kb-1024-bytes-no-1-kb-1000-bytes/
|
||||
const MAX_DATA_EVENT_SIZE = 10 * 1000
|
||||
const maxDataEventSize = 10 * 1000
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
import "net/http"
|
||||
|
||||
type applicationContext struct {
|
||||
DB db
|
||||
}
|
||||
|
||||
// url params
|
||||
type params map[string]string
|
||||
|
||||
func (p params) Get(key string) string {
|
||||
return p[key]
|
||||
}
|
||||
|
||||
// A contextHandler responds to an HTTP request with custom application context.
|
||||
type contextHandler interface {
|
||||
ServeWithContext(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
type contextHandlerFunc func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request)
|
||||
|
||||
func (c contextHandlerFunc) ServeWithContext(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
|
||||
c(ctx, p, w, r)
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// db represents a app database
|
||||
// For now it there is only one memory database implementation
|
||||
// but in the future I can write a sql implementation
|
||||
type db interface {
|
||||
GetAppByAppID(appID string) (*app, error)
|
||||
GetAppByKey(key string) (*app, error)
|
||||
AddApp(*app) error
|
||||
}
|
||||
|
||||
// memdb is a in memory implementation of db interface
|
||||
type memdb struct {
|
||||
sync.Mutex
|
||||
Apps []*app
|
||||
}
|
||||
|
||||
func newMemdb() *memdb {
|
||||
return &memdb{}
|
||||
}
|
||||
|
||||
func (db *memdb) AddApp(a *app) error {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
|
||||
db.Apps = append(db.Apps, a)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAppByAppID returns an App with by appID
|
||||
func (db *memdb) GetAppByAppID(appID string) (*app, error) {
|
||||
for _, a := range db.Apps {
|
||||
if a.AppID == appID {
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("App not found")
|
||||
}
|
||||
|
||||
// GetAppByKey returns an App with by key
|
||||
func (db *memdb) GetAppByKey(key string) (*app, error) {
|
||||
for _, a := range db.Apps {
|
||||
if a.Key == key {
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("App not found")
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_db_GetAppByAppID(t *testing.T) {
|
||||
app := &app{AppID: "123456", Name: "Example"}
|
||||
|
||||
db := newMemdb()
|
||||
db.AddApp(app)
|
||||
|
||||
a, err := db.GetAppByAppID("123456")
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("GetAppByAppID(%q) == %q, want %q", "123456", a, app)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_db_GetAppByAppID__error(t *testing.T) {
|
||||
app := &app{AppID: "123456", Name: "Example"}
|
||||
|
||||
db := newMemdb()
|
||||
db.AddApp(app)
|
||||
|
||||
a, err := db.GetAppByAppID("not-found")
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("GetAppByAppID(%q) == %q, want %q", "123456", a, app)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_db_GetAppByKey(t *testing.T) {
|
||||
app := &app{AppID: "123456", Name: "Example", Key: "654321"}
|
||||
|
||||
db := newMemdb()
|
||||
db.AddApp(app)
|
||||
|
||||
a, err := db.GetAppByKey("654321")
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("GetAppByKey(%q) == %q, want %q", "654321", a, app)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_db_GetAppByKey__error(t *testing.T) {
|
||||
app := &app{AppID: "123456", Name: "Example", Key: "654321"}
|
||||
|
||||
db := newMemdb()
|
||||
db.AddApp(app)
|
||||
|
||||
a, err := db.GetAppByKey("not-found")
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("GetAppByKey(%q) == %q, want %v", "not-found", a, nil)
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -31,7 +31,7 @@ type unsupportedProtocolVersionError struct {
|
||||
|
||||
func newUnsupportedProtocolVersionError() unsupportedProtocolVersionError {
|
||||
return unsupportedProtocolVersionError{
|
||||
baseWebsocketError{Code: UNSUPPORTED_PROTOCOL_VERSION, Msg: "Unsupported protocol version"},
|
||||
baseWebsocketError{Code: unsupportedProtocolVersion, Msg: "Unsupported protocol version"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ type applicationDoesNotExistsError struct {
|
||||
|
||||
func newApplicationDoesNotExistsError() applicationDoesNotExistsError {
|
||||
return applicationDoesNotExistsError{
|
||||
baseWebsocketError{Code: APPLICATION_DOES_NOT_EXISTS, Msg: "Could not found an app with the given key"},
|
||||
baseWebsocketError{Code: applicationDoesNotExists, Msg: "Could not found an app with the given key"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ type noProtocolVersionSuppliedError struct {
|
||||
|
||||
func newNoProtocolVersionSuppliedError() noProtocolVersionSuppliedError {
|
||||
return noProtocolVersionSuppliedError{
|
||||
baseWebsocketError{Code: NO_PROTOCOL_VERSION_SUPPLIED, Msg: "No protocol version supplied"},
|
||||
baseWebsocketError{Code: noProtocolVersionSupplied, Msg: "No protocol version supplied"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ type applicationDisabledError struct {
|
||||
|
||||
func newApplicationDisabledError() noProtocolVersionSuppliedError {
|
||||
return noProtocolVersionSuppliedError{
|
||||
baseWebsocketError{Code: APPLICATION_DISABLED, Msg: "Application disabled"},
|
||||
baseWebsocketError{Code: applicationDisabled, Msg: "Application disabled"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ type applicationOnlyAccepsSSLError struct {
|
||||
|
||||
func newApplicationOnlyAccepsSSLError() applicationOnlyAccepsSSLError {
|
||||
return applicationOnlyAccepsSSLError{
|
||||
baseWebsocketError{Code: APPLICATION_ONLY_ACCEPTS_SSL, Msg: "Application only accepts SSL connections, reconnect using wss://"},
|
||||
baseWebsocketError{Code: applicationOnlyAcceptsSSL, Msg: "Application only accepts SSL connections, reconnect using wss://"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ type invalidVersionStringFormatError struct {
|
||||
|
||||
func newInvalidVersionStringFormatError() invalidVersionStringFormatError {
|
||||
return invalidVersionStringFormatError{
|
||||
baseWebsocketError{Code: INVALID_VERSION_STRING_FORMAT, Msg: "Invalid version string format"},
|
||||
baseWebsocketError{Code: invalidVersionStringFormat, Msg: "Invalid version string format"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ type genericReconnectImmediatelyError struct {
|
||||
|
||||
func newGenericReconnectImmediatelyError() genericReconnectImmediatelyError {
|
||||
return genericReconnectImmediatelyError{
|
||||
baseWebsocketError{Code: GENERIC_RECONNECT_IMMEDIATELY, Msg: "Generic reconnect immediately"},
|
||||
baseWebsocketError{Code: genericReconnectImmediately, Msg: "Generic reconnect immediately"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,6 @@ type genericError struct {
|
||||
|
||||
func newGenericError(msg string) genericError {
|
||||
return genericError{
|
||||
baseWebsocketError{Code: GENERIC_ERROR, Msg: msg},
|
||||
baseWebsocketError{Code: otherError, Msg: msg},
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -164,7 +164,7 @@ type errorEvent struct {
|
||||
func newErrorEvent(code int, message string) errorEvent {
|
||||
var data interface{}
|
||||
|
||||
if code == GENERIC_ERROR {
|
||||
if code == otherError {
|
||||
data = struct {
|
||||
Code *int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
+110
-27
@@ -8,12 +8,100 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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(ctx *applicationContext, h contextHandler) contextHandler {
|
||||
return contextHandlerFunc(func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
|
||||
appID := p.Get("app_id")
|
||||
|
||||
app, err := ctx.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 {
|
||||
h.ServeWithContext(ctx, p, w, r)
|
||||
} else {
|
||||
log.Error("Not authorized")
|
||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Check if the application is disabled
|
||||
func restCheckAppDisabledHandler(ctx *applicationContext, h contextHandler) contextHandler {
|
||||
return contextHandlerFunc(func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
|
||||
appID := p.Get("app_id")
|
||||
|
||||
currentApp, err := ctx.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
|
||||
}
|
||||
|
||||
h.ServeWithContext(ctx, p, w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// commonHandlers combine restCheckAppDisabledHandler and restAuthenticationHandler handlers
|
||||
func commonHandlers(ctx *applicationContext, h contextHandlerFunc) contextHandler {
|
||||
return restCheckAppDisabledHandler(ctx, restAuthenticationHandler(ctx, h))
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
@@ -29,11 +117,10 @@ import (
|
||||
// Response is an empty JSON hash.
|
||||
//
|
||||
// POST /apps/{app_id}/events
|
||||
func postEvents(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
appID := vars["app_id"]
|
||||
func postEvents(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
|
||||
appID := p.Get("app_id")
|
||||
|
||||
app, err := conf.GetAppByAppID(appID)
|
||||
app, err := ctx.DB.GetAppByAppID(appID)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
||||
@@ -55,7 +142,7 @@ func postEvents(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
return
|
||||
}
|
||||
@@ -95,13 +182,12 @@ func postEvents(w http.ResponseWriter, r *http.Request) {
|
||||
// }
|
||||
//
|
||||
// GET /apps/{app_id}/channels
|
||||
func getChannels(w http.ResponseWriter, r *http.Request) {
|
||||
params := r.URL.Query()
|
||||
vars := mux.Vars(r)
|
||||
func getChannels(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
|
||||
appID := vars["app_id"]
|
||||
filter := params.Get("filter_by_prefix")
|
||||
info := params.Get("info")
|
||||
appID := p.Get("app_id")
|
||||
filter := query.Get("filter_by_prefix")
|
||||
info := query.Get("info")
|
||||
|
||||
attributes := strings.Split(info, ",")
|
||||
|
||||
@@ -120,7 +206,7 @@ func getChannels(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
app, err := conf.GetAppByAppID(appID)
|
||||
app, err := ctx.DB.GetAppByAppID(appID)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
||||
@@ -176,20 +262,19 @@ func getChannels(w http.ResponseWriter, r *http.Request) {
|
||||
// }
|
||||
//
|
||||
// GET /apps/{app_id}/channels/{channel_name}
|
||||
func getChannel(w http.ResponseWriter, r *http.Request) {
|
||||
func getChannel(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
params := r.URL.Query()
|
||||
vars := mux.Vars(r)
|
||||
query := r.URL.Query()
|
||||
|
||||
appID := vars["app_id"]
|
||||
app, err := conf.GetAppByAppID(appID)
|
||||
appID := p.Get("app_id")
|
||||
app, err := ctx.DB.GetAppByAppID(appID)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
channelName := vars["channel_name"]
|
||||
channelName := p.Get("channel_name")
|
||||
|
||||
// Channel name could not be empty
|
||||
if strings.TrimSpace(channelName) == "" {
|
||||
@@ -197,7 +282,7 @@ func getChannel(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
info := params.Get("info")
|
||||
info := query.Get("info")
|
||||
attributes := strings.Split(info, ",")
|
||||
|
||||
// Attributes requested
|
||||
@@ -266,20 +351,18 @@ func getChannel(w http.ResponseWriter, r *http.Request) {
|
||||
// }
|
||||
//
|
||||
// GET /apps/{app_id}/channels/{channel_name}/users
|
||||
func getChannelUsers(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
func getChannelUsers(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
|
||||
appID := p.Get("app_id")
|
||||
channelName := p.Get("channel_name")
|
||||
|
||||
appID := vars["app_id"]
|
||||
channelName := vars["channel_name"]
|
||||
|
||||
isPresence := strings.HasPrefix(channelName, "presence-")
|
||||
isPresence := utils.IsPresenceChannel(channelName)
|
||||
|
||||
if !isPresence {
|
||||
http.Error(w, "This api endpoint is restricted to presence channels.", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
app, err := conf.GetAppByAppID(appID)
|
||||
app, err := ctx.DB.GetAppByAppID(appID)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
||||
@@ -0,0 +1,207 @@
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
testApp *app
|
||||
ctx *applicationContext
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
ctx = &applicationContext{DB: db}
|
||||
}
|
||||
|
||||
// All Channels
|
||||
func Test_getChannels_all(t *testing.T) {
|
||||
|
||||
appID := testApp.AppID
|
||||
|
||||
p := map[string]string{}
|
||||
p["app_id"] = appID
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels", appID), nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
getChannels(ctx, params(p), 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
|
||||
|
||||
p := map[string]string{}
|
||||
p["app_id"] = appID
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-", appID), nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
getChannels(ctx, params(p), 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
|
||||
|
||||
p := map[string]string{}
|
||||
p["app_id"] = appID
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-&info=user_count", appID), nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
getChannels(ctx, params(p), 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
|
||||
|
||||
p := map[string]string{}
|
||||
p["app_id"] = appID
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-&info=user_count", appID), nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
getChannels(ctx, params(p), 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
|
||||
|
||||
p := map[string]string{}
|
||||
p["app_id"] = appID
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=public-", appID), nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
getChannels(ctx, params(p), 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
|
||||
|
||||
p := map[string]string{}
|
||||
p["app_id"] = appID
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-", appID), nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
getChannels(ctx, params(p), 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)
|
||||
}
|
||||
}
|
||||
+43
-15
@@ -6,34 +6,62 @@ package ipe
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/golang/glog"
|
||||
)
|
||||
|
||||
// Conf holds the global configuration state
|
||||
var conf configFile
|
||||
|
||||
// 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())
|
||||
file, err := ioutil.ReadFile(configfile)
|
||||
file, err := os.Open(filename)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(file, &conf); err != nil {
|
||||
return err
|
||||
// Reading config
|
||||
if err := json.NewDecoder(file).Decode(&conf); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
conf.Init()
|
||||
router := newRouter()
|
||||
// Using a in memory database
|
||||
db := newMemdb()
|
||||
|
||||
if err := http.ListenAndServe(conf.Host, router); err != nil {
|
||||
return err
|
||||
// Adding applications
|
||||
for _, a := range conf.Apps {
|
||||
db.AddApp(newAppFromConfig(a))
|
||||
}
|
||||
|
||||
return nil
|
||||
// Creating the global application context
|
||||
ctx := &applicationContext{DB: db}
|
||||
|
||||
// The router
|
||||
router := newRouter(ctx)
|
||||
|
||||
router.POST("/apps/{app_id}/events", commonHandlers(ctx, postEvents))
|
||||
|
||||
router.GET("/apps/{app_id}/channels", commonHandlers(ctx, getChannels))
|
||||
|
||||
router.GET("/apps/{app_id}/channels/{channel_name}", commonHandlers(ctx, getChannel))
|
||||
|
||||
router.GET("/apps/{app_id}/channels/{channel_name}/users", commonHandlers(ctx, getChannelUsers))
|
||||
|
||||
router.GET("/app/{key}", contextHandlerFunc(wsHandler))
|
||||
|
||||
if conf.SSL {
|
||||
go func() {
|
||||
log.Infof("Starting HTTPS service on %s ...", conf.SSLHost)
|
||||
log.Fatal(http.ListenAndServeTLS(conf.SSLHost, conf.SSLCertFile, conf.SSLKeyFile, router))
|
||||
}()
|
||||
}
|
||||
|
||||
log.Infof("Starting HTTP service on %s ...", conf.Host)
|
||||
log.Fatal(http.ListenAndServe(conf.Host, router))
|
||||
}
|
||||
|
||||
+30
-20
@@ -1,4 +1,4 @@
|
||||
// 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
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
@@ -10,23 +10,33 @@ import (
|
||||
"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
|
||||
type router struct {
|
||||
ctx *applicationContext
|
||||
mux *mux.Router
|
||||
routes map[string]contextHandler
|
||||
}
|
||||
|
||||
func newRouter(ctx *applicationContext) *router {
|
||||
return &router{
|
||||
ctx: ctx,
|
||||
mux: mux.NewRouter().StrictSlash(true),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *router) GET(path string, handler contextHandler) {
|
||||
a.Handle("GET", path, handler)
|
||||
}
|
||||
|
||||
func (a *router) POST(path string, handler contextHandler) {
|
||||
a.Handle("POST", path, handler)
|
||||
}
|
||||
|
||||
func (a *router) Handle(method, path string, handler contextHandler) {
|
||||
a.mux.Methods(method).Path(path).HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handler.ServeWithContext(a.ctx, params(mux.Vars(r)), w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (a router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
a.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
+9
-1
@@ -150,6 +150,7 @@ func triggerHook(name string, a *app, c *channel, event hookEvent) {
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Ipe UA; (+https://github.com/dimiro1/ipe)")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Pusher-Key", a.Key)
|
||||
req.Header.Set("X-Pusher-Signature", utils.HashMAC(js, []byte(a.Secret)))
|
||||
@@ -157,7 +158,14 @@ func triggerHook(name string, a *app, c *channel, event hookEvent) {
|
||||
log.V(1).Infof("%+v", req.Header)
|
||||
log.V(1).Infof("%+v", string(js))
|
||||
|
||||
if _, err := http.DefaultClient.Do(req); err != nil {
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
|
||||
// See: http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#close_http_resp_body
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Error posting %s event: %+v", name, err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"strings"
|
||||
|
||||
log "github.com/golang/glog"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/dimiro1/ipe/utils"
|
||||
@@ -39,12 +38,12 @@ func onOpen(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, sessio
|
||||
switch {
|
||||
case strings.TrimSpace(p) == "":
|
||||
return newNoProtocolVersionSuppliedError()
|
||||
case protocol != SUPPORTED_PROTOCOL_VERSION:
|
||||
case protocol != supportedProtocolVersion:
|
||||
return newUnsupportedProtocolVersionError()
|
||||
case app.ApplicationDisabled:
|
||||
return newApplicationDisabledError()
|
||||
case r.TLS != nil:
|
||||
if app.OnlySSL {
|
||||
case app.OnlySSL:
|
||||
if r.TLS == nil {
|
||||
return newApplicationOnlyAccepsSSLError()
|
||||
}
|
||||
}
|
||||
@@ -123,13 +122,13 @@ func onMessage(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, ses
|
||||
break
|
||||
}
|
||||
|
||||
isPresence := strings.HasPrefix(channelName, "presence-")
|
||||
isPrivate := strings.HasPrefix(channelName, "private-")
|
||||
isPresence := utils.IsPresenceChannel(channelName)
|
||||
isPrivate := utils.IsPrivateChannel(channelName)
|
||||
|
||||
if isPresence || isPrivate {
|
||||
toSign := []string{connection.SocketID, channelName}
|
||||
|
||||
if isPresence {
|
||||
if isPresence || len(subscribeEvent.Data.ChannelData) > 0 {
|
||||
toSign = append(toSign, subscribeEvent.Data.ChannelData)
|
||||
}
|
||||
|
||||
@@ -207,7 +206,7 @@ func onMessage(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, ses
|
||||
}
|
||||
|
||||
// Websocket GET /app/{key}
|
||||
func wsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func wsHandler(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
defer func() {
|
||||
if conn != nil {
|
||||
@@ -220,10 +219,9 @@ func wsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
appKey := vars["key"]
|
||||
appKey := p.Get("key")
|
||||
|
||||
app, err := conf.GetAppByKey(appKey)
|
||||
app, err := ctx.DB.GetAppByKey(appKey)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
@@ -9,32 +9,44 @@ import (
|
||||
"fmt"
|
||||
|
||||
"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 string = "version"
|
||||
buildstamp string = "buildstamp"
|
||||
githash string = "githash"
|
||||
)
|
||||
|
||||
// Main function, initialize the system
|
||||
func main() {
|
||||
var filename = flag.String("config", "config.json", "Config file location")
|
||||
flag.Parse()
|
||||
|
||||
printBanner()
|
||||
|
||||
if err := ipe.Start(*filename); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
ipe.Start(*filename)
|
||||
}
|
||||
|
||||
// Print a beautifull banner
|
||||
// Print a beautiful banner
|
||||
func printBanner() {
|
||||
fmt.Print("\033[36m")
|
||||
fmt.Print(`
|
||||
██╗██████╗ ███████╗
|
||||
██║██╔══██╗██╔════╝
|
||||
██║██████╔╝█████╗
|
||||
██║██╔═══╝ ██╔══╝
|
||||
██║██║ ███████╗
|
||||
╚═╝╚═╝ ╚══════╝`)
|
||||
fmt.Print("\033[31m")
|
||||
fmt.Print(`
|
||||
d8b
|
||||
Y8P
|
||||
|
||||
888 88888b. .d88b.
|
||||
888 888 "88b d8P Y8b
|
||||
888 888 888 88888888
|
||||
888 888 d88P Y8b.
|
||||
888 88888P" "Y8888
|
||||
888
|
||||
888
|
||||
888
|
||||
`)
|
||||
fmt.Println("\033[0m")
|
||||
fmt.Println("\033[32mWelcome to Ipê - Yet another Pusher server clone\033[0m")
|
||||
fmt.Println("\033[32mWelcome to Ipê - Yet another Pusher server clone (https://github.com/dimiro1/ipe)\033[0m")
|
||||
fmt.Printf("\033[32mVersion %s+%s.git.%s\033[0m\n", version, buildstamp, githash)
|
||||
fmt.Println("\033[33mBy: Claudemiro Alves Feitosa Neto <dimiro1@gmail.com>\033[0m")
|
||||
}
|
||||
|
||||
+17
-10
@@ -12,8 +12,15 @@ import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var validChannelName *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
validChannelName = regexp.MustCompile("^[A-Za-z0-9_\\-=@,.;]+$")
|
||||
}
|
||||
|
||||
// HashMAC Calculates the MAC signing with the given key and returns the hexadecimal encoded Result
|
||||
func HashMAC(message, key []byte) string {
|
||||
mac := hmac.New(sha256.New, key)
|
||||
@@ -25,18 +32,18 @@ func HashMAC(message, key []byte) string {
|
||||
|
||||
// GenerateSessionID Generate a new random Hash
|
||||
func GenerateSessionID() string {
|
||||
MAX := math.MaxInt64
|
||||
|
||||
return fmt.Sprintf("%d.%d", rand.Intn(MAX), rand.Intn(MAX))
|
||||
return fmt.Sprintf("%d.%d", rand.Intn(math.MaxInt64), rand.Intn(math.MaxInt64))
|
||||
}
|
||||
|
||||
// IsChannelNameValid Verify if the channel name is valid
|
||||
func IsChannelNameValid(channelName string) bool {
|
||||
matched, err := regexp.MatchString("^[A-Za-z0-9_\\-=@,.;]+$", channelName)
|
||||
|
||||
if err == nil && matched {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
return validChannelName.Match([]byte(channelName))
|
||||
}
|
||||
|
||||
func IsPrivateChannel(channelName string) bool {
|
||||
return strings.HasPrefix(channelName, "private-")
|
||||
}
|
||||
|
||||
func IsPresenceChannel(channelName string) bool {
|
||||
return strings.HasPrefix(channelName, "presence-")
|
||||
}
|
||||
|
||||
+81
-8
@@ -9,6 +9,18 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkGenerateSession(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
GenerateSessionID()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIsChannelNameValid(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
IsChannelNameValid("hello-world")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateSession(t *testing.T) {
|
||||
sessionID := GenerateSessionID()
|
||||
|
||||
@@ -18,19 +30,80 @@ func TestGenerateSession(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIsValidChannelName(t *testing.T) {
|
||||
if IsChannelNameValid("#@#hhh**sasas") {
|
||||
t.Errorf("Invalid Channel Name")
|
||||
name := "#@#hhh**sasas"
|
||||
ok := IsChannelNameValid(name)
|
||||
|
||||
if ok {
|
||||
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, false)
|
||||
}
|
||||
|
||||
if !IsChannelNameValid("private-hello") {
|
||||
t.Errorf("Must be Valid Channel Name")
|
||||
name = "private-hello"
|
||||
ok = IsChannelNameValid(name)
|
||||
|
||||
if !ok {
|
||||
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, true)
|
||||
}
|
||||
|
||||
if !IsChannelNameValid("presence-hello") {
|
||||
t.Errorf("Must be Valid Channel Name")
|
||||
name = "presence-hello"
|
||||
ok = IsChannelNameValid(name)
|
||||
|
||||
if !ok {
|
||||
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, true)
|
||||
}
|
||||
|
||||
if !IsChannelNameValid("public") {
|
||||
t.Errorf("Must be Valid Channel Name")
|
||||
name = "public"
|
||||
ok = IsChannelNameValid(name)
|
||||
|
||||
if !ok {
|
||||
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPrivateChannel_valid(t *testing.T) {
|
||||
name := "private-hello"
|
||||
ok := IsPrivateChannel(name)
|
||||
|
||||
if !ok {
|
||||
t.Errorf("IsPrivateChannel(%s) == %t, wants %t", name, ok, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPrivateChannel_invalid(t *testing.T) {
|
||||
name := "hello"
|
||||
ok := IsPrivateChannel(name)
|
||||
|
||||
if ok {
|
||||
t.Errorf("IsPrivateChannel(%s) == %t, wants %t", name, ok, false)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsIsPresenceChannel_valid(t *testing.T) {
|
||||
name := "presence-hello"
|
||||
ok := IsPresenceChannel(name)
|
||||
|
||||
if !ok {
|
||||
t.Errorf("IsPresenceChannel(%s) == %t, wants %t", name, ok, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPresenceChannel_invalid(t *testing.T) {
|
||||
name := "hello"
|
||||
ok := IsPresenceChannel(name)
|
||||
|
||||
if ok {
|
||||
t.Errorf("IsPresenceChannel(%s) == %t, wants %t", name, ok, false)
|
||||
}
|
||||
}
|
||||
|
||||
func 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)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ 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
|
||||
https://github.com/google/glog
|
||||
|
||||
By binding methods to booleans it is possible to use the log package
|
||||
without paying the expense of evaluating the arguments to the log.
|
||||
|
||||
+4
-1
@@ -676,7 +676,10 @@ func (l *loggingT) output(s severity, buf *buffer, file string, line int, alsoTo
|
||||
}
|
||||
}
|
||||
data := buf.Bytes()
|
||||
if l.toStderr {
|
||||
if !flag.Parsed() {
|
||||
os.Stderr.Write([]byte("ERROR: logging before flag.Parse: "))
|
||||
os.Stderr.Write(data)
|
||||
} else if l.toStderr {
|
||||
os.Stderr.Write(data)
|
||||
} else {
|
||||
if alsoToStderr || l.alsoToStderr || s >= l.stderrThreshold.get() {
|
||||
|
||||
+2
-3
@@ -1,9 +1,8 @@
|
||||
language: go
|
||||
sudo: false
|
||||
|
||||
go:
|
||||
- 1.0
|
||||
- 1.1
|
||||
- 1.2
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.5
|
||||
- tip
|
||||
|
||||
+11
-4
@@ -1,7 +1,14 @@
|
||||
language: go
|
||||
|
||||
sudo: false
|
||||
go:
|
||||
- 1.0
|
||||
- 1.1
|
||||
- 1.2
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.5
|
||||
- tip
|
||||
install:
|
||||
- go get golang.org/x/tools/cmd/vet
|
||||
script:
|
||||
- go get -t -v ./...
|
||||
- diff -u <(echo -n) <(gofmt -d -s .)
|
||||
- go tool vet .
|
||||
- go test -v -race ./...
|
||||
|
||||
+236
-3
@@ -1,7 +1,240 @@
|
||||
mux
|
||||
===
|
||||
[](https://travis-ci.org/gorilla/mux)
|
||||
[](https://godoc.org/github.com/gorilla/mux)
|
||||
[](https://travis-ci.org/gorilla/mux)
|
||||
|
||||
gorilla/mux is a powerful URL router and dispatcher.
|
||||
Package `gorilla/mux` implements a request router and dispatcher.
|
||||
|
||||
Read the full documentation here: http://www.gorillatoolkit.org/pkg/mux
|
||||
The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are:
|
||||
|
||||
* Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers.
|
||||
* URL hosts and paths can have variables with an optional regular expression.
|
||||
* Registered URLs can be built, or "reversed", which helps maintaining references to resources.
|
||||
* Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching.
|
||||
* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`.
|
||||
|
||||
Let's start registering a couple of URL paths and handlers:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", HomeHandler)
|
||||
r.HandleFunc("/products", ProductsHandler)
|
||||
r.HandleFunc("/articles", ArticlesHandler)
|
||||
http.Handle("/", r)
|
||||
}
|
||||
```
|
||||
|
||||
Here we register three routes mapping URL paths to handlers. This is equivalent to how `http.HandleFunc()` works: if an incoming request URL matches one of the paths, the corresponding handler is called passing (`http.ResponseWriter`, `*http.Request`) as parameters.
|
||||
|
||||
Paths can have variables. They are defined using the format `{name}` or `{name:pattern}`. If a regular expression pattern is not defined, the matched variable will be anything until the next slash. For example:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/products/{key}", ProductHandler)
|
||||
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
|
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
|
||||
```
|
||||
|
||||
The names are used to create a map of route variables which can be retrieved calling `mux.Vars()`:
|
||||
|
||||
```go
|
||||
vars := mux.Vars(request)
|
||||
category := vars["category"]
|
||||
```
|
||||
|
||||
And this is all you need to know about the basic usage. More advanced options are explained below.
|
||||
|
||||
Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
// Only matches if domain is "www.example.com".
|
||||
r.Host("www.example.com")
|
||||
// Matches a dynamic subdomain.
|
||||
r.Host("{subdomain:[a-z]+}.domain.com")
|
||||
```
|
||||
|
||||
There are several other matchers that can be added. To match path prefixes:
|
||||
|
||||
```go
|
||||
r.PathPrefix("/products/")
|
||||
```
|
||||
|
||||
...or HTTP methods:
|
||||
|
||||
```go
|
||||
r.Methods("GET", "POST")
|
||||
```
|
||||
|
||||
...or URL schemes:
|
||||
|
||||
```go
|
||||
r.Schemes("https")
|
||||
```
|
||||
|
||||
...or header values:
|
||||
|
||||
```go
|
||||
r.Headers("X-Requested-With", "XMLHttpRequest")
|
||||
```
|
||||
|
||||
...or query values:
|
||||
|
||||
```go
|
||||
r.Queries("key", "value")
|
||||
```
|
||||
|
||||
...or to use a custom matcher function:
|
||||
|
||||
```go
|
||||
r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool {
|
||||
return r.ProtoMajor == 0
|
||||
})
|
||||
```
|
||||
|
||||
...and finally, it is possible to combine several matchers in a single route:
|
||||
|
||||
```go
|
||||
r.HandleFunc("/products", ProductsHandler).
|
||||
Host("www.example.com").
|
||||
Methods("GET").
|
||||
Schemes("http")
|
||||
```
|
||||
|
||||
Setting the same matching conditions again and again can be boring, so we have a way to group several routes that share the same requirements. We call it "subrouting".
|
||||
|
||||
For example, let's say we have several URLs that should only match when the host is `www.example.com`. Create a route for that host and get a "subrouter" from it:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
s := r.Host("www.example.com").Subrouter()
|
||||
```
|
||||
|
||||
Then register routes in the subrouter:
|
||||
|
||||
```go
|
||||
s.HandleFunc("/products/", ProductsHandler)
|
||||
s.HandleFunc("/products/{key}", ProductHandler)
|
||||
s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
|
||||
```
|
||||
|
||||
The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route.
|
||||
|
||||
Subrouters can be used to create domain or path "namespaces": you define subrouters in a central place and then parts of the app can register its paths relatively to a given subrouter.
|
||||
|
||||
There's one more thing about subroutes. When a subrouter has a path prefix, the inner routes use it as base for their paths:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
s := r.PathPrefix("/products").Subrouter()
|
||||
// "/products/"
|
||||
s.HandleFunc("/", ProductsHandler)
|
||||
// "/products/{key}/"
|
||||
s.HandleFunc("/{key}/", ProductHandler)
|
||||
// "/products/{key}/details"
|
||||
s.HandleFunc("/{key}/details", ProductDetailsHandler)
|
||||
```
|
||||
|
||||
Now let's see how to build registered URLs.
|
||||
|
||||
Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
Name("article")
|
||||
```
|
||||
|
||||
To build a URL, get the route and call the `URL()` method, passing a sequence of key/value pairs for the route variables. For the previous route, we would do:
|
||||
|
||||
```go
|
||||
url, err := r.Get("article").URL("category", "technology", "id", "42")
|
||||
```
|
||||
|
||||
...and the result will be a `url.URL` with the following path:
|
||||
|
||||
```
|
||||
"/articles/technology/42"
|
||||
```
|
||||
|
||||
This also works for host variables:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.Host("{subdomain}.domain.com").
|
||||
Path("/articles/{category}/{id:[0-9]+}").
|
||||
HandlerFunc(ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
// url.String() will be "http://news.domain.com/articles/technology/42"
|
||||
url, err := r.Get("article").URL("subdomain", "news",
|
||||
"category", "technology",
|
||||
"id", "42")
|
||||
```
|
||||
|
||||
All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match.
|
||||
|
||||
Regex support also exists for matching Headers within a route. For example, we could do:
|
||||
|
||||
```go
|
||||
r.HeadersRegexp("Content-Type", "application/(text|json)")
|
||||
```
|
||||
|
||||
...and the route will match both requests with a Content-Type of `application/json` as well as `application/text`
|
||||
|
||||
There's also a way to build only the URL host or path for a route: use the methods `URLHost()` or `URLPath()` instead. For the previous route, we would do:
|
||||
|
||||
```go
|
||||
// "http://news.domain.com/"
|
||||
host, err := r.Get("article").URLHost("subdomain", "news")
|
||||
|
||||
// "/articles/technology/42"
|
||||
path, err := r.Get("article").URLPath("category", "technology", "id", "42")
|
||||
```
|
||||
|
||||
And if you use subrouters, host and path defined separately can be built as well:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
s := r.Host("{subdomain}.domain.com").Subrouter()
|
||||
s.Path("/articles/{category}/{id:[0-9]+}").
|
||||
HandlerFunc(ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
// "http://news.domain.com/articles/technology/42"
|
||||
url, err := r.Get("article").URL("subdomain", "news",
|
||||
"category", "technology",
|
||||
"id", "42")
|
||||
```
|
||||
|
||||
## Full Example
|
||||
|
||||
Here's a complete, runnable example of a small `mux` based server:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func YourHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("Gorilla!\n"))
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
// Routes consist of a path and a handler function.
|
||||
r.HandleFunc("/", YourHandler)
|
||||
|
||||
// Bind to a port and pass our router in
|
||||
http.ListenAndServe(":8000", r)
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
BSD licensed. See the LICENSE file for details.
|
||||
|
||||
+13
-6
@@ -60,8 +60,8 @@ 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")
|
||||
// Only matches if domain is "www.example.com".
|
||||
r.Host("www.example.com")
|
||||
// Matches a dynamic subdomain.
|
||||
r.Host("{subdomain:[a-z]+}.domain.com")
|
||||
|
||||
@@ -94,7 +94,7 @@ There are several other matchers that can be added. To match path prefixes:
|
||||
...and finally, it is possible to combine several matchers in a single route:
|
||||
|
||||
r.HandleFunc("/products", ProductsHandler).
|
||||
Host("www.domain.com").
|
||||
Host("www.example.com").
|
||||
Methods("GET").
|
||||
Schemes("http")
|
||||
|
||||
@@ -103,11 +103,11 @@ 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"
|
||||
host is "www.example.com". Create a route for that host and get a "subrouter"
|
||||
from it:
|
||||
|
||||
r := mux.NewRouter()
|
||||
s := r.Host("www.domain.com").Subrouter()
|
||||
s := r.Host("www.example.com").Subrouter()
|
||||
|
||||
Then register routes in the subrouter:
|
||||
|
||||
@@ -116,7 +116,7 @@ Then register routes in the subrouter:
|
||||
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
|
||||
"www.example.com", because the subrouter is tested first. This is not
|
||||
only convenient, but also optimizes request matching. You can create
|
||||
subrouters combining any attribute matchers accepted by a route.
|
||||
|
||||
@@ -172,6 +172,13 @@ conform to the corresponding patterns. These requirements guarantee that a
|
||||
generated URL will always match a registered route -- the only exception is
|
||||
for explicitly defined "build-only" routes which never match.
|
||||
|
||||
Regex support also exists for matching Headers within a route. For example, we could do:
|
||||
|
||||
r.HeadersRegexp("Content-Type", "application/(text|json)")
|
||||
|
||||
...and the route will match both requests with a Content-Type of `application/json` as well as
|
||||
`application/text`
|
||||
|
||||
There's also a way to build only the URL host or path for a route:
|
||||
use the methods URLHost() or URLPath() instead. For the previous route,
|
||||
we would do:
|
||||
|
||||
+128
-13
@@ -5,9 +5,11 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
|
||||
"github.com/gorilla/context"
|
||||
)
|
||||
@@ -57,6 +59,12 @@ func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Closest match for a router (includes sub-routers)
|
||||
if r.NotFoundHandler != nil {
|
||||
match.Handler = r.NotFoundHandler
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -68,7 +76,7 @@ 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.
|
||||
// Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query.
|
||||
// This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue:
|
||||
// http://code.google.com/p/go/issues/detail?id=5252
|
||||
url := *req.URL
|
||||
@@ -87,10 +95,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
setCurrentRoute(req, match.Route)
|
||||
}
|
||||
if handler == nil {
|
||||
handler = r.NotFoundHandler
|
||||
if handler == nil {
|
||||
handler = http.NotFoundHandler()
|
||||
}
|
||||
handler = http.NotFoundHandler()
|
||||
}
|
||||
if !r.KeepContext {
|
||||
defer context.Clear(req)
|
||||
@@ -237,6 +242,52 @@ func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route {
|
||||
return r.NewRoute().BuildVarsFunc(f)
|
||||
}
|
||||
|
||||
// Walk walks the router and all its sub-routers, calling walkFn for each route
|
||||
// in the tree. The routes are walked in the order they were added. Sub-routers
|
||||
// are explored depth-first.
|
||||
func (r *Router) Walk(walkFn WalkFunc) error {
|
||||
return r.walk(walkFn, []*Route{})
|
||||
}
|
||||
|
||||
// SkipRouter is used as a return value from WalkFuncs to indicate that the
|
||||
// router that walk is about to descend down to should be skipped.
|
||||
var SkipRouter = errors.New("skip this router")
|
||||
|
||||
// WalkFunc is the type of the function called for each route visited by Walk.
|
||||
// At every invocation, it is given the current route, and the current router,
|
||||
// and a list of ancestor routes that lead to the current route.
|
||||
type WalkFunc func(route *Route, router *Router, ancestors []*Route) error
|
||||
|
||||
func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error {
|
||||
for _, t := range r.routes {
|
||||
if t.regexp == nil || t.regexp.path == nil || t.regexp.path.template == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
err := walkFn(t, r, ancestors)
|
||||
if err == SkipRouter {
|
||||
continue
|
||||
}
|
||||
for _, sr := range t.matchers {
|
||||
if h, ok := sr.(*Router); ok {
|
||||
err := h.walk(walkFn, ancestors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if h, ok := t.handler.(*Router); ok {
|
||||
ancestors = append(ancestors, t)
|
||||
err := h.walk(walkFn, ancestors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ancestors = ancestors[:len(ancestors)-1]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Context
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -264,6 +315,10 @@ func Vars(r *http.Request) map[string]string {
|
||||
}
|
||||
|
||||
// CurrentRoute returns the matched route for the current request, if any.
|
||||
// This only works when called inside the handler of the matched route
|
||||
// because the matched route is stored in the request context which is cleared
|
||||
// after the handler returns, unless the KeepContext option is set on the
|
||||
// Router.
|
||||
func CurrentRoute(r *http.Request) *Route {
|
||||
if rv := context.Get(r, routeKey); rv != nil {
|
||||
return rv.(*Route)
|
||||
@@ -272,11 +327,15 @@ func CurrentRoute(r *http.Request) *Route {
|
||||
}
|
||||
|
||||
func setVars(r *http.Request, val interface{}) {
|
||||
context.Set(r, varsKey, val)
|
||||
if val != nil {
|
||||
context.Set(r, varsKey, val)
|
||||
}
|
||||
}
|
||||
|
||||
func setCurrentRoute(r *http.Request, val interface{}) {
|
||||
context.Set(r, routeKey, val)
|
||||
if val != nil {
|
||||
context.Set(r, routeKey, val)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -313,13 +372,24 @@ func uniqueVars(s1, s2 []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// mapFromPairs converts variadic string parameters to a string map.
|
||||
func mapFromPairs(pairs ...string) (map[string]string, error) {
|
||||
// checkPairs returns the count of strings passed in, and an error if
|
||||
// the count is not an even number.
|
||||
func checkPairs(pairs ...string) (int, error) {
|
||||
length := len(pairs)
|
||||
if length%2 != 0 {
|
||||
return nil, fmt.Errorf(
|
||||
return length, fmt.Errorf(
|
||||
"mux: number of parameters must be multiple of 2, got %v", pairs)
|
||||
}
|
||||
return length, nil
|
||||
}
|
||||
|
||||
// mapFromPairsToString converts variadic string parameters to a
|
||||
// string to string map.
|
||||
func mapFromPairsToString(pairs ...string) (map[string]string, error) {
|
||||
length, err := checkPairs(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]string, length/2)
|
||||
for i := 0; i < length; i += 2 {
|
||||
m[pairs[i]] = pairs[i+1]
|
||||
@@ -327,6 +397,24 @@ func mapFromPairs(pairs ...string) (map[string]string, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// mapFromPairsToRegex converts variadic string paramers to a
|
||||
// string to regex map.
|
||||
func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) {
|
||||
length, err := checkPairs(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]*regexp.Regexp, length/2)
|
||||
for i := 0; i < length; i += 2 {
|
||||
regex, err := regexp.Compile(pairs[i+1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m[pairs[i]] = regex
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// matchInArray returns true if the given string value is in the array.
|
||||
func matchInArray(arr []string, value string) bool {
|
||||
for _, v := range arr {
|
||||
@@ -337,9 +425,8 @@ func matchInArray(arr []string, value string) bool {
|
||||
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 {
|
||||
// matchMapWithString returns true if the given key/value pairs exist in a given map.
|
||||
func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, canonicalKey bool) bool {
|
||||
for k, v := range toCheck {
|
||||
// Check if key exists.
|
||||
if canonicalKey {
|
||||
@@ -364,3 +451,31 @@ func matchMap(toCheck map[string]string, toMatch map[string][]string,
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// matchMapWithRegex returns true if the given key/value pairs exist in a given map compiled against
|
||||
// the given regex
|
||||
func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]string, canonicalKey bool) bool {
|
||||
for k, v := range toCheck {
|
||||
// Check if key exists.
|
||||
if canonicalKey {
|
||||
k = http.CanonicalHeaderKey(k)
|
||||
}
|
||||
if values := toMatch[k]; values == nil {
|
||||
return false
|
||||
} else if v != nil {
|
||||
// If value was defined as an empty string we only check that the
|
||||
// key exists. Otherwise we also check for equality.
|
||||
valueExists := false
|
||||
for _, value := range values {
|
||||
if v.MatchString(value) {
|
||||
valueExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valueExists {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
+346
@@ -7,11 +7,24 @@ package mux
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/context"
|
||||
)
|
||||
|
||||
func (r *Route) GoString() string {
|
||||
matchers := make([]string, len(r.matchers))
|
||||
for i, m := range r.matchers {
|
||||
matchers[i] = fmt.Sprintf("%#v", m)
|
||||
}
|
||||
return fmt.Sprintf("&Route{matchers:[]matcher{%s}}", strings.Join(matchers, ", "))
|
||||
}
|
||||
|
||||
func (r *routeRegexp) GoString() string {
|
||||
return fmt.Sprintf("&routeRegexp{template: %q, matchHost: %t, matchQuery: %t, strictSlash: %t, regexp: regexp.MustCompile(%q), reverse: %q, varsN: %v, varsR: %v", r.template, r.matchHost, r.matchQuery, r.strictSlash, r.regexp.String(), r.reverse, r.varsN, r.varsR)
|
||||
}
|
||||
|
||||
type routeTest struct {
|
||||
title string // title of the test
|
||||
route *Route // the route being tested
|
||||
@@ -108,6 +121,15 @@ func TestHost(t *testing.T) {
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Host route with pattern, additional capturing group, match",
|
||||
route: new(Route).Host("aaa.{v1:[a-z]{2}(b|c)}.ccc"),
|
||||
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
|
||||
vars: map[string]string{"v1": "bbb"},
|
||||
host: "aaa.bbb.ccc",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Host route with pattern, wrong host in request URL",
|
||||
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"),
|
||||
@@ -135,6 +157,33 @@ func TestHost(t *testing.T) {
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Host route with hyphenated name and pattern, match",
|
||||
route: new(Route).Host("aaa.{v-1:[a-z]{3}}.ccc"),
|
||||
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
|
||||
vars: map[string]string{"v-1": "bbb"},
|
||||
host: "aaa.bbb.ccc",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Host route with hyphenated name and pattern, additional capturing group, match",
|
||||
route: new(Route).Host("aaa.{v-1:[a-z]{2}(b|c)}.ccc"),
|
||||
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
|
||||
vars: map[string]string{"v-1": "bbb"},
|
||||
host: "aaa.bbb.ccc",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Host route with multiple hyphenated names and patterns, match",
|
||||
route: new(Route).Host("{v-1:[a-z]{3}}.{v-2:[a-z]{3}}.{v-3:[a-z]{3}}"),
|
||||
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
|
||||
vars: map[string]string{"v-1": "aaa", "v-2": "bbb", "v-3": "ccc"},
|
||||
host: "aaa.bbb.ccc",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Path route with single pattern with pipe, match",
|
||||
route: new(Route).Path("/{category:a|b/c}"),
|
||||
@@ -260,6 +309,42 @@ func TestPath(t *testing.T) {
|
||||
path: "/111/222/333",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Path route with multiple patterns with pipe, match",
|
||||
route: new(Route).Path("/{category:a|(b/c)}/{product}/{id:[0-9]+}"),
|
||||
request: newRequest("GET", "http://localhost/a/product_name/1"),
|
||||
vars: map[string]string{"category": "a", "product": "product_name", "id": "1"},
|
||||
host: "",
|
||||
path: "/a/product_name/1",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Path route with hyphenated name and pattern, match",
|
||||
route: new(Route).Path("/111/{v-1:[0-9]{3}}/333"),
|
||||
request: newRequest("GET", "http://localhost/111/222/333"),
|
||||
vars: map[string]string{"v-1": "222"},
|
||||
host: "",
|
||||
path: "/111/222/333",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Path route with multiple hyphenated names and patterns, match",
|
||||
route: new(Route).Path("/{v-1:[0-9]{3}}/{v-2:[0-9]{3}}/{v-3:[0-9]{3}}"),
|
||||
request: newRequest("GET", "http://localhost/111/222/333"),
|
||||
vars: map[string]string{"v-1": "111", "v-2": "222", "v-3": "333"},
|
||||
host: "",
|
||||
path: "/111/222/333",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Path route with multiple hyphenated names and patterns with pipe, match",
|
||||
route: new(Route).Path("/{product-category:a|(b/c)}/{product-name}/{product-id:[0-9]+}"),
|
||||
request: newRequest("GET", "http://localhost/a/product_name/1"),
|
||||
vars: map[string]string{"product-category": "a", "product-name": "product_name", "product-id": "1"},
|
||||
host: "",
|
||||
path: "/a/product_name/1",
|
||||
shouldMatch: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -434,6 +519,24 @@ func TestHeaders(t *testing.T) {
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Headers route, regex header values to match",
|
||||
route: new(Route).Headers("foo", "ba[zr]"),
|
||||
request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar"}),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Headers route, regex header values to match",
|
||||
route: new(Route).HeadersRegexp("foo", "ba[zr]"),
|
||||
request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "baz"}),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -552,6 +655,150 @@ func TestQueries(t *testing.T) {
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Queries route with regexp pattern with quantifier, match",
|
||||
route: new(Route).Queries("foo", "{v1:[0-9]{1}}"),
|
||||
request: newRequest("GET", "http://localhost?foo=1"),
|
||||
vars: map[string]string{"v1": "1"},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Queries route with regexp pattern with quantifier, additional variable in query string, match",
|
||||
route: new(Route).Queries("foo", "{v1:[0-9]{1}}"),
|
||||
request: newRequest("GET", "http://localhost?bar=2&foo=1"),
|
||||
vars: map[string]string{"v1": "1"},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Queries route with regexp pattern with quantifier, regexp does not match",
|
||||
route: new(Route).Queries("foo", "{v1:[0-9]{1}}"),
|
||||
request: newRequest("GET", "http://localhost?foo=12"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Queries route with regexp pattern with quantifier, additional capturing group",
|
||||
route: new(Route).Queries("foo", "{v1:[0-9]{1}(a|b)}"),
|
||||
request: newRequest("GET", "http://localhost?foo=1a"),
|
||||
vars: map[string]string{"v1": "1a"},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Queries route with regexp pattern with quantifier, additional variable in query string, regexp does not match",
|
||||
route: new(Route).Queries("foo", "{v1:[0-9]{1}}"),
|
||||
request: newRequest("GET", "http://localhost?foo=12"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Queries route with hyphenated name, match",
|
||||
route: new(Route).Queries("foo", "{v-1}"),
|
||||
request: newRequest("GET", "http://localhost?foo=bar"),
|
||||
vars: map[string]string{"v-1": "bar"},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Queries route with multiple hyphenated names, match",
|
||||
route: new(Route).Queries("foo", "{v-1}", "baz", "{v-2}"),
|
||||
request: newRequest("GET", "http://localhost?foo=bar&baz=ding"),
|
||||
vars: map[string]string{"v-1": "bar", "v-2": "ding"},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Queries route with hyphenate name and pattern, match",
|
||||
route: new(Route).Queries("foo", "{v-1:[0-9]+}"),
|
||||
request: newRequest("GET", "http://localhost?foo=10"),
|
||||
vars: map[string]string{"v-1": "10"},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Queries route with hyphenated name and pattern with quantifier, additional capturing group",
|
||||
route: new(Route).Queries("foo", "{v-1:[0-9]{1}(a|b)}"),
|
||||
request: newRequest("GET", "http://localhost?foo=1a"),
|
||||
vars: map[string]string{"v-1": "1a"},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Queries route with empty value, should match",
|
||||
route: new(Route).Queries("foo", ""),
|
||||
request: newRequest("GET", "http://localhost?foo=bar"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Queries route with empty value and no parameter in request, should not match",
|
||||
route: new(Route).Queries("foo", ""),
|
||||
request: newRequest("GET", "http://localhost"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Queries route with empty value and empty parameter in request, should match",
|
||||
route: new(Route).Queries("foo", ""),
|
||||
request: newRequest("GET", "http://localhost?foo="),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Queries route with overlapping value, should not match",
|
||||
route: new(Route).Queries("foo", "bar"),
|
||||
request: newRequest("GET", "http://localhost?foo=barfoo"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Queries route with no parameter in request, should not match",
|
||||
route: new(Route).Queries("foo", "{bar}"),
|
||||
request: newRequest("GET", "http://localhost"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Queries route with empty parameter in request, should match",
|
||||
route: new(Route).Queries("foo", "{bar}"),
|
||||
request: newRequest("GET", "http://localhost?foo="),
|
||||
vars: map[string]string{"foo": ""},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Queries route, bad submatch",
|
||||
route: new(Route).Queries("foo", "bar", "baz", "ding"),
|
||||
request: newRequest("GET", "http://localhost?fffoo=bar&baz=dingggg"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -801,6 +1048,105 @@ func TestStrictSlash(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkSingleDepth(t *testing.T) {
|
||||
r0 := NewRouter()
|
||||
r1 := NewRouter()
|
||||
r2 := NewRouter()
|
||||
|
||||
r0.Path("/g")
|
||||
r0.Path("/o")
|
||||
r0.Path("/d").Handler(r1)
|
||||
r0.Path("/r").Handler(r2)
|
||||
r0.Path("/a")
|
||||
|
||||
r1.Path("/z")
|
||||
r1.Path("/i")
|
||||
r1.Path("/l")
|
||||
r1.Path("/l")
|
||||
|
||||
r2.Path("/i")
|
||||
r2.Path("/l")
|
||||
r2.Path("/l")
|
||||
|
||||
paths := []string{"g", "o", "r", "i", "l", "l", "a"}
|
||||
depths := []int{0, 0, 0, 1, 1, 1, 0}
|
||||
i := 0
|
||||
err := r0.Walk(func(route *Route, router *Router, ancestors []*Route) error {
|
||||
matcher := route.matchers[0].(*routeRegexp)
|
||||
if matcher.template == "/d" {
|
||||
return SkipRouter
|
||||
}
|
||||
if len(ancestors) != depths[i] {
|
||||
t.Errorf(`Expected depth of %d at i = %d; got "%d"`, depths[i], i, len(ancestors))
|
||||
}
|
||||
if matcher.template != "/"+paths[i] {
|
||||
t.Errorf(`Expected "/%s" at i = %d; got "%s"`, paths[i], i, matcher.template)
|
||||
}
|
||||
i++
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if i != len(paths) {
|
||||
t.Errorf("Expected %d routes, found %d", len(paths), i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkNested(t *testing.T) {
|
||||
router := NewRouter()
|
||||
|
||||
g := router.Path("/g").Subrouter()
|
||||
o := g.PathPrefix("/o").Subrouter()
|
||||
r := o.PathPrefix("/r").Subrouter()
|
||||
i := r.PathPrefix("/i").Subrouter()
|
||||
l1 := i.PathPrefix("/l").Subrouter()
|
||||
l2 := l1.PathPrefix("/l").Subrouter()
|
||||
l2.Path("/a")
|
||||
|
||||
paths := []string{"/g", "/g/o", "/g/o/r", "/g/o/r/i", "/g/o/r/i/l", "/g/o/r/i/l/l", "/g/o/r/i/l/l/a"}
|
||||
idx := 0
|
||||
err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error {
|
||||
path := paths[idx]
|
||||
tpl := route.regexp.path.template
|
||||
if tpl != path {
|
||||
t.Errorf(`Expected %s got %s`, path, tpl)
|
||||
}
|
||||
idx++
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if idx != len(paths) {
|
||||
t.Errorf("Expected %d routes, found %d", len(paths), idx)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubrouterErrorHandling(t *testing.T) {
|
||||
superRouterCalled := false
|
||||
subRouterCalled := false
|
||||
|
||||
router := NewRouter()
|
||||
router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
superRouterCalled = true
|
||||
})
|
||||
subRouter := router.PathPrefix("/bign8").Subrouter()
|
||||
subRouter.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
subRouterCalled = true
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("GET", "http://localhost/bign8/was/here", nil)
|
||||
router.ServeHTTP(NewRecorder(), req)
|
||||
|
||||
if superRouterCalled {
|
||||
t.Error("Super router 404 handler called when sub-router 404 handler is available.")
|
||||
}
|
||||
if !subRouterCalled {
|
||||
t.Error("Sub-router 404 handler was not called.")
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
+3
-3
@@ -545,7 +545,7 @@ func TestMatchedRouteName(t *testing.T) {
|
||||
router := NewRouter()
|
||||
route := router.NewRoute().Path("/products/").Name(routeName)
|
||||
|
||||
url := "http://www.domain.com/products/"
|
||||
url := "http://www.example.com/products/"
|
||||
request, _ := http.NewRequest("GET", url, nil)
|
||||
var rv RouteMatch
|
||||
ok := router.Match(request, &rv)
|
||||
@@ -563,10 +563,10 @@ func TestMatchedRouteName(t *testing.T) {
|
||||
func TestSubRouting(t *testing.T) {
|
||||
// Example from docs.
|
||||
router := NewRouter()
|
||||
subrouter := router.NewRoute().Host("www.domain.com").Subrouter()
|
||||
subrouter := router.NewRoute().Host("www.example.com").Subrouter()
|
||||
route := subrouter.NewRoute().Path("/products/").Name("products")
|
||||
|
||||
url := "http://www.domain.com/products/"
|
||||
url := "http://www.example.com/products/"
|
||||
request, _ := http.NewRequest("GET", url, nil)
|
||||
var rv RouteMatch
|
||||
ok := router.Match(request, &rv)
|
||||
|
||||
+62
-17
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -34,8 +35,7 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash
|
||||
// Now let's parse it.
|
||||
defaultPattern := "[^/]+"
|
||||
if matchQuery {
|
||||
defaultPattern = "[^?&]+"
|
||||
matchPrefix = true
|
||||
defaultPattern = "[^?&]*"
|
||||
} else if matchHost {
|
||||
defaultPattern = "[^.]+"
|
||||
matchPrefix = false
|
||||
@@ -53,9 +53,7 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash
|
||||
varsN := make([]string, len(idxs)/2)
|
||||
varsR := make([]*regexp.Regexp, len(idxs)/2)
|
||||
pattern := bytes.NewBufferString("")
|
||||
if !matchQuery {
|
||||
pattern.WriteByte('^')
|
||||
}
|
||||
pattern.WriteByte('^')
|
||||
reverse := bytes.NewBufferString("")
|
||||
var end int
|
||||
var err error
|
||||
@@ -75,12 +73,14 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash
|
||||
tpl[idxs[i]:end])
|
||||
}
|
||||
// Build the regexp pattern.
|
||||
fmt.Fprintf(pattern, "%s(%s)", regexp.QuoteMeta(raw), patt)
|
||||
varIdx := i / 2
|
||||
fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(varIdx), patt)
|
||||
// Build the reverse template.
|
||||
fmt.Fprintf(reverse, "%s%%s", raw)
|
||||
|
||||
// Append variable name and compiled pattern.
|
||||
varsN[i/2] = name
|
||||
varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt))
|
||||
varsN[varIdx] = name
|
||||
varsR[varIdx], err = regexp.Compile(fmt.Sprintf("^%s$", patt))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -91,6 +91,12 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash
|
||||
if strictSlash {
|
||||
pattern.WriteString("[/]?")
|
||||
}
|
||||
if matchQuery {
|
||||
// Add the default pattern if the query value is empty
|
||||
if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" {
|
||||
pattern.WriteString(defaultPattern)
|
||||
}
|
||||
}
|
||||
if !matchPrefix {
|
||||
pattern.WriteByte('$')
|
||||
}
|
||||
@@ -141,7 +147,7 @@ type routeRegexp struct {
|
||||
func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
|
||||
if !r.matchHost {
|
||||
if r.matchQuery {
|
||||
return r.regexp.MatchString(req.URL.RawQuery)
|
||||
return r.matchQueryString(req)
|
||||
} else {
|
||||
return r.regexp.MatchString(req.URL.Path)
|
||||
}
|
||||
@@ -175,6 +181,26 @@ func (r *routeRegexp) url(values map[string]string) (string, error) {
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
// getUrlQuery returns a single query parameter from a request URL.
|
||||
// For a URL with foo=bar&baz=ding, we return only the relevant key
|
||||
// value pair for the routeRegexp.
|
||||
func (r *routeRegexp) getUrlQuery(req *http.Request) string {
|
||||
if !r.matchQuery {
|
||||
return ""
|
||||
}
|
||||
templateKey := strings.SplitN(r.template, "=", 2)[0]
|
||||
for key, vals := range req.URL.Query() {
|
||||
if key == templateKey && len(vals) > 0 {
|
||||
return key + "=" + vals[0]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *routeRegexp) matchQueryString(req *http.Request) bool {
|
||||
return r.regexp.MatchString(r.getUrlQuery(req))
|
||||
}
|
||||
|
||||
// braceIndices returns the first level curly brace indices from a string.
|
||||
// It returns an error in case of unbalanced braces.
|
||||
func braceIndices(s string) ([]int, error) {
|
||||
@@ -200,6 +226,11 @@ func braceIndices(s string) ([]int, error) {
|
||||
return idxs, nil
|
||||
}
|
||||
|
||||
// varGroupName builds a capturing group name for the indexed variable.
|
||||
func varGroupName(idx int) string {
|
||||
return "v" + strconv.Itoa(idx)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// routeRegexpGroup
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -217,8 +248,13 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route)
|
||||
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]
|
||||
subexpNames := v.host.regexp.SubexpNames()
|
||||
varName := 0
|
||||
for i, name := range subexpNames[1:] {
|
||||
if name != "" && name == varGroupName(varName) {
|
||||
m.Vars[v.host.varsN[varName]] = hostVars[i+1]
|
||||
varName++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,8 +262,13 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route)
|
||||
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]
|
||||
subexpNames := v.path.regexp.SubexpNames()
|
||||
varName := 0
|
||||
for i, name := range subexpNames[1:] {
|
||||
if name != "" && name == varGroupName(varName) {
|
||||
m.Vars[v.path.varsN[varName]] = pathVars[i+1]
|
||||
varName++
|
||||
}
|
||||
}
|
||||
// Check if we should redirect.
|
||||
if v.path.strictSlash {
|
||||
@@ -246,12 +287,16 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route)
|
||||
}
|
||||
}
|
||||
// Store query string variables.
|
||||
rawQuery := req.URL.RawQuery
|
||||
for _, q := range v.queries {
|
||||
queryVars := q.regexp.FindStringSubmatch(rawQuery)
|
||||
queryVars := q.regexp.FindStringSubmatch(q.getUrlQuery(req))
|
||||
if queryVars != nil {
|
||||
for k, v := range q.varsN {
|
||||
m.Vars[v] = queryVars[k+1]
|
||||
subexpNames := q.regexp.SubexpNames()
|
||||
varName := 0
|
||||
for i, name := range subexpNames[1:] {
|
||||
if name != "" && name == varGroupName(varName) {
|
||||
m.Vars[q.varsN[varName]] = queryVars[i+1]
|
||||
varName++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+32
-8
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -188,7 +189,7 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery
|
||||
type headerMatcher map[string]string
|
||||
|
||||
func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return matchMap(m, r.Header, true)
|
||||
return matchMapWithString(m, r.Header, true)
|
||||
}
|
||||
|
||||
// Headers adds a matcher for request header values.
|
||||
@@ -199,17 +200,40 @@ func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
// "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.
|
||||
// If the value is an empty string, it will match any value if the key is set.
|
||||
func (r *Route) Headers(pairs ...string) *Route {
|
||||
if r.err == nil {
|
||||
var headers map[string]string
|
||||
headers, r.err = mapFromPairs(pairs...)
|
||||
headers, r.err = mapFromPairsToString(pairs...)
|
||||
return r.addMatcher(headerMatcher(headers))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// headerRegexMatcher matches the request against the route given a regex for the header
|
||||
type headerRegexMatcher map[string]*regexp.Regexp
|
||||
|
||||
func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return matchMapWithRegex(m, r.Header, true)
|
||||
}
|
||||
|
||||
// Regular expressions can be used with headers as well.
|
||||
// It accepts a sequence of key/value pairs, where the value has regex support. For example
|
||||
// r := mux.NewRouter()
|
||||
// r.HeadersRegexp("Content-Type", "application/(text|json)",
|
||||
// "X-Requested-With", "XMLHttpRequest")
|
||||
//
|
||||
// The above route will only match if both the request header matches both regular expressions.
|
||||
// It the value is an empty string, it will match any value if the key is set.
|
||||
func (r *Route) HeadersRegexp(pairs ...string) *Route {
|
||||
if r.err == nil {
|
||||
var headers map[string]*regexp.Regexp
|
||||
headers, r.err = mapFromPairsToRegex(pairs...)
|
||||
return r.addMatcher(headerRegexMatcher(headers))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Host -----------------------------------------------------------------------
|
||||
|
||||
// Host adds a matcher for the URL host.
|
||||
@@ -223,7 +247,7 @@ func (r *Route) Headers(pairs ...string) *Route {
|
||||
// For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Host("www.domain.com")
|
||||
// r.Host("www.example.com")
|
||||
// r.Host("{subdomain}.domain.com")
|
||||
// r.Host("{subdomain:[a-z]+}.domain.com")
|
||||
//
|
||||
@@ -336,7 +360,7 @@ func (r *Route) Queries(pairs ...string) *Route {
|
||||
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 {
|
||||
if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], false, false, true); r.err != nil {
|
||||
return r
|
||||
}
|
||||
}
|
||||
@@ -382,7 +406,7 @@ func (r *Route) BuildVarsFunc(f BuildVarsFunc) *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 := r.Host("www.example.com").Subrouter()
|
||||
// s.HandleFunc("/products/", ProductsHandler)
|
||||
// s.HandleFunc("/products/{key}", ProductHandler)
|
||||
// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
|
||||
@@ -511,7 +535,7 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) {
|
||||
// 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...)
|
||||
m, err := mapFromPairsToString(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+2
@@ -7,6 +7,8 @@ Gorilla WebSocket is a [Go](http://golang.org/) implementation of the
|
||||
|
||||
* [API Reference](http://godoc.org/github.com/gorilla/websocket)
|
||||
* [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat)
|
||||
* [Command example](https://github.com/gorilla/websocket/tree/master/examples/command)
|
||||
* [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo)
|
||||
* [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch)
|
||||
|
||||
### Status
|
||||
|
||||
+176
-95
@@ -5,8 +5,10 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -30,50 +32,17 @@ var ErrBadHandshake = errors.New("websocket: bad handshake")
|
||||
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
|
||||
// non-nil *http.Response so that callers can handle redirects, authentication,
|
||||
// etc.
|
||||
//
|
||||
// Deprecated: Use Dialer instead.
|
||||
func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) {
|
||||
challengeKey, err := generateChallengeKey()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
d := Dialer{
|
||||
ReadBufferSize: readBufSize,
|
||||
WriteBufferSize: writeBufSize,
|
||||
NetDial: func(net, addr string) (net.Conn, error) {
|
||||
return netConn, nil
|
||||
},
|
||||
}
|
||||
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
|
||||
return d.Dial(u.String(), requestHeader)
|
||||
}
|
||||
|
||||
// A Dialer contains options for connecting to WebSocket server.
|
||||
@@ -82,6 +51,12 @@ type Dialer struct {
|
||||
// NetDial is nil, net.Dial is used.
|
||||
NetDial func(network, addr string) (net.Conn, error)
|
||||
|
||||
// Proxy specifies a function to return a proxy for a given
|
||||
// Request. If the function returns a non-nil error, the
|
||||
// request is aborted with the provided error.
|
||||
// If Proxy is nil or returns a nil *URL, no proxy is used.
|
||||
Proxy func(*http.Request) (*url.URL, error)
|
||||
|
||||
// TLSClientConfig specifies the TLS configuration to use with tls.Client.
|
||||
// If nil, the default configuration is used.
|
||||
TLSClientConfig *tls.Config
|
||||
@@ -99,17 +74,15 @@ type Dialer struct {
|
||||
|
||||
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.
|
||||
// parseURL parses the URL.
|
||||
//
|
||||
// This function is a replacement for the standard library url.Parse function.
|
||||
// In Go 1.4 and earlier, url.Parse loses information from the path.
|
||||
func parseURL(s string) (*url.URL, error) {
|
||||
// From the RFC:
|
||||
//
|
||||
// ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
|
||||
// wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
|
||||
//
|
||||
// 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 {
|
||||
@@ -123,15 +96,23 @@ func parseURL(s string) (*url.URL, error) {
|
||||
return nil, errMalformedURL
|
||||
}
|
||||
|
||||
u.Host = s
|
||||
u.Opaque = "/"
|
||||
if i := strings.Index(s, "/"); i >= 0 {
|
||||
u.Host = s[:i]
|
||||
u.Opaque = s[i:]
|
||||
if i := strings.Index(s, "?"); i >= 0 {
|
||||
u.RawQuery = s[i+1:]
|
||||
s = s[:i]
|
||||
}
|
||||
|
||||
if i := strings.Index(s, "/"); i >= 0 {
|
||||
u.Opaque = s[i:]
|
||||
s = s[:i]
|
||||
} else {
|
||||
u.Opaque = "/"
|
||||
}
|
||||
|
||||
u.Host = s
|
||||
|
||||
if strings.Contains(u.Host, "@") {
|
||||
// WebSocket URIs do not contain user information.
|
||||
// Don't bother parsing user information because user information is
|
||||
// not allowed in websocket URIs.
|
||||
return nil, errMalformedURL
|
||||
}
|
||||
|
||||
@@ -144,9 +125,12 @@ func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) {
|
||||
if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") {
|
||||
hostNoPort = hostNoPort[:i]
|
||||
} else {
|
||||
if u.Scheme == "wss" {
|
||||
switch u.Scheme {
|
||||
case "wss":
|
||||
hostPort += ":443"
|
||||
} else {
|
||||
case "https":
|
||||
hostPort += ":443"
|
||||
default:
|
||||
hostPort += ":80"
|
||||
}
|
||||
}
|
||||
@@ -154,7 +138,9 @@ func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) {
|
||||
}
|
||||
|
||||
// DefaultDialer is a dialer with all fields set to the default zero values.
|
||||
var DefaultDialer *Dialer
|
||||
var DefaultDialer = &Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
|
||||
// Dial creates a new client connection. Use requestHeader to specify the
|
||||
// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie).
|
||||
@@ -166,15 +152,91 @@ var DefaultDialer *Dialer
|
||||
// etcetera. The response body may not contain the entire response and does not
|
||||
// need to be closed by the application.
|
||||
func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
|
||||
|
||||
if d == nil {
|
||||
d = &Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
}
|
||||
|
||||
challengeKey, err := generateChallengeKey()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
u, err := parseURL(urlStr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "ws":
|
||||
u.Scheme = "http"
|
||||
case "wss":
|
||||
u.Scheme = "https"
|
||||
default:
|
||||
return nil, nil, errMalformedURL
|
||||
}
|
||||
|
||||
if u.User != nil {
|
||||
// User name and password are not allowed in websocket URIs.
|
||||
return nil, nil, errMalformedURL
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
Method: "GET",
|
||||
URL: u,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: make(http.Header),
|
||||
Host: u.Host,
|
||||
}
|
||||
|
||||
// Set the request headers using the capitalization for names and values in
|
||||
// RFC examples. Although the capitalization shouldn't matter, there are
|
||||
// servers that depend on it. The Header.Set method is not used because the
|
||||
// method canonicalizes the header names.
|
||||
req.Header["Upgrade"] = []string{"websocket"}
|
||||
req.Header["Connection"] = []string{"Upgrade"}
|
||||
req.Header["Sec-WebSocket-Key"] = []string{challengeKey}
|
||||
req.Header["Sec-WebSocket-Version"] = []string{"13"}
|
||||
if len(d.Subprotocols) > 0 {
|
||||
req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")}
|
||||
}
|
||||
for k, vs := range requestHeader {
|
||||
switch {
|
||||
case k == "Host":
|
||||
if len(vs) > 0 {
|
||||
req.Host = vs[0]
|
||||
}
|
||||
case k == "Upgrade" ||
|
||||
k == "Connection" ||
|
||||
k == "Sec-Websocket-Key" ||
|
||||
k == "Sec-Websocket-Version" ||
|
||||
(k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0):
|
||||
return nil, nil, errors.New("websocket: duplicate header not allowed: " + k)
|
||||
default:
|
||||
req.Header[k] = vs
|
||||
}
|
||||
}
|
||||
|
||||
hostPort, hostNoPort := hostPortNoPort(u)
|
||||
|
||||
if d == nil {
|
||||
d = &Dialer{}
|
||||
var proxyURL *url.URL
|
||||
// Check wether the proxy method has been configured
|
||||
if d.Proxy != nil {
|
||||
proxyURL, err = d.Proxy(req)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var targetHostPort string
|
||||
if proxyURL != nil {
|
||||
targetHostPort, _ = hostPortNoPort(proxyURL)
|
||||
} else {
|
||||
targetHostPort = hostPort
|
||||
}
|
||||
|
||||
var deadline time.Time
|
||||
@@ -188,7 +250,7 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
|
||||
netDial = netDialer.Dial
|
||||
}
|
||||
|
||||
netConn, err := netDial("tcp", hostPort)
|
||||
netConn, err := netDial("tcp", targetHostPort)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -203,7 +265,39 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if u.Scheme == "wss" {
|
||||
if proxyURL != nil {
|
||||
connectHeader := make(http.Header)
|
||||
if user := proxyURL.User; user != nil {
|
||||
proxyUser := user.Username()
|
||||
if proxyPassword, passwordSet := user.Password(); passwordSet {
|
||||
credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword))
|
||||
connectHeader.Set("Proxy-Authorization", "Basic "+credential)
|
||||
}
|
||||
}
|
||||
connectReq := &http.Request{
|
||||
Method: "CONNECT",
|
||||
URL: &url.URL{Opaque: hostPort},
|
||||
Host: hostPort,
|
||||
Header: connectHeader,
|
||||
}
|
||||
|
||||
connectReq.Write(netConn)
|
||||
|
||||
// Read response.
|
||||
// Okay to use and discard buffered reader here, because
|
||||
// TLS server will not speak until spoken to.
|
||||
br := bufio.NewReader(netConn)
|
||||
resp, err := http.ReadResponse(br, connectReq)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
f := strings.SplitN(resp.Status, " ", 2)
|
||||
return nil, nil, errors.New(f[1])
|
||||
}
|
||||
}
|
||||
|
||||
if u.Scheme == "https" {
|
||||
cfg := d.TLSClientConfig
|
||||
if cfg == nil {
|
||||
cfg = &tls.Config{ServerName: hostNoPort}
|
||||
@@ -224,44 +318,31 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize)
|
||||
|
||||
if err := req.Write(netConn); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
resp, err := http.ReadResponse(conn.br, req)
|
||||
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
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.StatusCode != 101 ||
|
||||
!strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") ||
|
||||
!strings.EqualFold(resp.Header.Get("Connection"), "upgrade") ||
|
||||
resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) {
|
||||
// Before closing the network connection on return from this
|
||||
// function, slurp up some of the response to aid application
|
||||
// debugging.
|
||||
buf := make([]byte, 1024)
|
||||
n, _ := io.ReadFull(resp.Body, buf)
|
||||
resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n]))
|
||||
return nil, resp, ErrBadHandshake
|
||||
}
|
||||
|
||||
resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
|
||||
conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol")
|
||||
|
||||
netConn.SetDeadline(time.Time{})
|
||||
netConn = nil // to avoid close in defer.
|
||||
|
||||
+139
-11
@@ -7,6 +7,7 @@ package websocket
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
@@ -41,9 +42,16 @@ type cstServer struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
const (
|
||||
cstPath = "/a/b"
|
||||
cstRawQuery = "x=y"
|
||||
cstRequestURI = cstPath + "?" + cstRawQuery
|
||||
)
|
||||
|
||||
func newServer(t *testing.T) *cstServer {
|
||||
var s cstServer
|
||||
s.Server = httptest.NewServer(cstHandler{t})
|
||||
s.Server.URL += cstRequestURI
|
||||
s.URL = makeWsProto(s.Server.URL)
|
||||
return &s
|
||||
}
|
||||
@@ -51,14 +59,20 @@ func newServer(t *testing.T) *cstServer {
|
||||
func newTLSServer(t *testing.T) *cstServer {
|
||||
var s cstServer
|
||||
s.Server = httptest.NewTLSServer(cstHandler{t})
|
||||
s.Server.URL += cstRequestURI
|
||||
s.URL = makeWsProto(s.Server.URL)
|
||||
return &s
|
||||
}
|
||||
|
||||
func (t cstHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
t.Logf("method %s not allowed", r.Method)
|
||||
http.Error(w, "method not allowed", 405)
|
||||
if r.URL.Path != cstPath {
|
||||
t.Logf("path=%v, want %v", r.URL.Path, cstPath)
|
||||
http.Error(w, "bad path", 400)
|
||||
return
|
||||
}
|
||||
if r.URL.RawQuery != cstRawQuery {
|
||||
t.Logf("query=%v, want %v", r.URL.RawQuery, cstRawQuery)
|
||||
http.Error(w, "bad path", 400)
|
||||
return
|
||||
}
|
||||
subprotos := Subprotocols(r)
|
||||
@@ -123,6 +137,85 @@ func sendRecv(t *testing.T, ws *Conn) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDial(t *testing.T) {
|
||||
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
|
||||
surl, _ := url.Parse(s.URL)
|
||||
|
||||
cstDialer.Proxy = http.ProxyURL(surl)
|
||||
|
||||
connect := false
|
||||
origHandler := s.Server.Config.Handler
|
||||
|
||||
// Capture the request Host header.
|
||||
s.Server.Config.Handler = http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "CONNECT" {
|
||||
connect = true
|
||||
w.WriteHeader(200)
|
||||
return
|
||||
}
|
||||
|
||||
if !connect {
|
||||
t.Log("connect not recieved")
|
||||
http.Error(w, "connect not recieved", 405)
|
||||
return
|
||||
}
|
||||
origHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
ws, _, err := cstDialer.Dial(s.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
defer ws.Close()
|
||||
sendRecv(t, ws)
|
||||
|
||||
cstDialer.Proxy = http.ProxyFromEnvironment
|
||||
}
|
||||
|
||||
func TestProxyAuthorizationDial(t *testing.T) {
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
|
||||
surl, _ := url.Parse(s.URL)
|
||||
surl.User = url.UserPassword("username", "password")
|
||||
cstDialer.Proxy = http.ProxyURL(surl)
|
||||
|
||||
connect := false
|
||||
origHandler := s.Server.Config.Handler
|
||||
|
||||
// Capture the request Host header.
|
||||
s.Server.Config.Handler = http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
proxyAuth := r.Header.Get("Proxy-Authorization")
|
||||
expectedProxyAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("username:password"))
|
||||
if r.Method == "CONNECT" && proxyAuth == expectedProxyAuth {
|
||||
connect = true
|
||||
w.WriteHeader(200)
|
||||
return
|
||||
}
|
||||
|
||||
if !connect {
|
||||
t.Log("connect with proxy authorization not recieved")
|
||||
http.Error(w, "connect with proxy authorization not recieved", 405)
|
||||
return
|
||||
}
|
||||
origHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
ws, _, err := cstDialer.Dial(s.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
defer ws.Close()
|
||||
sendRecv(t, ws)
|
||||
|
||||
cstDialer.Proxy = http.ProxyFromEnvironment
|
||||
}
|
||||
|
||||
func TestDial(t *testing.T) {
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
@@ -154,7 +247,7 @@ func TestDialTLS(t *testing.T) {
|
||||
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)
|
||||
ws, _, err := d.Dial("wss://example.com"+cstRequestURI, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
@@ -229,6 +322,45 @@ func TestDialBadOrigin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialBadHeader(t *testing.T) {
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
|
||||
for _, k := range []string{"Upgrade",
|
||||
"Connection",
|
||||
"Sec-Websocket-Key",
|
||||
"Sec-Websocket-Version",
|
||||
"Sec-Websocket-Protocol"} {
|
||||
h := http.Header{}
|
||||
h.Set(k, "bad")
|
||||
ws, _, err := cstDialer.Dial(s.URL, http.Header{"Origin": {"bad"}})
|
||||
if err == nil {
|
||||
ws.Close()
|
||||
t.Errorf("Dial with header %s returned nil", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadMethod(t *testing.T) {
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := cstUpgrader.Upgrade(w, r, nil)
|
||||
if err == nil {
|
||||
t.Errorf("handshake succeeded, expect fail")
|
||||
ws.Close()
|
||||
}
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
resp, err := http.PostForm(s.URL, url.Values{})
|
||||
if err != nil {
|
||||
t.Fatalf("PostForm returned error %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusMethodNotAllowed {
|
||||
t.Errorf("Status = %d, want %d", resp.StatusCode, http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandshake(t *testing.T) {
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
@@ -289,8 +421,8 @@ func TestRespOnBadHandshake(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// If the Host header is specified in `Dial()`, the server must receive it as
|
||||
// the `Host:` header.
|
||||
// TestHostHeader confirms that the host header provided in the call to Dial is
|
||||
// sent to the server.
|
||||
func TestHostHeader(t *testing.T) {
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
@@ -305,16 +437,12 @@ func TestHostHeader(t *testing.T) {
|
||||
origHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
ws, resp, err := cstDialer.Dial(s.URL, http.Header{"Host": {"testhost"}})
|
||||
ws, _, 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)
|
||||
}
|
||||
|
||||
+20
-12
@@ -11,16 +11,19 @@ import (
|
||||
)
|
||||
|
||||
var parseURLTests = []struct {
|
||||
s string
|
||||
u *url.URL
|
||||
s string
|
||||
u *url.URL
|
||||
rui string
|
||||
}{
|
||||
{"ws://example.com/", &url.URL{Scheme: "ws", Host: "example.com", Opaque: "/"}},
|
||||
{"ws://example.com", &url.URL{Scheme: "ws", Host: "example.com", Opaque: "/"}},
|
||||
{"ws://example.com:7777/", &url.URL{Scheme: "ws", Host: "example.com:7777", Opaque: "/"}},
|
||||
{"wss://example.com/", &url.URL{Scheme: "wss", Host: "example.com", Opaque: "/"}},
|
||||
{"wss://example.com/a/b", &url.URL{Scheme: "wss", Host: "example.com", Opaque: "/a/b"}},
|
||||
{"ss://example.com/a/b", nil},
|
||||
{"ws://webmaster@example.com/", nil},
|
||||
{"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"}, "/a/b"},
|
||||
{"ss://example.com/a/b", nil, ""},
|
||||
{"ws://webmaster@example.com/", nil, ""},
|
||||
{"wss://example.com/a/b?x=y", &url.URL{Scheme: "wss", Host: "example.com", Opaque: "/a/b", RawQuery: "x=y"}, "/a/b?x=y"},
|
||||
{"wss://example.com?x=y", &url.URL{Scheme: "wss", Host: "example.com", Opaque: "/", RawQuery: "x=y"}, "/?x=y"},
|
||||
}
|
||||
|
||||
func TestParseURL(t *testing.T) {
|
||||
@@ -30,14 +33,19 @@ func TestParseURL(t *testing.T) {
|
||||
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)
|
||||
if tt.u == nil {
|
||||
if 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)
|
||||
t.Errorf("parseURL(%q) = %v, want %v", tt.s, u, tt.u)
|
||||
continue
|
||||
}
|
||||
if u.RequestURI() != tt.rui {
|
||||
t.Errorf("parseURL(%q).RequestURI() = %v, want %v", tt.s, u.RequestURI(), tt.rui)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+118
-28
@@ -88,19 +88,82 @@ 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
|
||||
// CloseError represents close frame.
|
||||
type CloseError struct {
|
||||
|
||||
// Code is defined in RFC 6455, section 11.7.
|
||||
Code int
|
||||
|
||||
// Text is the optional text payload.
|
||||
Text string
|
||||
}
|
||||
|
||||
func (e *closeError) Error() string {
|
||||
return "websocket: close " + strconv.Itoa(e.code) + " " + e.text
|
||||
func (e *CloseError) Error() string {
|
||||
s := []byte("websocket: close ")
|
||||
s = strconv.AppendInt(s, int64(e.Code), 10)
|
||||
switch e.Code {
|
||||
case CloseNormalClosure:
|
||||
s = append(s, " (normal)"...)
|
||||
case CloseGoingAway:
|
||||
s = append(s, " (going away)"...)
|
||||
case CloseProtocolError:
|
||||
s = append(s, " (protocol error)"...)
|
||||
case CloseUnsupportedData:
|
||||
s = append(s, " (unsupported data)"...)
|
||||
case CloseNoStatusReceived:
|
||||
s = append(s, " (no status)"...)
|
||||
case CloseAbnormalClosure:
|
||||
s = append(s, " (abnormal closure)"...)
|
||||
case CloseInvalidFramePayloadData:
|
||||
s = append(s, " (invalid payload data)"...)
|
||||
case ClosePolicyViolation:
|
||||
s = append(s, " (policy violation)"...)
|
||||
case CloseMessageTooBig:
|
||||
s = append(s, " (message too big)"...)
|
||||
case CloseMandatoryExtension:
|
||||
s = append(s, " (mandatory extension missing)"...)
|
||||
case CloseInternalServerErr:
|
||||
s = append(s, " (internal server error)"...)
|
||||
case CloseTLSHandshake:
|
||||
s = append(s, " (TLS handshake error)"...)
|
||||
}
|
||||
if e.Text != "" {
|
||||
s = append(s, ": "...)
|
||||
s = append(s, e.Text...)
|
||||
}
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// IsCloseError returns boolean indicating whether the error is a *CloseError
|
||||
// with one of the specified codes.
|
||||
func IsCloseError(err error, codes ...int) bool {
|
||||
if e, ok := err.(*CloseError); ok {
|
||||
for _, code := range codes {
|
||||
if e.Code == code {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsUnexpectedCloseError returns boolean indicating whether the error is a
|
||||
// *CloseError with a code not in the list of expected codes.
|
||||
func IsUnexpectedCloseError(err error, expectedCodes ...int) bool {
|
||||
if e, ok := err.(*CloseError); ok {
|
||||
for _, code := range expectedCodes {
|
||||
if e.Code == code {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
errWriteTimeout = &netError{msg: "websocket: write timeout", timeout: true}
|
||||
errUnexpectedEOF = &closeError{code: CloseAbnormalClosure, text: io.ErrUnexpectedEOF.Error()}
|
||||
errWriteTimeout = &netError{msg: "websocket: write timeout", timeout: true, temporary: 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")
|
||||
@@ -151,6 +214,7 @@ type Conn struct {
|
||||
writeFrameType int // type of the current frame.
|
||||
writeSeq int // incremented to invalidate message writers.
|
||||
writeDeadline time.Time
|
||||
isWriting bool // for best-effort concurrent write detection
|
||||
|
||||
// Read fields
|
||||
readErr error
|
||||
@@ -164,6 +228,7 @@ type Conn struct {
|
||||
readMaskKey [4]byte
|
||||
handlePong func(string) error
|
||||
handlePing func(string) error
|
||||
readErrCount int
|
||||
}
|
||||
|
||||
func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn {
|
||||
@@ -296,7 +361,7 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er
|
||||
if n != 0 && n != len(buf) {
|
||||
c.conn.Close()
|
||||
}
|
||||
return err
|
||||
return hideTempErr(err)
|
||||
}
|
||||
|
||||
// NextWriter returns a writer for the next message to send. The writer's
|
||||
@@ -304,9 +369,6 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er
|
||||
//
|
||||
// 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
|
||||
@@ -380,9 +442,22 @@ func (c *Conn) flushFrame(final bool, extra []byte) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Write the buffers to the connection.
|
||||
// Write the buffers to the connection with best-effort detection of
|
||||
// concurrent writes. See the concurrency section in the package
|
||||
// documentation for more info.
|
||||
|
||||
if c.isWriting {
|
||||
panic("concurrent write to websocket connection")
|
||||
}
|
||||
c.isWriting = true
|
||||
|
||||
c.writeErr = c.write(c.writeFrameType, c.writeDeadline, c.writeBuf[framePos:c.writePos], extra)
|
||||
|
||||
if !c.isWriting {
|
||||
panic("concurrent write to websocket connection")
|
||||
}
|
||||
c.isWriting = false
|
||||
|
||||
// Setup for next frame.
|
||||
c.writePos = maxFrameHeaderSize
|
||||
c.writeFrameType = continuationFrame
|
||||
@@ -666,19 +741,16 @@ func (c *Conn) advanceFrame() (int, error) {
|
||||
return noFrame, err
|
||||
}
|
||||
case CloseMessage:
|
||||
c.WriteControl(CloseMessage, []byte{}, time.Now().Add(writeWait))
|
||||
echoMessage := []byte{}
|
||||
closeCode := CloseNoStatusReceived
|
||||
closeText := ""
|
||||
if len(payload) >= 2 {
|
||||
echoMessage = 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}
|
||||
}
|
||||
c.WriteControl(CloseMessage, echoMessage, time.Now().Add(writeWait))
|
||||
return noFrame, &CloseError{Code: closeCode, Text: closeText}
|
||||
}
|
||||
|
||||
return frameType, nil
|
||||
@@ -695,8 +767,10 @@ func (c *Conn) handleProtocolError(message string) error {
|
||||
// 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.
|
||||
// Applications must break out of the application's read loop when this method
|
||||
// returns a non-nil error value. Errors returned from this method are
|
||||
// permanent. Once this method returns a non-nil error, all subsequent calls to
|
||||
// this method return the same error.
|
||||
func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {
|
||||
|
||||
c.readSeq++
|
||||
@@ -712,6 +786,15 @@ func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {
|
||||
return frameType, messageReader{c, c.readSeq}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Applications that do handle the error returned from this method spin in
|
||||
// tight loop on connection failure. To help application developers detect
|
||||
// this error, panic on repeated reads to the failed connection.
|
||||
c.readErrCount++
|
||||
if c.readErrCount >= 1000 {
|
||||
panic("repeated read on failed websocket connection")
|
||||
}
|
||||
|
||||
return noFrame, nil, c.readErr
|
||||
}
|
||||
|
||||
@@ -790,20 +873,27 @@ func (c *Conn) SetReadLimit(limit int64) {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// The appData argument to h is the PING frame application data. The default
|
||||
// ping handler sends a pong to the peer.
|
||||
func (c *Conn) SetPingHandler(h func(appData string) error) {
|
||||
if h == nil {
|
||||
h = func(message string) error {
|
||||
c.WriteControl(PongMessage, []byte(message), time.Now().Add(writeWait))
|
||||
return nil
|
||||
err := c.WriteControl(PongMessage, []byte(message), time.Now().Add(writeWait))
|
||||
if err == ErrCloseSent {
|
||||
return nil
|
||||
} else if e, ok := err.(net.Error); ok && e.Temporary() {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
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) {
|
||||
// The appData argument to h is the PONG frame application data. The default
|
||||
// pong handler does nothing.
|
||||
func (c *Conn) SetPongHandler(h func(appData string) error) {
|
||||
if h == nil {
|
||||
h = func(string) error { return nil }
|
||||
}
|
||||
|
||||
+134
-5
@@ -5,11 +5,14 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
"testing/iotest"
|
||||
"time"
|
||||
@@ -146,13 +149,15 @@ func TestControl(t *testing.T) {
|
||||
func TestCloseBeforeFinalFrame(t *testing.T) {
|
||||
const bufSize = 512
|
||||
|
||||
expectedErr := &CloseError{Code: CloseNormalClosure, Text: "hello"}
|
||||
|
||||
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))
|
||||
wc.WriteControl(CloseMessage, FormatCloseMessage(expectedErr.Code, expectedErr.Text), time.Now().Add(10*time.Second))
|
||||
w.Close()
|
||||
|
||||
op, r, err := rc.NextReader()
|
||||
@@ -160,12 +165,12 @@ func TestCloseBeforeFinalFrame(t *testing.T) {
|
||||
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)
|
||||
if !reflect.DeepEqual(err, expectedErr) {
|
||||
t.Fatalf("io.Copy() returned %v, want %v", err, expectedErr)
|
||||
}
|
||||
_, _, err = rc.NextReader()
|
||||
if err != io.EOF {
|
||||
t.Fatalf("NextReader() returned %v, want %v", err, io.EOF)
|
||||
if !reflect.DeepEqual(err, expectedErr) {
|
||||
t.Fatalf("NextReader() returned %v, want %v", err, expectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,3 +241,127 @@ func TestUnderlyingConn(t *testing.T) {
|
||||
t.Fatalf("Underlying conn is not what it should be.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBufioReadBytes(t *testing.T) {
|
||||
|
||||
// Test calling bufio.ReadBytes for value longer than read buffer size.
|
||||
|
||||
m := make([]byte, 512)
|
||||
m[len(m)-1] = '\n'
|
||||
|
||||
var b1, b2 bytes.Buffer
|
||||
wc := newConn(fakeNetConn{Reader: nil, Writer: &b1}, false, len(m)+64, len(m)+64)
|
||||
rc := newConn(fakeNetConn{Reader: &b1, Writer: &b2}, true, len(m)-64, len(m)-64)
|
||||
|
||||
w, _ := wc.NextWriter(BinaryMessage)
|
||||
w.Write(m)
|
||||
w.Close()
|
||||
|
||||
op, r, err := rc.NextReader()
|
||||
if op != BinaryMessage || err != nil {
|
||||
t.Fatalf("NextReader() returned %d, %v", op, err)
|
||||
}
|
||||
|
||||
br := bufio.NewReader(r)
|
||||
p, err := br.ReadBytes('\n')
|
||||
if err != nil {
|
||||
t.Fatalf("ReadBytes() returned %v", err)
|
||||
}
|
||||
if len(p) != len(m) {
|
||||
t.Fatalf("read returnd %d bytes, want %d bytes", len(p), len(m))
|
||||
}
|
||||
}
|
||||
|
||||
var closeErrorTests = []struct {
|
||||
err error
|
||||
codes []int
|
||||
ok bool
|
||||
}{
|
||||
{&CloseError{Code: CloseNormalClosure}, []int{CloseNormalClosure}, true},
|
||||
{&CloseError{Code: CloseNormalClosure}, []int{CloseNoStatusReceived}, false},
|
||||
{&CloseError{Code: CloseNormalClosure}, []int{CloseNoStatusReceived, CloseNormalClosure}, true},
|
||||
{errors.New("hello"), []int{CloseNormalClosure}, false},
|
||||
}
|
||||
|
||||
func TestCloseError(t *testing.T) {
|
||||
for _, tt := range closeErrorTests {
|
||||
ok := IsCloseError(tt.err, tt.codes...)
|
||||
if ok != tt.ok {
|
||||
t.Errorf("IsCloseError(%#v, %#v) returned %v, want %v", tt.err, tt.codes, ok, tt.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var unexpectedCloseErrorTests = []struct {
|
||||
err error
|
||||
codes []int
|
||||
ok bool
|
||||
}{
|
||||
{&CloseError{Code: CloseNormalClosure}, []int{CloseNormalClosure}, false},
|
||||
{&CloseError{Code: CloseNormalClosure}, []int{CloseNoStatusReceived}, true},
|
||||
{&CloseError{Code: CloseNormalClosure}, []int{CloseNoStatusReceived, CloseNormalClosure}, false},
|
||||
{errors.New("hello"), []int{CloseNormalClosure}, false},
|
||||
}
|
||||
|
||||
func TestUnexpectedCloseErrors(t *testing.T) {
|
||||
for _, tt := range unexpectedCloseErrorTests {
|
||||
ok := IsUnexpectedCloseError(tt.err, tt.codes...)
|
||||
if ok != tt.ok {
|
||||
t.Errorf("IsUnexpectedCloseError(%#v, %#v) returned %v, want %v", tt.err, tt.codes, ok, tt.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type blockingWriter struct {
|
||||
c1, c2 chan struct{}
|
||||
}
|
||||
|
||||
func (w blockingWriter) Write(p []byte) (int, error) {
|
||||
// Allow main to continue
|
||||
close(w.c1)
|
||||
// Wait for panic in main
|
||||
<-w.c2
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func TestConcurrentWritePanic(t *testing.T) {
|
||||
w := blockingWriter{make(chan struct{}), make(chan struct{})}
|
||||
c := newConn(fakeNetConn{Reader: nil, Writer: w}, false, 1024, 1024)
|
||||
go func() {
|
||||
c.WriteMessage(TextMessage, []byte{})
|
||||
}()
|
||||
|
||||
// wait for goroutine to block in write.
|
||||
<-w.c1
|
||||
|
||||
defer func() {
|
||||
close(w.c2)
|
||||
if v := recover(); v != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
c.WriteMessage(TextMessage, []byte{})
|
||||
t.Fatal("should not get here")
|
||||
}
|
||||
|
||||
type failingReader struct{}
|
||||
|
||||
func (r failingReader) Read(p []byte) (int, error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func TestFailedConnectionReadPanic(t *testing.T) {
|
||||
c := newConn(fakeNetConn{Reader: failingReader{}, Writer: nil}, false, 1024, 1024)
|
||||
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
for i := 0; i < 20000; i++ {
|
||||
c.ReadMessage()
|
||||
}
|
||||
t.Fatal("should not get here")
|
||||
}
|
||||
|
||||
+25
-25
@@ -46,8 +46,7 @@
|
||||
// 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:
|
||||
// shows how to echo messages using the NextWriter and NextReader methods:
|
||||
//
|
||||
// for {
|
||||
// messageType, r, err := conn.NextReader()
|
||||
@@ -86,31 +85,19 @@
|
||||
// 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 ping and pong messages by invoking callback
|
||||
// functions set with SetPingHandler and SetPongHandler methods. The default
|
||||
// ping handler sends a pong to the client. The callback functions can be
|
||||
// invoked from the NextReader, ReadMessage or the message Read method.
|
||||
//
|
||||
// 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
|
||||
// Connections handle received close messages by sending a close message to the
|
||||
// peer and returning a *CloseError from the the NextReader, ReadMessage or the
|
||||
// message Read method.
|
||||
//
|
||||
// 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:
|
||||
// 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 {
|
||||
@@ -121,6 +108,19 @@
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Concurrency
|
||||
//
|
||||
// Connections support one concurrent reader and one concurrent writer.
|
||||
//
|
||||
// Applications are responsible for ensuring that no more than one goroutine
|
||||
// calls the write methods (NextWriter, SetWriteDeadline, WriteMessage,
|
||||
// WriteJSON) concurrently and that no more than one goroutine calls the read
|
||||
// methods (NextReader, SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler,
|
||||
// SetPingHandler) concurrently.
|
||||
//
|
||||
// The Close and WriteControl methods can be called concurrently with all other
|
||||
// methods.
|
||||
//
|
||||
// Origin Considerations
|
||||
//
|
||||
// Web browsers allow Javascript applications to open a WebSocket connection to
|
||||
@@ -138,9 +138,9 @@
|
||||
// An application can allow connections from any origin by specifying a
|
||||
// function that always returns true:
|
||||
//
|
||||
// var upgrader = websocket.Upgrader{
|
||||
// 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
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright 2015 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_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// The websocket.IsUnexpectedCloseError function is useful for identifying
|
||||
// application and protocol errors.
|
||||
//
|
||||
// This server application works with a client application running in the
|
||||
// browser. The client application does not explicitly close the websocket. The
|
||||
// only expected close message from the client has the code
|
||||
// websocket.CloseGoingAway. All other other close messages are likely the
|
||||
// result of an application or protocol error and are logged to aid debugging.
|
||||
func ExampleIsUnexpectedCloseError(err error, c *websocket.Conn, req *http.Request) {
|
||||
for {
|
||||
messageType, p, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
|
||||
log.Printf("error: %v, user-agent: %v", err, req.Header.Get("User-Agent"))
|
||||
}
|
||||
return
|
||||
}
|
||||
processMesage(messageType, p)
|
||||
}
|
||||
}
|
||||
|
||||
func processMesage(mt int, p []byte) {}
|
||||
|
||||
// TestX prevents godoc from showing this entire file in the example. Remove
|
||||
// this function when a second example is added.
|
||||
func TestX(t *testing.T) {}
|
||||
Generated
Vendored
Executable → Regular
Generated
Vendored
Executable → Regular
Generated
Vendored
Executable → Regular
+1
@@ -17,3 +17,4 @@ using the following commands.
|
||||
$ cd `go list -f '{{.Dir}}' github.com/gorilla/websocket/examples/chat`
|
||||
$ go run *.go
|
||||
|
||||
To use the chat example, open http://localhost:8080/ in your browser.
|
||||
|
||||
+4
-5
@@ -51,6 +51,9 @@ func (c *connection) readPump() {
|
||||
for {
|
||||
_, message, err := c.ws.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
|
||||
log.Printf("error: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
h.broadcast <- message
|
||||
@@ -88,12 +91,8 @@ func (c *connection) writePump() {
|
||||
}
|
||||
}
|
||||
|
||||
// serverWs handles websocket requests from the peer.
|
||||
// serveWs 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)
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Command example
|
||||
|
||||
This example connects a websocket connection to stdin and stdout of a command.
|
||||
Received messages are written to stdin followed by a `\n`. Each line read from
|
||||
from standard out is sent as a message to the client.
|
||||
|
||||
$ go get github.com/gorilla/websocket
|
||||
$ cd `go list -f '{{.Dir}}' github.com/gorilla/websocket/examples/command`
|
||||
$ go run main.go <command and arguments to run>
|
||||
# Open http://localhost:8080/ .
|
||||
|
||||
Try the following commands.
|
||||
|
||||
# Echo sent messages to the output area.
|
||||
$ go run main.go cat
|
||||
|
||||
# Run a shell.Try sending "ls" and "cat main.go".
|
||||
$ go run main.go sh
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Command 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($("<pre/>").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;
|
||||
}
|
||||
|
||||
#log pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#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>
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
// Copyright 2015 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 (
|
||||
"bufio"
|
||||
"flag"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var (
|
||||
addr = flag.String("addr", "127.0.0.1:8080", "http service address")
|
||||
cmdPath string
|
||||
homeTempl = template.Must(template.ParseFiles("home.html"))
|
||||
)
|
||||
|
||||
const (
|
||||
// Time allowed to write a message to the peer.
|
||||
writeWait = 10 * time.Second
|
||||
|
||||
// Maximum message size allowed from peer.
|
||||
maxMessageSize = 8192
|
||||
|
||||
// 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
|
||||
)
|
||||
|
||||
func pumpStdin(ws *websocket.Conn, w io.Writer) {
|
||||
defer ws.Close()
|
||||
ws.SetReadLimit(maxMessageSize)
|
||||
ws.SetReadDeadline(time.Now().Add(pongWait))
|
||||
ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
||||
for {
|
||||
_, message, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
message = append(message, '\n')
|
||||
if _, err := w.Write(message); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pumpStdout(ws *websocket.Conn, r io.Reader, done chan struct{}) {
|
||||
defer func() {
|
||||
ws.Close()
|
||||
close(done)
|
||||
}()
|
||||
s := bufio.NewScanner(r)
|
||||
for s.Scan() {
|
||||
ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if err := ws.WriteMessage(websocket.TextMessage, s.Bytes()); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if s.Err() != nil {
|
||||
log.Println("scan:", s.Err())
|
||||
}
|
||||
}
|
||||
|
||||
func ping(ws *websocket.Conn, done chan struct{}) {
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeWait)); err != nil {
|
||||
log.Println("ping:", err)
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func internalError(ws *websocket.Conn, msg string, err error) {
|
||||
log.Println(msg, err)
|
||||
ws.WriteMessage(websocket.TextMessage, []byte("Internal server error."))
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{}
|
||||
|
||||
func serveWs(w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println("upgrade:", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer ws.Close()
|
||||
|
||||
outr, outw, err := os.Pipe()
|
||||
if err != nil {
|
||||
internalError(ws, "stdout:", err)
|
||||
return
|
||||
}
|
||||
defer outr.Close()
|
||||
defer outw.Close()
|
||||
|
||||
inr, inw, err := os.Pipe()
|
||||
if err != nil {
|
||||
internalError(ws, "stdin:", err)
|
||||
return
|
||||
}
|
||||
defer inr.Close()
|
||||
defer inw.Close()
|
||||
|
||||
proc, err := os.StartProcess(cmdPath, flag.Args(), &os.ProcAttr{
|
||||
Files: []*os.File{inr, outw, outw},
|
||||
})
|
||||
if err != nil {
|
||||
internalError(ws, "start:", err)
|
||||
return
|
||||
}
|
||||
|
||||
inr.Close()
|
||||
outw.Close()
|
||||
|
||||
stdoutDone := make(chan struct{})
|
||||
go pumpStdout(ws, outr, stdoutDone)
|
||||
go ping(ws, stdoutDone)
|
||||
|
||||
pumpStdin(ws, inw)
|
||||
|
||||
// Some commands will exit when stdin is closed.
|
||||
inw.Close()
|
||||
|
||||
// Other commands need a bonk on the head.
|
||||
if err := proc.Signal(os.Interrupt); err != nil {
|
||||
log.Println("inter:", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-stdoutDone:
|
||||
case <-time.After(time.Second):
|
||||
// A bigger bonk on the head.
|
||||
if err := proc.Signal(os.Kill); err != nil {
|
||||
log.Println("term:", err)
|
||||
}
|
||||
<-stdoutDone
|
||||
}
|
||||
|
||||
if _, err := proc.Wait(); err != nil {
|
||||
log.Println("wait:", err)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
if len(flag.Args()) < 1 {
|
||||
log.Fatal("must specify at least one argument")
|
||||
}
|
||||
var err error
|
||||
cmdPath, err = exec.LookPath(flag.Args()[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
http.HandleFunc("/", serveHome)
|
||||
http.HandleFunc("/ws", serveWs)
|
||||
log.Fatal(http.ListenAndServe(*addr, nil))
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# Client and server example
|
||||
|
||||
This example shows a simple client and server.
|
||||
|
||||
The server echoes messages sent to it. The client sends a message every second
|
||||
and prints all messages received.
|
||||
|
||||
To run the example, start the server:
|
||||
|
||||
$ go run server.go
|
||||
|
||||
Next, start the client:
|
||||
|
||||
$ go run client.go
|
||||
|
||||
The server includes a simple web client. To use the client, open
|
||||
http://127.0.0.1:8080 in the browser and follow the instructions on the page.
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var addr = flag.String("addr", "localhost:8080", "http service address")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
log.SetFlags(0)
|
||||
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, os.Interrupt)
|
||||
|
||||
u := url.URL{Scheme: "ws", Host: *addr, Path: "/echo"}
|
||||
log.Printf("connecting to %s", u.String())
|
||||
|
||||
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
|
||||
if err != nil {
|
||||
log.Fatal("dial:", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer c.Close()
|
||||
defer close(done)
|
||||
for {
|
||||
_, message, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("read:", err)
|
||||
return
|
||||
}
|
||||
log.Printf("recv: %s", message)
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case t := <-ticker.C:
|
||||
err := c.WriteMessage(websocket.TextMessage, []byte(t.String()))
|
||||
if err != nil {
|
||||
log.Println("write:", err)
|
||||
return
|
||||
}
|
||||
case <-interrupt:
|
||||
log.Println("interrupt")
|
||||
// To cleanly close a connection, a client should send a close
|
||||
// frame and wait for the server to close the connection.
|
||||
err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
||||
if err != nil {
|
||||
log.Println("write close:", err)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var addr = flag.String("addr", "localhost:8080", "http service address")
|
||||
|
||||
var upgrader = websocket.Upgrader{} // use default options
|
||||
|
||||
func echo(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Print("upgrade:", err)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
for {
|
||||
mt, message, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("read:", err)
|
||||
break
|
||||
}
|
||||
log.Printf("recv: %s", message)
|
||||
err = c.WriteMessage(mt, message)
|
||||
if err != nil {
|
||||
log.Println("write:", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func home(w http.ResponseWriter, r *http.Request) {
|
||||
homeTemplate.Execute(w, "ws://"+r.Host+"/echo")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
log.SetFlags(0)
|
||||
http.HandleFunc("/echo", echo)
|
||||
http.HandleFunc("/", home)
|
||||
log.Fatal(http.ListenAndServe(*addr, nil))
|
||||
}
|
||||
|
||||
var homeTemplate = template.Must(template.New("").Parse(`
|
||||
<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script>
|
||||
window.addEventListener("load", function(evt) {
|
||||
|
||||
var output = document.getElementById("output");
|
||||
var input = document.getElementById("input");
|
||||
var ws;
|
||||
|
||||
var print = function(message) {
|
||||
var d = document.createElement("div");
|
||||
d.innerHTML = message;
|
||||
output.appendChild(d);
|
||||
};
|
||||
|
||||
document.getElementById("open").onclick = function(evt) {
|
||||
if (ws) {
|
||||
return false;
|
||||
}
|
||||
ws = new WebSocket("{{.}}");
|
||||
ws.onopen = function(evt) {
|
||||
print("OPEN");
|
||||
}
|
||||
ws.onclose = function(evt) {
|
||||
print("CLOSE");
|
||||
ws = null;
|
||||
}
|
||||
ws.onmessage = function(evt) {
|
||||
print("RESPONSE: " + evt.data);
|
||||
}
|
||||
ws.onerror = function(evt) {
|
||||
print("ERROR: " + evt.data);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
document.getElementById("send").onclick = function(evt) {
|
||||
if (!ws) {
|
||||
return false;
|
||||
}
|
||||
print("SEND: " + input.value);
|
||||
ws.send(input.value);
|
||||
return false;
|
||||
};
|
||||
|
||||
document.getElementById("close").onclick = function(evt) {
|
||||
if (!ws) {
|
||||
return false;
|
||||
}
|
||||
ws.close();
|
||||
return false;
|
||||
};
|
||||
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<table>
|
||||
<tr><td valign="top" width="50%">
|
||||
<p>Click "Open" to create a connection to the server,
|
||||
"Send" to send a message to the server and "Close" to close the connection.
|
||||
You can change the message and send multiple times.
|
||||
<p>
|
||||
<form>
|
||||
<button id="open">Open</button>
|
||||
<button id="close">Close</button>
|
||||
<p><input id="input" type="text" value="Hello world!">
|
||||
<button id="send">Send</button>
|
||||
</form>
|
||||
</td><td valign="top" width="50%">
|
||||
<div id="output"></div>
|
||||
</td></tr></table>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
Generated
Vendored
Executable → Regular
Generated
Vendored
Executable → Regular
+1
-3
@@ -48,9 +48,7 @@ func (c *Conn) ReadJSON(v interface{}) error {
|
||||
}
|
||||
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.
|
||||
// One value is expected in the message.
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return err
|
||||
|
||||
+2
-2
@@ -38,7 +38,7 @@ func TestJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartialJsonRead(t *testing.T) {
|
||||
func TestPartialJSONRead(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
c := fakeNetConn{&buf, &buf}
|
||||
wc := newConn(c, true, 1024, 1024)
|
||||
@@ -87,7 +87,7 @@ func TestPartialJsonRead(t *testing.T) {
|
||||
}
|
||||
|
||||
err = rc.ReadJSON(&v)
|
||||
if err != io.EOF {
|
||||
if _, ok := err.(*CloseError); !ok {
|
||||
t.Error("final", err)
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -92,7 +92,13 @@ func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header
|
||||
// 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).
|
||||
//
|
||||
// If the upgrade fails, then Upgrade replies to the client with an HTTP error
|
||||
// response.
|
||||
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {
|
||||
if r.Method != "GET" {
|
||||
return u.returnError(w, r, http.StatusMethodNotAllowed, "websocket: method not GET")
|
||||
}
|
||||
if values := r.Header["Sec-Websocket-Version"]; len(values) == 0 || values[0] != "13" {
|
||||
return u.returnError(w, r, http.StatusBadRequest, "websocket: version != 13")
|
||||
}
|
||||
|
||||
+1
Submodule vendor/github.com/pusher/pusher-http-go added at 8d4ffe1576
Reference in New Issue
Block a user