Compare commits
120 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 | |||
| 56da1dea01 | |||
| 1971da652f | |||
| f81a365e0c | |||
| 1de947cedc | |||
| 45cffc17a8 | |||
| 59abf6ac5b | |||
| cbb34a928f | |||
| 403138e28f | |||
| 8dea85e71c | |||
| 3fc072373a | |||
| cad813e729 | |||
| 1054411b60 | |||
| b972058622 | |||
| f28e9da982 | |||
| 341909e600 | |||
| 0bf660183b | |||
| c697311be3 | |||
| f910433853 | |||
| 883376b0c8 | |||
| 8dbd3ccbea | |||
| 228ed284d9 | |||
| 10e6048d11 | |||
| e2d07e9760 | |||
| 485bc01abe | |||
| 7b578c4fb4 | |||
| 03607062aa | |||
| 846231f5f3 | |||
| 201cba86a9 | |||
| d74426127c | |||
| cd2e58d0c5 | |||
| eca114dcf1 | |||
| a3465f332e | |||
| e9bdc0859e | |||
| b426bc2534 | |||
| cd1fd8995d | |||
| c7f757e517 | |||
| 8d671751b3 | |||
| 018391ad66 | |||
| 8586b64bda | |||
| ef756d789a | |||
| 67676742a9 | |||
| 5218fb2ca4 | |||
| d1473197b8 | |||
| b93da8ccc8 | |||
| 8def052b9d | |||
| 0f5d238065 | |||
| 7367f06ccb | |||
| a4e08d1e85 | |||
| f6e1993eb6 | |||
| 1dc3105155 | |||
| a51629f7b5 | |||
| 0d4e9742b9 | |||
| 7032e69ffb | |||
| edf4b421bf | |||
| 874c07267f | |||
| 414c1d98ee | |||
| 5043b05c7b | |||
| 5695bb445d | |||
| dd78126371 | |||
| 431812f695 | |||
| 3999ad5aa0 | |||
| 2394f167a5 | |||
| 0e3440bc35 | |||
| 59457cc734 | |||
| 1f5ec97f57 | |||
| 4b08b0c658 | |||
| 7e91fbe35d | |||
| 2cb8b13dc6 | |||
| 0aec168ab3 | |||
| be66978479 | |||
| c125bc7206 | |||
| dc28fa90ff | |||
| 3a9b786a46 | |||
| ef1070887b | |||
| cadc72d482 | |||
| 71d5099ac3 | |||
| 387a2ae28d | |||
| e2412827cf | |||
| ff2fcf3c54 | |||
| e1b7bd8252 |
+161
@@ -0,0 +1,161 @@
|
||||
# Created by https://www.gitignore.io
|
||||
|
||||
### Go ###
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
|
||||
### SublimeText ###
|
||||
# cache files for sublime text
|
||||
*.tmlanguage.cache
|
||||
*.tmPreferences.cache
|
||||
*.stTheme.cache
|
||||
|
||||
# workspace files are user-specific
|
||||
*.sublime-workspace
|
||||
|
||||
# project files should be checked into the repository, unless a significant
|
||||
# proportion of contributors will probably not be using SublimeText
|
||||
# *.sublime-project
|
||||
|
||||
# sftp configuration file
|
||||
sftp-config.json
|
||||
|
||||
|
||||
### OSX ###
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear on external disk
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
|
||||
### Intellij ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
|
||||
|
||||
*.iml
|
||||
|
||||
## Directory-based project format:
|
||||
.idea/
|
||||
# if you remove the above rule, at least ignore the following:
|
||||
|
||||
# User-specific stuff:
|
||||
# .idea/workspace.xml
|
||||
# .idea/tasks.xml
|
||||
# .idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
# .idea/dataSources.ids
|
||||
# .idea/dataSources.xml
|
||||
# .idea/sqlDataSources.xml
|
||||
# .idea/dynamic.xml
|
||||
# .idea/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
# .idea/gradle.xml
|
||||
# .idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
# .idea/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
|
||||
### Emacs ###
|
||||
# -*- mode: gitignore; -*-
|
||||
*~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
*.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
flymake*
|
||||
|
||||
# Org-mode
|
||||
.org-id-locations
|
||||
*_archive
|
||||
|
||||
# flymake-mode
|
||||
*_flymake.*
|
||||
|
||||
# eshell files
|
||||
/eshell/history
|
||||
/eshell/lastdir
|
||||
|
||||
# elpa packages
|
||||
/elpa/
|
||||
|
||||
# reftex files
|
||||
*.rel
|
||||
|
||||
# AUCTeX auto folder
|
||||
/auto/
|
||||
|
||||
# cask packages
|
||||
.cask/
|
||||
|
||||
# Project Specific
|
||||
|
||||
ignore_http/*
|
||||
config.json
|
||||
*.pem
|
||||
build
|
||||
.vscode/*
|
||||
debug
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2015 Claudemiro Alves Feitosa Neto <dimiro1@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,157 @@
|
||||
[](http://goreportcard.com/report/dimiro1/ipe)
|
||||
|
||||
# IPÊ
|
||||
|
||||
This software is written in Go - the WYSIWYG lang
|
||||
|
||||
# Why I wrote this software?
|
||||
|
||||
1. I wanted to learn Go and I needed a non trivial application;
|
||||
2. I use Pusher in some projects;
|
||||
3. I really like Pusher;
|
||||
|
||||
# Features
|
||||
|
||||
* Public Channels;
|
||||
* Private Channels;
|
||||
* Presence Channels;
|
||||
* Web Hooks;
|
||||
* Client events;
|
||||
* Complete REST API;
|
||||
* Easy instalation;
|
||||
* A single binary without dependencies;
|
||||
* Easy configuration;
|
||||
* Protocol version 7;
|
||||
* Multiple apps in the same instance;
|
||||
* Drop in replacement for pusher server;
|
||||
|
||||
# Download pre built binaries
|
||||
|
||||
You can download pre built binaries from the [releases tab](https://github.com/dimiro1/ipe/releases).
|
||||
|
||||
I do not have a Windows machine, so I can only distribute binaries for amd64 linux and amd64 darwin.
|
||||
|
||||
# Building
|
||||
|
||||
```console
|
||||
$ go get github.com/dimiro1/ipe
|
||||
```
|
||||
|
||||
or simply
|
||||
|
||||
```console
|
||||
$ go install github.com/dimiro1/ipe
|
||||
```
|
||||
|
||||
# How to configure?
|
||||
|
||||
## The server
|
||||
|
||||
```javascript
|
||||
{
|
||||
"Host": ":8080", // Required
|
||||
"SSL": false, // Required but can be false
|
||||
"SSLHost": ":4433", // Required if SSL is true
|
||||
"SSLKeyFile": "A key.pem file", // Required if SSL is true
|
||||
"SSLCertFile": "A cert.pem file", // Required if SSL is true
|
||||
"Apps": [ // Required, A Json arrays with multiple apps
|
||||
{
|
||||
"ApplicationDisabled": false, // Required but can be false
|
||||
"Secret": "A really secret random string", // Required
|
||||
"Key": "A random Key string", // Required
|
||||
"OnlySSL": false, // Required but can be false
|
||||
"Name": "The app name", // Required
|
||||
"AppID": "The app ID", // Required
|
||||
"UserEvents": true, // Required but can be false
|
||||
"WebHooks": true, // Required but can be false
|
||||
"URLWebHook": "Some URL to send webhooks" // Required if WebHooks is true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Libraries
|
||||
|
||||
### Client javascript library
|
||||
|
||||
```javascript
|
||||
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"]
|
||||
});
|
||||
```
|
||||
|
||||
### Client server libraries
|
||||
|
||||
Ruby
|
||||
|
||||
```ruby
|
||||
Pusher.host = 'localhost'
|
||||
Pusher.port = 8080
|
||||
```
|
||||
|
||||
PHP
|
||||
|
||||
```php
|
||||
$pusher = new Pusher(APP_KEY, APP_SECRET, APP_ID, DEBUG, "http://localhost", "8080");
|
||||
```
|
||||
|
||||
NodeJS
|
||||
|
||||
```javascript
|
||||
var pusher = new Pusher({
|
||||
appId: APP_ID,
|
||||
key: APP_KEY,
|
||||
secret: APP_SECRET
|
||||
domain: 'localhost',
|
||||
port: 80
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
# Logging
|
||||
|
||||
This software uses the [glog](https://github.com/golang/glog) library
|
||||
|
||||
for more information about logging type the following in console.
|
||||
|
||||
```console
|
||||
$ ipe -h
|
||||
```
|
||||
|
||||
# When use this software?
|
||||
|
||||
* When you are offline;
|
||||
* When you want to control your infrastructure;
|
||||
* When you do not want to have external dependencies;
|
||||
* When you want extend the protocol;
|
||||
|
||||
# Contributing.
|
||||
|
||||
Feel free to fork this repo.
|
||||
|
||||
# Pusher
|
||||
|
||||
Pusher is an excelent service, their service is very reliable. I recomend for everyone.
|
||||
|
||||
# Where this name came from?
|
||||
|
||||
Here in Brazil we have this beautiful tree called [Ipê](http://en.wikipedia.org/wiki/Tabebuia_aurea), it comes in differente colors: yellow, pink, white, purple.
|
||||
|
||||
[I want to see pictures](https://www.flickr.com/search/?q=ipe)
|
||||
|
||||
# Author
|
||||
|
||||
Claudemiro Alves Feitosa Neto
|
||||
|
||||
# LICENSE
|
||||
|
||||
Copyright 2014, 2015, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
Use of this source code is governed by a MIT-style
|
||||
license that can be found in the LICENSE file.
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,41 @@
|
||||
IPÊ
|
||||
---
|
||||
|
||||
* TODO [12/14]
|
||||
* [X] Autenticação API Rest
|
||||
* [X] Autenticação Websockets
|
||||
* [X] Ping e Pong
|
||||
* [ ] Escrever testes automatizados
|
||||
* [X] SSL
|
||||
* [X] Expvar - Canais, inscritos
|
||||
* [X] Otimizações [3/3]
|
||||
* [X] Refatorar partes do código, remover repetições
|
||||
* [X] Alterar tipos de dados de slices para mapas em alguns locais.
|
||||
* [X] Remover Canais vazios.
|
||||
* [ ] Segurança, tempo de expiração, etc
|
||||
* [X] Dados extra na conexão do usuário. Ver Websockets onOpen
|
||||
* [X] Webhooks [5/5]
|
||||
* [X] Member added
|
||||
* [X] Member removed
|
||||
* [X] Channel Occupied
|
||||
* [X] Channel vacated
|
||||
* [X] Clients Events
|
||||
* [X] Events Presence channels [3/3]
|
||||
* [X] pusher_internal:subscription_succeeded para canais de presença
|
||||
* [X] pusher_internal:member_added
|
||||
* [X] pusher_internal:member_removed
|
||||
* [X] Remover inscrições quando o web socket for fechado
|
||||
* [X] Alterar os dados extras da inscrição são relacionados ao canal e não diretamente a inscrição
|
||||
* [X] Escrever um README
|
||||
|
||||
* Dúvidas
|
||||
* [X] Atualmente o ID do usuário em canais de presença está como string, o protocolo não deixa claro, já ví exemplos onde a chave é um inteiro. Nesse caso essa chave terá que ser um RawMessage, O campo Data inteiro??
|
||||
|
||||
* Objetivos [7/7]
|
||||
* [X] Implementação Funcional.
|
||||
* [X] WebHooks
|
||||
* [X] Presence channels
|
||||
* [X] Private Channels
|
||||
* [X] Public Channels
|
||||
* [X] Easy Instalation
|
||||
* [X] Easy configuration
|
||||
@@ -0,0 +1,2 @@
|
||||
client: go run client.go
|
||||
server: go run ../main.go -config ./functional-config.json -alsologtostderr
|
||||
@@ -0,0 +1,24 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Pusher Spec</title>
|
||||
<link href="https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="mocha"></div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.3.4/mocha.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.4.1/chai.min.js"></script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pusher/3.0.0/pusher.js"></script>
|
||||
|
||||
<script>mocha.setup('bdd')</script>
|
||||
<script src="test.pusher.js"></script>
|
||||
<script>
|
||||
mocha.checkLeaks();
|
||||
mocha.globals(['jQuery', 'Pusher']);
|
||||
mocha.run();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/pusher/pusher-http-go"
|
||||
)
|
||||
|
||||
var client pusher.Client
|
||||
|
||||
func init() {
|
||||
client = pusher.Client{
|
||||
AppId: "1",
|
||||
Key: "278d525bdf162c739803",
|
||||
Secret: "7ad3753142a6693b25b9",
|
||||
Host: ":8080",
|
||||
}
|
||||
}
|
||||
|
||||
func pusherPresenceAuth(res http.ResponseWriter, req *http.Request) {
|
||||
presenceData := pusher.MemberData{
|
||||
UserId: "1",
|
||||
UserInfo: map[string]string{},
|
||||
}
|
||||
|
||||
params, _ := ioutil.ReadAll(req.Body)
|
||||
response, err := client.AuthenticatePresenceChannel(params, presenceData)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(res, string(response))
|
||||
}
|
||||
|
||||
func pusherPrivateAuth(res http.ResponseWriter, req *http.Request) {
|
||||
params, _ := ioutil.ReadAll(req.Body)
|
||||
response, err := client.AuthenticatePrivateChannel(params)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(res, string(response))
|
||||
}
|
||||
|
||||
func triggerMessage(res http.ResponseWriter, req *http.Request) {
|
||||
client.Trigger("private-messages", "messages", "The message from server")
|
||||
|
||||
fmt.Fprintf(res, "OK")
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/pusher/presence/auth", pusherPresenceAuth)
|
||||
http.HandleFunc("/pusher/private/auth", pusherPrivateAuth)
|
||||
http.HandleFunc("/trigger", triggerMessage)
|
||||
http.Handle("/", http.FileServer(http.Dir("./")))
|
||||
http.ListenAndServe(":5000", nil)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
|
||||
var assert = chai.assert;
|
||||
|
||||
var APP_KEY = "278d525bdf162c739803";
|
||||
var HOST = "localhost";
|
||||
var PORT = 8080;
|
||||
var AUTH = "http://localhost:5000/pusher/private/auth"
|
||||
var AUTH_PRESENCE = "http://localhost:5000/pusher/presence/auth"
|
||||
|
||||
Pusher.log = function(msg) {
|
||||
console.log(msg);
|
||||
};
|
||||
|
||||
function getPusher(auth) {
|
||||
return new Pusher(APP_KEY, {
|
||||
wsHost: HOST,
|
||||
wsPort: PORT,
|
||||
authEndpoint: auth,
|
||||
enabledTransports: ["ws"],
|
||||
disabledTransports: ["flash"],
|
||||
cluster: "hello", // Should be ignored
|
||||
});
|
||||
}
|
||||
|
||||
describe("Pusher", function() {
|
||||
|
||||
describe("connection", function() {
|
||||
it("should connect sucessfully with correct config", function(done) {
|
||||
var pusher = getPusher(AUTH);
|
||||
|
||||
pusher.connection.bind('connected', function() {
|
||||
assert.ok(true, "Connected");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not connect without the correct config", function(done) {
|
||||
var pusher = new Pusher("INVALID_APP_KEY", {
|
||||
wsHost: HOST,
|
||||
wsPort: PORT,
|
||||
enabledTransports: ["ws"],
|
||||
disabledTransports: ["flash"]
|
||||
});
|
||||
|
||||
pusher.connection.bind('disconnected', function() {
|
||||
assert.ok(true, "Not Connected");
|
||||
done();
|
||||
});
|
||||
});
|
||||
}); // connection
|
||||
|
||||
describe("subscription", function() {
|
||||
it("should subscribe to a public channel", function(done) {
|
||||
var pusher = getPusher(AUTH);
|
||||
|
||||
var channel = pusher.subscribe('public-channel');
|
||||
channel.bind("pusher:subscription_succeeded", function(data) {
|
||||
assert.ok(true, "Connected to the channel");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should subscribe to a private channel", function(done) {
|
||||
var pusher = getPusher(AUTH);
|
||||
|
||||
var channel = pusher.subscribe('private-channel');
|
||||
channel.bind("pusher:subscription_succeeded", function(data) {
|
||||
assert.ok(true, "Connected to the channel");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should subscribe to a presence channel", function(done) {
|
||||
var pusher = getPusher(AUTH_PRESENCE);
|
||||
|
||||
var channel = pusher.subscribe('presence-channel');
|
||||
channel.bind("pusher:subscription_succeeded", function(data) {
|
||||
assert.ok(true, "Connected to the channel");
|
||||
done();
|
||||
});
|
||||
});
|
||||
}); // subscription
|
||||
|
||||
describe("events", function() {
|
||||
it('should not allowed client events on public channels', function(done) {
|
||||
var pusher = getPusher(AUTH);
|
||||
var channel = pusher.subscribe('public-channel');
|
||||
|
||||
channel.bind("pusher:subscription_succeeded", function(data) {
|
||||
channel.trigger("client-message", "The message");
|
||||
});
|
||||
|
||||
pusher.bind("pusher:error", function(data) {
|
||||
assert.ok(true, "Expected error");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow client events on private channels', function(done) {
|
||||
var pusher_a = getPusher(AUTH);
|
||||
var pusher_b = getPusher(AUTH);
|
||||
|
||||
var channel_a = pusher_a.subscribe('private-channel');
|
||||
var channel_b = pusher_b.subscribe('private-channel');
|
||||
|
||||
channel_a.bind("pusher:subscription_succeeded", function() {
|
||||
channel_a.trigger("client-message", "The message");
|
||||
});
|
||||
|
||||
channel_b.bind("client-message", function(data) {
|
||||
assert.equal(data, "The message");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should publish event on private channel', function(done) {
|
||||
var pusher_a = getPusher(AUTH);
|
||||
var pusher_b = getPusher(AUTH);
|
||||
|
||||
var channel_a = pusher_a.subscribe('private-messages');
|
||||
var channel_b = pusher_b.subscribe('private-messages');
|
||||
|
||||
channel_a.bind("pusher:subscription_succeeded", function() {
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.open("GET", "/trigger", true);
|
||||
xhttp.send();
|
||||
});
|
||||
|
||||
channel_b.bind("messages", function(data) {
|
||||
assert.equal(data, "The message from server");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
}); // events
|
||||
});
|
||||
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
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 268 B |
-194
@@ -1,194 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Ipe by dimiro1</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="stylesheets/normalize.css" media="screen">
|
||||
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'>
|
||||
<link rel="stylesheet" type="text/css" href="stylesheets/stylesheet.css" media="screen">
|
||||
<link rel="stylesheet" type="text/css" href="stylesheets/github-light.css" media="screen">
|
||||
</head>
|
||||
<body>
|
||||
<section class="page-header">
|
||||
<h1 class="project-name">Ipe</h1>
|
||||
<h2 class="project-tagline">A Pusher server implementation compatible with Pusher client libraries written in GO</h2>
|
||||
<a href="https://github.com/dimiro1/ipe" class="btn">View on GitHub</a>
|
||||
<a href="https://github.com/dimiro1/ipe/zipball/master" class="btn">Download .zip</a>
|
||||
<a href="https://github.com/dimiro1/ipe/tarball/master" class="btn">Download .tar.gz</a>
|
||||
</section>
|
||||
|
||||
<section class="main-content">
|
||||
<p><a href="http://goreportcard.com/report/dimiro1/ipe"><img src="http://goreportcard.com/badge/dimiro1/ipe" alt="Quality code report"></a></p>
|
||||
|
||||
<h1>
|
||||
<a id="ipÊ" class="anchor" href="#ip%C3%8A" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>IPÊ</h1>
|
||||
|
||||
<p>This software is written in Go - the WYSIWYG lang</p>
|
||||
|
||||
<h1>
|
||||
<a id="why-i-wrote-this-software" class="anchor" href="#why-i-wrote-this-software" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Why I wrote this software?</h1>
|
||||
|
||||
<ol>
|
||||
<li>I wanted to learn Go and I needed a non trivial application;</li>
|
||||
<li>I use Pusher in some projects;</li>
|
||||
<li>I really like Pusher;</li>
|
||||
</ol>
|
||||
|
||||
<h1>
|
||||
<a id="features" class="anchor" href="#features" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Features</h1>
|
||||
|
||||
<ul>
|
||||
<li>Public Channels;</li>
|
||||
<li>Private Channels;</li>
|
||||
<li>Presence Channels;</li>
|
||||
<li>Web Hooks;</li>
|
||||
<li>Client events;</li>
|
||||
<li>Complete REST API;</li>
|
||||
<li>Easy instalation;</li>
|
||||
<li>A single binary without dependencies;</li>
|
||||
<li>Easy configuration;</li>
|
||||
<li>Protocol version 7;</li>
|
||||
<li>Multiple apps in the same instance;</li>
|
||||
<li>Drop in replacement for pusher server;</li>
|
||||
</ul>
|
||||
|
||||
<h1>
|
||||
<a id="download-pre-built-binaries" class="anchor" href="#download-pre-built-binaries" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Download pre built binaries</h1>
|
||||
|
||||
<p>You can download pre built binaries from the <a href="https://github.com/dimiro1/ipe/releases">releases tab</a>.</p>
|
||||
|
||||
<p>I do not have a Windows machine, so I can only distribute binaries for amd64 linux and amd64 darwin.</p>
|
||||
|
||||
<h1>
|
||||
<a id="building" class="anchor" href="#building" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Building</h1>
|
||||
|
||||
<div class="highlight highlight-text-shell-session"><pre>$ <span class="pl-s1">go get github.com/dimiro1/ipe</span></pre></div>
|
||||
|
||||
<p>or simply</p>
|
||||
|
||||
<div class="highlight highlight-text-shell-session"><pre>$ <span class="pl-s1">go install github.com/dimiro1/ipe</span></pre></div>
|
||||
|
||||
<h1>
|
||||
<a id="how-to-configure" class="anchor" href="#how-to-configure" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>How to configure?</h1>
|
||||
|
||||
<h2>
|
||||
<a id="the-server" class="anchor" href="#the-server" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>The server</h2>
|
||||
|
||||
<div class="highlight highlight-source-json"><pre>{
|
||||
<span class="pl-s"><span class="pl-pds">"</span>Host<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>:8080<span class="pl-pds">"</span></span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>SSL<span class="pl-pds">"</span></span>: <span class="pl-c1">false</span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>SSLHost<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>:4433<span class="pl-pds">"</span></span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>SSLKeyFile<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>A key.pem file<span class="pl-pds">"</span></span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>SSLCertFile<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>A cert.pem file<span class="pl-pds">"</span></span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>Apps<span class="pl-pds">"</span></span>: [
|
||||
{
|
||||
<span class="pl-s"><span class="pl-pds">"</span>ApplicationDisabled<span class="pl-pds">"</span></span>: <span class="pl-c1">false</span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>Secret<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>A really secret random string<span class="pl-pds">"</span></span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>Key<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>A random Key string<span class="pl-pds">"</span></span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>OnlySSL<span class="pl-pds">"</span></span>: <span class="pl-c1">false</span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>Name<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>The app name<span class="pl-pds">"</span></span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>AppID<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>The app ID<span class="pl-pds">"</span></span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>UserEvents<span class="pl-pds">"</span></span>: <span class="pl-c1">true</span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>WebHooks<span class="pl-pds">"</span></span>: <span class="pl-c1">true</span>,
|
||||
<span class="pl-s"><span class="pl-pds">"</span>URLWebHook<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>Some URL to send webhooks<span class="pl-pds">"</span></span>
|
||||
}
|
||||
]
|
||||
}
|
||||
</pre></div>
|
||||
|
||||
<h2>
|
||||
<a id="libraries" class="anchor" href="#libraries" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Libraries</h2>
|
||||
|
||||
<h3>
|
||||
<a id="client-javascript-library" class="anchor" href="#client-javascript-library" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Client javascript library</h3>
|
||||
|
||||
<div class="highlight highlight-source-js"><pre><span class="pl-k">var</span> pusher <span class="pl-k">=</span> <span class="pl-k">new</span> <span class="pl-en">Pusher</span>(<span class="pl-c1">APP_KEY</span>, {
|
||||
wsHost<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">'</span>localhost<span class="pl-pds">'</span></span>,
|
||||
wsPort<span class="pl-k">:</span> <span class="pl-c1">8080</span>,
|
||||
enabledTransports<span class="pl-k">:</span> [<span class="pl-s"><span class="pl-pds">"</span>ws<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>flash<span class="pl-pds">"</span></span>],
|
||||
disabledTransports<span class="pl-k">:</span> [<span class="pl-s"><span class="pl-pds">"</span>flash<span class="pl-pds">"</span></span>]
|
||||
});</pre></div>
|
||||
|
||||
<h3>
|
||||
<a id="client-server-libraries" class="anchor" href="#client-server-libraries" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Client server libraries</h3>
|
||||
|
||||
<p>Ruby</p>
|
||||
|
||||
<div class="highlight highlight-source-ruby"><pre><span class="pl-c1">Pusher</span>.host <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">'</span>localhost<span class="pl-pds">'</span></span>
|
||||
<span class="pl-c1">Pusher</span>.port <span class="pl-k">=</span> <span class="pl-c1">8080</span></pre></div>
|
||||
|
||||
<p>PHP</p>
|
||||
|
||||
<div class="highlight highlight-text-html-php"><pre><span class="pl-s1"><span class="pl-smi">$pusher</span> <span class="pl-k">=</span> <span class="pl-k">new</span> <span class="pl-c1">Pusher</span>(<span class="pl-c1">APP_KEY</span>, <span class="pl-c1">APP_SECRET</span>, <span class="pl-c1">APP_ID</span>, <span class="pl-c1">DEBUG</span>, <span class="pl-s"><span class="pl-pds">"</span>http://localhost<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>8080<span class="pl-pds">"</span></span>);</span></pre></div>
|
||||
|
||||
<p>NodeJS</p>
|
||||
|
||||
<div class="highlight highlight-source-js"><pre><span class="pl-k">var</span> pusher <span class="pl-k">=</span> <span class="pl-k">new</span> <span class="pl-en">Pusher</span>({
|
||||
appId<span class="pl-k">:</span> <span class="pl-c1">APP_ID</span>,
|
||||
key<span class="pl-k">:</span> <span class="pl-c1">APP_KEY</span>,
|
||||
secret<span class="pl-k">:</span> <span class="pl-c1">APP_SECRET</span>
|
||||
domain<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">'</span>localhost<span class="pl-pds">'</span></span>,
|
||||
port<span class="pl-k">:</span> <span class="pl-c1">80</span>
|
||||
});
|
||||
</pre></div>
|
||||
|
||||
<h1>
|
||||
<a id="logging" class="anchor" href="#logging" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Logging</h1>
|
||||
|
||||
<p>This software uses the <a href="https://github.com/golang/glog">glog</a> library</p>
|
||||
|
||||
<p>for more information about logging type the following in console.</p>
|
||||
|
||||
<div class="highlight highlight-text-shell-session"><pre>$ <span class="pl-s1">ipe -h</span></pre></div>
|
||||
|
||||
<h1>
|
||||
<a id="when-use-this-software" class="anchor" href="#when-use-this-software" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>When use this software?</h1>
|
||||
|
||||
<ul>
|
||||
<li>When you are offline;</li>
|
||||
<li>When you want to control your infrastructure;</li>
|
||||
<li>When you do not want to have external dependencies;</li>
|
||||
<li>When you want extend the protocol;</li>
|
||||
</ul>
|
||||
|
||||
<h1>
|
||||
<a id="contributing" class="anchor" href="#contributing" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Contributing.</h1>
|
||||
|
||||
<p>Feel free to fork this repo.</p>
|
||||
|
||||
<h1>
|
||||
<a id="pusher" class="anchor" href="#pusher" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Pusher</h1>
|
||||
|
||||
<p>Pusher is an excelent service, their service is very reliable. I recomend for everyone.</p>
|
||||
|
||||
<h1>
|
||||
<a id="where-this-name-came-from" class="anchor" href="#where-this-name-came-from" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Where this name came from?</h1>
|
||||
|
||||
<p>Here in Brazil we have this beautiful tree called <a href="http://en.wikipedia.org/wiki/Tabebuia_aurea">Ipê</a>, it comes in differente colors: yellow, pink, white, purple.</p>
|
||||
|
||||
<p><a href="https://www.flickr.com/search/?q=ipe">I want to see pictures</a></p>
|
||||
|
||||
<h1>
|
||||
<a id="author" class="anchor" href="#author" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Author</h1>
|
||||
|
||||
<p>Claudemiro Alves Feitosa Neto</p>
|
||||
|
||||
<h1>
|
||||
<a id="license" class="anchor" href="#license" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>LICENSE</h1>
|
||||
|
||||
<p>Copyright 2014, 2015, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
Use of this source code is governed by a MIT-style
|
||||
license that can be found in the LICENSE file.</p>
|
||||
|
||||
<footer class="site-footer">
|
||||
<span class="site-footer-owner"><a href="https://github.com/dimiro1/ipe">Ipe</a> is maintained by <a href="https://github.com/dimiro1">dimiro1</a>.</span>
|
||||
|
||||
<span class="site-footer-credits">This page was generated by <a href="https://pages.github.com">GitHub Pages</a> using the <a href="https://github.com/jasonlong/cayman-theme">Cayman theme</a> by <a href="https://twitter.com/jasonlong">Jason Long</a>.</span>
|
||||
</footer>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Ipe
|
||||
After=syslog.target network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=ipe
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=ipe
|
||||
ExecStart=/path/to/ipe -logtostderr -config="path_to_config.json"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,14 @@
|
||||
description "Ipê Upstart job definition"
|
||||
author "Claudemiro Alves Feitosa Neto"
|
||||
|
||||
start on (net-device-up
|
||||
and local-filesystems
|
||||
and runlevel [2345])
|
||||
|
||||
stop on runlevel [016]
|
||||
|
||||
respawn
|
||||
|
||||
setuid ipe
|
||||
|
||||
exec /path/to/ipe -config="path_to_config.json" -log_dir="path_to_log_dir"
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
log "github.com/golang/glog"
|
||||
)
|
||||
|
||||
// An App
|
||||
type app struct {
|
||||
sync.Mutex
|
||||
|
||||
Name string
|
||||
AppID string
|
||||
Key string
|
||||
Secret string
|
||||
OnlySSL bool
|
||||
ApplicationDisabled bool
|
||||
UserEvents bool
|
||||
WebHooks bool
|
||||
URLWebHook string
|
||||
|
||||
Channels map[string]*channel
|
||||
Connections map[string]*connection
|
||||
|
||||
Stats *expvar.Map
|
||||
}
|
||||
|
||||
func newApp(name, appID, key, secret string, onlySSL, disabled, userEvents, webHooks bool, webHookURL string) *app {
|
||||
|
||||
a := &app{
|
||||
Name: name,
|
||||
AppID: appID,
|
||||
Key: key,
|
||||
Secret: secret,
|
||||
OnlySSL: onlySSL,
|
||||
ApplicationDisabled: disabled,
|
||||
UserEvents: userEvents,
|
||||
WebHooks: webHooks,
|
||||
URLWebHook: webHookURL,
|
||||
}
|
||||
|
||||
a.Connections = make(map[string]*connection)
|
||||
a.Channels = make(map[string]*channel)
|
||||
a.Stats = expvar.NewMap(fmt.Sprintf("%s (%s)", a.Name, a.AppID))
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// Only Presence channels
|
||||
func (a *app) PresenceChannels() []*channel {
|
||||
var channels []*channel
|
||||
|
||||
for _, c := range a.Channels {
|
||||
if c.IsPresence() {
|
||||
channels = append(channels, c)
|
||||
}
|
||||
}
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
// Only Private channels
|
||||
func (a *app) PrivateChannels() []*channel {
|
||||
var channels []*channel
|
||||
|
||||
for _, c := range a.Channels {
|
||||
if c.IsPrivate() {
|
||||
channels = append(channels, c)
|
||||
}
|
||||
}
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
// Only Public channels
|
||||
func (a *app) PublicChannels() []*channel {
|
||||
var channels []*channel
|
||||
|
||||
for _, c := range a.Channels {
|
||||
if c.IsPublic() {
|
||||
channels = append(channels, c)
|
||||
}
|
||||
}
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
// Disconnect Socket
|
||||
func (a *app) Disconnect(socketID string) {
|
||||
log.Infof("Disconnecting socket %+v", socketID)
|
||||
|
||||
conn, err := a.FindConnection(socketID)
|
||||
|
||||
if err != nil {
|
||||
log.Infof("Socket not found, %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Unsubscribe from channels
|
||||
for _, c := range a.Channels {
|
||||
if c.IsSubscribed(conn) {
|
||||
c.Unsubscribe(a, conn)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from app
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
|
||||
_, exists := a.Connections[conn.SocketID]
|
||||
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
delete(a.Connections, conn.SocketID)
|
||||
|
||||
a.Stats.Add("TotalConnections", -1)
|
||||
}
|
||||
|
||||
// Connect a new Subscriber
|
||||
func (a *app) Connect(conn *connection) {
|
||||
log.Infof("Adding a new Connection %s to app %s", conn.SocketID, a.Name)
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
|
||||
a.Connections[conn.SocketID] = conn
|
||||
|
||||
a.Stats.Add("TotalConnections", 1)
|
||||
}
|
||||
|
||||
// Find a Connection on this app
|
||||
func (a *app) FindConnection(socketID string) (*connection, error) {
|
||||
conn, exists := a.Connections[socketID]
|
||||
|
||||
if exists {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("Connection not found")
|
||||
}
|
||||
|
||||
// DeleteChannel removes the channel from app
|
||||
func (a *app) RemoveChannel(c *channel) {
|
||||
log.Infof("Remove the channel %s from app %s", c.ChannelID, a.Name)
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
|
||||
delete(a.Channels, c.ChannelID)
|
||||
|
||||
if c.IsPresence() {
|
||||
a.Stats.Add("TotalPresenceChannels", -1)
|
||||
}
|
||||
|
||||
if c.IsPrivate() {
|
||||
a.Stats.Add("TotalPrivateChannels", -1)
|
||||
}
|
||||
|
||||
if c.IsPublic() {
|
||||
a.Stats.Add("TotalPublicChannels", -1)
|
||||
}
|
||||
|
||||
a.Stats.Add("TotalChannels", -1)
|
||||
}
|
||||
|
||||
// Add a new Channel to this APP
|
||||
func (a *app) AddChannel(c *channel) {
|
||||
log.Infof("Adding a new channel %s to app %s", c.ChannelID, a.Name)
|
||||
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
|
||||
a.Channels[c.ChannelID] = c
|
||||
|
||||
if c.IsPresence() {
|
||||
a.Stats.Add("TotalPresenceChannels", 1)
|
||||
}
|
||||
|
||||
if c.IsPrivate() {
|
||||
a.Stats.Add("TotalPrivateChannels", 1)
|
||||
}
|
||||
|
||||
if c.IsPublic() {
|
||||
a.Stats.Add("TotalPublicChannels", 1)
|
||||
}
|
||||
|
||||
a.Stats.Add("TotalChannels", 1)
|
||||
}
|
||||
|
||||
// Returns a Channel from this app
|
||||
// If not found then the channel is created and added to this app
|
||||
func (a *app) FindOrCreateChannelByChannelID(n string) *channel {
|
||||
c, err := a.FindChannelByChannelID(n)
|
||||
|
||||
if err != nil {
|
||||
c = newChannel(n)
|
||||
a.AddChannel(c)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Find the channel by channel ID
|
||||
func (a *app) FindChannelByChannelID(n string) (*channel, error) {
|
||||
c, exists := a.Channels[n]
|
||||
|
||||
if exists {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("Channel does not exists")
|
||||
}
|
||||
|
||||
func (a *app) Publish(c *channel, event rawEvent, ignore string) error {
|
||||
a.Stats.Add("TotalUniqueMessages", 1)
|
||||
|
||||
return c.Publish(a, event, ignore)
|
||||
}
|
||||
|
||||
func (a *app) Unsubscribe(c *channel, conn *connection) error {
|
||||
return c.Unsubscribe(a, conn)
|
||||
}
|
||||
|
||||
func (a *app) Subscribe(c *channel, conn *connection, data string) error {
|
||||
return c.Subscribe(a, conn, data)
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var id = 0
|
||||
|
||||
func newTestApp() *app {
|
||||
|
||||
a := newApp("Test", strconv.Itoa(id), "123", "123", false, false, true, false, "")
|
||||
id++
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func TestConnect(t *testing.T) {
|
||||
app := newTestApp()
|
||||
|
||||
app.Connect(newConnection("socketID", mockSocket{}))
|
||||
|
||||
if len(app.Connections) != 1 {
|
||||
t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestDisconnect(t *testing.T) {
|
||||
app := newTestApp()
|
||||
|
||||
app.Connect(newConnection("socketID", mockSocket{}))
|
||||
app.Disconnect("socketID")
|
||||
|
||||
if len(app.Connections) != 0 {
|
||||
t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFindConnection(t *testing.T) {
|
||||
app := newTestApp()
|
||||
|
||||
app.Connect(newConnection("socketID", mockSocket{}))
|
||||
|
||||
if _, err := app.FindConnection("socketID"); err != nil {
|
||||
t.Errorf("app.FindConnection('socketID') == _, %q, wants %v", err, nil)
|
||||
}
|
||||
|
||||
if _, err := app.FindConnection("NotFound"); err == nil {
|
||||
t.Errorf("app.FindConnection('socketID') == _, %q, wants !nil", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFindChannelByChannelID(t *testing.T) {
|
||||
app := newTestApp()
|
||||
|
||||
channel := newChannel("ID")
|
||||
app.AddChannel(channel)
|
||||
|
||||
if _, err := app.FindChannelByChannelID("ID"); err != nil {
|
||||
t.Errorf("app.FindChannelByChannelID('ID') == _, %q, wants %v", err, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindOrCreateChannelByChannelID(t *testing.T) {
|
||||
app := newTestApp()
|
||||
|
||||
if len(app.Channels) != 0 {
|
||||
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0)
|
||||
}
|
||||
|
||||
app.FindOrCreateChannelByChannelID("ID")
|
||||
|
||||
if len(app.Channels) != 1 {
|
||||
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRemoveChannel(t *testing.T) {
|
||||
app := newTestApp()
|
||||
|
||||
if len(app.Channels) != 0 {
|
||||
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0)
|
||||
}
|
||||
|
||||
channel := newChannel("ID")
|
||||
app.AddChannel(channel)
|
||||
|
||||
if len(app.Channels) != 1 {
|
||||
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 1)
|
||||
}
|
||||
|
||||
app.RemoveChannel(channel)
|
||||
|
||||
if len(app.Channels) != 0 {
|
||||
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_add_channels(t *testing.T) {
|
||||
|
||||
app := newTestApp()
|
||||
|
||||
// Public
|
||||
|
||||
if len(app.PublicChannels()) != 0 {
|
||||
t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
|
||||
}
|
||||
|
||||
app.AddChannel(newChannel("ID"))
|
||||
|
||||
if len(app.PublicChannels()) != 1 {
|
||||
t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
|
||||
}
|
||||
|
||||
// Presence
|
||||
|
||||
if len(app.PresenceChannels()) != 0 {
|
||||
t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
|
||||
}
|
||||
|
||||
app.AddChannel(newChannel("presence-test"))
|
||||
|
||||
if len(app.PresenceChannels()) != 1 {
|
||||
t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
if len(app.PrivateChannels()) != 0 {
|
||||
t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
|
||||
}
|
||||
|
||||
app.AddChannel(newChannel("private-test"))
|
||||
|
||||
if len(app.PrivateChannels()) != 1 {
|
||||
t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_AllChannels(t *testing.T) {
|
||||
app := newTestApp()
|
||||
app.AddChannel(newChannel("private-test"))
|
||||
app.AddChannel(newChannel("presence-test"))
|
||||
app.AddChannel(newChannel("test"))
|
||||
|
||||
if len(app.Channels) != 3 {
|
||||
t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 3)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_New_Subscriber(t *testing.T) {
|
||||
app := newTestApp()
|
||||
|
||||
if len(app.Connections) != 0 {
|
||||
t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 0)
|
||||
}
|
||||
|
||||
conn := newConnection("1", mockSocket{})
|
||||
app.Connect(conn)
|
||||
|
||||
if len(app.Connections) != 1 {
|
||||
t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 1)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_find_subscriber(t *testing.T) {
|
||||
app := newTestApp()
|
||||
conn := newConnection("1", mockSocket{})
|
||||
app.Connect(conn)
|
||||
|
||||
conn, err := app.FindConnection("1")
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if conn.SocketID != "1" {
|
||||
t.Errorf("conn.SocketID == %s, wants %s", conn.SocketID, "1")
|
||||
}
|
||||
|
||||
// Find a wrong subscriber
|
||||
|
||||
conn, err = app.FindConnection("DoesNotExists")
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("err == %q, wants !nil", err)
|
||||
}
|
||||
|
||||
if conn != nil {
|
||||
t.Errorf("conn == %q, wants nil", conn)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_find_or_create_channels(t *testing.T) {
|
||||
app := newTestApp()
|
||||
|
||||
// Public
|
||||
if len(app.PublicChannels()) != 0 {
|
||||
t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0)
|
||||
}
|
||||
|
||||
c := app.FindOrCreateChannelByChannelID("id")
|
||||
|
||||
if len(app.PublicChannels()) != 1 {
|
||||
t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1)
|
||||
}
|
||||
|
||||
if c.ChannelID != "id" {
|
||||
t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "id")
|
||||
}
|
||||
|
||||
// Presence
|
||||
if len(app.PresenceChannels()) != 0 {
|
||||
t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0)
|
||||
}
|
||||
|
||||
c = app.FindOrCreateChannelByChannelID("presence-test")
|
||||
|
||||
if len(app.PresenceChannels()) != 1 {
|
||||
t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1)
|
||||
}
|
||||
|
||||
if c.ChannelID != "presence-test" {
|
||||
t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "presence-test")
|
||||
}
|
||||
|
||||
// Private
|
||||
if len(app.PrivateChannels()) != 0 {
|
||||
t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0)
|
||||
}
|
||||
|
||||
c = app.FindOrCreateChannelByChannelID("private-test")
|
||||
|
||||
if len(app.PrivateChannels()) != 1 {
|
||||
t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1)
|
||||
}
|
||||
|
||||
if c.ChannelID != "private-test" {
|
||||
t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "private-test")
|
||||
}
|
||||
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dimiro1/ipe/utils"
|
||||
log "github.com/golang/glog"
|
||||
)
|
||||
|
||||
// A Channel
|
||||
type channel struct {
|
||||
sync.Mutex
|
||||
|
||||
CreatedAt time.Time
|
||||
ChannelID string
|
||||
Subscriptions map[string]*subscription
|
||||
}
|
||||
|
||||
// Return true if the channel has at least one subscriber
|
||||
func (c *channel) IsOccupied() bool {
|
||||
return c.TotalSubscriptions() > 0
|
||||
}
|
||||
|
||||
// Check if the type of the channel is presence or is private
|
||||
func (c *channel) IsPresenceOrPrivate() bool {
|
||||
return c.IsPresence() || c.IsPrivate()
|
||||
}
|
||||
|
||||
// Check if the type of the channel is public
|
||||
func (c *channel) IsPublic() bool {
|
||||
return !c.IsPresenceOrPrivate()
|
||||
}
|
||||
|
||||
// Check if the type of the channel is presence
|
||||
func (c *channel) IsPresence() bool {
|
||||
return utils.IsPresenceChannel(c.ChannelID)
|
||||
}
|
||||
|
||||
// Check if the type of the channel is private
|
||||
func (c *channel) IsPrivate() bool {
|
||||
return utils.IsPrivateChannel(c.ChannelID)
|
||||
}
|
||||
|
||||
// Get the total of subscribers
|
||||
func (c *channel) TotalSubscriptions() int {
|
||||
return len(c.Subscriptions)
|
||||
}
|
||||
|
||||
// Get the total of users.
|
||||
func (c *channel) TotalUsers() int {
|
||||
total := make(map[string]int)
|
||||
|
||||
for _, s := range c.Subscriptions {
|
||||
total[s.ID]++
|
||||
}
|
||||
|
||||
return len(total)
|
||||
}
|
||||
|
||||
// Add a new subscriber to the channel
|
||||
func (c *channel) Subscribe(a *app, conn *connection, channelData string) error {
|
||||
log.Infof("Subscribing %s to channel %s", conn.SocketID, c.ChannelID)
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
subscription := newSubscription(conn, channelData)
|
||||
c.Subscriptions[conn.SocketID] = subscription
|
||||
|
||||
if c.IsPresence() {
|
||||
// User Info Data
|
||||
var info struct {
|
||||
UserID string `json:"user_id"`
|
||||
UserInfo json.RawMessage `json:"user_info"`
|
||||
}
|
||||
|
||||
log.Infof("%+v", channelData)
|
||||
|
||||
if err := json.Unmarshal([]byte(channelData), &info); err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
js, err := info.UserInfo.MarshalJSON()
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the Subscription
|
||||
subscription.ID = info.UserID
|
||||
subscription.Data = string(js)
|
||||
|
||||
// Publish pusher_internal:member_added
|
||||
c.PublishMemberAddedEvent(a, channelData, subscription)
|
||||
// WebHook
|
||||
a.TriggerMemberAddedHook(c, subscription)
|
||||
|
||||
// pusher_internal:subscription_succeeded
|
||||
data := make(map[string]subscriptionSucceeedEventPresenceData)
|
||||
data["presence"] = newSubscriptionSucceedEventPresenceData(c)
|
||||
|
||||
js, err = json.Marshal(data)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
conn.Publish(newSubscriptionSucceededEvent(c.ChannelID, string(js)))
|
||||
} else {
|
||||
conn.Publish(newSubscriptionSucceededEvent(c.ChannelID, "{}"))
|
||||
}
|
||||
|
||||
// WebHook
|
||||
if c.TotalSubscriptions() == 1 {
|
||||
a.TriggerChannelOccupiedHook(c)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsSubscribed check if the user is subscribed
|
||||
func (c *channel) IsSubscribed(conn *connection) bool {
|
||||
_, exists := c.Subscriptions[conn.SocketID]
|
||||
return exists
|
||||
}
|
||||
|
||||
// Remove the subscriber from the channel
|
||||
// It destroy the channel if the channels does not have any subscribers.
|
||||
func (c *channel) Unsubscribe(a *app, conn *connection) error {
|
||||
log.Infof("Unsubscribing %s from channel %s", conn.SocketID, c.ChannelID)
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
subscription, exists := c.Subscriptions[conn.SocketID]
|
||||
|
||||
if !exists {
|
||||
return errors.New("Subscription not found")
|
||||
}
|
||||
|
||||
delete(c.Subscriptions, conn.SocketID)
|
||||
|
||||
if c.IsPresence() {
|
||||
// Publish pusher_internal:member_removed
|
||||
c.PublishMemberRemovedEvent(a, subscription)
|
||||
// Webhook
|
||||
a.TriggerMemberRemovedHook(c, subscription)
|
||||
}
|
||||
|
||||
if !c.IsOccupied() {
|
||||
// WebHook
|
||||
a.TriggerChannelVacatedHook(c)
|
||||
|
||||
// Remove the empty Channel
|
||||
a.RemoveChannel(c)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new Channel
|
||||
func newChannel(channelID string) *channel {
|
||||
log.Infof("Creating a new channel: %s", channelID)
|
||||
|
||||
return &channel{ChannelID: channelID, CreatedAt: time.Now(), Subscriptions: make(map[string]*subscription)}
|
||||
}
|
||||
|
||||
// Publish a MemberAddedEvent to all subscriptions
|
||||
func (c *channel) PublishMemberAddedEvent(a *app, data string, subscription *subscription) {
|
||||
for _, subs := range c.Subscriptions {
|
||||
if subs != subscription {
|
||||
subs.Connection.Publish(newMemberAddedEvent(c.ChannelID, data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Publish a MemberRemovedEvent to all subscriptions
|
||||
func (c *channel) PublishMemberRemovedEvent(a *app, subscription *subscription) {
|
||||
for _, subs := range c.Subscriptions {
|
||||
if subs != subscription {
|
||||
subs.Connection.Publish(newMemberRemovedEvent(c.ChannelID, subscription))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Publish messages to all Subscribers
|
||||
func (c *channel) Publish(a *app, event rawEvent, ignore string) error {
|
||||
b, err := event.Data.MarshalJSON()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var v interface{}
|
||||
|
||||
if err := json.Unmarshal(b, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("Publishing message %+v to channel %s", v, c.ChannelID)
|
||||
|
||||
for _, subs := range c.Subscriptions {
|
||||
if subs.Connection.SocketID != ignore {
|
||||
subs.Connection.Publish(newResponseEvent(event.Event, event.Channel, v))
|
||||
} else {
|
||||
// Webhook
|
||||
if strings.HasPrefix(event.Event, "client-") {
|
||||
a.TriggerClientEventHook(c, subs, event.Event, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIsOccupied(t *testing.T) {
|
||||
c := newChannel("ID")
|
||||
|
||||
if c.IsOccupied() {
|
||||
t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), false)
|
||||
}
|
||||
|
||||
c.Subscriptions["ID"] = newSubscription(newConnection("ID", mockSocket{}), "")
|
||||
|
||||
if !c.IsOccupied() {
|
||||
t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPrivate(t *testing.T) {
|
||||
c := newChannel("private-channel")
|
||||
|
||||
if !c.IsPrivate() {
|
||||
t.Errorf("c.IsPrivate() == %t, wants %t", c.IsPrivate(), true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPresence(t *testing.T) {
|
||||
c := newChannel("presence-channel")
|
||||
|
||||
if !c.IsPresence() {
|
||||
t.Errorf("c.IsPresence() == %t, wants %t", c.IsPresence(), true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPublic(t *testing.T) {
|
||||
c := newChannel("channel")
|
||||
|
||||
if !c.IsPublic() {
|
||||
t.Errorf("c.IsPublic() == %t, wants %t", c.IsPublic(), true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPrivateOrPresence(t *testing.T) {
|
||||
c := newChannel("private-channel")
|
||||
|
||||
if !c.IsPresenceOrPrivate() {
|
||||
t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
|
||||
}
|
||||
|
||||
c = newChannel("presence-channel")
|
||||
|
||||
if !c.IsPresenceOrPrivate() {
|
||||
t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTotalSubscriptions(t *testing.T) {
|
||||
c := newChannel("ID")
|
||||
|
||||
if c.TotalSubscriptions() != len(c.Subscriptions) {
|
||||
t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.Subscriptions))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTotalUsers(t *testing.T) {
|
||||
c := newChannel("ID")
|
||||
|
||||
c.Subscriptions["1"] = newSubscription(newConnection("ID", mockSocket{}), "")
|
||||
c.Subscriptions["2"] = newSubscription(newConnection("ID", mockSocket{}), "")
|
||||
|
||||
if c.TotalSubscriptions() != len(c.Subscriptions) {
|
||||
t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.Subscriptions))
|
||||
}
|
||||
|
||||
if c.TotalUsers() != 1 {
|
||||
t.Errorf("c.TotalUsers() == %d, wants %d", c.TotalUsers(), 1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIsSubscribed(t *testing.T) {
|
||||
c := newChannel("ID")
|
||||
conn := newConnection("ID", mockSocket{})
|
||||
|
||||
if c.IsSubscribed(conn) {
|
||||
t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), false)
|
||||
}
|
||||
|
||||
c.Subscriptions["ID"] = newSubscription(conn, "")
|
||||
|
||||
if !c.IsSubscribed(conn) {
|
||||
t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"Host": ":8080",
|
||||
"SSL": false,
|
||||
"SSLHost": ":4433",
|
||||
"SSLKeyFile": "A key.pem file",
|
||||
"SSLCertFile": "A cert.pem file",
|
||||
"Apps": [
|
||||
{
|
||||
"ApplicationDisabled": false,
|
||||
"Secret": "A really secret random string",
|
||||
"Key": "A random Key string",
|
||||
"OnlySSL": false,
|
||||
"Name": "The app name",
|
||||
"AppID": "The app ID",
|
||||
"UserEvents": true,
|
||||
"WebHooks": true,
|
||||
"URLWebHook": "Some URL to send webhooks"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2014, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
// The config file
|
||||
type configFile struct {
|
||||
Host string // The host, eg: :8080 will start on 0.0.0.0:8080
|
||||
User string
|
||||
SSL bool
|
||||
SSLHost string
|
||||
SSLKeyFile string
|
||||
SSLCertFile string
|
||||
|
||||
Apps []configApp
|
||||
}
|
||||
|
||||
type configApp struct {
|
||||
Name string
|
||||
AppID string
|
||||
Key string
|
||||
Secret string
|
||||
OnlySSL bool
|
||||
ApplicationDisabled bool
|
||||
UserEvents bool
|
||||
WebHooks bool
|
||||
URLWebHook string
|
||||
}
|
||||
|
||||
func newAppFromConfig(a configApp) *app {
|
||||
return newApp(
|
||||
a.Name,
|
||||
a.AppID,
|
||||
a.Key,
|
||||
a.Secret,
|
||||
a.OnlySSL,
|
||||
a.ApplicationDisabled,
|
||||
a.UserEvents,
|
||||
a.WebHooks,
|
||||
a.URLWebHook,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
log "github.com/golang/glog"
|
||||
)
|
||||
|
||||
// socket interface to write to the client
|
||||
type socket interface {
|
||||
WriteJSON(interface{}) error
|
||||
}
|
||||
|
||||
// mockSocket is a mock implementation of socket
|
||||
// used in the test suite
|
||||
type mockSocket struct{}
|
||||
|
||||
func (s mockSocket) WriteJSON(i interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// An User Connection
|
||||
type connection struct {
|
||||
SocketID string
|
||||
Socket socket
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// Create a new Subscriber
|
||||
func newConnection(socketID string, s socket) *connection {
|
||||
log.Infof("Creating a new Subscriber %+v", socketID)
|
||||
|
||||
return &connection{SocketID: socketID, Socket: s, CreatedAt: time.Now()}
|
||||
}
|
||||
|
||||
// Publish the message to websocket atached to this client
|
||||
func (conn *connection) Publish(m interface{}) {
|
||||
conn.Socket.WriteJSON(m)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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 TestNewConnection(t *testing.T) {
|
||||
expectedSocketID := "socketID"
|
||||
expectedSocket := mockSocket{}
|
||||
|
||||
c := newConnection(expectedSocketID, expectedSocket)
|
||||
|
||||
if c.SocketID != expectedSocketID {
|
||||
t.Errorf("c.SocketID == %s, wants %s", c.SocketID, expectedSocketID)
|
||||
}
|
||||
|
||||
if c.Socket != expectedSocket {
|
||||
t.Errorf("c.Socket == %v, wants %v", c.Socket, expectedSocket)
|
||||
}
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
t.Errorf("c.CreatedAt.IsZero() == %t, wants %t", c.CreatedAt.IsZero(), false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
// Error Codes
|
||||
const (
|
||||
// 4000 - 4099
|
||||
// Indicates an error resulting in the connection being closed by Pusher,
|
||||
// and that attempting to reconnect using the same parameters will not succeed.
|
||||
applicationOnlyAcceptsSSL = 4000
|
||||
applicationDoesNotExists = 4001
|
||||
applicationDisabled = 4003
|
||||
applicationIsOverConnectionQuota = 4004 // Not Implemented
|
||||
pathNotFound = 4005 // Not Implemented
|
||||
invalidVersionStringFormat = 4006
|
||||
unsupportedProtocolVersion = 4007
|
||||
noProtocolVersionSupplied = 4008
|
||||
|
||||
// 4100 - 4199
|
||||
// Indicates an error resulting in the connection being closed by Pusher,
|
||||
// and the client may reconnect after 1s or more
|
||||
overCapacity = 4100 // Not Implemented
|
||||
|
||||
// 4200 - 4299
|
||||
// Indicate an error resulting in the connection being closed by Pusher,
|
||||
// and the client my reconnect immediately
|
||||
genericReconnectImmediately = 4200
|
||||
pongReplyNotReceived = 4201 // Ping was sent to the client, but no reply was received; Not Implemented
|
||||
closedAfterInactivity = 4202 // Client has been inactive for a long time (24 hours) and client does not suppot ping.; Not Implemented
|
||||
|
||||
// 4300 - 4399
|
||||
// Any other type of error
|
||||
clientRejectedDueToRateLimit = 4301 // Not Implemented
|
||||
|
||||
// Pusher send null, This app use this error code to send the null value
|
||||
// see ErrorEvent
|
||||
otherError = 0
|
||||
)
|
||||
|
||||
// Only this version is supported
|
||||
const supportedProtocolVersion = 7
|
||||
|
||||
// // Maximun event size permitted 10 kB
|
||||
// See: http://blogs.gnome.org/cneumair/2008/09/30/1-kb-1024-bytes-no-1-kb-1000-bytes/
|
||||
const maxDataEventSize = 10 * 1000
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
// Base interface
|
||||
type websocketError interface {
|
||||
GetCode() int
|
||||
GetMsg() string
|
||||
}
|
||||
|
||||
// Base struct
|
||||
type baseWebsocketError struct {
|
||||
Code int
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e baseWebsocketError) GetCode() int {
|
||||
return e.Code
|
||||
}
|
||||
|
||||
func (e baseWebsocketError) GetMsg() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
// Unsupprted protocol version
|
||||
type unsupportedProtocolVersionError struct {
|
||||
baseWebsocketError
|
||||
}
|
||||
|
||||
func newUnsupportedProtocolVersionError() unsupportedProtocolVersionError {
|
||||
return unsupportedProtocolVersionError{
|
||||
baseWebsocketError{Code: unsupportedProtocolVersion, Msg: "Unsupported protocol version"},
|
||||
}
|
||||
}
|
||||
|
||||
// The application does not exists
|
||||
// See the configuration file
|
||||
type applicationDoesNotExistsError struct {
|
||||
baseWebsocketError
|
||||
}
|
||||
|
||||
func newApplicationDoesNotExistsError() applicationDoesNotExistsError {
|
||||
return applicationDoesNotExistsError{
|
||||
baseWebsocketError{Code: applicationDoesNotExists, Msg: "Could not found an app with the given key"},
|
||||
}
|
||||
}
|
||||
|
||||
// The user did not send the protocol version
|
||||
type noProtocolVersionSuppliedError struct {
|
||||
baseWebsocketError
|
||||
}
|
||||
|
||||
func newNoProtocolVersionSuppliedError() noProtocolVersionSuppliedError {
|
||||
return noProtocolVersionSuppliedError{
|
||||
baseWebsocketError{Code: noProtocolVersionSupplied, Msg: "No protocol version supplied"},
|
||||
}
|
||||
}
|
||||
|
||||
// When the application is disabled.
|
||||
// See the configuration file
|
||||
type applicationDisabledError struct {
|
||||
baseWebsocketError
|
||||
}
|
||||
|
||||
func newApplicationDisabledError() noProtocolVersionSuppliedError {
|
||||
return noProtocolVersionSuppliedError{
|
||||
baseWebsocketError{Code: applicationDisabled, Msg: "Application disabled"},
|
||||
}
|
||||
}
|
||||
|
||||
// When the application only accepts SSL connections
|
||||
type applicationOnlyAccepsSSLError struct {
|
||||
baseWebsocketError
|
||||
}
|
||||
|
||||
func newApplicationOnlyAccepsSSLError() applicationOnlyAccepsSSLError {
|
||||
return applicationOnlyAccepsSSLError{
|
||||
baseWebsocketError{Code: applicationOnlyAcceptsSSL, Msg: "Application only accepts SSL connections, reconnect using wss://"},
|
||||
}
|
||||
}
|
||||
|
||||
// When the user send an invalid version
|
||||
type invalidVersionStringFormatError struct {
|
||||
baseWebsocketError
|
||||
}
|
||||
|
||||
func newInvalidVersionStringFormatError() invalidVersionStringFormatError {
|
||||
return invalidVersionStringFormatError{
|
||||
baseWebsocketError{Code: invalidVersionStringFormat, Msg: "Invalid version string format"},
|
||||
}
|
||||
}
|
||||
|
||||
// Used when the error was internal
|
||||
// * Decoding json
|
||||
// * Writing to output
|
||||
type genericReconnectImmediatelyError struct {
|
||||
baseWebsocketError
|
||||
}
|
||||
|
||||
func newGenericReconnectImmediatelyError() genericReconnectImmediatelyError {
|
||||
return genericReconnectImmediatelyError{
|
||||
baseWebsocketError{Code: genericReconnectImmediately, Msg: "Generic reconnect immediately"},
|
||||
}
|
||||
}
|
||||
|
||||
// When pusher wants to send an Generic error, it only send the message, the code become nil
|
||||
// Currently I do not know how to send nil, so I send GENERIC_ERROR
|
||||
type genericError struct {
|
||||
baseWebsocketError
|
||||
}
|
||||
|
||||
func newGenericError(msg string) genericError {
|
||||
return genericError{
|
||||
baseWebsocketError{Code: otherError, Msg: msg},
|
||||
}
|
||||
}
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
log "github.com/golang/glog"
|
||||
)
|
||||
|
||||
// {
|
||||
// "event": "pusher:subscribe",
|
||||
// "data": {
|
||||
// "channel": "the channel",
|
||||
// "auth": "the auth",
|
||||
// "channelData": "extra data"
|
||||
// }
|
||||
// }
|
||||
type subscribeEventData struct {
|
||||
Channel string `json:"channel"`
|
||||
Auth string `json:"auth,omitempty"`
|
||||
ChannelData string `json:"channel_data,omitempty"`
|
||||
}
|
||||
|
||||
type subscribeEvent struct {
|
||||
Event string `json:"event"`
|
||||
Data subscribeEventData `json:"data"`
|
||||
}
|
||||
|
||||
// Create a new subscribe event with the specified channel and data
|
||||
func newSubscribeEvent(channel, auth, channelData string) subscribeEvent {
|
||||
data := subscribeEventData{Channel: channel, Auth: auth, ChannelData: channelData}
|
||||
return subscribeEvent{Event: "pusher:subscribe", Data: data}
|
||||
}
|
||||
|
||||
type unsubscribeEventData struct {
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
|
||||
// {
|
||||
// "event": "pusher:unsubscribe",
|
||||
// "data": {
|
||||
// "channel": "The channel"
|
||||
// }
|
||||
// }
|
||||
type unsubscribeEvent struct {
|
||||
Event string `json:"event"`
|
||||
Data unsubscribeEventData `json:"data"`
|
||||
}
|
||||
|
||||
// Create a new unsubscribe event for the specified channel
|
||||
func newUnsubscribeEvent(channel string) unsubscribeEvent {
|
||||
data := unsubscribeEventData{Channel: channel}
|
||||
return unsubscribeEvent{Event: "pusher:unsubscribe", Data: data}
|
||||
}
|
||||
|
||||
// {
|
||||
// "event": "pusher_internal:subscription_succeeded",
|
||||
// "channel": "the channel"
|
||||
// }
|
||||
type subscriptionSucceededEvent struct {
|
||||
Event string `json:"event"`
|
||||
Channel string `json:"channel"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// Create a new subscription succeed event for the specified channel
|
||||
func newSubscriptionSucceededEvent(channel, data string) subscriptionSucceededEvent {
|
||||
return subscriptionSucceededEvent{Event: "pusher_internal:subscription_succeeded", Channel: channel, Data: data}
|
||||
}
|
||||
|
||||
// Data Subscription Succeed
|
||||
|
||||
// "{
|
||||
// \"presence\": {
|
||||
// \"ids\": [\"11814b369700141b222a3f3791cec2d9\",\"71dd6a29da2a4833336d2a964becf820\"],
|
||||
// \"hash\": {
|
||||
// \"11814b369700141b222a3f3791cec2d9\": {
|
||||
// \"name\":\"Phil Leggetter\",
|
||||
// \"twitter\": \"@leggetter\"
|
||||
// },
|
||||
// \"71dd6a29da2a4833336d2a964becf820\": {
|
||||
// \"name\":\"Max Williams\",
|
||||
// \"twitter\": \"@maxthelion\"
|
||||
// }
|
||||
// },
|
||||
// \"count\": 2
|
||||
// }
|
||||
// }"
|
||||
type subscriptionSucceeedEventPresenceData struct {
|
||||
Ids []string `json:"ids"`
|
||||
Hash map[string]interface{} `json:"hash"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func newSubscriptionSucceedEventPresenceData(c *channel) subscriptionSucceeedEventPresenceData {
|
||||
event := subscriptionSucceeedEventPresenceData{}
|
||||
|
||||
var ids []string
|
||||
hash := make(map[string]interface{}, c.TotalSubscriptions())
|
||||
|
||||
for _, s := range c.Subscriptions {
|
||||
// Do you have any other idea?
|
||||
var js interface{}
|
||||
json.Unmarshal([]byte(s.Data), &js)
|
||||
|
||||
hash[s.ID] = js
|
||||
ids = append(ids, s.ID)
|
||||
}
|
||||
|
||||
event.Ids = ids
|
||||
event.Hash = hash
|
||||
event.Count = c.TotalSubscriptions()
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// {
|
||||
// "event": "pusher:pong",
|
||||
// "data": {}
|
||||
// }
|
||||
type pongEvent struct {
|
||||
Event string `json:"event"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// Create a new pong event
|
||||
func newPongEvent() pongEvent {
|
||||
return pongEvent{Event: "pusher:pong", Data: "{}"}
|
||||
}
|
||||
|
||||
// {
|
||||
// "event": "pusher:ping",
|
||||
// "data": {}
|
||||
// }
|
||||
type pingEvent struct {
|
||||
Event string `json:"event"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// Create a new ping event
|
||||
func newPingEvent() pingEvent {
|
||||
return pingEvent{Event: "pusher:ping", Data: "{}"}
|
||||
}
|
||||
|
||||
// {
|
||||
// "event": "pusher:error",
|
||||
// "data": {
|
||||
// "message": "A Message",
|
||||
// "code": 4000
|
||||
// }
|
||||
// }
|
||||
type errorEvent struct {
|
||||
Event string `json:"event"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// Create a new error event
|
||||
// Pusher protocol is very strange in some parts
|
||||
// It send null in some errors.
|
||||
// So I created this GENERIC_ERROR thing, just to verify if the json must have null on the error code
|
||||
func newErrorEvent(code int, message string) errorEvent {
|
||||
var data interface{}
|
||||
|
||||
if code == otherError {
|
||||
data = struct {
|
||||
Code *int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
nil,
|
||||
message,
|
||||
}
|
||||
} else {
|
||||
data = struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
code,
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
return errorEvent{Event: "pusher:error", Data: data}
|
||||
}
|
||||
|
||||
// {
|
||||
// "event" : "pusher:connection_established",
|
||||
// "data" : {
|
||||
// "socket_id" : "123456",
|
||||
// "activity_timeout" : 120
|
||||
// }
|
||||
// }
|
||||
type connectionEstablishedEventData struct {
|
||||
SocketID string `json:"socket_id"`
|
||||
ActivityTimeout int `json:"activity_timeout"`
|
||||
}
|
||||
|
||||
type connectionEstablishedEvent struct {
|
||||
Event string `json:"event"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// Create a new connection established event using the specified socketId
|
||||
func newConnectionEstablishedEvent(socketID string) connectionEstablishedEvent {
|
||||
data := connectionEstablishedEventData{SocketID: socketID, ActivityTimeout: 120}
|
||||
|
||||
b, err := json.Marshal(data)
|
||||
|
||||
if err != nil {
|
||||
panic("events: Could not Marshal json ConnectionEstablishedEvent")
|
||||
}
|
||||
|
||||
return connectionEstablishedEvent{Event: "pusher:connection_established", Data: string(b)}
|
||||
}
|
||||
|
||||
// {
|
||||
// "event": "pusher_internal:member_added",
|
||||
// "channel": "presence-example-channel",
|
||||
// "data": String
|
||||
// }
|
||||
type memberAddedEvent struct {
|
||||
Event string `json:"event"`
|
||||
Channel string `json:"channel"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
func newMemberAddedEvent(channel, data string) memberAddedEvent {
|
||||
return memberAddedEvent{Event: "pusher_internal:member_added", Channel: channel, Data: data}
|
||||
}
|
||||
|
||||
// {
|
||||
// "event": "pusher_internal:member_removed",
|
||||
// "channel": "presence-example-channel",
|
||||
// "data": String
|
||||
// }
|
||||
type memberRemovedEvent struct {
|
||||
Event string `json:"event"`
|
||||
Channel string `json:"channel"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
func newMemberRemovedEvent(channel string, s *subscription) memberRemovedEvent {
|
||||
data, err := json.Marshal(struct {
|
||||
UserID string `json:"user_id"`
|
||||
}{
|
||||
UserID: s.ID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
return memberRemovedEvent{Event: "pusher_internal:member_removed", Channel: channel, Data: string(data)}
|
||||
}
|
||||
|
||||
// {
|
||||
// "event": "client-?",
|
||||
// "channel": "The channel",
|
||||
// "data": {}
|
||||
// }
|
||||
type rawEvent struct {
|
||||
Event string `json:"event"`
|
||||
Channel string `json:"channel"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
type responseEvent struct {
|
||||
Event string `json:"event"`
|
||||
Channel string `json:"channel"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// The response event that is broadcasted to the client sockets
|
||||
func newResponseEvent(name, channel string, data interface{}) responseEvent {
|
||||
return responseEvent{Event: name, Channel: channel, Data: data}
|
||||
}
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
log "github.com/golang/glog"
|
||||
|
||||
"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.
|
||||
//
|
||||
// The body should contain a Hash of parameters encoded as JSON where data parameter itself is JSON encoded.
|
||||
//
|
||||
// Not Implemented:
|
||||
// Note that these parameters may be provided in the query string, although this is discouraged.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// {"name":"foo","channels":["project-3"],"data":"{\"some\":\"data\"}"}
|
||||
//
|
||||
// Response is an empty JSON hash.
|
||||
//
|
||||
// POST /apps/{app_id}/events
|
||||
func postEvents(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
}
|
||||
|
||||
var input struct {
|
||||
Name string `json:"name"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Channels []string `json:"channels,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
SocketID string `json:"socket_id,omitempty"`
|
||||
}
|
||||
|
||||
err = json.NewDecoder(r.Body).Decode(&input)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// The event data should not be larger than 10KB.
|
||||
if len(input.Data) > maxDataEventSize {
|
||||
http.Error(w, "Request too large.", http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info(input.Channels)
|
||||
if len(input.Channel) > 0 && len(input.Channels) == 0 {
|
||||
input.Channels = append(input.Channels, input.Channel)
|
||||
}
|
||||
|
||||
for _, c := range input.Channels {
|
||||
channel := app.FindOrCreateChannelByChannelID(c)
|
||||
|
||||
app.Publish(channel, rawEvent{Event: input.Name, Channel: c, Data: input.Data}, input.SocketID)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("{}"))
|
||||
}
|
||||
|
||||
// Allows fetching a hash of occupied channels (optionally filtered by prefix),
|
||||
// and optionally one or more attributes for each channel.
|
||||
//
|
||||
// Notes:
|
||||
// 'user_count' is the only attribute documented on the Pusher API
|
||||
//
|
||||
// Example:
|
||||
// {
|
||||
// "channels": {
|
||||
// "presence-foobar": {
|
||||
// user_count: 42
|
||||
// },
|
||||
// "presence-another": {
|
||||
// user_count: 123
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// GET /apps/{app_id}/channels
|
||||
func getChannels(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
|
||||
appID := p.Get("app_id")
|
||||
filter := query.Get("filter_by_prefix")
|
||||
info := query.Get("info")
|
||||
|
||||
attributes := strings.Split(info, ",")
|
||||
|
||||
requestedUserCount := false
|
||||
|
||||
for _, a := range attributes {
|
||||
if a == "user_count" {
|
||||
requestedUserCount = true
|
||||
}
|
||||
}
|
||||
|
||||
// If an attribute such as user_count is requested, and the request is not limited
|
||||
// to presence channels, the API will return an error (400 code)
|
||||
if requestedUserCount && filter != "presence-" {
|
||||
http.Error(w, "Attribute user_count is restricted to presence channels", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
app, err := ctx.DB.GetAppByAppID(appID)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
channels := make(map[string]interface{})
|
||||
|
||||
switch filter {
|
||||
case "presence-":
|
||||
for _, c := range app.PresenceChannels() {
|
||||
if requestedUserCount {
|
||||
channels[c.ChannelID] = struct {
|
||||
UserCount int `json:"user_count"`
|
||||
}{
|
||||
c.TotalUsers(),
|
||||
}
|
||||
} else {
|
||||
channels[c.ChannelID] = struct{}{}
|
||||
}
|
||||
}
|
||||
case "public-":
|
||||
for _, c := range app.PublicChannels() {
|
||||
channels[c.ChannelID] = struct{}{}
|
||||
}
|
||||
case "private-":
|
||||
for _, c := range app.PrivateChannels() {
|
||||
channels[c.ChannelID] = struct{}{}
|
||||
}
|
||||
default:
|
||||
for _, c := range app.Channels {
|
||||
channels[c.ChannelID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
js := make(map[string]interface{}, 1)
|
||||
js["channels"] = channels
|
||||
|
||||
if err := json.NewEncoder(w).Encode(js); err != nil {
|
||||
log.Error(err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch info for one channel
|
||||
//
|
||||
// Example:
|
||||
// {
|
||||
// occupied: true,
|
||||
// user_count: 42,
|
||||
// subscription_count: 42
|
||||
// }
|
||||
//
|
||||
// GET /apps/{app_id}/channels/{channel_name}
|
||||
func getChannel(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
query := r.URL.Query()
|
||||
|
||||
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 := p.Get("channel_name")
|
||||
|
||||
// Channel name could not be empty
|
||||
if strings.TrimSpace(channelName) == "" {
|
||||
http.Error(w, "Empty channel name", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
info := query.Get("info")
|
||||
attributes := strings.Split(info, ",")
|
||||
|
||||
// Attributes requested
|
||||
requestedUserCount := false
|
||||
requestedSubscriptionCount := false
|
||||
|
||||
for _, a := range attributes {
|
||||
switch a {
|
||||
case "subscription_count":
|
||||
requestedSubscriptionCount = true
|
||||
case "user_count":
|
||||
requestedUserCount = true
|
||||
}
|
||||
}
|
||||
|
||||
channel, err := app.FindChannelByChannelID(channelName)
|
||||
|
||||
// Channel exists?
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Could not find a channel with id %s", channelName), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// If an attribute such as user_count is requested, and the request is not limited
|
||||
// to presence channels, the API will return an error (400 code)
|
||||
if requestedUserCount && !channel.IsPresence() {
|
||||
http.Error(w, "Attribute user_count is restricted to presence channels", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Output
|
||||
dtoChannel := struct {
|
||||
Occupied bool `json:"occupied"`
|
||||
UserCount int `json:"user_count,omitempty"`
|
||||
SubscriptionCount int `json:"subscription_count,omitempty"`
|
||||
}{Occupied: channel.IsOccupied()}
|
||||
|
||||
switch {
|
||||
case requestedSubscriptionCount && requestedUserCount:
|
||||
dtoChannel.UserCount = channel.TotalUsers()
|
||||
dtoChannel.SubscriptionCount = channel.TotalSubscriptions()
|
||||
|
||||
case requestedUserCount:
|
||||
dtoChannel.UserCount = channel.TotalUsers()
|
||||
|
||||
case requestedSubscriptionCount:
|
||||
dtoChannel.SubscriptionCount = channel.TotalSubscriptions()
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(dtoChannel); err != nil {
|
||||
log.Error(err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// Allowed only for presence-channels
|
||||
//
|
||||
// Example:
|
||||
// {
|
||||
// "users": [
|
||||
// { "id": "1" },
|
||||
// { "id": "2" }
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// GET /apps/{app_id}/channels/{channel_name}/users
|
||||
func getChannelUsers(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
|
||||
appID := p.Get("app_id")
|
||||
channelName := p.Get("channel_name")
|
||||
|
||||
isPresence := utils.IsPresenceChannel(channelName)
|
||||
|
||||
if !isPresence {
|
||||
http.Error(w, "This api endpoint is restricted to presence channels.", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Get the channel
|
||||
channel, err := app.FindChannelByChannelID(channelName)
|
||||
|
||||
// Channel exists?
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Could not find a channel with id %s", channelName), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
result := make(map[string][]interface{})
|
||||
|
||||
var users []interface{}
|
||||
|
||||
for _, s := range channel.Subscriptions {
|
||||
users = append(users, struct {
|
||||
ID string `json:"id"`
|
||||
}{s.ID})
|
||||
}
|
||||
|
||||
result["users"] = users
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(result); err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
// Copyright 2015 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/golang/glog"
|
||||
)
|
||||
|
||||
// Start Parse the configuration file and starts the ipe server
|
||||
// It Panic if could not start the HTTP or HTTPS server
|
||||
func Start(filename string) {
|
||||
var conf configFile
|
||||
|
||||
rand.Seed(time.Now().Unix())
|
||||
file, err := os.Open(filename)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Reading config
|
||||
if err := json.NewDecoder(file).Decode(&conf); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Using a in memory database
|
||||
db := newMemdb()
|
||||
|
||||
// Adding applications
|
||||
for _, a := range conf.Apps {
|
||||
db.AddApp(newAppFromConfig(a))
|
||||
}
|
||||
|
||||
// Creating the global application context
|
||||
ctx := &applicationContext{DB: db}
|
||||
|
||||
// The router
|
||||
router := newRouter(ctx)
|
||||
|
||||
router.POST("/apps/{app_id}/events", commonHandlers(ctx, postEvents))
|
||||
|
||||
router.GET("/apps/{app_id}/channels", commonHandlers(ctx, getChannels))
|
||||
|
||||
router.GET("/apps/{app_id}/channels/{channel_name}", commonHandlers(ctx, getChannel))
|
||||
|
||||
router.GET("/apps/{app_id}/channels/{channel_name}/users", commonHandlers(ctx, getChannelUsers))
|
||||
|
||||
router.GET("/app/{key}", contextHandlerFunc(wsHandler))
|
||||
|
||||
if conf.SSL {
|
||||
go func() {
|
||||
log.Infof("Starting HTTPS service on %s ...", conf.SSLHost)
|
||||
log.Fatal(http.ListenAndServeTLS(conf.SSLHost, conf.SSLCertFile, conf.SSLKeyFile, router))
|
||||
}()
|
||||
}
|
||||
|
||||
log.Infof("Starting HTTP service on %s ...", conf.Host)
|
||||
log.Fatal(http.ListenAndServe(conf.Host, router))
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2014, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type router struct {
|
||||
ctx *applicationContext
|
||||
mux *mux.Router
|
||||
routes map[string]contextHandler
|
||||
}
|
||||
|
||||
func newRouter(ctx *applicationContext) *router {
|
||||
return &router{
|
||||
ctx: ctx,
|
||||
mux: mux.NewRouter().StrictSlash(true),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *router) GET(path string, handler contextHandler) {
|
||||
a.Handle("GET", path, handler)
|
||||
}
|
||||
|
||||
func (a *router) POST(path string, handler contextHandler) {
|
||||
a.Handle("POST", path, handler)
|
||||
}
|
||||
|
||||
func (a *router) Handle(method, path string, handler contextHandler) {
|
||||
a.mux.Methods(method).Path(path).HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handler.ServeWithContext(a.ctx, params(mux.Vars(r)), w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (a router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
a.mux.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
// A Channel Subscription
|
||||
type subscription struct {
|
||||
Connection *connection
|
||||
ID string
|
||||
Data string
|
||||
}
|
||||
|
||||
// Create a new Subscription
|
||||
func newSubscription(conn *connection, data string) *subscription {
|
||||
return &subscription{Connection: conn, Data: data}
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/dimiro1/ipe/utils"
|
||||
log "github.com/golang/glog"
|
||||
)
|
||||
|
||||
// A WebHook is sent as a HTTP POST request to the url which you specify.
|
||||
// The POST request payload (body) contains a JSON document, and follows the following format:
|
||||
// {
|
||||
// "time_ms": 1327078148132
|
||||
// "events": [
|
||||
// { "name": "event_name", "some": "data" }
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// Security
|
||||
// Encryption
|
||||
//
|
||||
// You may use a HTTP or a HTTPS url for WebHooks. In most cases HTTP is sufficient, but HTTPS can be useful if your data is sensitive or if you wish to protect against replay attacks for example.
|
||||
// Authentication
|
||||
//
|
||||
// Since anyone could in principle send WebHooks to your application, it’s important to verify that these WebHooks originated from Pusher. Valid WebHooks will therefore contain these headers which contain a HMAC signature of the WebHook payload (body):
|
||||
//
|
||||
// X-Pusher-Key: The App Key.
|
||||
// X-Pusher-Signature: A HMAC SHA256 hex digest formed by signing the POST payload (body) with the token’s secret.
|
||||
type webHook struct {
|
||||
TimeMs int64 `json:"time_ms"`
|
||||
Events []hookEvent `json:"events"`
|
||||
}
|
||||
|
||||
type hookEvent struct {
|
||||
Name string `json:"name"`
|
||||
Channel string `json:"channel"`
|
||||
Event string `json:"event,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
SocketID string `json:"socket_id,omitempty"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
func newChannelOcuppiedHook(channel *channel) hookEvent {
|
||||
return hookEvent{Name: "channel_occupied", Channel: channel.ChannelID}
|
||||
}
|
||||
|
||||
func newChannelVacatedHook(channel *channel) hookEvent {
|
||||
return hookEvent{Name: "channel_vacated", Channel: channel.ChannelID}
|
||||
}
|
||||
|
||||
func newMemberAddedHook(channel *channel, s *subscription) hookEvent {
|
||||
return hookEvent{Name: "member_added", Channel: channel.ChannelID, UserID: s.ID}
|
||||
}
|
||||
|
||||
func newMemberRemovedHook(channel *channel, s *subscription) hookEvent {
|
||||
return hookEvent{Name: "member_removed", Channel: channel.ChannelID, UserID: s.ID}
|
||||
}
|
||||
|
||||
func newClientHook(channel *channel, s *subscription, event string, data interface{}) hookEvent {
|
||||
return hookEvent{Name: "client_event", Channel: channel.ChannelID, Event: event, Data: data, SocketID: s.Connection.SocketID}
|
||||
}
|
||||
|
||||
// channel_occupied
|
||||
// { "name": "channel_occupied", "channel": "test_channel" }
|
||||
func (a *app) TriggerChannelOccupiedHook(c *channel) {
|
||||
event := newChannelOcuppiedHook(c)
|
||||
triggerHook(event.Name, a, c, event)
|
||||
}
|
||||
|
||||
// channel_vacated
|
||||
// { "name": "channel_vacated", "channel": "test_channel" }
|
||||
func (a *app) TriggerChannelVacatedHook(c *channel) {
|
||||
event := newChannelVacatedHook(c)
|
||||
triggerHook(event.Name, a, c, event)
|
||||
}
|
||||
|
||||
// {
|
||||
// "name": "client_event",
|
||||
// "channel": "name of the channel the event was published on",
|
||||
// "event": "name of the event",
|
||||
// "data": "data associated with the event",
|
||||
// "socket_id": "socket_id of the sending socket",
|
||||
// "user_id": "user_id associated with the sending socket" # Only for presence channels
|
||||
// }
|
||||
func (a *app) TriggerClientEventHook(c *channel, s *subscription, clientEvent string, data interface{}) {
|
||||
event := newClientHook(c, s, clientEvent, data)
|
||||
|
||||
if c.IsPresence() {
|
||||
event.UserID = s.ID
|
||||
}
|
||||
|
||||
triggerHook(event.Name, a, c, event)
|
||||
}
|
||||
|
||||
// {
|
||||
// "name": "member_added",
|
||||
// "channel": "presence-your_channel_name",
|
||||
// "user_id": "a_user_id"
|
||||
// }
|
||||
func (a *app) TriggerMemberAddedHook(c *channel, s *subscription) {
|
||||
event := newMemberAddedHook(c, s)
|
||||
triggerHook(event.Name, a, c, event)
|
||||
}
|
||||
|
||||
// {
|
||||
// "name": "member_removed",
|
||||
// "channel": "presence-your_channel_name",
|
||||
// "user_id": "a_user_id"
|
||||
// }
|
||||
func (a *app) TriggerMemberRemovedHook(c *channel, s *subscription) {
|
||||
event := newMemberRemovedHook(c, s)
|
||||
triggerHook(event.Name, a, c, event)
|
||||
}
|
||||
|
||||
func triggerHook(name string, a *app, c *channel, event hookEvent) {
|
||||
if !a.WebHooks {
|
||||
log.Infof("Webhooks are not enabled for app: %s", a.Name)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Infof("Triggering %s event", name)
|
||||
|
||||
hook := webHook{TimeMs: time.Now().Unix()}
|
||||
|
||||
hook.Events = append(hook.Events, event)
|
||||
|
||||
var js []byte
|
||||
var err error
|
||||
|
||||
js, err = json.Marshal(hook)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Error decoding json: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var req *http.Request
|
||||
|
||||
req, err = http.NewRequest("POST", a.URLWebHook, bytes.NewReader(js))
|
||||
if err != nil {
|
||||
log.Errorf("Error creating request: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Ipe UA; (+https://github.com/dimiro1/ipe)")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Pusher-Key", a.Key)
|
||||
req.Header.Set("X-Pusher-Signature", utils.HashMAC(js, []byte(a.Secret)))
|
||||
|
||||
log.V(1).Infof("%+v", req.Header)
|
||||
log.V(1).Infof("%+v", string(js))
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
|
||||
// See: http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#close_http_resp_body
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Error posting %s event: %+v", name, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
log "github.com/golang/glog"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/dimiro1/ipe/utils"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
// Handle open Subscriber.
|
||||
func onOpen(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, sessionID string, app *app) websocketError {
|
||||
params := r.URL.Query()
|
||||
p := params.Get("protocol")
|
||||
|
||||
protocol, err := strconv.Atoi(p)
|
||||
|
||||
if err != nil {
|
||||
return newInvalidVersionStringFormatError()
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.TrimSpace(p) == "":
|
||||
return newNoProtocolVersionSuppliedError()
|
||||
case protocol != supportedProtocolVersion:
|
||||
return newUnsupportedProtocolVersionError()
|
||||
case app.ApplicationDisabled:
|
||||
return newApplicationDisabledError()
|
||||
case app.OnlySSL:
|
||||
if r.TLS == nil {
|
||||
return newApplicationOnlyAccepsSSLError()
|
||||
}
|
||||
}
|
||||
|
||||
// Create the new Subscriber
|
||||
connection := newConnection(sessionID, conn)
|
||||
app.Connect(connection)
|
||||
|
||||
// Everything went fine. Huhu.
|
||||
if err := conn.WriteJSON(newConnectionEstablishedEvent(connection.SocketID)); err != nil {
|
||||
return newGenericReconnectImmediatelyError()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle the close event
|
||||
func onClose(sessionID string, app *app) {
|
||||
app.Disconnect(sessionID)
|
||||
}
|
||||
|
||||
// Handle messages
|
||||
//
|
||||
// If there is an unrecoverable error then break the loop,
|
||||
// otherwise just keep going.
|
||||
func onMessage(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, sessionID string, app *app) {
|
||||
var event struct {
|
||||
Event string `json:"event"`
|
||||
}
|
||||
|
||||
for {
|
||||
_, message, err := conn.ReadMessage()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("%+v", err)
|
||||
switch err {
|
||||
case io.EOF:
|
||||
onClose(sessionID, app)
|
||||
default:
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(message, &event); err != nil {
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
break
|
||||
}
|
||||
|
||||
log.Infof("websockets: Handling %s event", event.Event)
|
||||
|
||||
switch event.Event {
|
||||
case "pusher:ping":
|
||||
if err := conn.WriteJSON(newPongEvent()); err != nil {
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
}
|
||||
case "pusher:subscribe":
|
||||
subscribeEvent := subscribeEvent{}
|
||||
|
||||
if err := json.Unmarshal(message, &subscribeEvent); err != nil {
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
break
|
||||
}
|
||||
|
||||
connection, err := app.FindConnection(sessionID)
|
||||
|
||||
if err != nil {
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
break
|
||||
}
|
||||
|
||||
channelName := strings.TrimSpace(subscribeEvent.Data.Channel)
|
||||
|
||||
if !utils.IsChannelNameValid(channelName) {
|
||||
emitWSError(newGenericError(fmt.Sprintf("This channel name is not valid")), conn)
|
||||
break
|
||||
}
|
||||
|
||||
isPresence := utils.IsPresenceChannel(channelName)
|
||||
isPrivate := utils.IsPrivateChannel(channelName)
|
||||
|
||||
if isPresence || isPrivate {
|
||||
toSign := []string{connection.SocketID, channelName}
|
||||
|
||||
if isPresence || len(subscribeEvent.Data.ChannelData) > 0 {
|
||||
toSign = append(toSign, subscribeEvent.Data.ChannelData)
|
||||
}
|
||||
|
||||
expectedAuthKey := fmt.Sprintf("%s:%s", app.Key, utils.HashMAC([]byte(strings.Join(toSign, ":")), []byte(app.Secret)))
|
||||
if subscribeEvent.Data.Auth != expectedAuthKey {
|
||||
emitWSError(newGenericError(fmt.Sprintf("Auth value for subscription to %s is invalid", channelName)), conn)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
channel := app.FindOrCreateChannelByChannelID(channelName)
|
||||
log.Info(subscribeEvent.Data.ChannelData)
|
||||
|
||||
if err := app.Subscribe(channel, connection, subscribeEvent.Data.ChannelData); err != nil {
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
}
|
||||
case "pusher:unsubscribe":
|
||||
unsubscribeEvent := unsubscribeEvent{}
|
||||
|
||||
if err := json.Unmarshal(message, &unsubscribeEvent); err != nil {
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
}
|
||||
|
||||
connection, err := app.FindConnection(sessionID)
|
||||
|
||||
if err != nil {
|
||||
emitWSError(newGenericError(fmt.Sprintf("Could not find a connection with the id %s", sessionID)), conn)
|
||||
}
|
||||
|
||||
channel, err := app.FindChannelByChannelID(unsubscribeEvent.Data.Channel)
|
||||
|
||||
if err != nil {
|
||||
emitWSError(newGenericError(fmt.Sprintf("Could not find a channel with the id %s", unsubscribeEvent.Data.Channel)), conn)
|
||||
}
|
||||
|
||||
if err := app.Unsubscribe(channel, connection); err != nil {
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
break
|
||||
}
|
||||
default: // CLient Events ??
|
||||
// see http://pusher.com/docs/client_api_guide/client_events#trigger-events
|
||||
if strings.HasPrefix(event.Event, "client-") {
|
||||
if !app.UserEvents {
|
||||
emitWSError(newGenericError("To send client events, you must enable this feature in the Settings."), conn)
|
||||
}
|
||||
|
||||
clientEvent := rawEvent{}
|
||||
|
||||
if err := json.Unmarshal(message, &clientEvent); err != nil {
|
||||
log.Error(err)
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
break
|
||||
}
|
||||
|
||||
channel, err := app.FindChannelByChannelID(clientEvent.Channel)
|
||||
|
||||
if err != nil {
|
||||
emitWSError(newGenericError(fmt.Sprintf("Could not find a channel with the id %s", clientEvent.Channel)), conn)
|
||||
}
|
||||
|
||||
if !channel.IsPresenceOrPrivate() {
|
||||
emitWSError(newGenericError("Client event rejected - only supported on private and presence channels"), conn)
|
||||
break
|
||||
}
|
||||
|
||||
if err := app.Publish(channel, clientEvent, sessionID); err != nil {
|
||||
log.Error(err)
|
||||
emitWSError(newGenericReconnectImmediatelyError(), conn)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
} // switch
|
||||
} // For
|
||||
}
|
||||
|
||||
// Websocket GET /app/{key}
|
||||
func wsHandler(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
defer func() {
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
appKey := p.Get("key")
|
||||
|
||||
app, err := ctx.DB.GetAppByKey(appKey)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
emitWSError(newApplicationDoesNotExistsError(), conn)
|
||||
return
|
||||
}
|
||||
|
||||
sessionID := utils.GenerateSessionID()
|
||||
|
||||
if err := onOpen(conn, w, r, sessionID, app); err != nil {
|
||||
emitWSError(err, conn)
|
||||
return
|
||||
}
|
||||
|
||||
onMessage(conn, w, r, sessionID, app)
|
||||
}
|
||||
|
||||
// Emit an Websocket ErrorEvent
|
||||
func emitWSError(err websocketError, conn *websocket.Conn) {
|
||||
|
||||
event := newErrorEvent(err.GetCode(), err.GetMsg())
|
||||
|
||||
if err := conn.WriteJSON(event); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
console.log('This would be the main JS file.');
|
||||
@@ -1,17 +0,0 @@
|
||||
var metas = document.getElementsByTagName('meta');
|
||||
var i;
|
||||
if (navigator.userAgent.match(/iPhone/i)) {
|
||||
for (i=0; i<metas.length; i++) {
|
||||
if (metas[i].name == "viewport") {
|
||||
metas[i].content = "width=device-width, minimum-scale=1.0, maximum-scale=1.0";
|
||||
}
|
||||
}
|
||||
document.addEventListener("gesturestart", gestureStart, false);
|
||||
}
|
||||
function gestureStart() {
|
||||
for (i=0; i<metas.length; i++) {
|
||||
if (metas[i].name == "viewport") {
|
||||
metas[i].content = "width=device-width, minimum-scale=0.25, maximum-scale=1.6";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/dimiro1/ipe/ipe"
|
||||
)
|
||||
|
||||
// These variables are generated by the linker
|
||||
// please see the makefile for mor information.
|
||||
var (
|
||||
version string = "version"
|
||||
buildstamp string = "buildstamp"
|
||||
githash string = "githash"
|
||||
)
|
||||
|
||||
// Main function, initialize the system
|
||||
func main() {
|
||||
var filename = flag.String("config", "config.json", "Config file location")
|
||||
flag.Parse()
|
||||
|
||||
printBanner()
|
||||
|
||||
ipe.Start(*filename)
|
||||
}
|
||||
|
||||
// Print a beautiful banner
|
||||
func printBanner() {
|
||||
fmt.Print("\033[31m")
|
||||
fmt.Print(`
|
||||
d8b
|
||||
Y8P
|
||||
|
||||
888 88888b. .d88b.
|
||||
888 888 "88b d8P Y8b
|
||||
888 888 888 88888888
|
||||
888 888 d88P Y8b.
|
||||
888 88888P" "Y8888
|
||||
888
|
||||
888
|
||||
888
|
||||
`)
|
||||
fmt.Println("\033[0m")
|
||||
fmt.Println("\033[32mWelcome to Ipê - Yet another Pusher server clone (https://github.com/dimiro1/ipe)\033[0m")
|
||||
fmt.Printf("\033[32mVersion %s+%s.git.%s\033[0m\n", version, buildstamp, githash)
|
||||
fmt.Println("\033[33mBy: Claudemiro Alves Feitosa Neto <dimiro1@gmail.com>\033[0m")
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"name":"Ipe","tagline":"A Pusher server implementation compatible with Pusher client libraries written in GO","body":"[](http://goreportcard.com/report/dimiro1/ipe)\r\n\r\n# IPÊ\r\n\r\nThis software is written in Go - the WYSIWYG lang\r\n\r\n# Why I wrote this software?\r\n\r\n1. I wanted to learn Go and I needed a non trivial application;\r\n2. I use Pusher in some projects;\r\n3. I really like Pusher;\r\n\r\n# Features\r\n\r\n* Public Channels;\r\n* Private Channels;\r\n* Presence Channels;\r\n* Web Hooks;\r\n* Client events;\r\n* Complete REST API;\r\n* Easy instalation;\r\n* A single binary without dependencies;\r\n* Easy configuration;\r\n* Protocol version 7;\r\n* Multiple apps in the same instance;\r\n* Drop in replacement for pusher server;\r\n\r\n# Download pre built binaries\r\n\r\nYou can download pre built binaries from the [releases tab](https://github.com/dimiro1/ipe/releases).\r\n\r\nI do not have a Windows machine, so I can only distribute binaries for amd64 linux and amd64 darwin.\r\n\r\n# Building\r\n\r\n```console\r\n$ go get github.com/dimiro1/ipe\r\n```\r\n\r\nor simply\r\n\r\n```console\r\n$ go install github.com/dimiro1/ipe\r\n```\r\n\r\n# How to configure?\r\n\r\n## The server\r\n\r\n```json\r\n{\r\n\t\"Host\": \":8080\",\r\n\t\"SSL\": false,\r\n\t\"SSLHost\": \":4433\",\r\n\t\"SSLKeyFile\": \"A key.pem file\",\r\n\t\"SSLCertFile\": \"A cert.pem file\",\r\n\t\"Apps\": [\r\n\t\t{\r\n\t\t\t\"ApplicationDisabled\": false,\r\n\t\t\t\"Secret\": \"A really secret random string\",\r\n\t\t\t\"Key\": \"A random Key string\",\r\n\t\t\t\"OnlySSL\": false,\r\n\t\t\t\"Name\": \"The app name\",\r\n\t\t\t\"AppID\": \"The app ID\",\r\n\t\t\t\"UserEvents\": true,\r\n\t\t\t\"WebHooks\": true,\r\n\t\t\t\"URLWebHook\": \"Some URL to send webhooks\"\r\n\t\t}\r\n\t]\r\n}\r\n\r\n```\r\n\r\n## Libraries\r\n\r\n### Client javascript library\r\n\r\n```javascript\r\nvar pusher = new Pusher(APP_KEY, {\r\n wsHost: 'localhost',\r\n wsPort: 8080,\r\n enabledTransports: [\"ws\", \"flash\"],\r\n disabledTransports: [\"flash\"]\r\n});\r\n```\r\n\r\n### Client server libraries\r\n\r\nRuby\r\n\r\n```ruby\r\nPusher.host = 'localhost'\r\nPusher.port = 8080\r\n```\r\n\r\nPHP\r\n\r\n```php\r\n$pusher = new Pusher(APP_KEY, APP_SECRET, APP_ID, DEBUG, \"http://localhost\", \"8080\");\r\n```\r\n\r\nNodeJS\r\n\r\n```javascript\r\nvar pusher = new Pusher({\r\n appId: APP_ID,\r\n key: APP_KEY,\r\n secret: APP_SECRET\r\n domain: 'localhost',\r\n port: 80\r\n});\r\n\r\n```\r\n\r\n# Logging\r\n\r\nThis software uses the [glog](https://github.com/golang/glog) library\r\n\r\nfor more information about logging type the following in console.\r\n\r\n```console\r\n$ ipe -h\r\n```\r\n\r\n# When use this software?\r\n\r\n* When you are offline;\r\n* When you want to control your infrastructure;\r\n* When you do not want to have external dependencies;\r\n* When you want extend the protocol;\r\n\r\n# Contributing.\r\n\r\nFeel free to fork this repo.\r\n\r\n# Pusher\r\n\r\nPusher is an excelent service, their service is very reliable. I recomend for everyone.\r\n\r\n# Where this name came from?\r\n\r\nHere in Brazil we have this beautiful tree called [Ipê](http://en.wikipedia.org/wiki/Tabebuia_aurea), it comes in differente colors: yellow, pink, white, purple.\r\n\r\n[I want to see pictures](https://www.flickr.com/search/?q=ipe)\r\n\r\n# Author\r\n\r\nClaudemiro Alves Feitosa Neto\r\n\r\n# LICENSE\r\n\r\nCopyright 2014, 2015, 2016 Claudemiro Alves Feitosa Neto. All rights reserved.\r\nUse of this source code is governed by a MIT-style\r\nlicense that can be found in the LICENSE file.\r\n\r\n","google":"","note":"Don't delete this file! It's used internally to help with page regeneration."}
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 GitHub Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
*/
|
||||
|
||||
.pl-c /* comment */ {
|
||||
color: #969896;
|
||||
}
|
||||
|
||||
.pl-c1 /* constant, markup.raw, meta.diff.header, meta.module-reference, meta.property-name, support, support.constant, support.variable, variable.other.constant */,
|
||||
.pl-s .pl-v /* string variable */ {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.pl-e /* entity */,
|
||||
.pl-en /* entity.name */ {
|
||||
color: #795da3;
|
||||
}
|
||||
|
||||
.pl-s .pl-s1 /* string source */,
|
||||
.pl-smi /* storage.modifier.import, storage.modifier.package, storage.type.java, variable.other, variable.parameter.function */ {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.pl-ent /* entity.name.tag */ {
|
||||
color: #63a35c;
|
||||
}
|
||||
|
||||
.pl-k /* keyword, storage, storage.type */ {
|
||||
color: #a71d5d;
|
||||
}
|
||||
|
||||
.pl-pds /* punctuation.definition.string, string.regexp.character-class */,
|
||||
.pl-s /* string */,
|
||||
.pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */,
|
||||
.pl-sr /* string.regexp */,
|
||||
.pl-sr .pl-cce /* string.regexp constant.character.escape */,
|
||||
.pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */,
|
||||
.pl-sr .pl-sre /* string.regexp source.ruby.embedded */ {
|
||||
color: #183691;
|
||||
}
|
||||
|
||||
.pl-v /* variable */ {
|
||||
color: #ed6a43;
|
||||
}
|
||||
|
||||
.pl-id /* invalid.deprecated */ {
|
||||
color: #b52a1d;
|
||||
}
|
||||
|
||||
.pl-ii /* invalid.illegal */ {
|
||||
background-color: #b52a1d;
|
||||
color: #f8f8f8;
|
||||
}
|
||||
|
||||
.pl-sr .pl-cce /* string.regexp constant.character.escape */ {
|
||||
color: #63a35c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pl-ml /* markup.list */ {
|
||||
color: #693a17;
|
||||
}
|
||||
|
||||
.pl-mh /* markup.heading */,
|
||||
.pl-mh .pl-en /* markup.heading entity.name */,
|
||||
.pl-ms /* meta.separator */ {
|
||||
color: #1d3e81;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pl-mq /* markup.quote */ {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.pl-mi /* markup.italic */ {
|
||||
color: #333;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.pl-mb /* markup.bold */ {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pl-md /* markup.deleted, meta.diff.header.from-file */ {
|
||||
background-color: #ffecec;
|
||||
color: #bd2c00;
|
||||
}
|
||||
|
||||
.pl-mi1 /* markup.inserted, meta.diff.header.to-file */ {
|
||||
background-color: #eaffea;
|
||||
color: #55a532;
|
||||
}
|
||||
|
||||
.pl-mdr /* meta.diff.range */ {
|
||||
color: #795da3;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pl-mo /* meta.output */ {
|
||||
color: #1d3e81;
|
||||
}
|
||||
|
||||
Vendored
-424
@@ -1,424 +0,0 @@
|
||||
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
|
||||
|
||||
/**
|
||||
* 1. Set default font family to sans-serif.
|
||||
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
||||
* user zoom.
|
||||
*/
|
||||
|
||||
html {
|
||||
font-family: sans-serif; /* 1 */
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default margin.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* HTML5 display definitions
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Correct `block` display not defined for any HTML5 element in IE 8/9.
|
||||
* Correct `block` display not defined for `details` or `summary` in IE 10/11
|
||||
* and Firefox.
|
||||
* Correct `block` display not defined for `main` in IE 11.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
menu,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `inline-block` display not defined in IE 8/9.
|
||||
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
progress,
|
||||
video {
|
||||
display: inline-block; /* 1 */
|
||||
vertical-align: baseline; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent modern browsers from displaying `audio` without controls.
|
||||
* Remove excess height in iOS 5 devices.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `[hidden]` styling not present in IE 8/9/10.
|
||||
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
|
||||
*/
|
||||
|
||||
[hidden],
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Links
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background color from active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve readability when focused and also mouse hovered in all browsers.
|
||||
*/
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in Safari and Chrome.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address variable `h1` font-size and margin within `section` and `article`
|
||||
* contexts in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent and variable font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove border when inside `a` element in IE 8/9/10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct overflow not hidden in IE 9/10/11.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address margin not present in IE 8/9 and Safari.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address differences between Firefox and other browsers.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contain overflow in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address odd `em`-unit font size rendering in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Known limitation: by default, Chrome and Safari on OS X allow very limited
|
||||
* styling of `select`, unless a `border` property is set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 1. Correct color not being inherited.
|
||||
* Known issue: affects color of disabled elements.
|
||||
* 2. Correct font properties not being inherited.
|
||||
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
color: inherit; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
margin: 0; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
button {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||
* All other form control elements do not inherit `text-transform` values.
|
||||
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
|
||||
* Correct `select` style inheritance in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||
* and `video` controls.
|
||||
* 2. Correct inability to style clickable `input` types in iOS.
|
||||
* 3. Improve usability and consistency of cursor style between image-type
|
||||
* `input` and others.
|
||||
*/
|
||||
|
||||
button,
|
||||
html input[type="button"], /* 1 */
|
||||
input[type="reset"],
|
||||
input[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
cursor: pointer; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-set default cursor for disabled elements.
|
||||
*/
|
||||
|
||||
button[disabled],
|
||||
html input[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and border in Firefox 4+.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||
* the UA stylesheet.
|
||||
*/
|
||||
|
||||
input {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended that you don't attempt to style these elements.
|
||||
* Firefox's implementation doesn't respect box-sizing, padding, or width.
|
||||
*
|
||||
* 1. Address box sizing set to `content-box` in IE 8/9/10.
|
||||
* 2. Remove excess padding in IE 8/9/10.
|
||||
*/
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
|
||||
* `font-size` values of the `input`, it causes the cursor style of the
|
||||
* decrement button to change from `default` to `text`.
|
||||
*/
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
|
||||
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
|
||||
* (include `-moz` to future-proof).
|
||||
*/
|
||||
|
||||
input[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */ /* 2 */
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
|
||||
* Safari (but not Chrome) clips the cancel button when the search input has
|
||||
* padding (and `textfield` appearance).
|
||||
*/
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define consistent border, margin, and padding.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #c0c0c0;
|
||||
margin: 0 2px;
|
||||
padding: 0.35em 0.625em 0.75em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `color` not being inherited in IE 8/9/10/11.
|
||||
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||
*/
|
||||
|
||||
legend {
|
||||
border: 0; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default vertical scrollbar in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't inherit the `font-weight` (applied by a rule above).
|
||||
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
|
||||
*/
|
||||
|
||||
optgroup {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Tables
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove most spacing between table cells.
|
||||
*/
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
.highlight .c { color: #999988; font-style: italic } /* Comment */
|
||||
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
|
||||
.highlight .k { font-weight: bold } /* Keyword */
|
||||
.highlight .o { font-weight: bold } /* Operator */
|
||||
.highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */
|
||||
.highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */
|
||||
.highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */
|
||||
.highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */
|
||||
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
|
||||
.highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */
|
||||
.highlight .ge { font-style: italic } /* Generic.Emph */
|
||||
.highlight .gr { color: #aa0000 } /* Generic.Error */
|
||||
.highlight .gh { color: #999999 } /* Generic.Heading */
|
||||
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
|
||||
.highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */
|
||||
.highlight .go { color: #888888 } /* Generic.Output */
|
||||
.highlight .gp { color: #555555 } /* Generic.Prompt */
|
||||
.highlight .gs { font-weight: bold } /* Generic.Strong */
|
||||
.highlight .gu { color: #800080; font-weight: bold; } /* Generic.Subheading */
|
||||
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
|
||||
.highlight .kc { font-weight: bold } /* Keyword.Constant */
|
||||
.highlight .kd { font-weight: bold } /* Keyword.Declaration */
|
||||
.highlight .kn { font-weight: bold } /* Keyword.Namespace */
|
||||
.highlight .kp { font-weight: bold } /* Keyword.Pseudo */
|
||||
.highlight .kr { font-weight: bold } /* Keyword.Reserved */
|
||||
.highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */
|
||||
.highlight .m { color: #009999 } /* Literal.Number */
|
||||
.highlight .s { color: #d14 } /* Literal.String */
|
||||
.highlight .na { color: #008080 } /* Name.Attribute */
|
||||
.highlight .nb { color: #0086B3 } /* Name.Builtin */
|
||||
.highlight .nc { color: #445588; font-weight: bold } /* Name.Class */
|
||||
.highlight .no { color: #008080 } /* Name.Constant */
|
||||
.highlight .ni { color: #800080 } /* Name.Entity */
|
||||
.highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */
|
||||
.highlight .nf { color: #990000; font-weight: bold } /* Name.Function */
|
||||
.highlight .nn { color: #555555 } /* Name.Namespace */
|
||||
.highlight .nt { color: #CBDFFF } /* Name.Tag */
|
||||
.highlight .nv { color: #008080 } /* Name.Variable */
|
||||
.highlight .ow { font-weight: bold } /* Operator.Word */
|
||||
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
|
||||
.highlight .mf { color: #009999 } /* Literal.Number.Float */
|
||||
.highlight .mh { color: #009999 } /* Literal.Number.Hex */
|
||||
.highlight .mi { color: #009999 } /* Literal.Number.Integer */
|
||||
.highlight .mo { color: #009999 } /* Literal.Number.Oct */
|
||||
.highlight .sb { color: #d14 } /* Literal.String.Backtick */
|
||||
.highlight .sc { color: #d14 } /* Literal.String.Char */
|
||||
.highlight .sd { color: #d14 } /* Literal.String.Doc */
|
||||
.highlight .s2 { color: #d14 } /* Literal.String.Double */
|
||||
.highlight .se { color: #d14 } /* Literal.String.Escape */
|
||||
.highlight .sh { color: #d14 } /* Literal.String.Heredoc */
|
||||
.highlight .si { color: #d14 } /* Literal.String.Interpol */
|
||||
.highlight .sx { color: #d14 } /* Literal.String.Other */
|
||||
.highlight .sr { color: #009926 } /* Literal.String.Regex */
|
||||
.highlight .s1 { color: #d14 } /* Literal.String.Single */
|
||||
.highlight .ss { color: #990073 } /* Literal.String.Symbol */
|
||||
.highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */
|
||||
.highlight .vc { color: #008080 } /* Name.Variable.Class */
|
||||
.highlight .vg { color: #008080 } /* Name.Variable.Global */
|
||||
.highlight .vi { color: #008080 } /* Name.Variable.Instance */
|
||||
.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */
|
||||
|
||||
.type-csharp .highlight .k { color: #0000FF }
|
||||
.type-csharp .highlight .kt { color: #0000FF }
|
||||
.type-csharp .highlight .nf { color: #000000; font-weight: normal }
|
||||
.type-csharp .highlight .nc { color: #2B91AF }
|
||||
.type-csharp .highlight .nn { color: #000000 }
|
||||
.type-csharp .highlight .s { color: #A31515 }
|
||||
.type-csharp .highlight .sc { color: #A31515 }
|
||||
@@ -1,324 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
src: url('../fonts/Noto-Sans-regular/Noto-Sans-regular.eot');
|
||||
src: url('../fonts/Noto-Sans-regular/Noto-Sans-regular.eot?#iefix') format('embedded-opentype'),
|
||||
local('Noto Sans'),
|
||||
local('Noto-Sans-regular'),
|
||||
url('../fonts/Noto-Sans-regular/Noto-Sans-regular.woff2') format('woff2'),
|
||||
url('../fonts/Noto-Sans-regular/Noto-Sans-regular.woff') format('woff'),
|
||||
url('../fonts/Noto-Sans-regular/Noto-Sans-regular.ttf') format('truetype'),
|
||||
url('../fonts/Noto-Sans-regular/Noto-Sans-regular.svg#NotoSans') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
src: url('../fonts/Noto-Sans-700/Noto-Sans-700.eot');
|
||||
src: url('../fonts/Noto-Sans-700/Noto-Sans-700.eot?#iefix') format('embedded-opentype'),
|
||||
local('Noto Sans Bold'),
|
||||
local('Noto-Sans-700'),
|
||||
url('../fonts/Noto-Sans-700/Noto-Sans-700.woff2') format('woff2'),
|
||||
url('../fonts/Noto-Sans-700/Noto-Sans-700.woff') format('woff'),
|
||||
url('../fonts/Noto-Sans-700/Noto-Sans-700.ttf') format('truetype'),
|
||||
url('../fonts/Noto-Sans-700/Noto-Sans-700.svg#NotoSans') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
src: url('../fonts/Noto-Sans-italic/Noto-Sans-italic.eot');
|
||||
src: url('../fonts/Noto-Sans-italic/Noto-Sans-italic.eot?#iefix') format('embedded-opentype'),
|
||||
local('Noto Sans Italic'),
|
||||
local('Noto-Sans-italic'),
|
||||
url('../fonts/Noto-Sans-italic/Noto-Sans-italic.woff2') format('woff2'),
|
||||
url('../fonts/Noto-Sans-italic/Noto-Sans-italic.woff') format('woff'),
|
||||
url('../fonts/Noto-Sans-italic/Noto-Sans-italic.ttf') format('truetype'),
|
||||
url('../fonts/Noto-Sans-italic/Noto-Sans-italic.svg#NotoSans') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
src: url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.eot');
|
||||
src: url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.eot?#iefix') format('embedded-opentype'),
|
||||
local('Noto Sans Bold Italic'),
|
||||
local('Noto-Sans-700italic'),
|
||||
url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.woff2') format('woff2'),
|
||||
url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.woff') format('woff'),
|
||||
url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.ttf') format('truetype'),
|
||||
url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.svg#NotoSans') format('svg');
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #fff;
|
||||
padding:50px;
|
||||
font: 14px/1.5 "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color:#727272;
|
||||
font-weight:400;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color:#222;
|
||||
margin:0 0 20px;
|
||||
}
|
||||
|
||||
p, ul, ol, table, pre, dl {
|
||||
margin:0 0 20px;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
line-height:1.1;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size:28px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color:#393939;
|
||||
}
|
||||
|
||||
h3, h4, h5, h6 {
|
||||
color:#494949;
|
||||
}
|
||||
|
||||
a {
|
||||
color:#39c;
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color:#069;
|
||||
}
|
||||
|
||||
a small {
|
||||
font-size:11px;
|
||||
color:#777;
|
||||
margin-top:-0.3em;
|
||||
display:block;
|
||||
}
|
||||
|
||||
a:hover small {
|
||||
color:#777;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width:860px;
|
||||
margin:0 auto;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left:1px solid #e5e5e5;
|
||||
margin:0;
|
||||
padding:0 0 0 20px;
|
||||
font-style:italic;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal, Consolas, Liberation Mono, DejaVu Sans Mono, Courier New, monospace;
|
||||
color:#333;
|
||||
font-size:12px;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding:8px 15px;
|
||||
background: #f8f8f8;
|
||||
border-radius:5px;
|
||||
border:1px solid #e5e5e5;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width:100%;
|
||||
border-collapse:collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align:left;
|
||||
padding:5px 10px;
|
||||
border-bottom:1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
dt {
|
||||
color:#444;
|
||||
font-weight:700;
|
||||
}
|
||||
|
||||
th {
|
||||
color:#444;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width:100%;
|
||||
}
|
||||
|
||||
header {
|
||||
width:270px;
|
||||
float:left;
|
||||
position:fixed;
|
||||
-webkit-font-smoothing:subpixel-antialiased;
|
||||
}
|
||||
|
||||
header ul {
|
||||
list-style:none;
|
||||
height:40px;
|
||||
padding:0;
|
||||
background: #f4f4f4;
|
||||
border-radius:5px;
|
||||
border:1px solid #e0e0e0;
|
||||
width:270px;
|
||||
}
|
||||
|
||||
header li {
|
||||
width:89px;
|
||||
float:left;
|
||||
border-right:1px solid #e0e0e0;
|
||||
height:40px;
|
||||
}
|
||||
|
||||
header li:first-child a {
|
||||
border-radius:5px 0 0 5px;
|
||||
}
|
||||
|
||||
header li:last-child a {
|
||||
border-radius:0 5px 5px 0;
|
||||
}
|
||||
|
||||
header ul a {
|
||||
line-height:1;
|
||||
font-size:11px;
|
||||
color:#999;
|
||||
display:block;
|
||||
text-align:center;
|
||||
padding-top:6px;
|
||||
height:34px;
|
||||
}
|
||||
|
||||
header ul a:hover {
|
||||
color:#999;
|
||||
}
|
||||
|
||||
header ul a:active {
|
||||
background-color:#f0f0f0;
|
||||
}
|
||||
|
||||
strong {
|
||||
color:#222;
|
||||
font-weight:700;
|
||||
}
|
||||
|
||||
header ul li + li + li {
|
||||
border-right:none;
|
||||
width:89px;
|
||||
}
|
||||
|
||||
header ul a strong {
|
||||
font-size:14px;
|
||||
display:block;
|
||||
color:#222;
|
||||
}
|
||||
|
||||
section {
|
||||
width:500px;
|
||||
float:right;
|
||||
padding-bottom:50px;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size:11px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border:0;
|
||||
background:#e5e5e5;
|
||||
height:1px;
|
||||
margin:0 0 20px;
|
||||
}
|
||||
|
||||
footer {
|
||||
width:270px;
|
||||
float:left;
|
||||
position:fixed;
|
||||
bottom:50px;
|
||||
-webkit-font-smoothing:subpixel-antialiased;
|
||||
}
|
||||
|
||||
@media print, screen and (max-width: 960px) {
|
||||
|
||||
div.wrapper {
|
||||
width:auto;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
header, section, footer {
|
||||
float:none;
|
||||
position:static;
|
||||
width:auto;
|
||||
}
|
||||
|
||||
header {
|
||||
padding-right:320px;
|
||||
}
|
||||
|
||||
section {
|
||||
border:1px solid #e5e5e5;
|
||||
border-width:1px 0;
|
||||
padding:20px 0;
|
||||
margin:0 0 20px;
|
||||
}
|
||||
|
||||
header a small {
|
||||
display:inline;
|
||||
}
|
||||
|
||||
header ul {
|
||||
position:absolute;
|
||||
right:50px;
|
||||
top:52px;
|
||||
}
|
||||
}
|
||||
|
||||
@media print, screen and (max-width: 720px) {
|
||||
body {
|
||||
word-wrap:break-word;
|
||||
}
|
||||
|
||||
header {
|
||||
padding:0;
|
||||
}
|
||||
|
||||
header ul, header p.view {
|
||||
position:static;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
word-wrap:normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media print, screen and (max-width: 480px) {
|
||||
body {
|
||||
padding:15px;
|
||||
}
|
||||
|
||||
header ul {
|
||||
width:99%;
|
||||
}
|
||||
|
||||
header li, header ul li + li + li {
|
||||
width:33%;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
padding:0.4in;
|
||||
font-size:12pt;
|
||||
color:#444;
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
* {
|
||||
box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: #606c71; }
|
||||
|
||||
a {
|
||||
color: #1e6bb8;
|
||||
text-decoration: none; }
|
||||
a:hover {
|
||||
text-decoration: underline; }
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
margin-bottom: 1rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 0.3rem;
|
||||
transition: color 0.2s, background-color 0.2s, border-color 0.2s; }
|
||||
.btn + .btn {
|
||||
margin-left: 1rem; }
|
||||
|
||||
.btn:hover {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-decoration: none;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.3); }
|
||||
|
||||
@media screen and (min-width: 64em) {
|
||||
.btn {
|
||||
padding: 0.75rem 1rem; } }
|
||||
|
||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
||||
.btn {
|
||||
padding: 0.6rem 0.9rem;
|
||||
font-size: 0.9rem; } }
|
||||
|
||||
@media screen and (max-width: 42em) {
|
||||
.btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
font-size: 0.9rem; }
|
||||
.btn + .btn {
|
||||
margin-top: 1rem;
|
||||
margin-left: 0; } }
|
||||
|
||||
.page-header {
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-color: #159957;
|
||||
background-image: linear-gradient(120deg, #155799, #159957); }
|
||||
|
||||
@media screen and (min-width: 64em) {
|
||||
.page-header {
|
||||
padding: 5rem 6rem; } }
|
||||
|
||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
||||
.page-header {
|
||||
padding: 3rem 4rem; } }
|
||||
|
||||
@media screen and (max-width: 42em) {
|
||||
.page-header {
|
||||
padding: 2rem 1rem; } }
|
||||
|
||||
.project-name {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.1rem; }
|
||||
|
||||
@media screen and (min-width: 64em) {
|
||||
.project-name {
|
||||
font-size: 3.25rem; } }
|
||||
|
||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
||||
.project-name {
|
||||
font-size: 2.25rem; } }
|
||||
|
||||
@media screen and (max-width: 42em) {
|
||||
.project-name {
|
||||
font-size: 1.75rem; } }
|
||||
|
||||
.project-tagline {
|
||||
margin-bottom: 2rem;
|
||||
font-weight: normal;
|
||||
opacity: 0.7; }
|
||||
|
||||
@media screen and (min-width: 64em) {
|
||||
.project-tagline {
|
||||
font-size: 1.25rem; } }
|
||||
|
||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
||||
.project-tagline {
|
||||
font-size: 1.15rem; } }
|
||||
|
||||
@media screen and (max-width: 42em) {
|
||||
.project-tagline {
|
||||
font-size: 1rem; } }
|
||||
|
||||
.main-content :first-child {
|
||||
margin-top: 0; }
|
||||
.main-content img {
|
||||
max-width: 100%; }
|
||||
.main-content h1, .main-content h2, .main-content h3, .main-content h4, .main-content h5, .main-content h6 {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: normal;
|
||||
color: #159957; }
|
||||
.main-content p {
|
||||
margin-bottom: 1em; }
|
||||
.main-content code {
|
||||
padding: 2px 4px;
|
||||
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
font-size: 0.9rem;
|
||||
color: #383e41;
|
||||
background-color: #f3f6fa;
|
||||
border-radius: 0.3rem; }
|
||||
.main-content pre {
|
||||
padding: 0.8rem;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
font: 1rem Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
color: #567482;
|
||||
word-wrap: normal;
|
||||
background-color: #f3f6fa;
|
||||
border: solid 1px #dce6f0;
|
||||
border-radius: 0.3rem; }
|
||||
.main-content pre > code {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: #567482;
|
||||
word-break: normal;
|
||||
white-space: pre;
|
||||
background: transparent;
|
||||
border: 0; }
|
||||
.main-content .highlight {
|
||||
margin-bottom: 1rem; }
|
||||
.main-content .highlight pre {
|
||||
margin-bottom: 0;
|
||||
word-break: normal; }
|
||||
.main-content .highlight pre, .main-content pre {
|
||||
padding: 0.8rem;
|
||||
overflow: auto;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.45;
|
||||
border-radius: 0.3rem; }
|
||||
.main-content pre code, .main-content pre tt {
|
||||
display: inline;
|
||||
max-width: initial;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: initial;
|
||||
line-height: inherit;
|
||||
word-wrap: normal;
|
||||
background-color: transparent;
|
||||
border: 0; }
|
||||
.main-content pre code:before, .main-content pre code:after, .main-content pre tt:before, .main-content pre tt:after {
|
||||
content: normal; }
|
||||
.main-content ul, .main-content ol {
|
||||
margin-top: 0; }
|
||||
.main-content blockquote {
|
||||
padding: 0 1rem;
|
||||
margin-left: 0;
|
||||
color: #819198;
|
||||
border-left: 0.3rem solid #dce6f0; }
|
||||
.main-content blockquote > :first-child {
|
||||
margin-top: 0; }
|
||||
.main-content blockquote > :last-child {
|
||||
margin-bottom: 0; }
|
||||
.main-content table {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
word-break: normal;
|
||||
word-break: keep-all; }
|
||||
.main-content table th {
|
||||
font-weight: bold; }
|
||||
.main-content table th, .main-content table td {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #e9ebec; }
|
||||
.main-content dl {
|
||||
padding: 0; }
|
||||
.main-content dl dt {
|
||||
padding: 0;
|
||||
margin-top: 1rem;
|
||||
font-size: 1rem;
|
||||
font-weight: bold; }
|
||||
.main-content dl dd {
|
||||
padding: 0;
|
||||
margin-bottom: 1rem; }
|
||||
.main-content hr {
|
||||
height: 2px;
|
||||
padding: 0;
|
||||
margin: 1rem 0;
|
||||
background-color: #eff0f1;
|
||||
border: 0; }
|
||||
|
||||
@media screen and (min-width: 64em) {
|
||||
.main-content {
|
||||
max-width: 64rem;
|
||||
padding: 2rem 6rem;
|
||||
margin: 0 auto;
|
||||
font-size: 1.1rem; } }
|
||||
|
||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
||||
.main-content {
|
||||
padding: 2rem 4rem;
|
||||
font-size: 1.1rem; } }
|
||||
|
||||
@media screen and (max-width: 42em) {
|
||||
.main-content {
|
||||
padding: 2rem 1rem;
|
||||
font-size: 1rem; } }
|
||||
|
||||
.site-footer {
|
||||
padding-top: 2rem;
|
||||
margin-top: 2rem;
|
||||
border-top: solid 1px #eff0f1; }
|
||||
|
||||
.site-footer-owner {
|
||||
display: block;
|
||||
font-weight: bold; }
|
||||
|
||||
.site-footer-credits {
|
||||
color: #819198; }
|
||||
|
||||
@media screen and (min-width: 64em) {
|
||||
.site-footer {
|
||||
font-size: 1rem; } }
|
||||
|
||||
@media screen and (min-width: 42em) and (max-width: 64em) {
|
||||
.site-footer {
|
||||
font-size: 1rem; } }
|
||||
|
||||
@media screen and (max-width: 42em) {
|
||||
.site-footer {
|
||||
font-size: 0.9rem; } }
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var 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)
|
||||
mac.Write(message)
|
||||
expected := mac.Sum(nil)
|
||||
|
||||
return hex.EncodeToString(expected)
|
||||
}
|
||||
|
||||
// GenerateSessionID Generate a new random Hash
|
||||
func GenerateSessionID() string {
|
||||
return fmt.Sprintf("%d.%d", rand.Intn(math.MaxInt64), rand.Intn(math.MaxInt64))
|
||||
}
|
||||
|
||||
// IsChannelNameValid Verify if the channel name is valid
|
||||
func IsChannelNameValid(channelName string) bool {
|
||||
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-")
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright 2015 Claudemiro Alves Feitosa Neto. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkGenerateSession(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
GenerateSessionID()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIsChannelNameValid(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
IsChannelNameValid("hello-world")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateSession(t *testing.T) {
|
||||
sessionID := GenerateSessionID()
|
||||
|
||||
if matched, _ := regexp.MatchString("^\\d+\\.\\d+$", sessionID); !matched {
|
||||
t.Errorf("Must match ^\\d+\\.\\d+$, value: '%s'", sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidChannelName(t *testing.T) {
|
||||
name := "#@#hhh**sasas"
|
||||
ok := IsChannelNameValid(name)
|
||||
|
||||
if ok {
|
||||
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, false)
|
||||
}
|
||||
|
||||
name = "private-hello"
|
||||
ok = IsChannelNameValid(name)
|
||||
|
||||
if !ok {
|
||||
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, true)
|
||||
}
|
||||
|
||||
name = "presence-hello"
|
||||
ok = IsChannelNameValid(name)
|
||||
|
||||
if !ok {
|
||||
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, true)
|
||||
}
|
||||
|
||||
name = "public"
|
||||
ok = IsChannelNameValid(name)
|
||||
|
||||
if !ok {
|
||||
t.Errorf("IsChannelNameValid(%s) == %t, wants %t", name, ok, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPrivateChannel_valid(t *testing.T) {
|
||||
name := "private-hello"
|
||||
ok := IsPrivateChannel(name)
|
||||
|
||||
if !ok {
|
||||
t.Errorf("IsPrivateChannel(%s) == %t, wants %t", name, ok, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPrivateChannel_invalid(t *testing.T) {
|
||||
name := "hello"
|
||||
ok := IsPrivateChannel(name)
|
||||
|
||||
if ok {
|
||||
t.Errorf("IsPrivateChannel(%s) == %t, wants %t", name, ok, false)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsIsPresenceChannel_valid(t *testing.T) {
|
||||
name := "presence-hello"
|
||||
ok := IsPresenceChannel(name)
|
||||
|
||||
if !ok {
|
||||
t.Errorf("IsPresenceChannel(%s) == %t, wants %t", name, ok, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPresenceChannel_invalid(t *testing.T) {
|
||||
name := "hello"
|
||||
ok := IsPresenceChannel(name)
|
||||
|
||||
if ok {
|
||||
t.Errorf("IsPresenceChannel(%s) == %t, wants %t", name, ok, false)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashMAC(t *testing.T) {
|
||||
message := []byte("hello world")
|
||||
key := []byte("my super secret key")
|
||||
digest := HashMAC(message, key)
|
||||
|
||||
// See: http://www.freeformatter.com/hmac-generator.html
|
||||
expected := "0811b8affc185a01e1a65b80089ebb1f7f68d287fc3b64581da9ec99136ad1db"
|
||||
|
||||
if digest != expected {
|
||||
t.Errorf("HashMAC(%s, %q) == %s, wants %s", message, key, digest, expected)
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, "control" means (i) the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
"submitted" means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution.
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
If the Work includes a "NOTICE" text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions.
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
6. Trademarks.
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
8. Limitation of Liability.
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability.
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets "[]" replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same "printed page" as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
glog
|
||||
====
|
||||
|
||||
Leveled execution logs for Go.
|
||||
|
||||
This is an efficient pure Go implementation of leveled logs in the
|
||||
manner of the open source C++ package
|
||||
https://github.com/google/glog
|
||||
|
||||
By binding methods to booleans it is possible to use the log package
|
||||
without paying the expense of evaluating the arguments to the log.
|
||||
Through the -vmodule flag, the package also provides fine-grained
|
||||
control over logging at the file level.
|
||||
|
||||
The comment from glog.go introduces the ideas:
|
||||
|
||||
Package glog implements logging analogous to the Google-internal
|
||||
C++ INFO/ERROR/V setup. It provides functions Info, Warning,
|
||||
Error, Fatal, plus formatting variants such as Infof. It
|
||||
also provides V-style logging controlled by the -v and
|
||||
-vmodule=file=2 flags.
|
||||
|
||||
Basic examples:
|
||||
|
||||
glog.Info("Prepare to repel boarders")
|
||||
|
||||
glog.Fatalf("Initialization failed: %s", err)
|
||||
|
||||
See the documentation for the V function for an explanation
|
||||
of these examples:
|
||||
|
||||
if glog.V(2) {
|
||||
glog.Info("Starting transaction...")
|
||||
}
|
||||
|
||||
glog.V(2).Infoln("Processed", nItems, "elements")
|
||||
|
||||
|
||||
The repository contains an open source version of the log package
|
||||
used inside Google. The master copy of the source lives inside
|
||||
Google, not here. The code in this repo is for export only and is not itself
|
||||
under development. Feature requests will be ignored.
|
||||
|
||||
Send bug reports to golang-nuts@googlegroups.com.
|
||||
+1180
File diff suppressed because it is too large
Load Diff
+124
@@ -0,0 +1,124 @@
|
||||
// Go support for leveled logs, analogous to https://code.google.com/p/google-glog/
|
||||
//
|
||||
// Copyright 2013 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// File I/O for logs.
|
||||
|
||||
package glog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MaxSize is the maximum size of a log file in bytes.
|
||||
var MaxSize uint64 = 1024 * 1024 * 1800
|
||||
|
||||
// logDirs lists the candidate directories for new log files.
|
||||
var logDirs []string
|
||||
|
||||
// If non-empty, overrides the choice of directory in which to write logs.
|
||||
// See createLogDirs for the full list of possible destinations.
|
||||
var logDir = flag.String("log_dir", "", "If non-empty, write log files in this directory")
|
||||
|
||||
func createLogDirs() {
|
||||
if *logDir != "" {
|
||||
logDirs = append(logDirs, *logDir)
|
||||
}
|
||||
logDirs = append(logDirs, os.TempDir())
|
||||
}
|
||||
|
||||
var (
|
||||
pid = os.Getpid()
|
||||
program = filepath.Base(os.Args[0])
|
||||
host = "unknownhost"
|
||||
userName = "unknownuser"
|
||||
)
|
||||
|
||||
func init() {
|
||||
h, err := os.Hostname()
|
||||
if err == nil {
|
||||
host = shortHostname(h)
|
||||
}
|
||||
|
||||
current, err := user.Current()
|
||||
if err == nil {
|
||||
userName = current.Username
|
||||
}
|
||||
|
||||
// Sanitize userName since it may contain filepath separators on Windows.
|
||||
userName = strings.Replace(userName, `\`, "_", -1)
|
||||
}
|
||||
|
||||
// shortHostname returns its argument, truncating at the first period.
|
||||
// For instance, given "www.google.com" it returns "www".
|
||||
func shortHostname(hostname string) string {
|
||||
if i := strings.Index(hostname, "."); i >= 0 {
|
||||
return hostname[:i]
|
||||
}
|
||||
return hostname
|
||||
}
|
||||
|
||||
// logName returns a new log file name containing tag, with start time t, and
|
||||
// the name for the symlink for tag.
|
||||
func logName(tag string, t time.Time) (name, link string) {
|
||||
name = fmt.Sprintf("%s.%s.%s.log.%s.%04d%02d%02d-%02d%02d%02d.%d",
|
||||
program,
|
||||
host,
|
||||
userName,
|
||||
tag,
|
||||
t.Year(),
|
||||
t.Month(),
|
||||
t.Day(),
|
||||
t.Hour(),
|
||||
t.Minute(),
|
||||
t.Second(),
|
||||
pid)
|
||||
return name, program + "." + tag
|
||||
}
|
||||
|
||||
var onceLogDirs sync.Once
|
||||
|
||||
// create creates a new log file and returns the file and its filename, which
|
||||
// contains tag ("INFO", "FATAL", etc.) and t. If the file is created
|
||||
// successfully, create also attempts to update the symlink for that tag, ignoring
|
||||
// errors.
|
||||
func create(tag string, t time.Time) (f *os.File, filename string, err error) {
|
||||
onceLogDirs.Do(createLogDirs)
|
||||
if len(logDirs) == 0 {
|
||||
return nil, "", errors.New("log: no log dirs")
|
||||
}
|
||||
name, link := logName(tag, t)
|
||||
var lastErr error
|
||||
for _, dir := range logDirs {
|
||||
fname := filepath.Join(dir, name)
|
||||
f, err := os.Create(fname)
|
||||
if err == nil {
|
||||
symlink := filepath.Join(dir, link)
|
||||
os.Remove(symlink) // ignore err
|
||||
os.Symlink(name, symlink) // ignore err
|
||||
return f, fname, nil
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
return nil, "", fmt.Errorf("log: cannot create log: %v", lastErr)
|
||||
}
|
||||
+415
@@ -0,0 +1,415 @@
|
||||
// Go support for leveled logs, analogous to https://code.google.com/p/google-glog/
|
||||
//
|
||||
// Copyright 2013 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package glog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
stdLog "log"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Test that shortHostname works as advertised.
|
||||
func TestShortHostname(t *testing.T) {
|
||||
for hostname, expect := range map[string]string{
|
||||
"": "",
|
||||
"host": "host",
|
||||
"host.google.com": "host",
|
||||
} {
|
||||
if got := shortHostname(hostname); expect != got {
|
||||
t.Errorf("shortHostname(%q): expected %q, got %q", hostname, expect, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// flushBuffer wraps a bytes.Buffer to satisfy flushSyncWriter.
|
||||
type flushBuffer struct {
|
||||
bytes.Buffer
|
||||
}
|
||||
|
||||
func (f *flushBuffer) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *flushBuffer) Sync() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// swap sets the log writers and returns the old array.
|
||||
func (l *loggingT) swap(writers [numSeverity]flushSyncWriter) (old [numSeverity]flushSyncWriter) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
old = l.file
|
||||
for i, w := range writers {
|
||||
logging.file[i] = w
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// newBuffers sets the log writers to all new byte buffers and returns the old array.
|
||||
func (l *loggingT) newBuffers() [numSeverity]flushSyncWriter {
|
||||
return l.swap([numSeverity]flushSyncWriter{new(flushBuffer), new(flushBuffer), new(flushBuffer), new(flushBuffer)})
|
||||
}
|
||||
|
||||
// contents returns the specified log value as a string.
|
||||
func contents(s severity) string {
|
||||
return logging.file[s].(*flushBuffer).String()
|
||||
}
|
||||
|
||||
// contains reports whether the string is contained in the log.
|
||||
func contains(s severity, str string, t *testing.T) bool {
|
||||
return strings.Contains(contents(s), str)
|
||||
}
|
||||
|
||||
// setFlags configures the logging flags how the test expects them.
|
||||
func setFlags() {
|
||||
logging.toStderr = false
|
||||
}
|
||||
|
||||
// Test that Info works as advertised.
|
||||
func TestInfo(t *testing.T) {
|
||||
setFlags()
|
||||
defer logging.swap(logging.newBuffers())
|
||||
Info("test")
|
||||
if !contains(infoLog, "I", t) {
|
||||
t.Errorf("Info has wrong character: %q", contents(infoLog))
|
||||
}
|
||||
if !contains(infoLog, "test", t) {
|
||||
t.Error("Info failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoDepth(t *testing.T) {
|
||||
setFlags()
|
||||
defer logging.swap(logging.newBuffers())
|
||||
|
||||
f := func() { InfoDepth(1, "depth-test1") }
|
||||
|
||||
// The next three lines must stay together
|
||||
_, _, wantLine, _ := runtime.Caller(0)
|
||||
InfoDepth(0, "depth-test0")
|
||||
f()
|
||||
|
||||
msgs := strings.Split(strings.TrimSuffix(contents(infoLog), "\n"), "\n")
|
||||
if len(msgs) != 2 {
|
||||
t.Fatalf("Got %d lines, expected 2", len(msgs))
|
||||
}
|
||||
|
||||
for i, m := range msgs {
|
||||
if !strings.HasPrefix(m, "I") {
|
||||
t.Errorf("InfoDepth[%d] has wrong character: %q", i, m)
|
||||
}
|
||||
w := fmt.Sprintf("depth-test%d", i)
|
||||
if !strings.Contains(m, w) {
|
||||
t.Errorf("InfoDepth[%d] missing %q: %q", i, w, m)
|
||||
}
|
||||
|
||||
// pull out the line number (between : and ])
|
||||
msg := m[strings.LastIndex(m, ":")+1:]
|
||||
x := strings.Index(msg, "]")
|
||||
if x < 0 {
|
||||
t.Errorf("InfoDepth[%d]: missing ']': %q", i, m)
|
||||
continue
|
||||
}
|
||||
line, err := strconv.Atoi(msg[:x])
|
||||
if err != nil {
|
||||
t.Errorf("InfoDepth[%d]: bad line number: %q", i, m)
|
||||
continue
|
||||
}
|
||||
wantLine++
|
||||
if wantLine != line {
|
||||
t.Errorf("InfoDepth[%d]: got line %d, want %d", i, line, wantLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
CopyStandardLogTo("INFO")
|
||||
}
|
||||
|
||||
// Test that CopyStandardLogTo panics on bad input.
|
||||
func TestCopyStandardLogToPanic(t *testing.T) {
|
||||
defer func() {
|
||||
if s, ok := recover().(string); !ok || !strings.Contains(s, "LOG") {
|
||||
t.Errorf(`CopyStandardLogTo("LOG") should have panicked: %v`, s)
|
||||
}
|
||||
}()
|
||||
CopyStandardLogTo("LOG")
|
||||
}
|
||||
|
||||
// Test that using the standard log package logs to INFO.
|
||||
func TestStandardLog(t *testing.T) {
|
||||
setFlags()
|
||||
defer logging.swap(logging.newBuffers())
|
||||
stdLog.Print("test")
|
||||
if !contains(infoLog, "I", t) {
|
||||
t.Errorf("Info has wrong character: %q", contents(infoLog))
|
||||
}
|
||||
if !contains(infoLog, "test", t) {
|
||||
t.Error("Info failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the header has the correct format.
|
||||
func TestHeader(t *testing.T) {
|
||||
setFlags()
|
||||
defer logging.swap(logging.newBuffers())
|
||||
defer func(previous func() time.Time) { timeNow = previous }(timeNow)
|
||||
timeNow = func() time.Time {
|
||||
return time.Date(2006, 1, 2, 15, 4, 5, .067890e9, time.Local)
|
||||
}
|
||||
pid = 1234
|
||||
Info("test")
|
||||
var line int
|
||||
format := "I0102 15:04:05.067890 1234 glog_test.go:%d] test\n"
|
||||
n, err := fmt.Sscanf(contents(infoLog), format, &line)
|
||||
if n != 1 || err != nil {
|
||||
t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(infoLog))
|
||||
}
|
||||
// Scanf treats multiple spaces as equivalent to a single space,
|
||||
// so check for correct space-padding also.
|
||||
want := fmt.Sprintf(format, line)
|
||||
if contents(infoLog) != want {
|
||||
t.Errorf("log format error: got:\n\t%q\nwant:\t%q", contents(infoLog), want)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that an Error log goes to Warning and Info.
|
||||
// Even in the Info log, the source character will be E, so the data should
|
||||
// all be identical.
|
||||
func TestError(t *testing.T) {
|
||||
setFlags()
|
||||
defer logging.swap(logging.newBuffers())
|
||||
Error("test")
|
||||
if !contains(errorLog, "E", t) {
|
||||
t.Errorf("Error has wrong character: %q", contents(errorLog))
|
||||
}
|
||||
if !contains(errorLog, "test", t) {
|
||||
t.Error("Error failed")
|
||||
}
|
||||
str := contents(errorLog)
|
||||
if !contains(warningLog, str, t) {
|
||||
t.Error("Warning failed")
|
||||
}
|
||||
if !contains(infoLog, str, t) {
|
||||
t.Error("Info failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that a Warning log goes to Info.
|
||||
// Even in the Info log, the source character will be W, so the data should
|
||||
// all be identical.
|
||||
func TestWarning(t *testing.T) {
|
||||
setFlags()
|
||||
defer logging.swap(logging.newBuffers())
|
||||
Warning("test")
|
||||
if !contains(warningLog, "W", t) {
|
||||
t.Errorf("Warning has wrong character: %q", contents(warningLog))
|
||||
}
|
||||
if !contains(warningLog, "test", t) {
|
||||
t.Error("Warning failed")
|
||||
}
|
||||
str := contents(warningLog)
|
||||
if !contains(infoLog, str, t) {
|
||||
t.Error("Info failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that a V log goes to Info.
|
||||
func TestV(t *testing.T) {
|
||||
setFlags()
|
||||
defer logging.swap(logging.newBuffers())
|
||||
logging.verbosity.Set("2")
|
||||
defer logging.verbosity.Set("0")
|
||||
V(2).Info("test")
|
||||
if !contains(infoLog, "I", t) {
|
||||
t.Errorf("Info has wrong character: %q", contents(infoLog))
|
||||
}
|
||||
if !contains(infoLog, "test", t) {
|
||||
t.Error("Info failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that a vmodule enables a log in this file.
|
||||
func TestVmoduleOn(t *testing.T) {
|
||||
setFlags()
|
||||
defer logging.swap(logging.newBuffers())
|
||||
logging.vmodule.Set("glog_test=2")
|
||||
defer logging.vmodule.Set("")
|
||||
if !V(1) {
|
||||
t.Error("V not enabled for 1")
|
||||
}
|
||||
if !V(2) {
|
||||
t.Error("V not enabled for 2")
|
||||
}
|
||||
if V(3) {
|
||||
t.Error("V enabled for 3")
|
||||
}
|
||||
V(2).Info("test")
|
||||
if !contains(infoLog, "I", t) {
|
||||
t.Errorf("Info has wrong character: %q", contents(infoLog))
|
||||
}
|
||||
if !contains(infoLog, "test", t) {
|
||||
t.Error("Info failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that a vmodule of another file does not enable a log in this file.
|
||||
func TestVmoduleOff(t *testing.T) {
|
||||
setFlags()
|
||||
defer logging.swap(logging.newBuffers())
|
||||
logging.vmodule.Set("notthisfile=2")
|
||||
defer logging.vmodule.Set("")
|
||||
for i := 1; i <= 3; i++ {
|
||||
if V(Level(i)) {
|
||||
t.Errorf("V enabled for %d", i)
|
||||
}
|
||||
}
|
||||
V(2).Info("test")
|
||||
if contents(infoLog) != "" {
|
||||
t.Error("V logged incorrectly")
|
||||
}
|
||||
}
|
||||
|
||||
// vGlobs are patterns that match/don't match this file at V=2.
|
||||
var vGlobs = map[string]bool{
|
||||
// Easy to test the numeric match here.
|
||||
"glog_test=1": false, // If -vmodule sets V to 1, V(2) will fail.
|
||||
"glog_test=2": true,
|
||||
"glog_test=3": true, // If -vmodule sets V to 1, V(3) will succeed.
|
||||
// These all use 2 and check the patterns. All are true.
|
||||
"*=2": true,
|
||||
"?l*=2": true,
|
||||
"????_*=2": true,
|
||||
"??[mno]?_*t=2": true,
|
||||
// These all use 2 and check the patterns. All are false.
|
||||
"*x=2": false,
|
||||
"m*=2": false,
|
||||
"??_*=2": false,
|
||||
"?[abc]?_*t=2": false,
|
||||
}
|
||||
|
||||
// Test that vmodule globbing works as advertised.
|
||||
func testVmoduleGlob(pat string, match bool, t *testing.T) {
|
||||
setFlags()
|
||||
defer logging.swap(logging.newBuffers())
|
||||
defer logging.vmodule.Set("")
|
||||
logging.vmodule.Set(pat)
|
||||
if V(2) != Verbose(match) {
|
||||
t.Errorf("incorrect match for %q: got %t expected %t", pat, V(2), match)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that a vmodule globbing works as advertised.
|
||||
func TestVmoduleGlob(t *testing.T) {
|
||||
for glob, match := range vGlobs {
|
||||
testVmoduleGlob(glob, match, t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRollover(t *testing.T) {
|
||||
setFlags()
|
||||
var err error
|
||||
defer func(previous func(error)) { logExitFunc = previous }(logExitFunc)
|
||||
logExitFunc = func(e error) {
|
||||
err = e
|
||||
}
|
||||
defer func(previous uint64) { MaxSize = previous }(MaxSize)
|
||||
MaxSize = 512
|
||||
|
||||
Info("x") // Be sure we have a file.
|
||||
info, ok := logging.file[infoLog].(*syncBuffer)
|
||||
if !ok {
|
||||
t.Fatal("info wasn't created")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("info has initial error: %v", err)
|
||||
}
|
||||
fname0 := info.file.Name()
|
||||
Info(strings.Repeat("x", int(MaxSize))) // force a rollover
|
||||
if err != nil {
|
||||
t.Fatalf("info has error after big write: %v", err)
|
||||
}
|
||||
|
||||
// Make sure the next log file gets a file name with a different
|
||||
// time stamp.
|
||||
//
|
||||
// TODO: determine whether we need to support subsecond log
|
||||
// rotation. C++ does not appear to handle this case (nor does it
|
||||
// handle Daylight Savings Time properly).
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
Info("x") // create a new file
|
||||
if err != nil {
|
||||
t.Fatalf("error after rotation: %v", err)
|
||||
}
|
||||
fname1 := info.file.Name()
|
||||
if fname0 == fname1 {
|
||||
t.Errorf("info.f.Name did not change: %v", fname0)
|
||||
}
|
||||
if info.nbytes >= MaxSize {
|
||||
t.Errorf("file size was not reset: %d", info.nbytes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogBacktraceAt(t *testing.T) {
|
||||
setFlags()
|
||||
defer logging.swap(logging.newBuffers())
|
||||
// The peculiar style of this code simplifies line counting and maintenance of the
|
||||
// tracing block below.
|
||||
var infoLine string
|
||||
setTraceLocation := func(file string, line int, ok bool, delta int) {
|
||||
if !ok {
|
||||
t.Fatal("could not get file:line")
|
||||
}
|
||||
_, file = filepath.Split(file)
|
||||
infoLine = fmt.Sprintf("%s:%d", file, line+delta)
|
||||
err := logging.traceLocation.Set(infoLine)
|
||||
if err != nil {
|
||||
t.Fatal("error setting log_backtrace_at: ", err)
|
||||
}
|
||||
}
|
||||
{
|
||||
// Start of tracing block. These lines know about each other's relative position.
|
||||
_, file, line, ok := runtime.Caller(0)
|
||||
setTraceLocation(file, line, ok, +2) // Two lines between Caller and Info calls.
|
||||
Info("we want a stack trace here")
|
||||
}
|
||||
numAppearances := strings.Count(contents(infoLog), infoLine)
|
||||
if numAppearances < 2 {
|
||||
// Need 2 appearances, one in the log header and one in the trace:
|
||||
// log_test.go:281: I0511 16:36:06.952398 02238 log_test.go:280] we want a stack trace here
|
||||
// ...
|
||||
// github.com/glog/glog_test.go:280 (0x41ba91)
|
||||
// ...
|
||||
// We could be more precise but that would require knowing the details
|
||||
// of the traceback format, which may not be dependable.
|
||||
t.Fatal("got no trace back; log is ", contents(infoLog))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHeader(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf, _, _ := logging.header(infoLog, 0)
|
||||
logging.putBuffer(buf)
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
language: go
|
||||
sudo: false
|
||||
|
||||
go:
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.5
|
||||
- tip
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
context
|
||||
=======
|
||||
[](https://travis-ci.org/gorilla/context)
|
||||
|
||||
gorilla/context is a general purpose registry for global request variables.
|
||||
|
||||
Read the full documentation here: http://www.gorillatoolkit.org/pkg/context
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
mutex sync.RWMutex
|
||||
data = make(map[*http.Request]map[interface{}]interface{})
|
||||
datat = make(map[*http.Request]int64)
|
||||
)
|
||||
|
||||
// Set stores a value for a given key in a given request.
|
||||
func Set(r *http.Request, key, val interface{}) {
|
||||
mutex.Lock()
|
||||
if data[r] == nil {
|
||||
data[r] = make(map[interface{}]interface{})
|
||||
datat[r] = time.Now().Unix()
|
||||
}
|
||||
data[r][key] = val
|
||||
mutex.Unlock()
|
||||
}
|
||||
|
||||
// Get returns a value stored for a given key in a given request.
|
||||
func Get(r *http.Request, key interface{}) interface{} {
|
||||
mutex.RLock()
|
||||
if ctx := data[r]; ctx != nil {
|
||||
value := ctx[key]
|
||||
mutex.RUnlock()
|
||||
return value
|
||||
}
|
||||
mutex.RUnlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOk returns stored value and presence state like multi-value return of map access.
|
||||
func GetOk(r *http.Request, key interface{}) (interface{}, bool) {
|
||||
mutex.RLock()
|
||||
if _, ok := data[r]; ok {
|
||||
value, ok := data[r][key]
|
||||
mutex.RUnlock()
|
||||
return value, ok
|
||||
}
|
||||
mutex.RUnlock()
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// GetAll returns all stored values for the request as a map. Nil is returned for invalid requests.
|
||||
func GetAll(r *http.Request) map[interface{}]interface{} {
|
||||
mutex.RLock()
|
||||
if context, ok := data[r]; ok {
|
||||
result := make(map[interface{}]interface{}, len(context))
|
||||
for k, v := range context {
|
||||
result[k] = v
|
||||
}
|
||||
mutex.RUnlock()
|
||||
return result
|
||||
}
|
||||
mutex.RUnlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllOk returns all stored values for the request as a map and a boolean value that indicates if
|
||||
// the request was registered.
|
||||
func GetAllOk(r *http.Request) (map[interface{}]interface{}, bool) {
|
||||
mutex.RLock()
|
||||
context, ok := data[r]
|
||||
result := make(map[interface{}]interface{}, len(context))
|
||||
for k, v := range context {
|
||||
result[k] = v
|
||||
}
|
||||
mutex.RUnlock()
|
||||
return result, ok
|
||||
}
|
||||
|
||||
// Delete removes a value stored for a given key in a given request.
|
||||
func Delete(r *http.Request, key interface{}) {
|
||||
mutex.Lock()
|
||||
if data[r] != nil {
|
||||
delete(data[r], key)
|
||||
}
|
||||
mutex.Unlock()
|
||||
}
|
||||
|
||||
// Clear removes all values stored for a given request.
|
||||
//
|
||||
// This is usually called by a handler wrapper to clean up request
|
||||
// variables at the end of a request lifetime. See ClearHandler().
|
||||
func Clear(r *http.Request) {
|
||||
mutex.Lock()
|
||||
clear(r)
|
||||
mutex.Unlock()
|
||||
}
|
||||
|
||||
// clear is Clear without the lock.
|
||||
func clear(r *http.Request) {
|
||||
delete(data, r)
|
||||
delete(datat, r)
|
||||
}
|
||||
|
||||
// Purge removes request data stored for longer than maxAge, in seconds.
|
||||
// It returns the amount of requests removed.
|
||||
//
|
||||
// If maxAge <= 0, all request data is removed.
|
||||
//
|
||||
// This is only used for sanity check: in case context cleaning was not
|
||||
// properly set some request data can be kept forever, consuming an increasing
|
||||
// amount of memory. In case this is detected, Purge() must be called
|
||||
// periodically until the problem is fixed.
|
||||
func Purge(maxAge int) int {
|
||||
mutex.Lock()
|
||||
count := 0
|
||||
if maxAge <= 0 {
|
||||
count = len(data)
|
||||
data = make(map[*http.Request]map[interface{}]interface{})
|
||||
datat = make(map[*http.Request]int64)
|
||||
} else {
|
||||
min := time.Now().Unix() - int64(maxAge)
|
||||
for r := range data {
|
||||
if datat[r] < min {
|
||||
clear(r)
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
mutex.Unlock()
|
||||
return count
|
||||
}
|
||||
|
||||
// ClearHandler wraps an http.Handler and clears request values at the end
|
||||
// of a request lifetime.
|
||||
func ClearHandler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer Clear(r)
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type keyType int
|
||||
|
||||
const (
|
||||
key1 keyType = iota
|
||||
key2
|
||||
)
|
||||
|
||||
func TestContext(t *testing.T) {
|
||||
assertEqual := func(val interface{}, exp interface{}) {
|
||||
if val != exp {
|
||||
t.Errorf("Expected %v, got %v.", exp, val)
|
||||
}
|
||||
}
|
||||
|
||||
r, _ := http.NewRequest("GET", "http://localhost:8080/", nil)
|
||||
emptyR, _ := http.NewRequest("GET", "http://localhost:8080/", nil)
|
||||
|
||||
// Get()
|
||||
assertEqual(Get(r, key1), nil)
|
||||
|
||||
// Set()
|
||||
Set(r, key1, "1")
|
||||
assertEqual(Get(r, key1), "1")
|
||||
assertEqual(len(data[r]), 1)
|
||||
|
||||
Set(r, key2, "2")
|
||||
assertEqual(Get(r, key2), "2")
|
||||
assertEqual(len(data[r]), 2)
|
||||
|
||||
//GetOk
|
||||
value, ok := GetOk(r, key1)
|
||||
assertEqual(value, "1")
|
||||
assertEqual(ok, true)
|
||||
|
||||
value, ok = GetOk(r, "not exists")
|
||||
assertEqual(value, nil)
|
||||
assertEqual(ok, false)
|
||||
|
||||
Set(r, "nil value", nil)
|
||||
value, ok = GetOk(r, "nil value")
|
||||
assertEqual(value, nil)
|
||||
assertEqual(ok, true)
|
||||
|
||||
// GetAll()
|
||||
values := GetAll(r)
|
||||
assertEqual(len(values), 3)
|
||||
|
||||
// GetAll() for empty request
|
||||
values = GetAll(emptyR)
|
||||
if values != nil {
|
||||
t.Error("GetAll didn't return nil value for invalid request")
|
||||
}
|
||||
|
||||
// GetAllOk()
|
||||
values, ok = GetAllOk(r)
|
||||
assertEqual(len(values), 3)
|
||||
assertEqual(ok, true)
|
||||
|
||||
// GetAllOk() for empty request
|
||||
values, ok = GetAllOk(emptyR)
|
||||
assertEqual(value, nil)
|
||||
assertEqual(ok, false)
|
||||
|
||||
// Delete()
|
||||
Delete(r, key1)
|
||||
assertEqual(Get(r, key1), nil)
|
||||
assertEqual(len(data[r]), 2)
|
||||
|
||||
Delete(r, key2)
|
||||
assertEqual(Get(r, key2), nil)
|
||||
assertEqual(len(data[r]), 1)
|
||||
|
||||
// Clear()
|
||||
Clear(r)
|
||||
assertEqual(len(data), 0)
|
||||
}
|
||||
|
||||
func parallelReader(r *http.Request, key string, iterations int, wait, done chan struct{}) {
|
||||
<-wait
|
||||
for i := 0; i < iterations; i++ {
|
||||
Get(r, key)
|
||||
}
|
||||
done <- struct{}{}
|
||||
|
||||
}
|
||||
|
||||
func parallelWriter(r *http.Request, key, value string, iterations int, wait, done chan struct{}) {
|
||||
<-wait
|
||||
for i := 0; i < iterations; i++ {
|
||||
Set(r, key, value)
|
||||
}
|
||||
done <- struct{}{}
|
||||
|
||||
}
|
||||
|
||||
func benchmarkMutex(b *testing.B, numReaders, numWriters, iterations int) {
|
||||
|
||||
b.StopTimer()
|
||||
r, _ := http.NewRequest("GET", "http://localhost:8080/", nil)
|
||||
done := make(chan struct{})
|
||||
b.StartTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
wait := make(chan struct{})
|
||||
|
||||
for i := 0; i < numReaders; i++ {
|
||||
go parallelReader(r, "test", iterations, wait, done)
|
||||
}
|
||||
|
||||
for i := 0; i < numWriters; i++ {
|
||||
go parallelWriter(r, "test", "123", iterations, wait, done)
|
||||
}
|
||||
|
||||
close(wait)
|
||||
|
||||
for i := 0; i < numReaders+numWriters; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func BenchmarkMutexSameReadWrite1(b *testing.B) {
|
||||
benchmarkMutex(b, 1, 1, 32)
|
||||
}
|
||||
func BenchmarkMutexSameReadWrite2(b *testing.B) {
|
||||
benchmarkMutex(b, 2, 2, 32)
|
||||
}
|
||||
func BenchmarkMutexSameReadWrite4(b *testing.B) {
|
||||
benchmarkMutex(b, 4, 4, 32)
|
||||
}
|
||||
func BenchmarkMutex1(b *testing.B) {
|
||||
benchmarkMutex(b, 2, 8, 32)
|
||||
}
|
||||
func BenchmarkMutex2(b *testing.B) {
|
||||
benchmarkMutex(b, 16, 4, 64)
|
||||
}
|
||||
func BenchmarkMutex3(b *testing.B) {
|
||||
benchmarkMutex(b, 1, 2, 128)
|
||||
}
|
||||
func BenchmarkMutex4(b *testing.B) {
|
||||
benchmarkMutex(b, 128, 32, 256)
|
||||
}
|
||||
func BenchmarkMutex5(b *testing.B) {
|
||||
benchmarkMutex(b, 1024, 2048, 64)
|
||||
}
|
||||
func BenchmarkMutex6(b *testing.B) {
|
||||
benchmarkMutex(b, 2048, 1024, 512)
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package context stores values shared during a request lifetime.
|
||||
|
||||
For example, a router can set variables extracted from the URL and later
|
||||
application handlers can access those values, or it can be used to store
|
||||
sessions values to be saved at the end of a request. There are several
|
||||
others common uses.
|
||||
|
||||
The idea was posted by Brad Fitzpatrick to the go-nuts mailing list:
|
||||
|
||||
http://groups.google.com/group/golang-nuts/msg/e2d679d303aa5d53
|
||||
|
||||
Here's the basic usage: first define the keys that you will need. The key
|
||||
type is interface{} so a key can be of any type that supports equality.
|
||||
Here we define a key using a custom int type to avoid name collisions:
|
||||
|
||||
package foo
|
||||
|
||||
import (
|
||||
"github.com/gorilla/context"
|
||||
)
|
||||
|
||||
type key int
|
||||
|
||||
const MyKey key = 0
|
||||
|
||||
Then set a variable. Variables are bound to an http.Request object, so you
|
||||
need a request instance to set a value:
|
||||
|
||||
context.Set(r, MyKey, "bar")
|
||||
|
||||
The application can later access the variable using the same key you provided:
|
||||
|
||||
func MyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// val is "bar".
|
||||
val := context.Get(r, foo.MyKey)
|
||||
|
||||
// returns ("bar", true)
|
||||
val, ok := context.GetOk(r, foo.MyKey)
|
||||
// ...
|
||||
}
|
||||
|
||||
And that's all about the basic usage. We discuss some other ideas below.
|
||||
|
||||
Any type can be stored in the context. To enforce a given type, make the key
|
||||
private and wrap Get() and Set() to accept and return values of a specific
|
||||
type:
|
||||
|
||||
type key int
|
||||
|
||||
const mykey key = 0
|
||||
|
||||
// GetMyKey returns a value for this package from the request values.
|
||||
func GetMyKey(r *http.Request) SomeType {
|
||||
if rv := context.Get(r, mykey); rv != nil {
|
||||
return rv.(SomeType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetMyKey sets a value for this package in the request values.
|
||||
func SetMyKey(r *http.Request, val SomeType) {
|
||||
context.Set(r, mykey, val)
|
||||
}
|
||||
|
||||
Variables must be cleared at the end of a request, to remove all values
|
||||
that were stored. This can be done in an http.Handler, after a request was
|
||||
served. Just call Clear() passing the request:
|
||||
|
||||
context.Clear(r)
|
||||
|
||||
...or use ClearHandler(), which conveniently wraps an http.Handler to clear
|
||||
variables at the end of a request lifetime.
|
||||
|
||||
The Routers from the packages gorilla/mux and gorilla/pat call Clear()
|
||||
so if you are using either of them you don't need to clear the context manually.
|
||||
*/
|
||||
package context
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.5
|
||||
- tip
|
||||
install:
|
||||
- go get golang.org/x/tools/cmd/vet
|
||||
script:
|
||||
- go get -t -v ./...
|
||||
- diff -u <(echo -n) <(gofmt -d -s .)
|
||||
- go tool vet .
|
||||
- go test -v -race ./...
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
mux
|
||||
===
|
||||
[](https://godoc.org/github.com/gorilla/mux)
|
||||
[](https://travis-ci.org/gorilla/mux)
|
||||
|
||||
Package `gorilla/mux` implements a request router and dispatcher.
|
||||
|
||||
The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are:
|
||||
|
||||
* Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers.
|
||||
* URL hosts and paths can have variables with an optional regular expression.
|
||||
* Registered URLs can be built, or "reversed", which helps maintaining references to resources.
|
||||
* Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching.
|
||||
* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`.
|
||||
|
||||
Let's start registering a couple of URL paths and handlers:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", HomeHandler)
|
||||
r.HandleFunc("/products", ProductsHandler)
|
||||
r.HandleFunc("/articles", ArticlesHandler)
|
||||
http.Handle("/", r)
|
||||
}
|
||||
```
|
||||
|
||||
Here we register three routes mapping URL paths to handlers. This is equivalent to how `http.HandleFunc()` works: if an incoming request URL matches one of the paths, the corresponding handler is called passing (`http.ResponseWriter`, `*http.Request`) as parameters.
|
||||
|
||||
Paths can have variables. They are defined using the format `{name}` or `{name:pattern}`. If a regular expression pattern is not defined, the matched variable will be anything until the next slash. For example:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/products/{key}", ProductHandler)
|
||||
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
|
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
|
||||
```
|
||||
|
||||
The names are used to create a map of route variables which can be retrieved calling `mux.Vars()`:
|
||||
|
||||
```go
|
||||
vars := mux.Vars(request)
|
||||
category := vars["category"]
|
||||
```
|
||||
|
||||
And this is all you need to know about the basic usage. More advanced options are explained below.
|
||||
|
||||
Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
// Only matches if domain is "www.example.com".
|
||||
r.Host("www.example.com")
|
||||
// Matches a dynamic subdomain.
|
||||
r.Host("{subdomain:[a-z]+}.domain.com")
|
||||
```
|
||||
|
||||
There are several other matchers that can be added. To match path prefixes:
|
||||
|
||||
```go
|
||||
r.PathPrefix("/products/")
|
||||
```
|
||||
|
||||
...or HTTP methods:
|
||||
|
||||
```go
|
||||
r.Methods("GET", "POST")
|
||||
```
|
||||
|
||||
...or URL schemes:
|
||||
|
||||
```go
|
||||
r.Schemes("https")
|
||||
```
|
||||
|
||||
...or header values:
|
||||
|
||||
```go
|
||||
r.Headers("X-Requested-With", "XMLHttpRequest")
|
||||
```
|
||||
|
||||
...or query values:
|
||||
|
||||
```go
|
||||
r.Queries("key", "value")
|
||||
```
|
||||
|
||||
...or to use a custom matcher function:
|
||||
|
||||
```go
|
||||
r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool {
|
||||
return r.ProtoMajor == 0
|
||||
})
|
||||
```
|
||||
|
||||
...and finally, it is possible to combine several matchers in a single route:
|
||||
|
||||
```go
|
||||
r.HandleFunc("/products", ProductsHandler).
|
||||
Host("www.example.com").
|
||||
Methods("GET").
|
||||
Schemes("http")
|
||||
```
|
||||
|
||||
Setting the same matching conditions again and again can be boring, so we have a way to group several routes that share the same requirements. We call it "subrouting".
|
||||
|
||||
For example, let's say we have several URLs that should only match when the host is `www.example.com`. Create a route for that host and get a "subrouter" from it:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
s := r.Host("www.example.com").Subrouter()
|
||||
```
|
||||
|
||||
Then register routes in the subrouter:
|
||||
|
||||
```go
|
||||
s.HandleFunc("/products/", ProductsHandler)
|
||||
s.HandleFunc("/products/{key}", ProductHandler)
|
||||
s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
|
||||
```
|
||||
|
||||
The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route.
|
||||
|
||||
Subrouters can be used to create domain or path "namespaces": you define subrouters in a central place and then parts of the app can register its paths relatively to a given subrouter.
|
||||
|
||||
There's one more thing about subroutes. When a subrouter has a path prefix, the inner routes use it as base for their paths:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
s := r.PathPrefix("/products").Subrouter()
|
||||
// "/products/"
|
||||
s.HandleFunc("/", ProductsHandler)
|
||||
// "/products/{key}/"
|
||||
s.HandleFunc("/{key}/", ProductHandler)
|
||||
// "/products/{key}/details"
|
||||
s.HandleFunc("/{key}/details", ProductDetailsHandler)
|
||||
```
|
||||
|
||||
Now let's see how to build registered URLs.
|
||||
|
||||
Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
Name("article")
|
||||
```
|
||||
|
||||
To build a URL, get the route and call the `URL()` method, passing a sequence of key/value pairs for the route variables. For the previous route, we would do:
|
||||
|
||||
```go
|
||||
url, err := r.Get("article").URL("category", "technology", "id", "42")
|
||||
```
|
||||
|
||||
...and the result will be a `url.URL` with the following path:
|
||||
|
||||
```
|
||||
"/articles/technology/42"
|
||||
```
|
||||
|
||||
This also works for host variables:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.Host("{subdomain}.domain.com").
|
||||
Path("/articles/{category}/{id:[0-9]+}").
|
||||
HandlerFunc(ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
// url.String() will be "http://news.domain.com/articles/technology/42"
|
||||
url, err := r.Get("article").URL("subdomain", "news",
|
||||
"category", "technology",
|
||||
"id", "42")
|
||||
```
|
||||
|
||||
All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match.
|
||||
|
||||
Regex support also exists for matching Headers within a route. For example, we could do:
|
||||
|
||||
```go
|
||||
r.HeadersRegexp("Content-Type", "application/(text|json)")
|
||||
```
|
||||
|
||||
...and the route will match both requests with a Content-Type of `application/json` as well as `application/text`
|
||||
|
||||
There's also a way to build only the URL host or path for a route: use the methods `URLHost()` or `URLPath()` instead. For the previous route, we would do:
|
||||
|
||||
```go
|
||||
// "http://news.domain.com/"
|
||||
host, err := r.Get("article").URLHost("subdomain", "news")
|
||||
|
||||
// "/articles/technology/42"
|
||||
path, err := r.Get("article").URLPath("category", "technology", "id", "42")
|
||||
```
|
||||
|
||||
And if you use subrouters, host and path defined separately can be built as well:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
s := r.Host("{subdomain}.domain.com").Subrouter()
|
||||
s.Path("/articles/{category}/{id:[0-9]+}").
|
||||
HandlerFunc(ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
// "http://news.domain.com/articles/technology/42"
|
||||
url, err := r.Get("article").URL("subdomain", "news",
|
||||
"category", "technology",
|
||||
"id", "42")
|
||||
```
|
||||
|
||||
## Full Example
|
||||
|
||||
Here's a complete, runnable example of a small `mux` based server:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func YourHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("Gorilla!\n"))
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
// Routes consist of a path and a handler function.
|
||||
r.HandleFunc("/", YourHandler)
|
||||
|
||||
// Bind to a port and pass our router in
|
||||
http.ListenAndServe(":8000", r)
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
BSD licensed. See the LICENSE file for details.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkMux(b *testing.B) {
|
||||
router := new(Router)
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {}
|
||||
router.HandleFunc("/v1/{v1}", handler)
|
||||
|
||||
request, _ := http.NewRequest("GET", "/v1/anything", nil)
|
||||
for i := 0; i < b.N; i++ {
|
||||
router.ServeHTTP(nil, request)
|
||||
}
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package gorilla/mux implements a request router and dispatcher.
|
||||
|
||||
The name mux stands for "HTTP request multiplexer". Like the standard
|
||||
http.ServeMux, mux.Router matches incoming requests against a list of
|
||||
registered routes and calls a handler for the route that matches the URL
|
||||
or other conditions. The main features are:
|
||||
|
||||
* Requests can be matched based on URL host, path, path prefix, schemes,
|
||||
header and query values, HTTP methods or using custom matchers.
|
||||
* URL hosts and paths can have variables with an optional regular
|
||||
expression.
|
||||
* Registered URLs can be built, or "reversed", which helps maintaining
|
||||
references to resources.
|
||||
* Routes can be used as subrouters: nested routes are only tested if the
|
||||
parent route matches. This is useful to define groups of routes that
|
||||
share common conditions like a host, a path prefix or other repeated
|
||||
attributes. As a bonus, this optimizes request matching.
|
||||
* It implements the http.Handler interface so it is compatible with the
|
||||
standard http.ServeMux.
|
||||
|
||||
Let's start registering a couple of URL paths and handlers:
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", HomeHandler)
|
||||
r.HandleFunc("/products", ProductsHandler)
|
||||
r.HandleFunc("/articles", ArticlesHandler)
|
||||
http.Handle("/", r)
|
||||
}
|
||||
|
||||
Here we register three routes mapping URL paths to handlers. This is
|
||||
equivalent to how http.HandleFunc() works: if an incoming request URL matches
|
||||
one of the paths, the corresponding handler is called passing
|
||||
(http.ResponseWriter, *http.Request) as parameters.
|
||||
|
||||
Paths can have variables. They are defined using the format {name} or
|
||||
{name:pattern}. If a regular expression pattern is not defined, the matched
|
||||
variable will be anything until the next slash. For example:
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/products/{key}", ProductHandler)
|
||||
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
|
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
|
||||
|
||||
The names are used to create a map of route variables which can be retrieved
|
||||
calling mux.Vars():
|
||||
|
||||
vars := mux.Vars(request)
|
||||
category := vars["category"]
|
||||
|
||||
And this is all you need to know about the basic usage. More advanced options
|
||||
are explained below.
|
||||
|
||||
Routes can also be restricted to a domain or subdomain. Just define a host
|
||||
pattern to be matched. They can also have variables:
|
||||
|
||||
r := mux.NewRouter()
|
||||
// Only matches if domain is "www.example.com".
|
||||
r.Host("www.example.com")
|
||||
// Matches a dynamic subdomain.
|
||||
r.Host("{subdomain:[a-z]+}.domain.com")
|
||||
|
||||
There are several other matchers that can be added. To match path prefixes:
|
||||
|
||||
r.PathPrefix("/products/")
|
||||
|
||||
...or HTTP methods:
|
||||
|
||||
r.Methods("GET", "POST")
|
||||
|
||||
...or URL schemes:
|
||||
|
||||
r.Schemes("https")
|
||||
|
||||
...or header values:
|
||||
|
||||
r.Headers("X-Requested-With", "XMLHttpRequest")
|
||||
|
||||
...or query values:
|
||||
|
||||
r.Queries("key", "value")
|
||||
|
||||
...or to use a custom matcher function:
|
||||
|
||||
r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool {
|
||||
return r.ProtoMajor == 0
|
||||
})
|
||||
|
||||
...and finally, it is possible to combine several matchers in a single route:
|
||||
|
||||
r.HandleFunc("/products", ProductsHandler).
|
||||
Host("www.example.com").
|
||||
Methods("GET").
|
||||
Schemes("http")
|
||||
|
||||
Setting the same matching conditions again and again can be boring, so we have
|
||||
a way to group several routes that share the same requirements.
|
||||
We call it "subrouting".
|
||||
|
||||
For example, let's say we have several URLs that should only match when the
|
||||
host is "www.example.com". Create a route for that host and get a "subrouter"
|
||||
from it:
|
||||
|
||||
r := mux.NewRouter()
|
||||
s := r.Host("www.example.com").Subrouter()
|
||||
|
||||
Then register routes in the subrouter:
|
||||
|
||||
s.HandleFunc("/products/", ProductsHandler)
|
||||
s.HandleFunc("/products/{key}", ProductHandler)
|
||||
s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
|
||||
|
||||
The three URL paths we registered above will only be tested if the domain is
|
||||
"www.example.com", because the subrouter is tested first. This is not
|
||||
only convenient, but also optimizes request matching. You can create
|
||||
subrouters combining any attribute matchers accepted by a route.
|
||||
|
||||
Subrouters can be used to create domain or path "namespaces": you define
|
||||
subrouters in a central place and then parts of the app can register its
|
||||
paths relatively to a given subrouter.
|
||||
|
||||
There's one more thing about subroutes. When a subrouter has a path prefix,
|
||||
the inner routes use it as base for their paths:
|
||||
|
||||
r := mux.NewRouter()
|
||||
s := r.PathPrefix("/products").Subrouter()
|
||||
// "/products/"
|
||||
s.HandleFunc("/", ProductsHandler)
|
||||
// "/products/{key}/"
|
||||
s.HandleFunc("/{key}/", ProductHandler)
|
||||
// "/products/{key}/details"
|
||||
s.HandleFunc("/{key}/details", ProductDetailsHandler)
|
||||
|
||||
Now let's see how to build registered URLs.
|
||||
|
||||
Routes can be named. All routes that define a name can have their URLs built,
|
||||
or "reversed". We define a name calling Name() on a route. For example:
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
To build a URL, get the route and call the URL() method, passing a sequence of
|
||||
key/value pairs for the route variables. For the previous route, we would do:
|
||||
|
||||
url, err := r.Get("article").URL("category", "technology", "id", "42")
|
||||
|
||||
...and the result will be a url.URL with the following path:
|
||||
|
||||
"/articles/technology/42"
|
||||
|
||||
This also works for host variables:
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.Host("{subdomain}.domain.com").
|
||||
Path("/articles/{category}/{id:[0-9]+}").
|
||||
HandlerFunc(ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
// url.String() will be "http://news.domain.com/articles/technology/42"
|
||||
url, err := r.Get("article").URL("subdomain", "news",
|
||||
"category", "technology",
|
||||
"id", "42")
|
||||
|
||||
All variables defined in the route are required, and their values must
|
||||
conform to the corresponding patterns. These requirements guarantee that a
|
||||
generated URL will always match a registered route -- the only exception is
|
||||
for explicitly defined "build-only" routes which never match.
|
||||
|
||||
Regex support also exists for matching Headers within a route. For example, we could do:
|
||||
|
||||
r.HeadersRegexp("Content-Type", "application/(text|json)")
|
||||
|
||||
...and the route will match both requests with a Content-Type of `application/json` as well as
|
||||
`application/text`
|
||||
|
||||
There's also a way to build only the URL host or path for a route:
|
||||
use the methods URLHost() or URLPath() instead. For the previous route,
|
||||
we would do:
|
||||
|
||||
// "http://news.domain.com/"
|
||||
host, err := r.Get("article").URLHost("subdomain", "news")
|
||||
|
||||
// "/articles/technology/42"
|
||||
path, err := r.Get("article").URLPath("category", "technology", "id", "42")
|
||||
|
||||
And if you use subrouters, host and path defined separately can be built
|
||||
as well:
|
||||
|
||||
r := mux.NewRouter()
|
||||
s := r.Host("{subdomain}.domain.com").Subrouter()
|
||||
s.Path("/articles/{category}/{id:[0-9]+}").
|
||||
HandlerFunc(ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
// "http://news.domain.com/articles/technology/42"
|
||||
url, err := r.Get("article").URL("subdomain", "news",
|
||||
"category", "technology",
|
||||
"id", "42")
|
||||
*/
|
||||
package mux
|
||||
+481
@@ -0,0 +1,481 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
|
||||
"github.com/gorilla/context"
|
||||
)
|
||||
|
||||
// NewRouter returns a new router instance.
|
||||
func NewRouter() *Router {
|
||||
return &Router{namedRoutes: make(map[string]*Route), KeepContext: false}
|
||||
}
|
||||
|
||||
// Router registers routes to be matched and dispatches a handler.
|
||||
//
|
||||
// It implements the http.Handler interface, so it can be registered to serve
|
||||
// requests:
|
||||
//
|
||||
// var router = mux.NewRouter()
|
||||
//
|
||||
// func main() {
|
||||
// http.Handle("/", router)
|
||||
// }
|
||||
//
|
||||
// Or, for Google App Engine, register it in a init() function:
|
||||
//
|
||||
// func init() {
|
||||
// http.Handle("/", router)
|
||||
// }
|
||||
//
|
||||
// This will send all incoming requests to the router.
|
||||
type Router struct {
|
||||
// Configurable Handler to be used when no route matches.
|
||||
NotFoundHandler http.Handler
|
||||
// Parent route, if this is a subrouter.
|
||||
parent parentRoute
|
||||
// Routes to be matched, in order.
|
||||
routes []*Route
|
||||
// Routes by name for URL building.
|
||||
namedRoutes map[string]*Route
|
||||
// See Router.StrictSlash(). This defines the flag for new routes.
|
||||
strictSlash bool
|
||||
// If true, do not clear the request context after handling the request
|
||||
KeepContext bool
|
||||
}
|
||||
|
||||
// Match matches registered routes against the request.
|
||||
func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
|
||||
for _, route := range r.routes {
|
||||
if route.Match(req, match) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Closest match for a router (includes sub-routers)
|
||||
if r.NotFoundHandler != nil {
|
||||
match.Handler = r.NotFoundHandler
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ServeHTTP dispatches the handler registered in the matched route.
|
||||
//
|
||||
// When there is a match, the route variables can be retrieved calling
|
||||
// mux.Vars(request).
|
||||
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Clean path to canonical form and redirect.
|
||||
if p := cleanPath(req.URL.Path); p != req.URL.Path {
|
||||
|
||||
// Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query.
|
||||
// This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue:
|
||||
// http://code.google.com/p/go/issues/detail?id=5252
|
||||
url := *req.URL
|
||||
url.Path = p
|
||||
p = url.String()
|
||||
|
||||
w.Header().Set("Location", p)
|
||||
w.WriteHeader(http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
var match RouteMatch
|
||||
var handler http.Handler
|
||||
if r.Match(req, &match) {
|
||||
handler = match.Handler
|
||||
setVars(req, match.Vars)
|
||||
setCurrentRoute(req, match.Route)
|
||||
}
|
||||
if handler == nil {
|
||||
handler = http.NotFoundHandler()
|
||||
}
|
||||
if !r.KeepContext {
|
||||
defer context.Clear(req)
|
||||
}
|
||||
handler.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
// Get returns a route registered with the given name.
|
||||
func (r *Router) Get(name string) *Route {
|
||||
return r.getNamedRoutes()[name]
|
||||
}
|
||||
|
||||
// GetRoute returns a route registered with the given name. This method
|
||||
// was renamed to Get() and remains here for backwards compatibility.
|
||||
func (r *Router) GetRoute(name string) *Route {
|
||||
return r.getNamedRoutes()[name]
|
||||
}
|
||||
|
||||
// StrictSlash defines the trailing slash behavior for new routes. The initial
|
||||
// value is false.
|
||||
//
|
||||
// When true, if the route path is "/path/", accessing "/path" will redirect
|
||||
// to the former and vice versa. In other words, your application will always
|
||||
// see the path as specified in the route.
|
||||
//
|
||||
// When false, if the route path is "/path", accessing "/path/" will not match
|
||||
// this route and vice versa.
|
||||
//
|
||||
// Special case: when a route sets a path prefix using the PathPrefix() method,
|
||||
// strict slash is ignored for that route because the redirect behavior can't
|
||||
// be determined from a prefix alone. However, any subrouters created from that
|
||||
// route inherit the original StrictSlash setting.
|
||||
func (r *Router) StrictSlash(value bool) *Router {
|
||||
r.strictSlash = value
|
||||
return r
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// parentRoute
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// getNamedRoutes returns the map where named routes are registered.
|
||||
func (r *Router) getNamedRoutes() map[string]*Route {
|
||||
if r.namedRoutes == nil {
|
||||
if r.parent != nil {
|
||||
r.namedRoutes = r.parent.getNamedRoutes()
|
||||
} else {
|
||||
r.namedRoutes = make(map[string]*Route)
|
||||
}
|
||||
}
|
||||
return r.namedRoutes
|
||||
}
|
||||
|
||||
// getRegexpGroup returns regexp definitions from the parent route, if any.
|
||||
func (r *Router) getRegexpGroup() *routeRegexpGroup {
|
||||
if r.parent != nil {
|
||||
return r.parent.getRegexpGroup()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) buildVars(m map[string]string) map[string]string {
|
||||
if r.parent != nil {
|
||||
m = r.parent.buildVars(m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Route factories
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// NewRoute registers an empty route.
|
||||
func (r *Router) NewRoute() *Route {
|
||||
route := &Route{parent: r, strictSlash: r.strictSlash}
|
||||
r.routes = append(r.routes, route)
|
||||
return route
|
||||
}
|
||||
|
||||
// Handle registers a new route with a matcher for the URL path.
|
||||
// See Route.Path() and Route.Handler().
|
||||
func (r *Router) Handle(path string, handler http.Handler) *Route {
|
||||
return r.NewRoute().Path(path).Handler(handler)
|
||||
}
|
||||
|
||||
// HandleFunc registers a new route with a matcher for the URL path.
|
||||
// See Route.Path() and Route.HandlerFunc().
|
||||
func (r *Router) HandleFunc(path string, f func(http.ResponseWriter,
|
||||
*http.Request)) *Route {
|
||||
return r.NewRoute().Path(path).HandlerFunc(f)
|
||||
}
|
||||
|
||||
// Headers registers a new route with a matcher for request header values.
|
||||
// See Route.Headers().
|
||||
func (r *Router) Headers(pairs ...string) *Route {
|
||||
return r.NewRoute().Headers(pairs...)
|
||||
}
|
||||
|
||||
// Host registers a new route with a matcher for the URL host.
|
||||
// See Route.Host().
|
||||
func (r *Router) Host(tpl string) *Route {
|
||||
return r.NewRoute().Host(tpl)
|
||||
}
|
||||
|
||||
// MatcherFunc registers a new route with a custom matcher function.
|
||||
// See Route.MatcherFunc().
|
||||
func (r *Router) MatcherFunc(f MatcherFunc) *Route {
|
||||
return r.NewRoute().MatcherFunc(f)
|
||||
}
|
||||
|
||||
// Methods registers a new route with a matcher for HTTP methods.
|
||||
// See Route.Methods().
|
||||
func (r *Router) Methods(methods ...string) *Route {
|
||||
return r.NewRoute().Methods(methods...)
|
||||
}
|
||||
|
||||
// Path registers a new route with a matcher for the URL path.
|
||||
// See Route.Path().
|
||||
func (r *Router) Path(tpl string) *Route {
|
||||
return r.NewRoute().Path(tpl)
|
||||
}
|
||||
|
||||
// PathPrefix registers a new route with a matcher for the URL path prefix.
|
||||
// See Route.PathPrefix().
|
||||
func (r *Router) PathPrefix(tpl string) *Route {
|
||||
return r.NewRoute().PathPrefix(tpl)
|
||||
}
|
||||
|
||||
// Queries registers a new route with a matcher for URL query values.
|
||||
// See Route.Queries().
|
||||
func (r *Router) Queries(pairs ...string) *Route {
|
||||
return r.NewRoute().Queries(pairs...)
|
||||
}
|
||||
|
||||
// Schemes registers a new route with a matcher for URL schemes.
|
||||
// See Route.Schemes().
|
||||
func (r *Router) Schemes(schemes ...string) *Route {
|
||||
return r.NewRoute().Schemes(schemes...)
|
||||
}
|
||||
|
||||
// BuildVars registers a new route with a custom function for modifying
|
||||
// route variables before building a URL.
|
||||
func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route {
|
||||
return r.NewRoute().BuildVarsFunc(f)
|
||||
}
|
||||
|
||||
// Walk walks the router and all its sub-routers, calling walkFn for each route
|
||||
// in the tree. The routes are walked in the order they were added. Sub-routers
|
||||
// are explored depth-first.
|
||||
func (r *Router) Walk(walkFn WalkFunc) error {
|
||||
return r.walk(walkFn, []*Route{})
|
||||
}
|
||||
|
||||
// SkipRouter is used as a return value from WalkFuncs to indicate that the
|
||||
// router that walk is about to descend down to should be skipped.
|
||||
var SkipRouter = errors.New("skip this router")
|
||||
|
||||
// WalkFunc is the type of the function called for each route visited by Walk.
|
||||
// At every invocation, it is given the current route, and the current router,
|
||||
// and a list of ancestor routes that lead to the current route.
|
||||
type WalkFunc func(route *Route, router *Router, ancestors []*Route) error
|
||||
|
||||
func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error {
|
||||
for _, t := range r.routes {
|
||||
if t.regexp == nil || t.regexp.path == nil || t.regexp.path.template == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
err := walkFn(t, r, ancestors)
|
||||
if err == SkipRouter {
|
||||
continue
|
||||
}
|
||||
for _, sr := range t.matchers {
|
||||
if h, ok := sr.(*Router); ok {
|
||||
err := h.walk(walkFn, ancestors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if h, ok := t.handler.(*Router); ok {
|
||||
ancestors = append(ancestors, t)
|
||||
err := h.walk(walkFn, ancestors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ancestors = ancestors[:len(ancestors)-1]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Context
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// RouteMatch stores information about a matched route.
|
||||
type RouteMatch struct {
|
||||
Route *Route
|
||||
Handler http.Handler
|
||||
Vars map[string]string
|
||||
}
|
||||
|
||||
type contextKey int
|
||||
|
||||
const (
|
||||
varsKey contextKey = iota
|
||||
routeKey
|
||||
)
|
||||
|
||||
// Vars returns the route variables for the current request, if any.
|
||||
func Vars(r *http.Request) map[string]string {
|
||||
if rv := context.Get(r, varsKey); rv != nil {
|
||||
return rv.(map[string]string)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CurrentRoute returns the matched route for the current request, if any.
|
||||
// This only works when called inside the handler of the matched route
|
||||
// because the matched route is stored in the request context which is cleared
|
||||
// after the handler returns, unless the KeepContext option is set on the
|
||||
// Router.
|
||||
func CurrentRoute(r *http.Request) *Route {
|
||||
if rv := context.Get(r, routeKey); rv != nil {
|
||||
return rv.(*Route)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setVars(r *http.Request, val interface{}) {
|
||||
if val != nil {
|
||||
context.Set(r, varsKey, val)
|
||||
}
|
||||
}
|
||||
|
||||
func setCurrentRoute(r *http.Request, val interface{}) {
|
||||
if val != nil {
|
||||
context.Set(r, routeKey, val)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// cleanPath returns the canonical path for p, eliminating . and .. elements.
|
||||
// Borrowed from the net/http package.
|
||||
func cleanPath(p string) string {
|
||||
if p == "" {
|
||||
return "/"
|
||||
}
|
||||
if p[0] != '/' {
|
||||
p = "/" + p
|
||||
}
|
||||
np := path.Clean(p)
|
||||
// path.Clean removes trailing slash except for root;
|
||||
// put the trailing slash back if necessary.
|
||||
if p[len(p)-1] == '/' && np != "/" {
|
||||
np += "/"
|
||||
}
|
||||
return np
|
||||
}
|
||||
|
||||
// uniqueVars returns an error if two slices contain duplicated strings.
|
||||
func uniqueVars(s1, s2 []string) error {
|
||||
for _, v1 := range s1 {
|
||||
for _, v2 := range s2 {
|
||||
if v1 == v2 {
|
||||
return fmt.Errorf("mux: duplicated route variable %q", v2)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkPairs returns the count of strings passed in, and an error if
|
||||
// the count is not an even number.
|
||||
func checkPairs(pairs ...string) (int, error) {
|
||||
length := len(pairs)
|
||||
if length%2 != 0 {
|
||||
return length, fmt.Errorf(
|
||||
"mux: number of parameters must be multiple of 2, got %v", pairs)
|
||||
}
|
||||
return length, nil
|
||||
}
|
||||
|
||||
// mapFromPairsToString converts variadic string parameters to a
|
||||
// string to string map.
|
||||
func mapFromPairsToString(pairs ...string) (map[string]string, error) {
|
||||
length, err := checkPairs(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]string, length/2)
|
||||
for i := 0; i < length; i += 2 {
|
||||
m[pairs[i]] = pairs[i+1]
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// mapFromPairsToRegex converts variadic string paramers to a
|
||||
// string to regex map.
|
||||
func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) {
|
||||
length, err := checkPairs(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]*regexp.Regexp, length/2)
|
||||
for i := 0; i < length; i += 2 {
|
||||
regex, err := regexp.Compile(pairs[i+1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m[pairs[i]] = regex
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// matchInArray returns true if the given string value is in the array.
|
||||
func matchInArray(arr []string, value string) bool {
|
||||
for _, v := range arr {
|
||||
if v == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchMapWithString returns true if the given key/value pairs exist in a given map.
|
||||
func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, canonicalKey bool) bool {
|
||||
for k, v := range toCheck {
|
||||
// Check if key exists.
|
||||
if canonicalKey {
|
||||
k = http.CanonicalHeaderKey(k)
|
||||
}
|
||||
if values := toMatch[k]; values == nil {
|
||||
return false
|
||||
} else if v != "" {
|
||||
// If value was defined as an empty string we only check that the
|
||||
// key exists. Otherwise we also check for equality.
|
||||
valueExists := false
|
||||
for _, value := range values {
|
||||
if v == value {
|
||||
valueExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valueExists {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// matchMapWithRegex returns true if the given key/value pairs exist in a given map compiled against
|
||||
// the given regex
|
||||
func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]string, canonicalKey bool) bool {
|
||||
for k, v := range toCheck {
|
||||
// Check if key exists.
|
||||
if canonicalKey {
|
||||
k = http.CanonicalHeaderKey(k)
|
||||
}
|
||||
if values := toMatch[k]; values == nil {
|
||||
return false
|
||||
} else if v != nil {
|
||||
// If value was defined as an empty string we only check that the
|
||||
// key exists. Otherwise we also check for equality.
|
||||
valueExists := false
|
||||
for _, value := range values {
|
||||
if v.MatchString(value) {
|
||||
valueExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valueExists {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
+1358
File diff suppressed because it is too large
Load Diff
+714
@@ -0,0 +1,714 @@
|
||||
// Old tests ported to Go1. This is a mess. Want to drop it one day.
|
||||
|
||||
// Copyright 2011 Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// ResponseRecorder
|
||||
// ----------------------------------------------------------------------------
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// ResponseRecorder is an implementation of http.ResponseWriter that
|
||||
// records its mutations for later inspection in tests.
|
||||
type ResponseRecorder struct {
|
||||
Code int // the HTTP response code from WriteHeader
|
||||
HeaderMap http.Header // the HTTP response headers
|
||||
Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
|
||||
Flushed bool
|
||||
}
|
||||
|
||||
// NewRecorder returns an initialized ResponseRecorder.
|
||||
func NewRecorder() *ResponseRecorder {
|
||||
return &ResponseRecorder{
|
||||
HeaderMap: make(http.Header),
|
||||
Body: new(bytes.Buffer),
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultRemoteAddr is the default remote address to return in RemoteAddr if
|
||||
// an explicit DefaultRemoteAddr isn't set on ResponseRecorder.
|
||||
const DefaultRemoteAddr = "1.2.3.4"
|
||||
|
||||
// Header returns the response headers.
|
||||
func (rw *ResponseRecorder) Header() http.Header {
|
||||
return rw.HeaderMap
|
||||
}
|
||||
|
||||
// Write always succeeds and writes to rw.Body, if not nil.
|
||||
func (rw *ResponseRecorder) Write(buf []byte) (int, error) {
|
||||
if rw.Body != nil {
|
||||
rw.Body.Write(buf)
|
||||
}
|
||||
if rw.Code == 0 {
|
||||
rw.Code = http.StatusOK
|
||||
}
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
// WriteHeader sets rw.Code.
|
||||
func (rw *ResponseRecorder) WriteHeader(code int) {
|
||||
rw.Code = code
|
||||
}
|
||||
|
||||
// Flush sets rw.Flushed to true.
|
||||
func (rw *ResponseRecorder) Flush() {
|
||||
rw.Flushed = true
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func TestRouteMatchers(t *testing.T) {
|
||||
var scheme, host, path, query, method string
|
||||
var headers map[string]string
|
||||
var resultVars map[bool]map[string]string
|
||||
|
||||
router := NewRouter()
|
||||
router.NewRoute().Host("{var1}.google.com").
|
||||
Path("/{var2:[a-z]+}/{var3:[0-9]+}").
|
||||
Queries("foo", "bar").
|
||||
Methods("GET").
|
||||
Schemes("https").
|
||||
Headers("x-requested-with", "XMLHttpRequest")
|
||||
router.NewRoute().Host("www.{var4}.com").
|
||||
PathPrefix("/foo/{var5:[a-z]+}/{var6:[0-9]+}").
|
||||
Queries("baz", "ding").
|
||||
Methods("POST").
|
||||
Schemes("http").
|
||||
Headers("Content-Type", "application/json")
|
||||
|
||||
reset := func() {
|
||||
// Everything match.
|
||||
scheme = "https"
|
||||
host = "www.google.com"
|
||||
path = "/product/42"
|
||||
query = "?foo=bar"
|
||||
method = "GET"
|
||||
headers = map[string]string{"X-Requested-With": "XMLHttpRequest"}
|
||||
resultVars = map[bool]map[string]string{
|
||||
true: {"var1": "www", "var2": "product", "var3": "42"},
|
||||
false: {},
|
||||
}
|
||||
}
|
||||
|
||||
reset2 := func() {
|
||||
// Everything match.
|
||||
scheme = "http"
|
||||
host = "www.google.com"
|
||||
path = "/foo/product/42/path/that/is/ignored"
|
||||
query = "?baz=ding"
|
||||
method = "POST"
|
||||
headers = map[string]string{"Content-Type": "application/json"}
|
||||
resultVars = map[bool]map[string]string{
|
||||
true: {"var4": "google", "var5": "product", "var6": "42"},
|
||||
false: {},
|
||||
}
|
||||
}
|
||||
|
||||
match := func(shouldMatch bool) {
|
||||
url := scheme + "://" + host + path + query
|
||||
request, _ := http.NewRequest(method, url, nil)
|
||||
for key, value := range headers {
|
||||
request.Header.Add(key, value)
|
||||
}
|
||||
|
||||
var routeMatch RouteMatch
|
||||
matched := router.Match(request, &routeMatch)
|
||||
if matched != shouldMatch {
|
||||
// Need better messages. :)
|
||||
if matched {
|
||||
t.Errorf("Should match.")
|
||||
} else {
|
||||
t.Errorf("Should not match.")
|
||||
}
|
||||
}
|
||||
|
||||
if matched {
|
||||
currentRoute := routeMatch.Route
|
||||
if currentRoute == nil {
|
||||
t.Errorf("Expected a current route.")
|
||||
}
|
||||
vars := routeMatch.Vars
|
||||
expectedVars := resultVars[shouldMatch]
|
||||
if len(vars) != len(expectedVars) {
|
||||
t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars)
|
||||
}
|
||||
for name, value := range vars {
|
||||
if expectedVars[name] != value {
|
||||
t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1st route --------------------------------------------------------------
|
||||
|
||||
// Everything match.
|
||||
reset()
|
||||
match(true)
|
||||
|
||||
// Scheme doesn't match.
|
||||
reset()
|
||||
scheme = "http"
|
||||
match(false)
|
||||
|
||||
// Host doesn't match.
|
||||
reset()
|
||||
host = "www.mygoogle.com"
|
||||
match(false)
|
||||
|
||||
// Path doesn't match.
|
||||
reset()
|
||||
path = "/product/notdigits"
|
||||
match(false)
|
||||
|
||||
// Query doesn't match.
|
||||
reset()
|
||||
query = "?foo=baz"
|
||||
match(false)
|
||||
|
||||
// Method doesn't match.
|
||||
reset()
|
||||
method = "POST"
|
||||
match(false)
|
||||
|
||||
// Header doesn't match.
|
||||
reset()
|
||||
headers = map[string]string{}
|
||||
match(false)
|
||||
|
||||
// Everything match, again.
|
||||
reset()
|
||||
match(true)
|
||||
|
||||
// 2nd route --------------------------------------------------------------
|
||||
|
||||
// Everything match.
|
||||
reset2()
|
||||
match(true)
|
||||
|
||||
// Scheme doesn't match.
|
||||
reset2()
|
||||
scheme = "https"
|
||||
match(false)
|
||||
|
||||
// Host doesn't match.
|
||||
reset2()
|
||||
host = "sub.google.com"
|
||||
match(false)
|
||||
|
||||
// Path doesn't match.
|
||||
reset2()
|
||||
path = "/bar/product/42"
|
||||
match(false)
|
||||
|
||||
// Query doesn't match.
|
||||
reset2()
|
||||
query = "?foo=baz"
|
||||
match(false)
|
||||
|
||||
// Method doesn't match.
|
||||
reset2()
|
||||
method = "GET"
|
||||
match(false)
|
||||
|
||||
// Header doesn't match.
|
||||
reset2()
|
||||
headers = map[string]string{}
|
||||
match(false)
|
||||
|
||||
// Everything match, again.
|
||||
reset2()
|
||||
match(true)
|
||||
}
|
||||
|
||||
type headerMatcherTest struct {
|
||||
matcher headerMatcher
|
||||
headers map[string]string
|
||||
result bool
|
||||
}
|
||||
|
||||
var headerMatcherTests = []headerMatcherTest{
|
||||
{
|
||||
matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}),
|
||||
headers: map[string]string{"X-Requested-With": "XMLHttpRequest"},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: headerMatcher(map[string]string{"x-requested-with": ""}),
|
||||
headers: map[string]string{"X-Requested-With": "anything"},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}),
|
||||
headers: map[string]string{},
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
type hostMatcherTest struct {
|
||||
matcher *Route
|
||||
url string
|
||||
vars map[string]string
|
||||
result bool
|
||||
}
|
||||
|
||||
var hostMatcherTests = []hostMatcherTest{
|
||||
{
|
||||
matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"),
|
||||
url: "http://abc.def.ghi/",
|
||||
vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"),
|
||||
url: "http://a.b.c/",
|
||||
vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"},
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
type methodMatcherTest struct {
|
||||
matcher methodMatcher
|
||||
method string
|
||||
result bool
|
||||
}
|
||||
|
||||
var methodMatcherTests = []methodMatcherTest{
|
||||
{
|
||||
matcher: methodMatcher([]string{"GET", "POST", "PUT"}),
|
||||
method: "GET",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: methodMatcher([]string{"GET", "POST", "PUT"}),
|
||||
method: "POST",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: methodMatcher([]string{"GET", "POST", "PUT"}),
|
||||
method: "PUT",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: methodMatcher([]string{"GET", "POST", "PUT"}),
|
||||
method: "DELETE",
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
type pathMatcherTest struct {
|
||||
matcher *Route
|
||||
url string
|
||||
vars map[string]string
|
||||
result bool
|
||||
}
|
||||
|
||||
var pathMatcherTests = []pathMatcherTest{
|
||||
{
|
||||
matcher: NewRouter().NewRoute().Path("/{foo:[0-9][0-9][0-9]}/{bar:[0-9][0-9][0-9]}/{baz:[0-9][0-9][0-9]}"),
|
||||
url: "http://localhost:8080/123/456/789",
|
||||
vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: NewRouter().NewRoute().Path("/{foo:[0-9][0-9][0-9]}/{bar:[0-9][0-9][0-9]}/{baz:[0-9][0-9][0-9]}"),
|
||||
url: "http://localhost:8080/1/2/3",
|
||||
vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"},
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
type schemeMatcherTest struct {
|
||||
matcher schemeMatcher
|
||||
url string
|
||||
result bool
|
||||
}
|
||||
|
||||
var schemeMatcherTests = []schemeMatcherTest{
|
||||
{
|
||||
matcher: schemeMatcher([]string{"http", "https"}),
|
||||
url: "http://localhost:8080/",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: schemeMatcher([]string{"http", "https"}),
|
||||
url: "https://localhost:8080/",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: schemeMatcher([]string{"https"}),
|
||||
url: "http://localhost:8080/",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
matcher: schemeMatcher([]string{"http"}),
|
||||
url: "https://localhost:8080/",
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
type urlBuildingTest struct {
|
||||
route *Route
|
||||
vars []string
|
||||
url string
|
||||
}
|
||||
|
||||
var urlBuildingTests = []urlBuildingTest{
|
||||
{
|
||||
route: new(Route).Host("foo.domain.com"),
|
||||
vars: []string{},
|
||||
url: "http://foo.domain.com",
|
||||
},
|
||||
{
|
||||
route: new(Route).Host("{subdomain}.domain.com"),
|
||||
vars: []string{"subdomain", "bar"},
|
||||
url: "http://bar.domain.com",
|
||||
},
|
||||
{
|
||||
route: new(Route).Host("foo.domain.com").Path("/articles"),
|
||||
vars: []string{},
|
||||
url: "http://foo.domain.com/articles",
|
||||
},
|
||||
{
|
||||
route: new(Route).Path("/articles"),
|
||||
vars: []string{},
|
||||
url: "/articles",
|
||||
},
|
||||
{
|
||||
route: new(Route).Path("/articles/{category}/{id:[0-9]+}"),
|
||||
vars: []string{"category", "technology", "id", "42"},
|
||||
url: "/articles/technology/42",
|
||||
},
|
||||
{
|
||||
route: new(Route).Host("{subdomain}.domain.com").Path("/articles/{category}/{id:[0-9]+}"),
|
||||
vars: []string{"subdomain", "foo", "category", "technology", "id", "42"},
|
||||
url: "http://foo.domain.com/articles/technology/42",
|
||||
},
|
||||
}
|
||||
|
||||
func TestHeaderMatcher(t *testing.T) {
|
||||
for _, v := range headerMatcherTests {
|
||||
request, _ := http.NewRequest("GET", "http://localhost:8080/", nil)
|
||||
for key, value := range v.headers {
|
||||
request.Header.Add(key, value)
|
||||
}
|
||||
var routeMatch RouteMatch
|
||||
result := v.matcher.Match(request, &routeMatch)
|
||||
if result != v.result {
|
||||
if v.result {
|
||||
t.Errorf("%#v: should match %v.", v.matcher, request.Header)
|
||||
} else {
|
||||
t.Errorf("%#v: should not match %v.", v.matcher, request.Header)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostMatcher(t *testing.T) {
|
||||
for _, v := range hostMatcherTests {
|
||||
request, _ := http.NewRequest("GET", v.url, nil)
|
||||
var routeMatch RouteMatch
|
||||
result := v.matcher.Match(request, &routeMatch)
|
||||
vars := routeMatch.Vars
|
||||
if result != v.result {
|
||||
if v.result {
|
||||
t.Errorf("%#v: should match %v.", v.matcher, v.url)
|
||||
} else {
|
||||
t.Errorf("%#v: should not match %v.", v.matcher, v.url)
|
||||
}
|
||||
}
|
||||
if result {
|
||||
if len(vars) != len(v.vars) {
|
||||
t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars))
|
||||
}
|
||||
for name, value := range vars {
|
||||
if v.vars[name] != value {
|
||||
t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(vars) != 0 {
|
||||
t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMethodMatcher(t *testing.T) {
|
||||
for _, v := range methodMatcherTests {
|
||||
request, _ := http.NewRequest(v.method, "http://localhost:8080/", nil)
|
||||
var routeMatch RouteMatch
|
||||
result := v.matcher.Match(request, &routeMatch)
|
||||
if result != v.result {
|
||||
if v.result {
|
||||
t.Errorf("%#v: should match %v.", v.matcher, v.method)
|
||||
} else {
|
||||
t.Errorf("%#v: should not match %v.", v.matcher, v.method)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathMatcher(t *testing.T) {
|
||||
for _, v := range pathMatcherTests {
|
||||
request, _ := http.NewRequest("GET", v.url, nil)
|
||||
var routeMatch RouteMatch
|
||||
result := v.matcher.Match(request, &routeMatch)
|
||||
vars := routeMatch.Vars
|
||||
if result != v.result {
|
||||
if v.result {
|
||||
t.Errorf("%#v: should match %v.", v.matcher, v.url)
|
||||
} else {
|
||||
t.Errorf("%#v: should not match %v.", v.matcher, v.url)
|
||||
}
|
||||
}
|
||||
if result {
|
||||
if len(vars) != len(v.vars) {
|
||||
t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars))
|
||||
}
|
||||
for name, value := range vars {
|
||||
if v.vars[name] != value {
|
||||
t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(vars) != 0 {
|
||||
t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemeMatcher(t *testing.T) {
|
||||
for _, v := range schemeMatcherTests {
|
||||
request, _ := http.NewRequest("GET", v.url, nil)
|
||||
var routeMatch RouteMatch
|
||||
result := v.matcher.Match(request, &routeMatch)
|
||||
if result != v.result {
|
||||
if v.result {
|
||||
t.Errorf("%#v: should match %v.", v.matcher, v.url)
|
||||
} else {
|
||||
t.Errorf("%#v: should not match %v.", v.matcher, v.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUrlBuilding(t *testing.T) {
|
||||
|
||||
for _, v := range urlBuildingTests {
|
||||
u, _ := v.route.URL(v.vars...)
|
||||
url := u.String()
|
||||
if url != v.url {
|
||||
t.Errorf("expected %v, got %v", v.url, url)
|
||||
/*
|
||||
reversePath := ""
|
||||
reverseHost := ""
|
||||
if v.route.pathTemplate != nil {
|
||||
reversePath = v.route.pathTemplate.Reverse
|
||||
}
|
||||
if v.route.hostTemplate != nil {
|
||||
reverseHost = v.route.hostTemplate.Reverse
|
||||
}
|
||||
|
||||
t.Errorf("%#v:\nexpected: %q\ngot: %q\nreverse path: %q\nreverse host: %q", v.route, v.url, url, reversePath, reverseHost)
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
ArticleHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
router := NewRouter()
|
||||
router.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).Name("article")
|
||||
|
||||
url, _ := router.Get("article").URL("category", "technology", "id", "42")
|
||||
expected := "/articles/technology/42"
|
||||
if url.String() != expected {
|
||||
t.Errorf("Expected %v, got %v", expected, url.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchedRouteName(t *testing.T) {
|
||||
routeName := "stock"
|
||||
router := NewRouter()
|
||||
route := router.NewRoute().Path("/products/").Name(routeName)
|
||||
|
||||
url := "http://www.example.com/products/"
|
||||
request, _ := http.NewRequest("GET", url, nil)
|
||||
var rv RouteMatch
|
||||
ok := router.Match(request, &rv)
|
||||
|
||||
if !ok || rv.Route != route {
|
||||
t.Errorf("Expected same route, got %+v.", rv.Route)
|
||||
}
|
||||
|
||||
retName := rv.Route.GetName()
|
||||
if retName != routeName {
|
||||
t.Errorf("Expected %q, got %q.", routeName, retName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubRouting(t *testing.T) {
|
||||
// Example from docs.
|
||||
router := NewRouter()
|
||||
subrouter := router.NewRoute().Host("www.example.com").Subrouter()
|
||||
route := subrouter.NewRoute().Path("/products/").Name("products")
|
||||
|
||||
url := "http://www.example.com/products/"
|
||||
request, _ := http.NewRequest("GET", url, nil)
|
||||
var rv RouteMatch
|
||||
ok := router.Match(request, &rv)
|
||||
|
||||
if !ok || rv.Route != route {
|
||||
t.Errorf("Expected same route, got %+v.", rv.Route)
|
||||
}
|
||||
|
||||
u, _ := router.Get("products").URL()
|
||||
builtUrl := u.String()
|
||||
// Yay, subroute aware of the domain when building!
|
||||
if builtUrl != url {
|
||||
t.Errorf("Expected %q, got %q.", url, builtUrl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVariableNames(t *testing.T) {
|
||||
route := new(Route).Host("{arg1}.domain.com").Path("/{arg1}/{arg2:[0-9]+}")
|
||||
if route.err == nil {
|
||||
t.Errorf("Expected error for duplicated variable names")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirectSlash(t *testing.T) {
|
||||
var route *Route
|
||||
var routeMatch RouteMatch
|
||||
r := NewRouter()
|
||||
|
||||
r.StrictSlash(false)
|
||||
route = r.NewRoute()
|
||||
if route.strictSlash != false {
|
||||
t.Errorf("Expected false redirectSlash.")
|
||||
}
|
||||
|
||||
r.StrictSlash(true)
|
||||
route = r.NewRoute()
|
||||
if route.strictSlash != true {
|
||||
t.Errorf("Expected true redirectSlash.")
|
||||
}
|
||||
|
||||
route = new(Route)
|
||||
route.strictSlash = true
|
||||
route.Path("/{arg1}/{arg2:[0-9]+}/")
|
||||
request, _ := http.NewRequest("GET", "http://localhost/foo/123", nil)
|
||||
routeMatch = RouteMatch{}
|
||||
_ = route.Match(request, &routeMatch)
|
||||
vars := routeMatch.Vars
|
||||
if vars["arg1"] != "foo" {
|
||||
t.Errorf("Expected foo.")
|
||||
}
|
||||
if vars["arg2"] != "123" {
|
||||
t.Errorf("Expected 123.")
|
||||
}
|
||||
rsp := NewRecorder()
|
||||
routeMatch.Handler.ServeHTTP(rsp, request)
|
||||
if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123/" {
|
||||
t.Errorf("Expected redirect header.")
|
||||
}
|
||||
|
||||
route = new(Route)
|
||||
route.strictSlash = true
|
||||
route.Path("/{arg1}/{arg2:[0-9]+}")
|
||||
request, _ = http.NewRequest("GET", "http://localhost/foo/123/", nil)
|
||||
routeMatch = RouteMatch{}
|
||||
_ = route.Match(request, &routeMatch)
|
||||
vars = routeMatch.Vars
|
||||
if vars["arg1"] != "foo" {
|
||||
t.Errorf("Expected foo.")
|
||||
}
|
||||
if vars["arg2"] != "123" {
|
||||
t.Errorf("Expected 123.")
|
||||
}
|
||||
rsp = NewRecorder()
|
||||
routeMatch.Handler.ServeHTTP(rsp, request)
|
||||
if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123" {
|
||||
t.Errorf("Expected redirect header.")
|
||||
}
|
||||
}
|
||||
|
||||
// Test for the new regexp library, still not available in stable Go.
|
||||
func TestNewRegexp(t *testing.T) {
|
||||
var p *routeRegexp
|
||||
var matches []string
|
||||
|
||||
tests := map[string]map[string][]string{
|
||||
"/{foo:a{2}}": {
|
||||
"/a": nil,
|
||||
"/aa": {"aa"},
|
||||
"/aaa": nil,
|
||||
"/aaaa": nil,
|
||||
},
|
||||
"/{foo:a{2,}}": {
|
||||
"/a": nil,
|
||||
"/aa": {"aa"},
|
||||
"/aaa": {"aaa"},
|
||||
"/aaaa": {"aaaa"},
|
||||
},
|
||||
"/{foo:a{2,3}}": {
|
||||
"/a": nil,
|
||||
"/aa": {"aa"},
|
||||
"/aaa": {"aaa"},
|
||||
"/aaaa": nil,
|
||||
},
|
||||
"/{foo:[a-z]{3}}/{bar:[a-z]{2}}": {
|
||||
"/a": nil,
|
||||
"/ab": nil,
|
||||
"/abc": nil,
|
||||
"/abcd": nil,
|
||||
"/abc/ab": {"abc", "ab"},
|
||||
"/abc/abc": nil,
|
||||
"/abcd/ab": nil,
|
||||
},
|
||||
`/{foo:\w{3,}}/{bar:\d{2,}}`: {
|
||||
"/a": nil,
|
||||
"/ab": nil,
|
||||
"/abc": nil,
|
||||
"/abc/1": nil,
|
||||
"/abc/12": {"abc", "12"},
|
||||
"/abcd/12": {"abcd", "12"},
|
||||
"/abcd/123": {"abcd", "123"},
|
||||
},
|
||||
}
|
||||
|
||||
for pattern, paths := range tests {
|
||||
p, _ = newRouteRegexp(pattern, false, false, false, false)
|
||||
for path, result := range paths {
|
||||
matches = p.regexp.FindStringSubmatch(path)
|
||||
if result == nil {
|
||||
if matches != nil {
|
||||
t.Errorf("%v should not match %v.", pattern, path)
|
||||
}
|
||||
} else {
|
||||
if len(matches) != len(result)+1 {
|
||||
t.Errorf("Expected %v matches, got %v.", len(result)+1, len(matches))
|
||||
} else {
|
||||
for k, v := range result {
|
||||
if matches[k+1] != v {
|
||||
t.Errorf("Expected %v, got %v.", v, matches[k+1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// newRouteRegexp parses a route template and returns a routeRegexp,
|
||||
// used to match a host, a path or a query string.
|
||||
//
|
||||
// It will extract named variables, assemble a regexp to be matched, create
|
||||
// a "reverse" template to build URLs and compile regexps to validate variable
|
||||
// values used in URL building.
|
||||
//
|
||||
// Previously we accepted only Python-like identifiers for variable
|
||||
// names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that
|
||||
// name and pattern can't be empty, and names can't contain a colon.
|
||||
func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash bool) (*routeRegexp, error) {
|
||||
// Check if it is well-formed.
|
||||
idxs, errBraces := braceIndices(tpl)
|
||||
if errBraces != nil {
|
||||
return nil, errBraces
|
||||
}
|
||||
// Backup the original.
|
||||
template := tpl
|
||||
// Now let's parse it.
|
||||
defaultPattern := "[^/]+"
|
||||
if matchQuery {
|
||||
defaultPattern = "[^?&]*"
|
||||
} else if matchHost {
|
||||
defaultPattern = "[^.]+"
|
||||
matchPrefix = false
|
||||
}
|
||||
// Only match strict slash if not matching
|
||||
if matchPrefix || matchHost || matchQuery {
|
||||
strictSlash = false
|
||||
}
|
||||
// Set a flag for strictSlash.
|
||||
endSlash := false
|
||||
if strictSlash && strings.HasSuffix(tpl, "/") {
|
||||
tpl = tpl[:len(tpl)-1]
|
||||
endSlash = true
|
||||
}
|
||||
varsN := make([]string, len(idxs)/2)
|
||||
varsR := make([]*regexp.Regexp, len(idxs)/2)
|
||||
pattern := bytes.NewBufferString("")
|
||||
pattern.WriteByte('^')
|
||||
reverse := bytes.NewBufferString("")
|
||||
var end int
|
||||
var err error
|
||||
for i := 0; i < len(idxs); i += 2 {
|
||||
// Set all values we are interested in.
|
||||
raw := tpl[end:idxs[i]]
|
||||
end = idxs[i+1]
|
||||
parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2)
|
||||
name := parts[0]
|
||||
patt := defaultPattern
|
||||
if len(parts) == 2 {
|
||||
patt = parts[1]
|
||||
}
|
||||
// Name or pattern can't be empty.
|
||||
if name == "" || patt == "" {
|
||||
return nil, fmt.Errorf("mux: missing name or pattern in %q",
|
||||
tpl[idxs[i]:end])
|
||||
}
|
||||
// Build the regexp pattern.
|
||||
varIdx := i / 2
|
||||
fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(varIdx), patt)
|
||||
// Build the reverse template.
|
||||
fmt.Fprintf(reverse, "%s%%s", raw)
|
||||
|
||||
// Append variable name and compiled pattern.
|
||||
varsN[varIdx] = name
|
||||
varsR[varIdx], err = regexp.Compile(fmt.Sprintf("^%s$", patt))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Add the remaining.
|
||||
raw := tpl[end:]
|
||||
pattern.WriteString(regexp.QuoteMeta(raw))
|
||||
if strictSlash {
|
||||
pattern.WriteString("[/]?")
|
||||
}
|
||||
if matchQuery {
|
||||
// Add the default pattern if the query value is empty
|
||||
if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" {
|
||||
pattern.WriteString(defaultPattern)
|
||||
}
|
||||
}
|
||||
if !matchPrefix {
|
||||
pattern.WriteByte('$')
|
||||
}
|
||||
reverse.WriteString(raw)
|
||||
if endSlash {
|
||||
reverse.WriteByte('/')
|
||||
}
|
||||
// Compile full regexp.
|
||||
reg, errCompile := regexp.Compile(pattern.String())
|
||||
if errCompile != nil {
|
||||
return nil, errCompile
|
||||
}
|
||||
// Done!
|
||||
return &routeRegexp{
|
||||
template: template,
|
||||
matchHost: matchHost,
|
||||
matchQuery: matchQuery,
|
||||
strictSlash: strictSlash,
|
||||
regexp: reg,
|
||||
reverse: reverse.String(),
|
||||
varsN: varsN,
|
||||
varsR: varsR,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// routeRegexp stores a regexp to match a host or path and information to
|
||||
// collect and validate route variables.
|
||||
type routeRegexp struct {
|
||||
// The unmodified template.
|
||||
template string
|
||||
// True for host match, false for path or query string match.
|
||||
matchHost bool
|
||||
// True for query string match, false for path and host match.
|
||||
matchQuery bool
|
||||
// The strictSlash value defined on the route, but disabled if PathPrefix was used.
|
||||
strictSlash bool
|
||||
// Expanded regexp.
|
||||
regexp *regexp.Regexp
|
||||
// Reverse template.
|
||||
reverse string
|
||||
// Variable names.
|
||||
varsN []string
|
||||
// Variable regexps (validators).
|
||||
varsR []*regexp.Regexp
|
||||
}
|
||||
|
||||
// Match matches the regexp against the URL host or path.
|
||||
func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
|
||||
if !r.matchHost {
|
||||
if r.matchQuery {
|
||||
return r.matchQueryString(req)
|
||||
} else {
|
||||
return r.regexp.MatchString(req.URL.Path)
|
||||
}
|
||||
}
|
||||
return r.regexp.MatchString(getHost(req))
|
||||
}
|
||||
|
||||
// url builds a URL part using the given values.
|
||||
func (r *routeRegexp) url(values map[string]string) (string, error) {
|
||||
urlValues := make([]interface{}, len(r.varsN))
|
||||
for k, v := range r.varsN {
|
||||
value, ok := values[v]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("mux: missing route variable %q", v)
|
||||
}
|
||||
urlValues[k] = value
|
||||
}
|
||||
rv := fmt.Sprintf(r.reverse, urlValues...)
|
||||
if !r.regexp.MatchString(rv) {
|
||||
// The URL is checked against the full regexp, instead of checking
|
||||
// individual variables. This is faster but to provide a good error
|
||||
// message, we check individual regexps if the URL doesn't match.
|
||||
for k, v := range r.varsN {
|
||||
if !r.varsR[k].MatchString(values[v]) {
|
||||
return "", fmt.Errorf(
|
||||
"mux: variable %q doesn't match, expected %q", values[v],
|
||||
r.varsR[k].String())
|
||||
}
|
||||
}
|
||||
}
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
// getUrlQuery returns a single query parameter from a request URL.
|
||||
// For a URL with foo=bar&baz=ding, we return only the relevant key
|
||||
// value pair for the routeRegexp.
|
||||
func (r *routeRegexp) getUrlQuery(req *http.Request) string {
|
||||
if !r.matchQuery {
|
||||
return ""
|
||||
}
|
||||
templateKey := strings.SplitN(r.template, "=", 2)[0]
|
||||
for key, vals := range req.URL.Query() {
|
||||
if key == templateKey && len(vals) > 0 {
|
||||
return key + "=" + vals[0]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *routeRegexp) matchQueryString(req *http.Request) bool {
|
||||
return r.regexp.MatchString(r.getUrlQuery(req))
|
||||
}
|
||||
|
||||
// braceIndices returns the first level curly brace indices from a string.
|
||||
// It returns an error in case of unbalanced braces.
|
||||
func braceIndices(s string) ([]int, error) {
|
||||
var level, idx int
|
||||
idxs := make([]int, 0)
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '{':
|
||||
if level++; level == 1 {
|
||||
idx = i
|
||||
}
|
||||
case '}':
|
||||
if level--; level == 0 {
|
||||
idxs = append(idxs, idx, i+1)
|
||||
} else if level < 0 {
|
||||
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
if level != 0 {
|
||||
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
|
||||
}
|
||||
return idxs, nil
|
||||
}
|
||||
|
||||
// varGroupName builds a capturing group name for the indexed variable.
|
||||
func varGroupName(idx int) string {
|
||||
return "v" + strconv.Itoa(idx)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// routeRegexpGroup
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// routeRegexpGroup groups the route matchers that carry variables.
|
||||
type routeRegexpGroup struct {
|
||||
host *routeRegexp
|
||||
path *routeRegexp
|
||||
queries []*routeRegexp
|
||||
}
|
||||
|
||||
// setMatch extracts the variables from the URL once a route matches.
|
||||
func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) {
|
||||
// Store host variables.
|
||||
if v.host != nil {
|
||||
hostVars := v.host.regexp.FindStringSubmatch(getHost(req))
|
||||
if hostVars != nil {
|
||||
subexpNames := v.host.regexp.SubexpNames()
|
||||
varName := 0
|
||||
for i, name := range subexpNames[1:] {
|
||||
if name != "" && name == varGroupName(varName) {
|
||||
m.Vars[v.host.varsN[varName]] = hostVars[i+1]
|
||||
varName++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Store path variables.
|
||||
if v.path != nil {
|
||||
pathVars := v.path.regexp.FindStringSubmatch(req.URL.Path)
|
||||
if pathVars != nil {
|
||||
subexpNames := v.path.regexp.SubexpNames()
|
||||
varName := 0
|
||||
for i, name := range subexpNames[1:] {
|
||||
if name != "" && name == varGroupName(varName) {
|
||||
m.Vars[v.path.varsN[varName]] = pathVars[i+1]
|
||||
varName++
|
||||
}
|
||||
}
|
||||
// Check if we should redirect.
|
||||
if v.path.strictSlash {
|
||||
p1 := strings.HasSuffix(req.URL.Path, "/")
|
||||
p2 := strings.HasSuffix(v.path.template, "/")
|
||||
if p1 != p2 {
|
||||
u, _ := url.Parse(req.URL.String())
|
||||
if p1 {
|
||||
u.Path = u.Path[:len(u.Path)-1]
|
||||
} else {
|
||||
u.Path += "/"
|
||||
}
|
||||
m.Handler = http.RedirectHandler(u.String(), 301)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Store query string variables.
|
||||
for _, q := range v.queries {
|
||||
queryVars := q.regexp.FindStringSubmatch(q.getUrlQuery(req))
|
||||
if queryVars != nil {
|
||||
subexpNames := q.regexp.SubexpNames()
|
||||
varName := 0
|
||||
for i, name := range subexpNames[1:] {
|
||||
if name != "" && name == varGroupName(varName) {
|
||||
m.Vars[q.varsN[varName]] = queryVars[i+1]
|
||||
varName++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getHost tries its best to return the request host.
|
||||
func getHost(r *http.Request) string {
|
||||
if r.URL.IsAbs() {
|
||||
return r.URL.Host
|
||||
}
|
||||
host := r.Host
|
||||
// Slice off any port information.
|
||||
if i := strings.Index(host, ":"); i != -1 {
|
||||
host = host[:i]
|
||||
}
|
||||
return host
|
||||
|
||||
}
|
||||
+595
@@ -0,0 +1,595 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Route stores information to match a request and build URLs.
|
||||
type Route struct {
|
||||
// Parent where the route was registered (a Router).
|
||||
parent parentRoute
|
||||
// Request handler for the route.
|
||||
handler http.Handler
|
||||
// List of matchers.
|
||||
matchers []matcher
|
||||
// Manager for the variables from host and path.
|
||||
regexp *routeRegexpGroup
|
||||
// If true, when the path pattern is "/path/", accessing "/path" will
|
||||
// redirect to the former and vice versa.
|
||||
strictSlash bool
|
||||
// If true, this route never matches: it is only used to build URLs.
|
||||
buildOnly bool
|
||||
// The name used to build URLs.
|
||||
name string
|
||||
// Error resulted from building a route.
|
||||
err error
|
||||
|
||||
buildVarsFunc BuildVarsFunc
|
||||
}
|
||||
|
||||
// Match matches the route against the request.
|
||||
func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
|
||||
if r.buildOnly || r.err != nil {
|
||||
return false
|
||||
}
|
||||
// Match everything.
|
||||
for _, m := range r.matchers {
|
||||
if matched := m.Match(req, match); !matched {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Yay, we have a match. Let's collect some info about it.
|
||||
if match.Route == nil {
|
||||
match.Route = r
|
||||
}
|
||||
if match.Handler == nil {
|
||||
match.Handler = r.handler
|
||||
}
|
||||
if match.Vars == nil {
|
||||
match.Vars = make(map[string]string)
|
||||
}
|
||||
// Set variables.
|
||||
if r.regexp != nil {
|
||||
r.regexp.setMatch(req, match, r)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Route attributes
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// GetError returns an error resulted from building the route, if any.
|
||||
func (r *Route) GetError() error {
|
||||
return r.err
|
||||
}
|
||||
|
||||
// BuildOnly sets the route to never match: it is only used to build URLs.
|
||||
func (r *Route) BuildOnly() *Route {
|
||||
r.buildOnly = true
|
||||
return r
|
||||
}
|
||||
|
||||
// Handler --------------------------------------------------------------------
|
||||
|
||||
// Handler sets a handler for the route.
|
||||
func (r *Route) Handler(handler http.Handler) *Route {
|
||||
if r.err == nil {
|
||||
r.handler = handler
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// HandlerFunc sets a handler function for the route.
|
||||
func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route {
|
||||
return r.Handler(http.HandlerFunc(f))
|
||||
}
|
||||
|
||||
// GetHandler returns the handler for the route, if any.
|
||||
func (r *Route) GetHandler() http.Handler {
|
||||
return r.handler
|
||||
}
|
||||
|
||||
// Name -----------------------------------------------------------------------
|
||||
|
||||
// Name sets the name for the route, used to build URLs.
|
||||
// If the name was registered already it will be overwritten.
|
||||
func (r *Route) Name(name string) *Route {
|
||||
if r.name != "" {
|
||||
r.err = fmt.Errorf("mux: route already has name %q, can't set %q",
|
||||
r.name, name)
|
||||
}
|
||||
if r.err == nil {
|
||||
r.name = name
|
||||
r.getNamedRoutes()[name] = r
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// GetName returns the name for the route, if any.
|
||||
func (r *Route) GetName() string {
|
||||
return r.name
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Matchers
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// matcher types try to match a request.
|
||||
type matcher interface {
|
||||
Match(*http.Request, *RouteMatch) bool
|
||||
}
|
||||
|
||||
// addMatcher adds a matcher to the route.
|
||||
func (r *Route) addMatcher(m matcher) *Route {
|
||||
if r.err == nil {
|
||||
r.matchers = append(r.matchers, m)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// addRegexpMatcher adds a host or path matcher and builder to a route.
|
||||
func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery bool) error {
|
||||
if r.err != nil {
|
||||
return r.err
|
||||
}
|
||||
r.regexp = r.getRegexpGroup()
|
||||
if !matchHost && !matchQuery {
|
||||
if len(tpl) == 0 || tpl[0] != '/' {
|
||||
return fmt.Errorf("mux: path must start with a slash, got %q", tpl)
|
||||
}
|
||||
if r.regexp.path != nil {
|
||||
tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl
|
||||
}
|
||||
}
|
||||
rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, matchQuery, r.strictSlash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, q := range r.regexp.queries {
|
||||
if err = uniqueVars(rr.varsN, q.varsN); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if matchHost {
|
||||
if r.regexp.path != nil {
|
||||
if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
r.regexp.host = rr
|
||||
} else {
|
||||
if r.regexp.host != nil {
|
||||
if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if matchQuery {
|
||||
r.regexp.queries = append(r.regexp.queries, rr)
|
||||
} else {
|
||||
r.regexp.path = rr
|
||||
}
|
||||
}
|
||||
r.addMatcher(rr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Headers --------------------------------------------------------------------
|
||||
|
||||
// headerMatcher matches the request against header values.
|
||||
type headerMatcher map[string]string
|
||||
|
||||
func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return matchMapWithString(m, r.Header, true)
|
||||
}
|
||||
|
||||
// Headers adds a matcher for request header values.
|
||||
// It accepts a sequence of key/value pairs to be matched. For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Headers("Content-Type", "application/json",
|
||||
// "X-Requested-With", "XMLHttpRequest")
|
||||
//
|
||||
// The above route will only match if both request header values match.
|
||||
// If the value is an empty string, it will match any value if the key is set.
|
||||
func (r *Route) Headers(pairs ...string) *Route {
|
||||
if r.err == nil {
|
||||
var headers map[string]string
|
||||
headers, r.err = mapFromPairsToString(pairs...)
|
||||
return r.addMatcher(headerMatcher(headers))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// headerRegexMatcher matches the request against the route given a regex for the header
|
||||
type headerRegexMatcher map[string]*regexp.Regexp
|
||||
|
||||
func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return matchMapWithRegex(m, r.Header, true)
|
||||
}
|
||||
|
||||
// Regular expressions can be used with headers as well.
|
||||
// It accepts a sequence of key/value pairs, where the value has regex support. For example
|
||||
// r := mux.NewRouter()
|
||||
// r.HeadersRegexp("Content-Type", "application/(text|json)",
|
||||
// "X-Requested-With", "XMLHttpRequest")
|
||||
//
|
||||
// The above route will only match if both the request header matches both regular expressions.
|
||||
// It the value is an empty string, it will match any value if the key is set.
|
||||
func (r *Route) HeadersRegexp(pairs ...string) *Route {
|
||||
if r.err == nil {
|
||||
var headers map[string]*regexp.Regexp
|
||||
headers, r.err = mapFromPairsToRegex(pairs...)
|
||||
return r.addMatcher(headerRegexMatcher(headers))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Host -----------------------------------------------------------------------
|
||||
|
||||
// Host adds a matcher for the URL host.
|
||||
// It accepts a template with zero or more URL variables enclosed by {}.
|
||||
// Variables can define an optional regexp pattern to be matched:
|
||||
//
|
||||
// - {name} matches anything until the next dot.
|
||||
//
|
||||
// - {name:pattern} matches the given regexp pattern.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Host("www.example.com")
|
||||
// r.Host("{subdomain}.domain.com")
|
||||
// r.Host("{subdomain:[a-z]+}.domain.com")
|
||||
//
|
||||
// Variable names must be unique in a given route. They can be retrieved
|
||||
// calling mux.Vars(request).
|
||||
func (r *Route) Host(tpl string) *Route {
|
||||
r.err = r.addRegexpMatcher(tpl, true, false, false)
|
||||
return r
|
||||
}
|
||||
|
||||
// MatcherFunc ----------------------------------------------------------------
|
||||
|
||||
// MatcherFunc is the function signature used by custom matchers.
|
||||
type MatcherFunc func(*http.Request, *RouteMatch) bool
|
||||
|
||||
func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return m(r, match)
|
||||
}
|
||||
|
||||
// MatcherFunc adds a custom function to be used as request matcher.
|
||||
func (r *Route) MatcherFunc(f MatcherFunc) *Route {
|
||||
return r.addMatcher(f)
|
||||
}
|
||||
|
||||
// Methods --------------------------------------------------------------------
|
||||
|
||||
// methodMatcher matches the request against HTTP methods.
|
||||
type methodMatcher []string
|
||||
|
||||
func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return matchInArray(m, r.Method)
|
||||
}
|
||||
|
||||
// Methods adds a matcher for HTTP methods.
|
||||
// It accepts a sequence of one or more methods to be matched, e.g.:
|
||||
// "GET", "POST", "PUT".
|
||||
func (r *Route) Methods(methods ...string) *Route {
|
||||
for k, v := range methods {
|
||||
methods[k] = strings.ToUpper(v)
|
||||
}
|
||||
return r.addMatcher(methodMatcher(methods))
|
||||
}
|
||||
|
||||
// Path -----------------------------------------------------------------------
|
||||
|
||||
// Path adds a matcher for the URL path.
|
||||
// It accepts a template with zero or more URL variables enclosed by {}. The
|
||||
// template must start with a "/".
|
||||
// Variables can define an optional regexp pattern to be matched:
|
||||
//
|
||||
// - {name} matches anything until the next slash.
|
||||
//
|
||||
// - {name:pattern} matches the given regexp pattern.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Path("/products/").Handler(ProductsHandler)
|
||||
// r.Path("/products/{key}").Handler(ProductsHandler)
|
||||
// r.Path("/articles/{category}/{id:[0-9]+}").
|
||||
// Handler(ArticleHandler)
|
||||
//
|
||||
// Variable names must be unique in a given route. They can be retrieved
|
||||
// calling mux.Vars(request).
|
||||
func (r *Route) Path(tpl string) *Route {
|
||||
r.err = r.addRegexpMatcher(tpl, false, false, false)
|
||||
return r
|
||||
}
|
||||
|
||||
// PathPrefix -----------------------------------------------------------------
|
||||
|
||||
// PathPrefix adds a matcher for the URL path prefix. This matches if the given
|
||||
// template is a prefix of the full URL path. See Route.Path() for details on
|
||||
// the tpl argument.
|
||||
//
|
||||
// Note that it does not treat slashes specially ("/foobar/" will be matched by
|
||||
// the prefix "/foo") so you may want to use a trailing slash here.
|
||||
//
|
||||
// Also note that the setting of Router.StrictSlash() has no effect on routes
|
||||
// with a PathPrefix matcher.
|
||||
func (r *Route) PathPrefix(tpl string) *Route {
|
||||
r.err = r.addRegexpMatcher(tpl, false, true, false)
|
||||
return r
|
||||
}
|
||||
|
||||
// Query ----------------------------------------------------------------------
|
||||
|
||||
// Queries adds a matcher for URL query values.
|
||||
// It accepts a sequence of key/value pairs. Values may define variables.
|
||||
// For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Queries("foo", "bar", "id", "{id:[0-9]+}")
|
||||
//
|
||||
// The above route will only match if the URL contains the defined queries
|
||||
// values, e.g.: ?foo=bar&id=42.
|
||||
//
|
||||
// It the value is an empty string, it will match any value if the key is set.
|
||||
//
|
||||
// Variables can define an optional regexp pattern to be matched:
|
||||
//
|
||||
// - {name} matches anything until the next slash.
|
||||
//
|
||||
// - {name:pattern} matches the given regexp pattern.
|
||||
func (r *Route) Queries(pairs ...string) *Route {
|
||||
length := len(pairs)
|
||||
if length%2 != 0 {
|
||||
r.err = fmt.Errorf(
|
||||
"mux: number of parameters must be multiple of 2, got %v", pairs)
|
||||
return nil
|
||||
}
|
||||
for i := 0; i < length; i += 2 {
|
||||
if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], false, false, true); r.err != nil {
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Schemes --------------------------------------------------------------------
|
||||
|
||||
// schemeMatcher matches the request against URL schemes.
|
||||
type schemeMatcher []string
|
||||
|
||||
func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return matchInArray(m, r.URL.Scheme)
|
||||
}
|
||||
|
||||
// Schemes adds a matcher for URL schemes.
|
||||
// It accepts a sequence of schemes to be matched, e.g.: "http", "https".
|
||||
func (r *Route) Schemes(schemes ...string) *Route {
|
||||
for k, v := range schemes {
|
||||
schemes[k] = strings.ToLower(v)
|
||||
}
|
||||
return r.addMatcher(schemeMatcher(schemes))
|
||||
}
|
||||
|
||||
// BuildVarsFunc --------------------------------------------------------------
|
||||
|
||||
// BuildVarsFunc is the function signature used by custom build variable
|
||||
// functions (which can modify route variables before a route's URL is built).
|
||||
type BuildVarsFunc func(map[string]string) map[string]string
|
||||
|
||||
// BuildVarsFunc adds a custom function to be used to modify build variables
|
||||
// before a route's URL is built.
|
||||
func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route {
|
||||
r.buildVarsFunc = f
|
||||
return r
|
||||
}
|
||||
|
||||
// Subrouter ------------------------------------------------------------------
|
||||
|
||||
// Subrouter creates a subrouter for the route.
|
||||
//
|
||||
// It will test the inner routes only if the parent route matched. For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// s := r.Host("www.example.com").Subrouter()
|
||||
// s.HandleFunc("/products/", ProductsHandler)
|
||||
// s.HandleFunc("/products/{key}", ProductHandler)
|
||||
// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
|
||||
//
|
||||
// Here, the routes registered in the subrouter won't be tested if the host
|
||||
// doesn't match.
|
||||
func (r *Route) Subrouter() *Router {
|
||||
router := &Router{parent: r, strictSlash: r.strictSlash}
|
||||
r.addMatcher(router)
|
||||
return router
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// URL building
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// URL builds a URL for the route.
|
||||
//
|
||||
// It accepts a sequence of key/value pairs for the route variables. For
|
||||
// example, given this route:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
// Name("article")
|
||||
//
|
||||
// ...a URL for it can be built using:
|
||||
//
|
||||
// url, err := r.Get("article").URL("category", "technology", "id", "42")
|
||||
//
|
||||
// ...which will return an url.URL with the following path:
|
||||
//
|
||||
// "/articles/technology/42"
|
||||
//
|
||||
// This also works for host variables:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Host("{subdomain}.domain.com").
|
||||
// HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
// Name("article")
|
||||
//
|
||||
// // url.String() will be "http://news.domain.com/articles/technology/42"
|
||||
// url, err := r.Get("article").URL("subdomain", "news",
|
||||
// "category", "technology",
|
||||
// "id", "42")
|
||||
//
|
||||
// All variables defined in the route are required, and their values must
|
||||
// conform to the corresponding patterns.
|
||||
func (r *Route) URL(pairs ...string) (*url.URL, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.regexp == nil {
|
||||
return nil, errors.New("mux: route doesn't have a host or path")
|
||||
}
|
||||
values, err := r.prepareVars(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var scheme, host, path string
|
||||
if r.regexp.host != nil {
|
||||
// Set a default scheme.
|
||||
scheme = "http"
|
||||
if host, err = r.regexp.host.url(values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if r.regexp.path != nil {
|
||||
if path, err = r.regexp.path.url(values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: host,
|
||||
Path: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// URLHost builds the host part of the URL for a route. See Route.URL().
|
||||
//
|
||||
// The route must have a host defined.
|
||||
func (r *Route) URLHost(pairs ...string) (*url.URL, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.regexp == nil || r.regexp.host == nil {
|
||||
return nil, errors.New("mux: route doesn't have a host")
|
||||
}
|
||||
values, err := r.prepareVars(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
host, err := r.regexp.host.url(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &url.URL{
|
||||
Scheme: "http",
|
||||
Host: host,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// URLPath builds the path part of the URL for a route. See Route.URL().
|
||||
//
|
||||
// The route must have a path defined.
|
||||
func (r *Route) URLPath(pairs ...string) (*url.URL, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.regexp == nil || r.regexp.path == nil {
|
||||
return nil, errors.New("mux: route doesn't have a path")
|
||||
}
|
||||
values, err := r.prepareVars(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path, err := r.regexp.path.url(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &url.URL{
|
||||
Path: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// prepareVars converts the route variable pairs into a map. If the route has a
|
||||
// BuildVarsFunc, it is invoked.
|
||||
func (r *Route) prepareVars(pairs ...string) (map[string]string, error) {
|
||||
m, err := mapFromPairsToString(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.buildVars(m), nil
|
||||
}
|
||||
|
||||
func (r *Route) buildVars(m map[string]string) map[string]string {
|
||||
if r.parent != nil {
|
||||
m = r.parent.buildVars(m)
|
||||
}
|
||||
if r.buildVarsFunc != nil {
|
||||
m = r.buildVarsFunc(m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// parentRoute
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// parentRoute allows routes to know about parent host and path definitions.
|
||||
type parentRoute interface {
|
||||
getNamedRoutes() map[string]*Route
|
||||
getRegexpGroup() *routeRegexpGroup
|
||||
buildVars(map[string]string) map[string]string
|
||||
}
|
||||
|
||||
// getNamedRoutes returns the map where named routes are registered.
|
||||
func (r *Route) getNamedRoutes() map[string]*Route {
|
||||
if r.parent == nil {
|
||||
// During tests router is not always set.
|
||||
r.parent = NewRouter()
|
||||
}
|
||||
return r.parent.getNamedRoutes()
|
||||
}
|
||||
|
||||
// getRegexpGroup returns regexp definitions from this route.
|
||||
func (r *Route) getRegexpGroup() *routeRegexpGroup {
|
||||
if r.regexp == nil {
|
||||
if r.parent == nil {
|
||||
// During tests router is not always set.
|
||||
r.parent = NewRouter()
|
||||
}
|
||||
regexp := r.parent.getRegexpGroup()
|
||||
if regexp == nil {
|
||||
r.regexp = new(routeRegexpGroup)
|
||||
} else {
|
||||
// Copy.
|
||||
r.regexp = &routeRegexpGroup{
|
||||
host: regexp.host,
|
||||
path: regexp.path,
|
||||
queries: regexp.queries,
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.regexp
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.1
|
||||
- 1.2
|
||||
- tip
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
# This is the official list of Gorilla WebSocket authors for copyright
|
||||
# purposes.
|
||||
#
|
||||
# Please keep the list sorted.
|
||||
|
||||
Gary Burd <gary@beagledreams.com>
|
||||
Joachim Bauch <mail@joachim-bauch.de>
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
# Gorilla WebSocket
|
||||
|
||||
Gorilla WebSocket is a [Go](http://golang.org/) implementation of the
|
||||
[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol.
|
||||
|
||||
### Documentation
|
||||
|
||||
* [API Reference](http://godoc.org/github.com/gorilla/websocket)
|
||||
* [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat)
|
||||
* [Command example](https://github.com/gorilla/websocket/tree/master/examples/command)
|
||||
* [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo)
|
||||
* [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch)
|
||||
|
||||
### Status
|
||||
|
||||
The Gorilla WebSocket package provides a complete and tested implementation of
|
||||
the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. The
|
||||
package API is stable.
|
||||
|
||||
### Installation
|
||||
|
||||
go get github.com/gorilla/websocket
|
||||
|
||||
### Protocol Compliance
|
||||
|
||||
The Gorilla WebSocket package passes the server tests in the [Autobahn Test
|
||||
Suite](http://autobahn.ws/testsuite) using the application in the [examples/autobahn
|
||||
subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn).
|
||||
|
||||
### Gorilla WebSocket compared with other packages
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th><a href="http://godoc.org/github.com/gorilla/websocket">github.com/gorilla</a></th>
|
||||
<th><a href="http://godoc.org/golang.org/x/net/websocket">golang.org/x/net</a></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr><td colspan="3"><a href="http://tools.ietf.org/html/rfc6455">RFC 6455</a> Features</td></tr>
|
||||
<tr><td>Passes <a href="http://autobahn.ws/testsuite/">Autobahn Test Suite</a></td><td><a href="https://github.com/gorilla/websocket/tree/master/examples/autobahn">Yes</a></td><td>No</td></tr>
|
||||
<tr><td>Receive <a href="https://tools.ietf.org/html/rfc6455#section-5.4">fragmented</a> message<td>Yes</td><td><a href="https://code.google.com/p/go/issues/detail?id=7632">No</a>, see note 1</td></tr>
|
||||
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.1">close</a> message</td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td><a href="https://code.google.com/p/go/issues/detail?id=4588">No</a></td></tr>
|
||||
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.2">pings</a> and receive <a href="https://tools.ietf.org/html/rfc6455#section-5.5.3">pongs</a></td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td>No</td></tr>
|
||||
<tr><td>Get the <a href="https://tools.ietf.org/html/rfc6455#section-5.6">type</a> of a received data message</td><td>Yes</td><td>Yes, see note 2</td></tr>
|
||||
<tr><td colspan="3">Other Features</tr></td>
|
||||
<tr><td>Limit size of received message</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.SetReadLimit">Yes</a></td><td><a href="https://code.google.com/p/go/issues/detail?id=5082">No</a></td></tr>
|
||||
<tr><td>Read message using io.Reader</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.NextReader">Yes</a></td><td>No, see note 3</td></tr>
|
||||
<tr><td>Write message using io.WriteCloser</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.NextWriter">Yes</a></td><td>No, see note 3</td></tr>
|
||||
</table>
|
||||
|
||||
Notes:
|
||||
|
||||
1. Large messages are fragmented in [Chrome's new WebSocket implementation](http://www.ietf.org/mail-archive/web/hybi/current/msg10503.html).
|
||||
2. The application can get the type of a received data message by implementing
|
||||
a [Codec marshal](http://godoc.org/golang.org/x/net/websocket#Codec.Marshal)
|
||||
function.
|
||||
3. The go.net io.Reader and io.Writer operate across WebSocket frame boundaries.
|
||||
Read returns when the input buffer is full or a frame boundary is
|
||||
encountered. Each call to Write sends a single frame message. The Gorilla
|
||||
io.Reader and io.WriteCloser operate on a single WebSocket message.
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// Copyright 2014 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkMaskBytes(b *testing.B) {
|
||||
var key [4]byte
|
||||
data := make([]byte, 1024)
|
||||
pos := 0
|
||||
for i := 0; i < b.N; i++ {
|
||||
pos = maskBytes(key, pos, data)
|
||||
}
|
||||
b.SetBytes(int64(len(data)))
|
||||
}
|
||||
+350
@@ -0,0 +1,350 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrBadHandshake is returned when the server response to opening handshake is
|
||||
// invalid.
|
||||
var ErrBadHandshake = errors.New("websocket: bad handshake")
|
||||
|
||||
// NewClient creates a new client connection using the given net connection.
|
||||
// The URL u specifies the host and request URI. Use requestHeader to specify
|
||||
// the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies
|
||||
// (Cookie). Use the response.Header to get the selected subprotocol
|
||||
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
|
||||
//
|
||||
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
|
||||
// non-nil *http.Response so that callers can handle redirects, authentication,
|
||||
// etc.
|
||||
//
|
||||
// Deprecated: Use Dialer instead.
|
||||
func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) {
|
||||
d := Dialer{
|
||||
ReadBufferSize: readBufSize,
|
||||
WriteBufferSize: writeBufSize,
|
||||
NetDial: func(net, addr string) (net.Conn, error) {
|
||||
return netConn, nil
|
||||
},
|
||||
}
|
||||
return d.Dial(u.String(), requestHeader)
|
||||
}
|
||||
|
||||
// A Dialer contains options for connecting to WebSocket server.
|
||||
type Dialer struct {
|
||||
// NetDial specifies the dial function for creating TCP connections. If
|
||||
// NetDial is nil, net.Dial is used.
|
||||
NetDial func(network, addr string) (net.Conn, error)
|
||||
|
||||
// Proxy specifies a function to return a proxy for a given
|
||||
// Request. If the function returns a non-nil error, the
|
||||
// request is aborted with the provided error.
|
||||
// If Proxy is nil or returns a nil *URL, no proxy is used.
|
||||
Proxy func(*http.Request) (*url.URL, error)
|
||||
|
||||
// TLSClientConfig specifies the TLS configuration to use with tls.Client.
|
||||
// If nil, the default configuration is used.
|
||||
TLSClientConfig *tls.Config
|
||||
|
||||
// HandshakeTimeout specifies the duration for the handshake to complete.
|
||||
HandshakeTimeout time.Duration
|
||||
|
||||
// Input and output buffer sizes. If the buffer size is zero, then a
|
||||
// default value of 4096 is used.
|
||||
ReadBufferSize, WriteBufferSize int
|
||||
|
||||
// Subprotocols specifies the client's requested subprotocols.
|
||||
Subprotocols []string
|
||||
}
|
||||
|
||||
var errMalformedURL = errors.New("malformed ws or wss URL")
|
||||
|
||||
// parseURL parses the URL.
|
||||
//
|
||||
// This function is a replacement for the standard library url.Parse function.
|
||||
// In Go 1.4 and earlier, url.Parse loses information from the path.
|
||||
func parseURL(s string) (*url.URL, error) {
|
||||
// From the RFC:
|
||||
//
|
||||
// ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
|
||||
// wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
|
||||
|
||||
var u url.URL
|
||||
switch {
|
||||
case strings.HasPrefix(s, "ws://"):
|
||||
u.Scheme = "ws"
|
||||
s = s[len("ws://"):]
|
||||
case strings.HasPrefix(s, "wss://"):
|
||||
u.Scheme = "wss"
|
||||
s = s[len("wss://"):]
|
||||
default:
|
||||
return nil, errMalformedURL
|
||||
}
|
||||
|
||||
if i := strings.Index(s, "?"); i >= 0 {
|
||||
u.RawQuery = s[i+1:]
|
||||
s = s[:i]
|
||||
}
|
||||
|
||||
if i := strings.Index(s, "/"); i >= 0 {
|
||||
u.Opaque = s[i:]
|
||||
s = s[:i]
|
||||
} else {
|
||||
u.Opaque = "/"
|
||||
}
|
||||
|
||||
u.Host = s
|
||||
|
||||
if strings.Contains(u.Host, "@") {
|
||||
// Don't bother parsing user information because user information is
|
||||
// not allowed in websocket URIs.
|
||||
return nil, errMalformedURL
|
||||
}
|
||||
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) {
|
||||
hostPort = u.Host
|
||||
hostNoPort = u.Host
|
||||
if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") {
|
||||
hostNoPort = hostNoPort[:i]
|
||||
} else {
|
||||
switch u.Scheme {
|
||||
case "wss":
|
||||
hostPort += ":443"
|
||||
case "https":
|
||||
hostPort += ":443"
|
||||
default:
|
||||
hostPort += ":80"
|
||||
}
|
||||
}
|
||||
return hostPort, hostNoPort
|
||||
}
|
||||
|
||||
// DefaultDialer is a dialer with all fields set to the default zero values.
|
||||
var DefaultDialer = &Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
|
||||
// Dial creates a new client connection. Use requestHeader to specify the
|
||||
// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie).
|
||||
// Use the response.Header to get the selected subprotocol
|
||||
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
|
||||
//
|
||||
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
|
||||
// non-nil *http.Response so that callers can handle redirects, authentication,
|
||||
// etcetera. The response body may not contain the entire response and does not
|
||||
// need to be closed by the application.
|
||||
func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
|
||||
|
||||
if d == nil {
|
||||
d = &Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
}
|
||||
|
||||
challengeKey, err := generateChallengeKey()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
u, err := parseURL(urlStr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "ws":
|
||||
u.Scheme = "http"
|
||||
case "wss":
|
||||
u.Scheme = "https"
|
||||
default:
|
||||
return nil, nil, errMalformedURL
|
||||
}
|
||||
|
||||
if u.User != nil {
|
||||
// User name and password are not allowed in websocket URIs.
|
||||
return nil, nil, errMalformedURL
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
Method: "GET",
|
||||
URL: u,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: make(http.Header),
|
||||
Host: u.Host,
|
||||
}
|
||||
|
||||
// Set the request headers using the capitalization for names and values in
|
||||
// RFC examples. Although the capitalization shouldn't matter, there are
|
||||
// servers that depend on it. The Header.Set method is not used because the
|
||||
// method canonicalizes the header names.
|
||||
req.Header["Upgrade"] = []string{"websocket"}
|
||||
req.Header["Connection"] = []string{"Upgrade"}
|
||||
req.Header["Sec-WebSocket-Key"] = []string{challengeKey}
|
||||
req.Header["Sec-WebSocket-Version"] = []string{"13"}
|
||||
if len(d.Subprotocols) > 0 {
|
||||
req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")}
|
||||
}
|
||||
for k, vs := range requestHeader {
|
||||
switch {
|
||||
case k == "Host":
|
||||
if len(vs) > 0 {
|
||||
req.Host = vs[0]
|
||||
}
|
||||
case k == "Upgrade" ||
|
||||
k == "Connection" ||
|
||||
k == "Sec-Websocket-Key" ||
|
||||
k == "Sec-Websocket-Version" ||
|
||||
(k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0):
|
||||
return nil, nil, errors.New("websocket: duplicate header not allowed: " + k)
|
||||
default:
|
||||
req.Header[k] = vs
|
||||
}
|
||||
}
|
||||
|
||||
hostPort, hostNoPort := hostPortNoPort(u)
|
||||
|
||||
var proxyURL *url.URL
|
||||
// Check wether the proxy method has been configured
|
||||
if d.Proxy != nil {
|
||||
proxyURL, err = d.Proxy(req)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var targetHostPort string
|
||||
if proxyURL != nil {
|
||||
targetHostPort, _ = hostPortNoPort(proxyURL)
|
||||
} else {
|
||||
targetHostPort = hostPort
|
||||
}
|
||||
|
||||
var deadline time.Time
|
||||
if d.HandshakeTimeout != 0 {
|
||||
deadline = time.Now().Add(d.HandshakeTimeout)
|
||||
}
|
||||
|
||||
netDial := d.NetDial
|
||||
if netDial == nil {
|
||||
netDialer := &net.Dialer{Deadline: deadline}
|
||||
netDial = netDialer.Dial
|
||||
}
|
||||
|
||||
netConn, err := netDial("tcp", targetHostPort)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if netConn != nil {
|
||||
netConn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if err := netConn.SetDeadline(deadline); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if proxyURL != nil {
|
||||
connectHeader := make(http.Header)
|
||||
if user := proxyURL.User; user != nil {
|
||||
proxyUser := user.Username()
|
||||
if proxyPassword, passwordSet := user.Password(); passwordSet {
|
||||
credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword))
|
||||
connectHeader.Set("Proxy-Authorization", "Basic "+credential)
|
||||
}
|
||||
}
|
||||
connectReq := &http.Request{
|
||||
Method: "CONNECT",
|
||||
URL: &url.URL{Opaque: hostPort},
|
||||
Host: hostPort,
|
||||
Header: connectHeader,
|
||||
}
|
||||
|
||||
connectReq.Write(netConn)
|
||||
|
||||
// Read response.
|
||||
// Okay to use and discard buffered reader here, because
|
||||
// TLS server will not speak until spoken to.
|
||||
br := bufio.NewReader(netConn)
|
||||
resp, err := http.ReadResponse(br, connectReq)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
f := strings.SplitN(resp.Status, " ", 2)
|
||||
return nil, nil, errors.New(f[1])
|
||||
}
|
||||
}
|
||||
|
||||
if u.Scheme == "https" {
|
||||
cfg := d.TLSClientConfig
|
||||
if cfg == nil {
|
||||
cfg = &tls.Config{ServerName: hostNoPort}
|
||||
} else if cfg.ServerName == "" {
|
||||
shallowCopy := *cfg
|
||||
cfg = &shallowCopy
|
||||
cfg.ServerName = hostNoPort
|
||||
}
|
||||
tlsConn := tls.Client(netConn, cfg)
|
||||
netConn = tlsConn
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !cfg.InsecureSkipVerify {
|
||||
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize)
|
||||
|
||||
if err := req.Write(netConn); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(conn.br, req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.StatusCode != 101 ||
|
||||
!strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") ||
|
||||
!strings.EqualFold(resp.Header.Get("Connection"), "upgrade") ||
|
||||
resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) {
|
||||
// Before closing the network connection on return from this
|
||||
// function, slurp up some of the response to aid application
|
||||
// debugging.
|
||||
buf := make([]byte, 1024)
|
||||
n, _ := io.ReadFull(resp.Body, buf)
|
||||
resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n]))
|
||||
return nil, resp, ErrBadHandshake
|
||||
}
|
||||
|
||||
resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
|
||||
conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol")
|
||||
|
||||
netConn.SetDeadline(time.Time{})
|
||||
netConn = nil // to avoid close in defer.
|
||||
return conn, resp, nil
|
||||
}
|
||||
+451
@@ -0,0 +1,451 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var cstUpgrader = Upgrader{
|
||||
Subprotocols: []string{"p0", "p1"},
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) {
|
||||
http.Error(w, reason.Error(), status)
|
||||
},
|
||||
}
|
||||
|
||||
var cstDialer = Dialer{
|
||||
Subprotocols: []string{"p1", "p2"},
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
type cstHandler struct{ *testing.T }
|
||||
|
||||
type cstServer struct {
|
||||
*httptest.Server
|
||||
URL string
|
||||
}
|
||||
|
||||
const (
|
||||
cstPath = "/a/b"
|
||||
cstRawQuery = "x=y"
|
||||
cstRequestURI = cstPath + "?" + cstRawQuery
|
||||
)
|
||||
|
||||
func newServer(t *testing.T) *cstServer {
|
||||
var s cstServer
|
||||
s.Server = httptest.NewServer(cstHandler{t})
|
||||
s.Server.URL += cstRequestURI
|
||||
s.URL = makeWsProto(s.Server.URL)
|
||||
return &s
|
||||
}
|
||||
|
||||
func newTLSServer(t *testing.T) *cstServer {
|
||||
var s cstServer
|
||||
s.Server = httptest.NewTLSServer(cstHandler{t})
|
||||
s.Server.URL += cstRequestURI
|
||||
s.URL = makeWsProto(s.Server.URL)
|
||||
return &s
|
||||
}
|
||||
|
||||
func (t cstHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != cstPath {
|
||||
t.Logf("path=%v, want %v", r.URL.Path, cstPath)
|
||||
http.Error(w, "bad path", 400)
|
||||
return
|
||||
}
|
||||
if r.URL.RawQuery != cstRawQuery {
|
||||
t.Logf("query=%v, want %v", r.URL.RawQuery, cstRawQuery)
|
||||
http.Error(w, "bad path", 400)
|
||||
return
|
||||
}
|
||||
subprotos := Subprotocols(r)
|
||||
if !reflect.DeepEqual(subprotos, cstDialer.Subprotocols) {
|
||||
t.Logf("subprotols=%v, want %v", subprotos, cstDialer.Subprotocols)
|
||||
http.Error(w, "bad protocol", 400)
|
||||
return
|
||||
}
|
||||
ws, err := cstUpgrader.Upgrade(w, r, http.Header{"Set-Cookie": {"sessionID=1234"}})
|
||||
if err != nil {
|
||||
t.Logf("Upgrade: %v", err)
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
if ws.Subprotocol() != "p1" {
|
||||
t.Logf("Subprotocol() = %s, want p1", ws.Subprotocol())
|
||||
ws.Close()
|
||||
return
|
||||
}
|
||||
op, rd, err := ws.NextReader()
|
||||
if err != nil {
|
||||
t.Logf("NextReader: %v", err)
|
||||
return
|
||||
}
|
||||
wr, err := ws.NextWriter(op)
|
||||
if err != nil {
|
||||
t.Logf("NextWriter: %v", err)
|
||||
return
|
||||
}
|
||||
if _, err = io.Copy(wr, rd); err != nil {
|
||||
t.Logf("NextWriter: %v", err)
|
||||
return
|
||||
}
|
||||
if err := wr.Close(); err != nil {
|
||||
t.Logf("Close: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func makeWsProto(s string) string {
|
||||
return "ws" + strings.TrimPrefix(s, "http")
|
||||
}
|
||||
|
||||
func sendRecv(t *testing.T, ws *Conn) {
|
||||
const message = "Hello World!"
|
||||
if err := ws.SetWriteDeadline(time.Now().Add(time.Second)); err != nil {
|
||||
t.Fatalf("SetWriteDeadline: %v", err)
|
||||
}
|
||||
if err := ws.WriteMessage(TextMessage, []byte(message)); err != nil {
|
||||
t.Fatalf("WriteMessage: %v", err)
|
||||
}
|
||||
if err := ws.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
|
||||
t.Fatalf("SetReadDeadline: %v", err)
|
||||
}
|
||||
_, p, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
t.Fatalf("ReadMessage: %v", err)
|
||||
}
|
||||
if string(p) != message {
|
||||
t.Fatalf("message=%s, want %s", p, message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDial(t *testing.T) {
|
||||
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
|
||||
surl, _ := url.Parse(s.URL)
|
||||
|
||||
cstDialer.Proxy = http.ProxyURL(surl)
|
||||
|
||||
connect := false
|
||||
origHandler := s.Server.Config.Handler
|
||||
|
||||
// Capture the request Host header.
|
||||
s.Server.Config.Handler = http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "CONNECT" {
|
||||
connect = true
|
||||
w.WriteHeader(200)
|
||||
return
|
||||
}
|
||||
|
||||
if !connect {
|
||||
t.Log("connect not recieved")
|
||||
http.Error(w, "connect not recieved", 405)
|
||||
return
|
||||
}
|
||||
origHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
ws, _, err := cstDialer.Dial(s.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
defer ws.Close()
|
||||
sendRecv(t, ws)
|
||||
|
||||
cstDialer.Proxy = http.ProxyFromEnvironment
|
||||
}
|
||||
|
||||
func TestProxyAuthorizationDial(t *testing.T) {
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
|
||||
surl, _ := url.Parse(s.URL)
|
||||
surl.User = url.UserPassword("username", "password")
|
||||
cstDialer.Proxy = http.ProxyURL(surl)
|
||||
|
||||
connect := false
|
||||
origHandler := s.Server.Config.Handler
|
||||
|
||||
// Capture the request Host header.
|
||||
s.Server.Config.Handler = http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
proxyAuth := r.Header.Get("Proxy-Authorization")
|
||||
expectedProxyAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("username:password"))
|
||||
if r.Method == "CONNECT" && proxyAuth == expectedProxyAuth {
|
||||
connect = true
|
||||
w.WriteHeader(200)
|
||||
return
|
||||
}
|
||||
|
||||
if !connect {
|
||||
t.Log("connect with proxy authorization not recieved")
|
||||
http.Error(w, "connect with proxy authorization not recieved", 405)
|
||||
return
|
||||
}
|
||||
origHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
ws, _, err := cstDialer.Dial(s.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
defer ws.Close()
|
||||
sendRecv(t, ws)
|
||||
|
||||
cstDialer.Proxy = http.ProxyFromEnvironment
|
||||
}
|
||||
|
||||
func TestDial(t *testing.T) {
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
|
||||
ws, _, err := cstDialer.Dial(s.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
defer ws.Close()
|
||||
sendRecv(t, ws)
|
||||
}
|
||||
|
||||
func TestDialTLS(t *testing.T) {
|
||||
s := newTLSServer(t)
|
||||
defer s.Close()
|
||||
|
||||
certs := x509.NewCertPool()
|
||||
for _, c := range s.TLS.Certificates {
|
||||
roots, err := x509.ParseCertificates(c.Certificate[len(c.Certificate)-1])
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing server's root cert: %v", err)
|
||||
}
|
||||
for _, root := range roots {
|
||||
certs.AddCert(root)
|
||||
}
|
||||
}
|
||||
|
||||
u, _ := url.Parse(s.URL)
|
||||
d := cstDialer
|
||||
d.NetDial = func(network, addr string) (net.Conn, error) { return net.Dial(network, u.Host) }
|
||||
d.TLSClientConfig = &tls.Config{RootCAs: certs}
|
||||
ws, _, err := d.Dial("wss://example.com"+cstRequestURI, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
defer ws.Close()
|
||||
sendRecv(t, ws)
|
||||
}
|
||||
|
||||
func xTestDialTLSBadCert(t *testing.T) {
|
||||
// This test is deactivated because of noisy logging from the net/http package.
|
||||
s := newTLSServer(t)
|
||||
defer s.Close()
|
||||
|
||||
ws, _, err := cstDialer.Dial(s.URL, nil)
|
||||
if err == nil {
|
||||
ws.Close()
|
||||
t.Fatalf("Dial: nil")
|
||||
}
|
||||
}
|
||||
|
||||
func xTestDialTLSNoVerify(t *testing.T) {
|
||||
s := newTLSServer(t)
|
||||
defer s.Close()
|
||||
|
||||
d := cstDialer
|
||||
d.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
ws, _, err := d.Dial(s.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
defer ws.Close()
|
||||
sendRecv(t, ws)
|
||||
}
|
||||
|
||||
func TestDialTimeout(t *testing.T) {
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
|
||||
d := cstDialer
|
||||
d.HandshakeTimeout = -1
|
||||
ws, _, err := d.Dial(s.URL, nil)
|
||||
if err == nil {
|
||||
ws.Close()
|
||||
t.Fatalf("Dial: nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialBadScheme(t *testing.T) {
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
|
||||
ws, _, err := cstDialer.Dial(s.Server.URL, nil)
|
||||
if err == nil {
|
||||
ws.Close()
|
||||
t.Fatalf("Dial: nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialBadOrigin(t *testing.T) {
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
|
||||
ws, resp, err := cstDialer.Dial(s.URL, http.Header{"Origin": {"bad"}})
|
||||
if err == nil {
|
||||
ws.Close()
|
||||
t.Fatalf("Dial: nil")
|
||||
}
|
||||
if resp == nil {
|
||||
t.Fatalf("resp=nil, err=%v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusForbidden {
|
||||
t.Fatalf("status=%d, want %d", resp.StatusCode, http.StatusForbidden)
|
||||
}
|
||||
}
|
||||
|
||||
func 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()
|
||||
|
||||
ws, resp, err := cstDialer.Dial(s.URL, http.Header{"Origin": {s.URL}})
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
var sessionID string
|
||||
for _, c := range resp.Cookies() {
|
||||
if c.Name == "sessionID" {
|
||||
sessionID = c.Value
|
||||
}
|
||||
}
|
||||
if sessionID != "1234" {
|
||||
t.Error("Set-Cookie not received from the server.")
|
||||
}
|
||||
|
||||
if ws.Subprotocol() != "p1" {
|
||||
t.Errorf("ws.Subprotocol() = %s, want p1", ws.Subprotocol())
|
||||
}
|
||||
sendRecv(t, ws)
|
||||
}
|
||||
|
||||
func TestRespOnBadHandshake(t *testing.T) {
|
||||
const expectedStatus = http.StatusGone
|
||||
const expectedBody = "This is the response body."
|
||||
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(expectedStatus)
|
||||
io.WriteString(w, expectedBody)
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
ws, resp, err := cstDialer.Dial(makeWsProto(s.URL), nil)
|
||||
if err == nil {
|
||||
ws.Close()
|
||||
t.Fatalf("Dial: nil")
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
t.Fatalf("resp=nil, err=%v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != expectedStatus {
|
||||
t.Errorf("resp.StatusCode=%d, want %d", resp.StatusCode, expectedStatus)
|
||||
}
|
||||
|
||||
p, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFull(resp.Body) returned error %v", err)
|
||||
}
|
||||
|
||||
if string(p) != expectedBody {
|
||||
t.Errorf("resp.Body=%s, want %s", p, expectedBody)
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
specifiedHost := make(chan string, 1)
|
||||
origHandler := s.Server.Config.Handler
|
||||
|
||||
// Capture the request Host header.
|
||||
s.Server.Config.Handler = http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
specifiedHost <- r.Host
|
||||
origHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
ws, _, err := cstDialer.Dial(s.URL, http.Header{"Host": {"testhost"}})
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
if gotHost := <-specifiedHost; gotHost != "testhost" {
|
||||
t.Fatalf("gotHost = %q, want \"testhost\"", gotHost)
|
||||
}
|
||||
|
||||
sendRecv(t, ws)
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
// Copyright 2014 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var parseURLTests = []struct {
|
||||
s string
|
||||
u *url.URL
|
||||
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"}, "/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) {
|
||||
for _, tt := range parseURLTests {
|
||||
u, err := parseURL(tt.s)
|
||||
if tt.u != nil && err != nil {
|
||||
t.Errorf("parseURL(%q) returned error %v", tt.s, err)
|
||||
continue
|
||||
}
|
||||
if tt.u == nil {
|
||||
if err == nil {
|
||||
t.Errorf("parseURL(%q) did not return error", tt.s)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hostPortNoPortTests = []struct {
|
||||
u *url.URL
|
||||
hostPort, hostNoPort string
|
||||
}{
|
||||
{&url.URL{Scheme: "ws", Host: "example.com"}, "example.com:80", "example.com"},
|
||||
{&url.URL{Scheme: "wss", Host: "example.com"}, "example.com:443", "example.com"},
|
||||
{&url.URL{Scheme: "ws", Host: "example.com:7777"}, "example.com:7777", "example.com"},
|
||||
{&url.URL{Scheme: "wss", Host: "example.com:7777"}, "example.com:7777", "example.com"},
|
||||
}
|
||||
|
||||
func TestHostPortNoPort(t *testing.T) {
|
||||
for _, tt := range hostPortNoPortTests {
|
||||
hostPort, hostNoPort := hostPortNoPort(tt.u)
|
||||
if hostPort != tt.hostPort {
|
||||
t.Errorf("hostPortNoPort(%v) returned hostPort %q, want %q", tt.u, hostPort, tt.hostPort)
|
||||
}
|
||||
if hostNoPort != tt.hostNoPort {
|
||||
t.Errorf("hostPortNoPort(%v) returned hostNoPort %q, want %q", tt.u, hostNoPort, tt.hostNoPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
+915
@@ -0,0 +1,915 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
maxFrameHeaderSize = 2 + 8 + 4 // Fixed header + length + mask
|
||||
maxControlFramePayloadSize = 125
|
||||
finalBit = 1 << 7
|
||||
maskBit = 1 << 7
|
||||
writeWait = time.Second
|
||||
|
||||
defaultReadBufferSize = 4096
|
||||
defaultWriteBufferSize = 4096
|
||||
|
||||
continuationFrame = 0
|
||||
noFrame = -1
|
||||
)
|
||||
|
||||
// Close codes defined in RFC 6455, section 11.7.
|
||||
const (
|
||||
CloseNormalClosure = 1000
|
||||
CloseGoingAway = 1001
|
||||
CloseProtocolError = 1002
|
||||
CloseUnsupportedData = 1003
|
||||
CloseNoStatusReceived = 1005
|
||||
CloseAbnormalClosure = 1006
|
||||
CloseInvalidFramePayloadData = 1007
|
||||
ClosePolicyViolation = 1008
|
||||
CloseMessageTooBig = 1009
|
||||
CloseMandatoryExtension = 1010
|
||||
CloseInternalServerErr = 1011
|
||||
CloseTLSHandshake = 1015
|
||||
)
|
||||
|
||||
// The message types are defined in RFC 6455, section 11.8.
|
||||
const (
|
||||
// TextMessage denotes a text data message. The text message payload is
|
||||
// interpreted as UTF-8 encoded text data.
|
||||
TextMessage = 1
|
||||
|
||||
// BinaryMessage denotes a binary data message.
|
||||
BinaryMessage = 2
|
||||
|
||||
// CloseMessage denotes a close control message. The optional message
|
||||
// payload contains a numeric code and text. Use the FormatCloseMessage
|
||||
// function to format a close message payload.
|
||||
CloseMessage = 8
|
||||
|
||||
// PingMessage denotes a ping control message. The optional message payload
|
||||
// is UTF-8 encoded text.
|
||||
PingMessage = 9
|
||||
|
||||
// PongMessage denotes a ping control message. The optional message payload
|
||||
// is UTF-8 encoded text.
|
||||
PongMessage = 10
|
||||
)
|
||||
|
||||
// ErrCloseSent is returned when the application writes a message to the
|
||||
// connection after sending a close message.
|
||||
var ErrCloseSent = errors.New("websocket: close sent")
|
||||
|
||||
// ErrReadLimit is returned when reading a message that is larger than the
|
||||
// read limit set for the connection.
|
||||
var ErrReadLimit = errors.New("websocket: read limit exceeded")
|
||||
|
||||
// netError satisfies the net Error interface.
|
||||
type netError struct {
|
||||
msg string
|
||||
temporary bool
|
||||
timeout bool
|
||||
}
|
||||
|
||||
func (e *netError) Error() string { return e.msg }
|
||||
func (e *netError) Temporary() bool { return e.temporary }
|
||||
func (e *netError) Timeout() bool { return e.timeout }
|
||||
|
||||
// CloseError represents close frame.
|
||||
type CloseError struct {
|
||||
|
||||
// Code is defined in RFC 6455, section 11.7.
|
||||
Code int
|
||||
|
||||
// Text is the optional text payload.
|
||||
Text string
|
||||
}
|
||||
|
||||
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, 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")
|
||||
)
|
||||
|
||||
func hideTempErr(err error) error {
|
||||
if e, ok := err.(net.Error); ok && e.Temporary() {
|
||||
err = &netError{msg: e.Error(), timeout: e.Timeout()}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func isControl(frameType int) bool {
|
||||
return frameType == CloseMessage || frameType == PingMessage || frameType == PongMessage
|
||||
}
|
||||
|
||||
func isData(frameType int) bool {
|
||||
return frameType == TextMessage || frameType == BinaryMessage
|
||||
}
|
||||
|
||||
func maskBytes(key [4]byte, pos int, b []byte) int {
|
||||
for i := range b {
|
||||
b[i] ^= key[pos&3]
|
||||
pos++
|
||||
}
|
||||
return pos & 3
|
||||
}
|
||||
|
||||
func newMaskKey() [4]byte {
|
||||
n := rand.Uint32()
|
||||
return [4]byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)}
|
||||
}
|
||||
|
||||
// Conn represents a WebSocket connection.
|
||||
type Conn struct {
|
||||
conn net.Conn
|
||||
isServer bool
|
||||
subprotocol string
|
||||
|
||||
// Write fields
|
||||
mu chan bool // used as mutex to protect write to conn and closeSent
|
||||
closeSent bool // true if close message was sent
|
||||
|
||||
// Message writer fields.
|
||||
writeErr error
|
||||
writeBuf []byte // frame is constructed in this buffer.
|
||||
writePos int // end of data in writeBuf.
|
||||
writeFrameType int // type of the current frame.
|
||||
writeSeq int // incremented to invalidate message writers.
|
||||
writeDeadline time.Time
|
||||
isWriting bool // for best-effort concurrent write detection
|
||||
|
||||
// Read fields
|
||||
readErr error
|
||||
br *bufio.Reader
|
||||
readRemaining int64 // bytes remaining in current frame.
|
||||
readFinal bool // true the current message has more frames.
|
||||
readSeq int // incremented to invalidate message readers.
|
||||
readLength int64 // Message size.
|
||||
readLimit int64 // Maximum message size.
|
||||
readMaskPos int
|
||||
readMaskKey [4]byte
|
||||
handlePong func(string) error
|
||||
handlePing func(string) error
|
||||
readErrCount int
|
||||
}
|
||||
|
||||
func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn {
|
||||
mu := make(chan bool, 1)
|
||||
mu <- true
|
||||
|
||||
if readBufferSize == 0 {
|
||||
readBufferSize = defaultReadBufferSize
|
||||
}
|
||||
if writeBufferSize == 0 {
|
||||
writeBufferSize = defaultWriteBufferSize
|
||||
}
|
||||
|
||||
c := &Conn{
|
||||
isServer: isServer,
|
||||
br: bufio.NewReaderSize(conn, readBufferSize),
|
||||
conn: conn,
|
||||
mu: mu,
|
||||
readFinal: true,
|
||||
writeBuf: make([]byte, writeBufferSize+maxFrameHeaderSize),
|
||||
writeFrameType: noFrame,
|
||||
writePos: maxFrameHeaderSize,
|
||||
}
|
||||
c.SetPingHandler(nil)
|
||||
c.SetPongHandler(nil)
|
||||
return c
|
||||
}
|
||||
|
||||
// Subprotocol returns the negotiated protocol for the connection.
|
||||
func (c *Conn) Subprotocol() string {
|
||||
return c.subprotocol
|
||||
}
|
||||
|
||||
// Close closes the underlying network connection without sending or waiting for a close frame.
|
||||
func (c *Conn) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
// LocalAddr returns the local network address.
|
||||
func (c *Conn) LocalAddr() net.Addr {
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
// RemoteAddr returns the remote network address.
|
||||
func (c *Conn) RemoteAddr() net.Addr {
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
// Write methods
|
||||
|
||||
func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error {
|
||||
<-c.mu
|
||||
defer func() { c.mu <- true }()
|
||||
|
||||
if c.closeSent {
|
||||
return ErrCloseSent
|
||||
} else if frameType == CloseMessage {
|
||||
c.closeSent = true
|
||||
}
|
||||
|
||||
c.conn.SetWriteDeadline(deadline)
|
||||
for _, buf := range bufs {
|
||||
if len(buf) > 0 {
|
||||
n, err := c.conn.Write(buf)
|
||||
if n != len(buf) {
|
||||
// Close on partial write.
|
||||
c.conn.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteControl writes a control message with the given deadline. The allowed
|
||||
// message types are CloseMessage, PingMessage and PongMessage.
|
||||
func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) error {
|
||||
if !isControl(messageType) {
|
||||
return errBadWriteOpCode
|
||||
}
|
||||
if len(data) > maxControlFramePayloadSize {
|
||||
return errInvalidControlFrame
|
||||
}
|
||||
|
||||
b0 := byte(messageType) | finalBit
|
||||
b1 := byte(len(data))
|
||||
if !c.isServer {
|
||||
b1 |= maskBit
|
||||
}
|
||||
|
||||
buf := make([]byte, 0, maxFrameHeaderSize+maxControlFramePayloadSize)
|
||||
buf = append(buf, b0, b1)
|
||||
|
||||
if c.isServer {
|
||||
buf = append(buf, data...)
|
||||
} else {
|
||||
key := newMaskKey()
|
||||
buf = append(buf, key[:]...)
|
||||
buf = append(buf, data...)
|
||||
maskBytes(key, 0, buf[6:])
|
||||
}
|
||||
|
||||
d := time.Hour * 1000
|
||||
if !deadline.IsZero() {
|
||||
d = deadline.Sub(time.Now())
|
||||
if d < 0 {
|
||||
return errWriteTimeout
|
||||
}
|
||||
}
|
||||
|
||||
timer := time.NewTimer(d)
|
||||
select {
|
||||
case <-c.mu:
|
||||
timer.Stop()
|
||||
case <-timer.C:
|
||||
return errWriteTimeout
|
||||
}
|
||||
defer func() { c.mu <- true }()
|
||||
|
||||
if c.closeSent {
|
||||
return ErrCloseSent
|
||||
} else if messageType == CloseMessage {
|
||||
c.closeSent = true
|
||||
}
|
||||
|
||||
c.conn.SetWriteDeadline(deadline)
|
||||
n, err := c.conn.Write(buf)
|
||||
if n != 0 && n != len(buf) {
|
||||
c.conn.Close()
|
||||
}
|
||||
return hideTempErr(err)
|
||||
}
|
||||
|
||||
// NextWriter returns a writer for the next message to send. The writer's
|
||||
// Close method flushes the complete message to the network.
|
||||
//
|
||||
// There can be at most one open writer on a connection. NextWriter closes the
|
||||
// previous writer if the application has not already done so.
|
||||
func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) {
|
||||
if c.writeErr != nil {
|
||||
return nil, c.writeErr
|
||||
}
|
||||
|
||||
if c.writeFrameType != noFrame {
|
||||
if err := c.flushFrame(true, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !isControl(messageType) && !isData(messageType) {
|
||||
return nil, errBadWriteOpCode
|
||||
}
|
||||
|
||||
c.writeFrameType = messageType
|
||||
return messageWriter{c, c.writeSeq}, nil
|
||||
}
|
||||
|
||||
func (c *Conn) flushFrame(final bool, extra []byte) error {
|
||||
length := c.writePos - maxFrameHeaderSize + len(extra)
|
||||
|
||||
// Check for invalid control frames.
|
||||
if isControl(c.writeFrameType) &&
|
||||
(!final || length > maxControlFramePayloadSize) {
|
||||
c.writeSeq++
|
||||
c.writeFrameType = noFrame
|
||||
c.writePos = maxFrameHeaderSize
|
||||
return errInvalidControlFrame
|
||||
}
|
||||
|
||||
b0 := byte(c.writeFrameType)
|
||||
if final {
|
||||
b0 |= finalBit
|
||||
}
|
||||
b1 := byte(0)
|
||||
if !c.isServer {
|
||||
b1 |= maskBit
|
||||
}
|
||||
|
||||
// Assume that the frame starts at beginning of c.writeBuf.
|
||||
framePos := 0
|
||||
if c.isServer {
|
||||
// Adjust up if mask not included in the header.
|
||||
framePos = 4
|
||||
}
|
||||
|
||||
switch {
|
||||
case length >= 65536:
|
||||
c.writeBuf[framePos] = b0
|
||||
c.writeBuf[framePos+1] = b1 | 127
|
||||
binary.BigEndian.PutUint64(c.writeBuf[framePos+2:], uint64(length))
|
||||
case length > 125:
|
||||
framePos += 6
|
||||
c.writeBuf[framePos] = b0
|
||||
c.writeBuf[framePos+1] = b1 | 126
|
||||
binary.BigEndian.PutUint16(c.writeBuf[framePos+2:], uint16(length))
|
||||
default:
|
||||
framePos += 8
|
||||
c.writeBuf[framePos] = b0
|
||||
c.writeBuf[framePos+1] = b1 | byte(length)
|
||||
}
|
||||
|
||||
if !c.isServer {
|
||||
key := newMaskKey()
|
||||
copy(c.writeBuf[maxFrameHeaderSize-4:], key[:])
|
||||
maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:c.writePos])
|
||||
if len(extra) > 0 {
|
||||
c.writeErr = errors.New("websocket: internal error, extra used in client mode")
|
||||
return c.writeErr
|
||||
}
|
||||
}
|
||||
|
||||
// Write the buffers to the connection 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
|
||||
if final {
|
||||
c.writeSeq++
|
||||
c.writeFrameType = noFrame
|
||||
}
|
||||
return c.writeErr
|
||||
}
|
||||
|
||||
type messageWriter struct {
|
||||
c *Conn
|
||||
seq int
|
||||
}
|
||||
|
||||
func (w messageWriter) err() error {
|
||||
c := w.c
|
||||
if c.writeSeq != w.seq {
|
||||
return errWriteClosed
|
||||
}
|
||||
if c.writeErr != nil {
|
||||
return c.writeErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w messageWriter) ncopy(max int) (int, error) {
|
||||
n := len(w.c.writeBuf) - w.c.writePos
|
||||
if n <= 0 {
|
||||
if err := w.c.flushFrame(false, nil); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n = len(w.c.writeBuf) - w.c.writePos
|
||||
}
|
||||
if n > max {
|
||||
n = max
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (w messageWriter) write(final bool, p []byte) (int, error) {
|
||||
if err := w.err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(p) > 2*len(w.c.writeBuf) && w.c.isServer {
|
||||
// Don't buffer large messages.
|
||||
err := w.c.flushFrame(final, p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
nn := len(p)
|
||||
for len(p) > 0 {
|
||||
n, err := w.ncopy(len(p))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
copy(w.c.writeBuf[w.c.writePos:], p[:n])
|
||||
w.c.writePos += n
|
||||
p = p[n:]
|
||||
}
|
||||
return nn, nil
|
||||
}
|
||||
|
||||
func (w messageWriter) Write(p []byte) (int, error) {
|
||||
return w.write(false, p)
|
||||
}
|
||||
|
||||
func (w messageWriter) WriteString(p string) (int, error) {
|
||||
if err := w.err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
nn := len(p)
|
||||
for len(p) > 0 {
|
||||
n, err := w.ncopy(len(p))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
copy(w.c.writeBuf[w.c.writePos:], p[:n])
|
||||
w.c.writePos += n
|
||||
p = p[n:]
|
||||
}
|
||||
return nn, nil
|
||||
}
|
||||
|
||||
func (w messageWriter) ReadFrom(r io.Reader) (nn int64, err error) {
|
||||
if err := w.err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for {
|
||||
if w.c.writePos == len(w.c.writeBuf) {
|
||||
err = w.c.flushFrame(false, nil)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
var n int
|
||||
n, err = r.Read(w.c.writeBuf[w.c.writePos:])
|
||||
w.c.writePos += n
|
||||
nn += int64(n)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return nn, err
|
||||
}
|
||||
|
||||
func (w messageWriter) Close() error {
|
||||
if err := w.err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.c.flushFrame(true, nil)
|
||||
}
|
||||
|
||||
// WriteMessage is a helper method for getting a writer using NextWriter,
|
||||
// writing the message and closing the writer.
|
||||
func (c *Conn) WriteMessage(messageType int, data []byte) error {
|
||||
wr, err := c.NextWriter(messageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w := wr.(messageWriter)
|
||||
if _, err := w.write(true, data); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.writeSeq == w.seq {
|
||||
if err := c.flushFrame(true, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetWriteDeadline sets the write deadline on the underlying network
|
||||
// connection. After a write has timed out, the websocket state is corrupt and
|
||||
// all future writes will return an error. A zero value for t means writes will
|
||||
// not time out.
|
||||
func (c *Conn) SetWriteDeadline(t time.Time) error {
|
||||
c.writeDeadline = t
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read methods
|
||||
|
||||
// readFull is like io.ReadFull except that io.EOF is never returned.
|
||||
func (c *Conn) readFull(p []byte) (err error) {
|
||||
var n int
|
||||
for n < len(p) && err == nil {
|
||||
var nn int
|
||||
nn, err = c.br.Read(p[n:])
|
||||
n += nn
|
||||
}
|
||||
if n == len(p) {
|
||||
err = nil
|
||||
} else if err == io.EOF {
|
||||
err = errUnexpectedEOF
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) advanceFrame() (int, error) {
|
||||
|
||||
// 1. Skip remainder of previous frame.
|
||||
|
||||
if c.readRemaining > 0 {
|
||||
if _, err := io.CopyN(ioutil.Discard, c.br, c.readRemaining); err != nil {
|
||||
return noFrame, err
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Read and parse first two bytes of frame header.
|
||||
|
||||
var b [8]byte
|
||||
if err := c.readFull(b[:2]); err != nil {
|
||||
return noFrame, err
|
||||
}
|
||||
|
||||
final := b[0]&finalBit != 0
|
||||
frameType := int(b[0] & 0xf)
|
||||
reserved := int((b[0] >> 4) & 0x7)
|
||||
mask := b[1]&maskBit != 0
|
||||
c.readRemaining = int64(b[1] & 0x7f)
|
||||
|
||||
if reserved != 0 {
|
||||
return noFrame, c.handleProtocolError("unexpected reserved bits " + strconv.Itoa(reserved))
|
||||
}
|
||||
|
||||
switch frameType {
|
||||
case CloseMessage, PingMessage, PongMessage:
|
||||
if c.readRemaining > maxControlFramePayloadSize {
|
||||
return noFrame, c.handleProtocolError("control frame length > 125")
|
||||
}
|
||||
if !final {
|
||||
return noFrame, c.handleProtocolError("control frame not final")
|
||||
}
|
||||
case TextMessage, BinaryMessage:
|
||||
if !c.readFinal {
|
||||
return noFrame, c.handleProtocolError("message start before final message frame")
|
||||
}
|
||||
c.readFinal = final
|
||||
case continuationFrame:
|
||||
if c.readFinal {
|
||||
return noFrame, c.handleProtocolError("continuation after final message frame")
|
||||
}
|
||||
c.readFinal = final
|
||||
default:
|
||||
return noFrame, c.handleProtocolError("unknown opcode " + strconv.Itoa(frameType))
|
||||
}
|
||||
|
||||
// 3. Read and parse frame length.
|
||||
|
||||
switch c.readRemaining {
|
||||
case 126:
|
||||
if err := c.readFull(b[:2]); err != nil {
|
||||
return noFrame, err
|
||||
}
|
||||
c.readRemaining = int64(binary.BigEndian.Uint16(b[:2]))
|
||||
case 127:
|
||||
if err := c.readFull(b[:8]); err != nil {
|
||||
return noFrame, err
|
||||
}
|
||||
c.readRemaining = int64(binary.BigEndian.Uint64(b[:8]))
|
||||
}
|
||||
|
||||
// 4. Handle frame masking.
|
||||
|
||||
if mask != c.isServer {
|
||||
return noFrame, c.handleProtocolError("incorrect mask flag")
|
||||
}
|
||||
|
||||
if mask {
|
||||
c.readMaskPos = 0
|
||||
if err := c.readFull(c.readMaskKey[:]); err != nil {
|
||||
return noFrame, err
|
||||
}
|
||||
}
|
||||
|
||||
// 5. For text and binary messages, enforce read limit and return.
|
||||
|
||||
if frameType == continuationFrame || frameType == TextMessage || frameType == BinaryMessage {
|
||||
|
||||
c.readLength += c.readRemaining
|
||||
if c.readLimit > 0 && c.readLength > c.readLimit {
|
||||
c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait))
|
||||
return noFrame, ErrReadLimit
|
||||
}
|
||||
|
||||
return frameType, nil
|
||||
}
|
||||
|
||||
// 6. Read control frame payload.
|
||||
|
||||
var payload []byte
|
||||
if c.readRemaining > 0 {
|
||||
payload = make([]byte, c.readRemaining)
|
||||
c.readRemaining = 0
|
||||
if err := c.readFull(payload); err != nil {
|
||||
return noFrame, err
|
||||
}
|
||||
if c.isServer {
|
||||
maskBytes(c.readMaskKey, 0, payload)
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Process control frame payload.
|
||||
|
||||
switch frameType {
|
||||
case PongMessage:
|
||||
if err := c.handlePong(string(payload)); err != nil {
|
||||
return noFrame, err
|
||||
}
|
||||
case PingMessage:
|
||||
if err := c.handlePing(string(payload)); err != nil {
|
||||
return noFrame, err
|
||||
}
|
||||
case CloseMessage:
|
||||
echoMessage := []byte{}
|
||||
closeCode := CloseNoStatusReceived
|
||||
closeText := ""
|
||||
if len(payload) >= 2 {
|
||||
echoMessage = payload[:2]
|
||||
closeCode = int(binary.BigEndian.Uint16(payload))
|
||||
closeText = string(payload[2:])
|
||||
}
|
||||
c.WriteControl(CloseMessage, echoMessage, time.Now().Add(writeWait))
|
||||
return noFrame, &CloseError{Code: closeCode, Text: closeText}
|
||||
}
|
||||
|
||||
return frameType, nil
|
||||
}
|
||||
|
||||
func (c *Conn) handleProtocolError(message string) error {
|
||||
c.WriteControl(CloseMessage, FormatCloseMessage(CloseProtocolError, message), time.Now().Add(writeWait))
|
||||
return errors.New("websocket: " + message)
|
||||
}
|
||||
|
||||
// NextReader returns the next data message received from the peer. The
|
||||
// returned messageType is either TextMessage or BinaryMessage.
|
||||
//
|
||||
// There can be at most one open reader on a connection. NextReader discards
|
||||
// the previous message if the application has not already consumed it.
|
||||
//
|
||||
// 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++
|
||||
c.readLength = 0
|
||||
|
||||
for c.readErr == nil {
|
||||
frameType, err := c.advanceFrame()
|
||||
if err != nil {
|
||||
c.readErr = hideTempErr(err)
|
||||
break
|
||||
}
|
||||
if frameType == TextMessage || frameType == BinaryMessage {
|
||||
return frameType, messageReader{c, c.readSeq}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
type messageReader struct {
|
||||
c *Conn
|
||||
seq int
|
||||
}
|
||||
|
||||
func (r messageReader) Read(b []byte) (int, error) {
|
||||
|
||||
if r.seq != r.c.readSeq {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
for r.c.readErr == nil {
|
||||
|
||||
if r.c.readRemaining > 0 {
|
||||
if int64(len(b)) > r.c.readRemaining {
|
||||
b = b[:r.c.readRemaining]
|
||||
}
|
||||
n, err := r.c.br.Read(b)
|
||||
r.c.readErr = hideTempErr(err)
|
||||
if r.c.isServer {
|
||||
r.c.readMaskPos = maskBytes(r.c.readMaskKey, r.c.readMaskPos, b[:n])
|
||||
}
|
||||
r.c.readRemaining -= int64(n)
|
||||
return n, r.c.readErr
|
||||
}
|
||||
|
||||
if r.c.readFinal {
|
||||
r.c.readSeq++
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
frameType, err := r.c.advanceFrame()
|
||||
switch {
|
||||
case err != nil:
|
||||
r.c.readErr = hideTempErr(err)
|
||||
case frameType == TextMessage || frameType == BinaryMessage:
|
||||
r.c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader")
|
||||
}
|
||||
}
|
||||
|
||||
err := r.c.readErr
|
||||
if err == io.EOF && r.seq == r.c.readSeq {
|
||||
err = errUnexpectedEOF
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// ReadMessage is a helper method for getting a reader using NextReader and
|
||||
// reading from that reader to a buffer.
|
||||
func (c *Conn) ReadMessage() (messageType int, p []byte, err error) {
|
||||
var r io.Reader
|
||||
messageType, r, err = c.NextReader()
|
||||
if err != nil {
|
||||
return messageType, nil, err
|
||||
}
|
||||
p, err = ioutil.ReadAll(r)
|
||||
return messageType, p, err
|
||||
}
|
||||
|
||||
// SetReadDeadline sets the read deadline on the underlying network connection.
|
||||
// After a read has timed out, the websocket connection state is corrupt and
|
||||
// all future reads will return an error. A zero value for t means reads will
|
||||
// not time out.
|
||||
func (c *Conn) SetReadDeadline(t time.Time) error {
|
||||
return c.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
// SetReadLimit sets the maximum size for a message read from the peer. If a
|
||||
// message exceeds the limit, the connection sends a close frame to the peer
|
||||
// and returns ErrReadLimit to the application.
|
||||
func (c *Conn) SetReadLimit(limit int64) {
|
||||
c.readLimit = limit
|
||||
}
|
||||
|
||||
// SetPingHandler sets the handler for ping messages received from the peer.
|
||||
// The 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 {
|
||||
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 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 }
|
||||
}
|
||||
c.handlePong = h
|
||||
}
|
||||
|
||||
// UnderlyingConn returns the internal net.Conn. This can be used to further
|
||||
// modifications to connection specific flags.
|
||||
func (c *Conn) UnderlyingConn() net.Conn {
|
||||
return c.conn
|
||||
}
|
||||
|
||||
// FormatCloseMessage formats closeCode and text as a WebSocket close message.
|
||||
func FormatCloseMessage(closeCode int, text string) []byte {
|
||||
buf := make([]byte, 2+len(text))
|
||||
binary.BigEndian.PutUint16(buf, uint16(closeCode))
|
||||
copy(buf[2:], text)
|
||||
return buf
|
||||
}
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
"testing/iotest"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ net.Error = errWriteTimeout
|
||||
|
||||
type fakeNetConn struct {
|
||||
io.Reader
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (c fakeNetConn) Close() error { return nil }
|
||||
func (c fakeNetConn) LocalAddr() net.Addr { return nil }
|
||||
func (c fakeNetConn) RemoteAddr() net.Addr { return nil }
|
||||
func (c fakeNetConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (c fakeNetConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (c fakeNetConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
func TestFraming(t *testing.T) {
|
||||
frameSizes := []int{0, 1, 2, 124, 125, 126, 127, 128, 129, 65534, 65535, 65536, 65537}
|
||||
var readChunkers = []struct {
|
||||
name string
|
||||
f func(io.Reader) io.Reader
|
||||
}{
|
||||
{"half", iotest.HalfReader},
|
||||
{"one", iotest.OneByteReader},
|
||||
{"asis", func(r io.Reader) io.Reader { return r }},
|
||||
}
|
||||
|
||||
writeBuf := make([]byte, 65537)
|
||||
for i := range writeBuf {
|
||||
writeBuf[i] = byte(i)
|
||||
}
|
||||
|
||||
for _, isServer := range []bool{true, false} {
|
||||
for _, chunker := range readChunkers {
|
||||
|
||||
var connBuf bytes.Buffer
|
||||
wc := newConn(fakeNetConn{Reader: nil, Writer: &connBuf}, isServer, 1024, 1024)
|
||||
rc := newConn(fakeNetConn{Reader: chunker.f(&connBuf), Writer: nil}, !isServer, 1024, 1024)
|
||||
|
||||
for _, n := range frameSizes {
|
||||
for _, iocopy := range []bool{true, false} {
|
||||
name := fmt.Sprintf("s:%v, r:%s, n:%d c:%v", isServer, chunker.name, n, iocopy)
|
||||
|
||||
w, err := wc.NextWriter(TextMessage)
|
||||
if err != nil {
|
||||
t.Errorf("%s: wc.NextWriter() returned %v", name, err)
|
||||
continue
|
||||
}
|
||||
var nn int
|
||||
if iocopy {
|
||||
var n64 int64
|
||||
n64, err = io.Copy(w, bytes.NewReader(writeBuf[:n]))
|
||||
nn = int(n64)
|
||||
} else {
|
||||
nn, err = w.Write(writeBuf[:n])
|
||||
}
|
||||
if err != nil || nn != n {
|
||||
t.Errorf("%s: w.Write(writeBuf[:n]) returned %d, %v", name, nn, err)
|
||||
continue
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
t.Errorf("%s: w.Close() returned %v", name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
opCode, r, err := rc.NextReader()
|
||||
if err != nil || opCode != TextMessage {
|
||||
t.Errorf("%s: NextReader() returned %d, r, %v", name, opCode, err)
|
||||
continue
|
||||
}
|
||||
rbuf, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Errorf("%s: ReadFull() returned rbuf, %v", name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(rbuf) != n {
|
||||
t.Errorf("%s: len(rbuf) is %d, want %d", name, len(rbuf), n)
|
||||
continue
|
||||
}
|
||||
|
||||
for i, b := range rbuf {
|
||||
if byte(i) != b {
|
||||
t.Errorf("%s: bad byte at offset %d", name, i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestControl(t *testing.T) {
|
||||
const message = "this is a ping/pong messsage"
|
||||
for _, isServer := range []bool{true, false} {
|
||||
for _, isWriteControl := range []bool{true, false} {
|
||||
name := fmt.Sprintf("s:%v, wc:%v", isServer, isWriteControl)
|
||||
var connBuf bytes.Buffer
|
||||
wc := newConn(fakeNetConn{Reader: nil, Writer: &connBuf}, isServer, 1024, 1024)
|
||||
rc := newConn(fakeNetConn{Reader: &connBuf, Writer: nil}, !isServer, 1024, 1024)
|
||||
if isWriteControl {
|
||||
wc.WriteControl(PongMessage, []byte(message), time.Now().Add(time.Second))
|
||||
} else {
|
||||
w, err := wc.NextWriter(PongMessage)
|
||||
if err != nil {
|
||||
t.Errorf("%s: wc.NextWriter() returned %v", name, err)
|
||||
continue
|
||||
}
|
||||
if _, err := w.Write([]byte(message)); err != nil {
|
||||
t.Errorf("%s: w.Write() returned %v", name, err)
|
||||
continue
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
t.Errorf("%s: w.Close() returned %v", name, err)
|
||||
continue
|
||||
}
|
||||
var actualMessage string
|
||||
rc.SetPongHandler(func(s string) error { actualMessage = s; return nil })
|
||||
rc.NextReader()
|
||||
if actualMessage != message {
|
||||
t.Errorf("%s: pong=%q, want %q", name, actualMessage, message)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloseBeforeFinalFrame(t *testing.T) {
|
||||
const bufSize = 512
|
||||
|
||||
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(expectedErr.Code, expectedErr.Text), time.Now().Add(10*time.Second))
|
||||
w.Close()
|
||||
|
||||
op, r, err := rc.NextReader()
|
||||
if op != BinaryMessage || err != nil {
|
||||
t.Fatalf("NextReader() returned %d, %v", op, err)
|
||||
}
|
||||
_, err = io.Copy(ioutil.Discard, r)
|
||||
if !reflect.DeepEqual(err, expectedErr) {
|
||||
t.Fatalf("io.Copy() returned %v, want %v", err, expectedErr)
|
||||
}
|
||||
_, _, err = rc.NextReader()
|
||||
if !reflect.DeepEqual(err, expectedErr) {
|
||||
t.Fatalf("NextReader() returned %v, want %v", err, expectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEOFBeforeFinalFrame(t *testing.T) {
|
||||
const bufSize = 512
|
||||
|
||||
var b1, b2 bytes.Buffer
|
||||
wc := newConn(fakeNetConn{Reader: nil, Writer: &b1}, false, 1024, bufSize)
|
||||
rc := newConn(fakeNetConn{Reader: &b1, Writer: &b2}, true, 1024, 1024)
|
||||
|
||||
w, _ := wc.NextWriter(BinaryMessage)
|
||||
w.Write(make([]byte, bufSize+bufSize/2))
|
||||
|
||||
op, r, err := rc.NextReader()
|
||||
if op != BinaryMessage || err != nil {
|
||||
t.Fatalf("NextReader() returned %d, %v", op, err)
|
||||
}
|
||||
_, err = io.Copy(ioutil.Discard, r)
|
||||
if err != errUnexpectedEOF {
|
||||
t.Fatalf("io.Copy() returned %v, want %v", err, errUnexpectedEOF)
|
||||
}
|
||||
_, _, err = rc.NextReader()
|
||||
if err != errUnexpectedEOF {
|
||||
t.Fatalf("NextReader() returned %v, want %v", err, errUnexpectedEOF)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadLimit(t *testing.T) {
|
||||
|
||||
const readLimit = 512
|
||||
message := make([]byte, readLimit+1)
|
||||
|
||||
var b1, b2 bytes.Buffer
|
||||
wc := newConn(fakeNetConn{Reader: nil, Writer: &b1}, false, 1024, readLimit-2)
|
||||
rc := newConn(fakeNetConn{Reader: &b1, Writer: &b2}, true, 1024, 1024)
|
||||
rc.SetReadLimit(readLimit)
|
||||
|
||||
// Send message at the limit with interleaved pong.
|
||||
w, _ := wc.NextWriter(BinaryMessage)
|
||||
w.Write(message[:readLimit-1])
|
||||
wc.WriteControl(PongMessage, []byte("this is a pong"), time.Now().Add(10*time.Second))
|
||||
w.Write(message[:1])
|
||||
w.Close()
|
||||
|
||||
// Send message larger than the limit.
|
||||
wc.WriteMessage(BinaryMessage, message[:readLimit+1])
|
||||
|
||||
op, _, err := rc.NextReader()
|
||||
if op != BinaryMessage || err != nil {
|
||||
t.Fatalf("1: NextReader() returned %d, %v", op, err)
|
||||
}
|
||||
op, r, err := rc.NextReader()
|
||||
if op != BinaryMessage || err != nil {
|
||||
t.Fatalf("2: NextReader() returned %d, %v", op, err)
|
||||
}
|
||||
_, err = io.Copy(ioutil.Discard, r)
|
||||
if err != ErrReadLimit {
|
||||
t.Fatalf("io.Copy() returned %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnderlyingConn(t *testing.T) {
|
||||
var b1, b2 bytes.Buffer
|
||||
fc := fakeNetConn{Reader: &b1, Writer: &b2}
|
||||
c := newConn(fc, true, 1024, 1024)
|
||||
ul := c.UnderlyingConn()
|
||||
if ul != fc {
|
||||
t.Fatalf("Underlying conn is not what it should be.")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package websocket implements the WebSocket protocol defined in RFC 6455.
|
||||
//
|
||||
// Overview
|
||||
//
|
||||
// The Conn type represents a WebSocket connection. A server application uses
|
||||
// the Upgrade function from an Upgrader object with a HTTP request handler
|
||||
// to get a pointer to a Conn:
|
||||
//
|
||||
// var upgrader = websocket.Upgrader{
|
||||
// ReadBufferSize: 1024,
|
||||
// WriteBufferSize: 1024,
|
||||
// }
|
||||
//
|
||||
// func handler(w http.ResponseWriter, r *http.Request) {
|
||||
// conn, err := upgrader.Upgrade(w, r, nil)
|
||||
// if err != nil {
|
||||
// log.Println(err)
|
||||
// return
|
||||
// }
|
||||
// ... Use conn to send and receive messages.
|
||||
// }
|
||||
//
|
||||
// Call the connection's WriteMessage and ReadMessage methods to send and
|
||||
// receive messages as a slice of bytes. This snippet of code shows how to echo
|
||||
// messages using these methods:
|
||||
//
|
||||
// for {
|
||||
// messageType, p, err := conn.ReadMessage()
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// if err = conn.WriteMessage(messageType, p); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// In above snippet of code, p is a []byte and messageType is an int with value
|
||||
// websocket.BinaryMessage or websocket.TextMessage.
|
||||
//
|
||||
// An application can also send and receive messages using the io.WriteCloser
|
||||
// and io.Reader interfaces. To send a message, call the connection NextWriter
|
||||
// method to get an io.WriteCloser, write the message to the writer and close
|
||||
// the writer when done. To receive a message, call the connection NextReader
|
||||
// method to get an io.Reader and read until io.EOF is returned. This snippet
|
||||
// shows how to echo messages using the NextWriter and NextReader methods:
|
||||
//
|
||||
// for {
|
||||
// messageType, r, err := conn.NextReader()
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// w, err := conn.NextWriter(messageType)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if _, err := io.Copy(w, r); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err := w.Close(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Data Messages
|
||||
//
|
||||
// The WebSocket protocol distinguishes between text and binary data messages.
|
||||
// Text messages are interpreted as UTF-8 encoded text. The interpretation of
|
||||
// binary messages is left to the application.
|
||||
//
|
||||
// This package uses the TextMessage and BinaryMessage integer constants to
|
||||
// identify the two data message types. The ReadMessage and NextReader methods
|
||||
// return the type of the received message. The messageType argument to the
|
||||
// WriteMessage and NextWriter methods specifies the type of a sent message.
|
||||
//
|
||||
// It is the application's responsibility to ensure that text messages are
|
||||
// valid UTF-8 encoded text.
|
||||
//
|
||||
// Control Messages
|
||||
//
|
||||
// The WebSocket protocol defines three types of control messages: close, ping
|
||||
// and pong. Call the connection WriteControl, WriteMessage or NextWriter
|
||||
// methods to send a control message to the peer.
|
||||
//
|
||||
// Connections handle received ping and pong messages by invoking 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 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:
|
||||
//
|
||||
// func readLoop(c *websocket.Conn) {
|
||||
// for {
|
||||
// if _, _, err := c.NextReader(); err != nil {
|
||||
// c.Close()
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 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
|
||||
// any host. It's up to the server to enforce an origin policy using the Origin
|
||||
// request header sent by the browser.
|
||||
//
|
||||
// The Upgrader calls the function specified in the CheckOrigin field to check
|
||||
// the origin. If the CheckOrigin function returns false, then the Upgrade
|
||||
// method fails the WebSocket handshake with HTTP status 403.
|
||||
//
|
||||
// If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail
|
||||
// the handshake if the Origin request header is present and not equal to the
|
||||
// Host request header.
|
||||
//
|
||||
// An application can allow connections from any origin by specifying a
|
||||
// function that always returns true:
|
||||
//
|
||||
// var upgrader = websocket.Upgrader{
|
||||
// CheckOrigin: func(r *http.Request) bool { return true },
|
||||
// }
|
||||
//
|
||||
// The deprecated Upgrade function does not enforce an origin policy. It's the
|
||||
// application's responsibility to check the Origin header before calling
|
||||
// Upgrade.
|
||||
package websocket
|
||||
+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) {}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Test Server
|
||||
|
||||
This package contains a server for the [Autobahn WebSockets Test Suite](http://autobahn.ws/testsuite).
|
||||
|
||||
To test the server, run
|
||||
|
||||
go run server.go
|
||||
|
||||
and start the client test driver
|
||||
|
||||
wstest -m fuzzingclient -s fuzzingclient.json
|
||||
|
||||
When the client completes, it writes a report to reports/clients/index.html.
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
|
||||
{
|
||||
"options": {"failByDrop": false},
|
||||
"outdir": "./reports/clients",
|
||||
"servers": [
|
||||
{"agent": "ReadAllWriteMessage", "url": "ws://localhost:9000/m", "options": {"version": 18}},
|
||||
{"agent": "ReadAllWrite", "url": "ws://localhost:9000/r", "options": {"version": 18}},
|
||||
{"agent": "CopyFull", "url": "ws://localhost:9000/f", "options": {"version": 18}},
|
||||
{"agent": "CopyWriterOnly", "url": "ws://localhost:9000/c", "options": {"version": 18}}
|
||||
],
|
||||
"cases": ["*"],
|
||||
"exclude-cases": [],
|
||||
"exclude-agent-cases": {}
|
||||
}
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Command server is a test server for the Autobahn WebSockets Test Suite.
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"github.com/gorilla/websocket"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// echoCopy echoes messages from the client using io.Copy.
|
||||
func echoCopy(w http.ResponseWriter, r *http.Request, writerOnly bool) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println("Upgrade:", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
for {
|
||||
mt, r, err := conn.NextReader()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Println("NextReader:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if mt == websocket.TextMessage {
|
||||
r = &validator{r: r}
|
||||
}
|
||||
w, err := conn.NextWriter(mt)
|
||||
if err != nil {
|
||||
log.Println("NextWriter:", err)
|
||||
return
|
||||
}
|
||||
if mt == websocket.TextMessage {
|
||||
r = &validator{r: r}
|
||||
}
|
||||
if writerOnly {
|
||||
_, err = io.Copy(struct{ io.Writer }{w}, r)
|
||||
} else {
|
||||
_, err = io.Copy(w, r)
|
||||
}
|
||||
if err != nil {
|
||||
if err == errInvalidUTF8 {
|
||||
conn.WriteControl(websocket.CloseMessage,
|
||||
websocket.FormatCloseMessage(websocket.CloseInvalidFramePayloadData, ""),
|
||||
time.Time{})
|
||||
}
|
||||
log.Println("Copy:", err)
|
||||
return
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
log.Println("Close:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func echoCopyWriterOnly(w http.ResponseWriter, r *http.Request) {
|
||||
echoCopy(w, r, true)
|
||||
}
|
||||
|
||||
func echoCopyFull(w http.ResponseWriter, r *http.Request) {
|
||||
echoCopy(w, r, false)
|
||||
}
|
||||
|
||||
// echoReadAll echoes messages from the client by reading the entire message
|
||||
// with ioutil.ReadAll.
|
||||
func echoReadAll(w http.ResponseWriter, r *http.Request, writeMessage bool) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println("Upgrade:", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
for {
|
||||
mt, b, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Println("NextReader:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if mt == websocket.TextMessage {
|
||||
if !utf8.Valid(b) {
|
||||
conn.WriteControl(websocket.CloseMessage,
|
||||
websocket.FormatCloseMessage(websocket.CloseInvalidFramePayloadData, ""),
|
||||
time.Time{})
|
||||
log.Println("ReadAll: invalid utf8")
|
||||
}
|
||||
}
|
||||
if writeMessage {
|
||||
err = conn.WriteMessage(mt, b)
|
||||
if err != nil {
|
||||
log.Println("WriteMessage:", err)
|
||||
}
|
||||
} else {
|
||||
w, err := conn.NextWriter(mt)
|
||||
if err != nil {
|
||||
log.Println("NextWriter:", err)
|
||||
return
|
||||
}
|
||||
if _, err := w.Write(b); err != nil {
|
||||
log.Println("Writer:", err)
|
||||
return
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
log.Println("Close:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func echoReadAllWriter(w http.ResponseWriter, r *http.Request) {
|
||||
echoReadAll(w, r, false)
|
||||
}
|
||||
|
||||
func echoReadAllWriteMessage(w http.ResponseWriter, r *http.Request) {
|
||||
echoReadAll(w, r, true)
|
||||
}
|
||||
|
||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.Error(w, "Not found.", 404)
|
||||
return
|
||||
}
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "Method not allowed", 405)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
io.WriteString(w, "<html><body>Echo Server</body></html>")
|
||||
}
|
||||
|
||||
var addr = flag.String("addr", ":9000", "http service address")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
http.HandleFunc("/", serveHome)
|
||||
http.HandleFunc("/c", echoCopyWriterOnly)
|
||||
http.HandleFunc("/f", echoCopyFull)
|
||||
http.HandleFunc("/r", echoReadAllWriter)
|
||||
http.HandleFunc("/m", echoReadAllWriteMessage)
|
||||
err := http.ListenAndServe(*addr, nil)
|
||||
if err != nil {
|
||||
log.Fatal("ListenAndServe: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
type validator struct {
|
||||
state int
|
||||
x rune
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
var errInvalidUTF8 = errors.New("invalid utf8")
|
||||
|
||||
func (r *validator) Read(p []byte) (int, error) {
|
||||
n, err := r.r.Read(p)
|
||||
state := r.state
|
||||
x := r.x
|
||||
for _, b := range p[:n] {
|
||||
state, x = decode(state, x, b)
|
||||
if state == utf8Reject {
|
||||
break
|
||||
}
|
||||
}
|
||||
r.state = state
|
||||
r.x = x
|
||||
if state == utf8Reject || (err == io.EOF && state != utf8Accept) {
|
||||
return n, errInvalidUTF8
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// UTF-8 decoder from http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
|
||||
//
|
||||
// Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
var utf8d = [...]byte{
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00..1f
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20..3f
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40..5f
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60..7f
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // 80..9f
|
||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // a0..bf
|
||||
8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // c0..df
|
||||
0xa, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, // e0..ef
|
||||
0xb, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, // f0..ff
|
||||
0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, // s0..s0
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, // s1..s2
|
||||
1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, // s3..s4
|
||||
1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, // s5..s6
|
||||
1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // s7..s8
|
||||
}
|
||||
|
||||
const (
|
||||
utf8Accept = 0
|
||||
utf8Reject = 1
|
||||
)
|
||||
|
||||
func decode(state int, x rune, b byte) (int, rune) {
|
||||
t := utf8d[b]
|
||||
if state != utf8Accept {
|
||||
x = rune(b&0x3f) | (x << 6)
|
||||
} else {
|
||||
x = rune((0xff >> t) & b)
|
||||
}
|
||||
state = int(utf8d[256+state*16+int(t)])
|
||||
return state, x
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# Chat Example
|
||||
|
||||
This application shows how to use use the
|
||||
[websocket](https://github.com/gorilla/websocket) package and
|
||||
[jQuery](http://jquery.com) to implement a simple web chat application.
|
||||
|
||||
## Running the example
|
||||
|
||||
The example requires a working Go development environment. The [Getting
|
||||
Started](http://golang.org/doc/install) page describes how to install the
|
||||
development environment.
|
||||
|
||||
Once you have Go up and running, you can download, build and run the example
|
||||
using the following commands.
|
||||
|
||||
$ go get github.com/gorilla/websocket
|
||||
$ cd `go list -f '{{.Dir}}' github.com/gorilla/websocket/examples/chat`
|
||||
$ go run *.go
|
||||
|
||||
To use the chat example, open http://localhost:8080/ in your browser.
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gorilla/websocket"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// Time allowed to write a message to the peer.
|
||||
writeWait = 10 * time.Second
|
||||
|
||||
// Time allowed to read the next pong message from the peer.
|
||||
pongWait = 60 * time.Second
|
||||
|
||||
// Send pings to peer with this period. Must be less than pongWait.
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
|
||||
// Maximum message size allowed from peer.
|
||||
maxMessageSize = 512
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
// connection is an middleman between the websocket connection and the hub.
|
||||
type connection struct {
|
||||
// The websocket connection.
|
||||
ws *websocket.Conn
|
||||
|
||||
// Buffered channel of outbound messages.
|
||||
send chan []byte
|
||||
}
|
||||
|
||||
// readPump pumps messages from the websocket connection to the hub.
|
||||
func (c *connection) readPump() {
|
||||
defer func() {
|
||||
h.unregister <- c
|
||||
c.ws.Close()
|
||||
}()
|
||||
c.ws.SetReadLimit(maxMessageSize)
|
||||
c.ws.SetReadDeadline(time.Now().Add(pongWait))
|
||||
c.ws.SetPongHandler(func(string) error { c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
||||
for {
|
||||
_, message, err := c.ws.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
|
||||
log.Printf("error: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
h.broadcast <- message
|
||||
}
|
||||
}
|
||||
|
||||
// write writes a message with the given message type and payload.
|
||||
func (c *connection) write(mt int, payload []byte) error {
|
||||
c.ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
return c.ws.WriteMessage(mt, payload)
|
||||
}
|
||||
|
||||
// writePump pumps messages from the hub to the websocket connection.
|
||||
func (c *connection) writePump() {
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
c.ws.Close()
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case message, ok := <-c.send:
|
||||
if !ok {
|
||||
c.write(websocket.CloseMessage, []byte{})
|
||||
return
|
||||
}
|
||||
if err := c.write(websocket.TextMessage, message); err != nil {
|
||||
return
|
||||
}
|
||||
case <-ticker.C:
|
||||
if err := c.write(websocket.PingMessage, []byte{}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// serveWs handles websocket requests from the peer.
|
||||
func serveWs(w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
c := &connection{send: make(chan []byte, 256), ws: ws}
|
||||
h.register <- c
|
||||
go c.writePump()
|
||||
c.readPump()
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Chat Example</title>
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
|
||||
var conn;
|
||||
var msg = $("#msg");
|
||||
var log = $("#log");
|
||||
|
||||
function appendLog(msg) {
|
||||
var d = log[0]
|
||||
var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight;
|
||||
msg.appendTo(log)
|
||||
if (doScroll) {
|
||||
d.scrollTop = d.scrollHeight - d.clientHeight;
|
||||
}
|
||||
}
|
||||
|
||||
$("#form").submit(function() {
|
||||
if (!conn) {
|
||||
return false;
|
||||
}
|
||||
if (!msg.val()) {
|
||||
return false;
|
||||
}
|
||||
conn.send(msg.val());
|
||||
msg.val("");
|
||||
return false
|
||||
});
|
||||
|
||||
if (window["WebSocket"]) {
|
||||
conn = new WebSocket("ws://{{$}}/ws");
|
||||
conn.onclose = function(evt) {
|
||||
appendLog($("<div><b>Connection closed.</b></div>"))
|
||||
}
|
||||
conn.onmessage = function(evt) {
|
||||
appendLog($("<div/>").text(evt.data))
|
||||
}
|
||||
} else {
|
||||
appendLog($("<div><b>Your browser does not support WebSockets.</b></div>"))
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style type="text/css">
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: gray;
|
||||
}
|
||||
|
||||
#log {
|
||||
background: white;
|
||||
margin: 0;
|
||||
padding: 0.5em 0.5em 0.5em 0.5em;
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
left: 0.5em;
|
||||
right: 0.5em;
|
||||
bottom: 3em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#form {
|
||||
padding: 0 0.5em 0 0.5em;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="log"></div>
|
||||
<form id="form">
|
||||
<input type="submit" value="Send" />
|
||||
<input type="text" id="msg" size="64"/>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
// hub maintains the set of active connections and broadcasts messages to the
|
||||
// connections.
|
||||
type hub struct {
|
||||
// Registered connections.
|
||||
connections map[*connection]bool
|
||||
|
||||
// Inbound messages from the connections.
|
||||
broadcast chan []byte
|
||||
|
||||
// Register requests from the connections.
|
||||
register chan *connection
|
||||
|
||||
// Unregister requests from connections.
|
||||
unregister chan *connection
|
||||
}
|
||||
|
||||
var h = hub{
|
||||
broadcast: make(chan []byte),
|
||||
register: make(chan *connection),
|
||||
unregister: make(chan *connection),
|
||||
connections: make(map[*connection]bool),
|
||||
}
|
||||
|
||||
func (h *hub) run() {
|
||||
for {
|
||||
select {
|
||||
case c := <-h.register:
|
||||
h.connections[c] = true
|
||||
case c := <-h.unregister:
|
||||
if _, ok := h.connections[c]; ok {
|
||||
delete(h.connections, c)
|
||||
close(c.send)
|
||||
}
|
||||
case m := <-h.broadcast:
|
||||
for c := range h.connections {
|
||||
select {
|
||||
case c.send <- m:
|
||||
default:
|
||||
close(c.send)
|
||||
delete(h.connections, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var addr = flag.String("addr", ":8080", "http service address")
|
||||
var homeTempl = template.Must(template.ParseFiles("home.html"))
|
||||
|
||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.Error(w, "Not found", 404)
|
||||
return
|
||||
}
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "Method not allowed", 405)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
homeTempl.Execute(w, r.Host)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
go h.run()
|
||||
http.HandleFunc("/", serveHome)
|
||||
http.HandleFunc("/ws", serveWs)
|
||||
err := http.ListenAndServe(*addr, nil)
|
||||
if err != nil {
|
||||
log.Fatal("ListenAndServe: ", err)
|
||||
}
|
||||
}
|
||||
+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>
|
||||
`))
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
# File Watch example.
|
||||
|
||||
This example sends a file to the browser client for display whenever the file is modified.
|
||||
|
||||
$ go get github.com/gorilla/websocket
|
||||
$ cd `go list -f '{{.Dir}}' github.com/gorilla/websocket/examples/filewatch`
|
||||
$ go run main.go <name of file to watch>
|
||||
# Open http://localhost:8080/ .
|
||||
# Modify the file to see it update in the browser.
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
// Time allowed to write the file to the client.
|
||||
writeWait = 10 * time.Second
|
||||
|
||||
// Time allowed to read the next pong message from the client.
|
||||
pongWait = 60 * time.Second
|
||||
|
||||
// Send pings to client with this period. Must be less than pongWait.
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
|
||||
// Poll file for changes with this period.
|
||||
filePeriod = 10 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
addr = flag.String("addr", ":8080", "http service address")
|
||||
homeTempl = template.Must(template.New("").Parse(homeHTML))
|
||||
filename string
|
||||
upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
)
|
||||
|
||||
func readFileIfModified(lastMod time.Time) ([]byte, time.Time, error) {
|
||||
fi, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return nil, lastMod, err
|
||||
}
|
||||
if !fi.ModTime().After(lastMod) {
|
||||
return nil, lastMod, nil
|
||||
}
|
||||
p, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fi.ModTime(), err
|
||||
}
|
||||
return p, fi.ModTime(), nil
|
||||
}
|
||||
|
||||
func reader(ws *websocket.Conn) {
|
||||
defer ws.Close()
|
||||
ws.SetReadLimit(512)
|
||||
ws.SetReadDeadline(time.Now().Add(pongWait))
|
||||
ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
||||
for {
|
||||
_, _, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writer(ws *websocket.Conn, lastMod time.Time) {
|
||||
lastError := ""
|
||||
pingTicker := time.NewTicker(pingPeriod)
|
||||
fileTicker := time.NewTicker(filePeriod)
|
||||
defer func() {
|
||||
pingTicker.Stop()
|
||||
fileTicker.Stop()
|
||||
ws.Close()
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-fileTicker.C:
|
||||
var p []byte
|
||||
var err error
|
||||
|
||||
p, lastMod, err = readFileIfModified(lastMod)
|
||||
|
||||
if err != nil {
|
||||
if s := err.Error(); s != lastError {
|
||||
lastError = s
|
||||
p = []byte(lastError)
|
||||
}
|
||||
} else {
|
||||
lastError = ""
|
||||
}
|
||||
|
||||
if p != nil {
|
||||
ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if err := ws.WriteMessage(websocket.TextMessage, p); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
case <-pingTicker.C:
|
||||
ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func serveWs(w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
if _, ok := err.(websocket.HandshakeError); !ok {
|
||||
log.Println(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var lastMod time.Time
|
||||
if n, err := strconv.ParseInt(r.FormValue("lastMod"), 16, 64); err != nil {
|
||||
lastMod = time.Unix(0, n)
|
||||
}
|
||||
|
||||
go writer(ws, lastMod)
|
||||
reader(ws)
|
||||
}
|
||||
|
||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.Error(w, "Not found", 404)
|
||||
return
|
||||
}
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "Method not allowed", 405)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
p, lastMod, err := readFileIfModified(time.Time{})
|
||||
if err != nil {
|
||||
p = []byte(err.Error())
|
||||
lastMod = time.Unix(0, 0)
|
||||
}
|
||||
var v = struct {
|
||||
Host string
|
||||
Data string
|
||||
LastMod string
|
||||
}{
|
||||
r.Host,
|
||||
string(p),
|
||||
strconv.FormatInt(lastMod.UnixNano(), 16),
|
||||
}
|
||||
homeTempl.Execute(w, &v)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if flag.NArg() != 1 {
|
||||
log.Fatal("filename not specified")
|
||||
}
|
||||
filename = flag.Args()[0]
|
||||
http.HandleFunc("/", serveHome)
|
||||
http.HandleFunc("/ws", serveWs)
|
||||
if err := http.ListenAndServe(*addr, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
const homeHTML = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>WebSocket Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<pre id="fileData">{{.Data}}</pre>
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
var data = document.getElementById("fileData");
|
||||
var conn = new WebSocket("ws://{{.Host}}/ws?lastMod={{.LastMod}}");
|
||||
conn.onclose = function(evt) {
|
||||
data.textContent = 'Connection closed';
|
||||
}
|
||||
conn.onmessage = function(evt) {
|
||||
console.log('file updated');
|
||||
data.textContent = evt.data;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
// WriteJSON is deprecated, use c.WriteJSON instead.
|
||||
func WriteJSON(c *Conn, v interface{}) error {
|
||||
return c.WriteJSON(v)
|
||||
}
|
||||
|
||||
// WriteJSON writes the JSON encoding of v to the connection.
|
||||
//
|
||||
// See the documentation for encoding/json Marshal for details about the
|
||||
// conversion of Go values to JSON.
|
||||
func (c *Conn) WriteJSON(v interface{}) error {
|
||||
w, err := c.NextWriter(TextMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err1 := json.NewEncoder(w).Encode(v)
|
||||
err2 := w.Close()
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
return err2
|
||||
}
|
||||
|
||||
// ReadJSON is deprecated, use c.ReadJSON instead.
|
||||
func ReadJSON(c *Conn, v interface{}) error {
|
||||
return c.ReadJSON(v)
|
||||
}
|
||||
|
||||
// ReadJSON reads the next JSON-encoded message from the connection and stores
|
||||
// it in the value pointed to by v.
|
||||
//
|
||||
// See the documentation for the encoding/json Unmarshal function for details
|
||||
// about the conversion of JSON to a Go value.
|
||||
func (c *Conn) ReadJSON(v interface{}) error {
|
||||
_, r, err := c.NextReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.NewDecoder(r).Decode(v)
|
||||
if err == io.EOF {
|
||||
// One value is expected in the message.
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return err
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user