Compare commits

..

24 Commits

Author SHA1 Message Date
b24ad687df tsconfig.json: commit 2017-11-12 12:57:51 +13:00
dd1c6b7968 bindata.go: rebuild 2017-11-12 12:57:42 +13:00
224263afad ts: fix this parameter when calling minifier-helper versions of document functions 2017-11-12 12:57:38 +13:00
d2dd96dcd3 ts: add minifier helpers, add a ton of function comments 2017-11-12 12:52:51 +13:00
48d58f4cc2 typescript: fix issue with el() function removal 2017-11-12 12:37:12 +13:00
19a665d61d typescript: initial package conversion 2017-11-12 12:21:52 +13:00
d5f6a5f2cb makefile: add separate all / dist targets 2017-11-12 11:09:52 +13:00
a17886e300 webpack: fix loading socket.io dependency inside webpack build 2017-11-12 11:09:44 +13:00
3af9423871 css: simplify nesting by employing some LESS features 2017-11-12 10:57:55 +13:00
5a6c798e93 package.json: re-sort dependency entries 2017-11-12 10:51:31 +13:00
02e8b407c3 reinstate css minification, rename *.css to *.less 2017-11-12 10:51:09 +13:00
e44298c172 hgignore 2017-11-12 10:39:21 +13:00
8df09ddb11 package.json: fill in metadata fields to suppress some warnings from npm 2017-11-12 10:39:17 +13:00
bd59534bac makefile: use explicit paths to binary dependencies in $GOPATH/bin 2017-11-12 10:39:05 +13:00
0679ad31a7 makefile: reinstate copying in remaining file assets 2017-11-12 10:38:47 +13:00
83ecd84d78 makefile: fix webpack path to bundle.min.js file 2017-11-12 10:38:35 +13:00
055a73fb05 makefile: use hg/git archive command to build source tarball 2017-11-12 10:36:42 +13:00
584a6b632f makefile: add clean-deps target 2017-11-12 10:35:46 +13:00
d90b08f45e node: commit package-lock.json 2017-11-12 10:31:25 +13:00
7934c926d9 Replace install deps script with a simple "npm i" 2017-11-10 09:53:14 +00:00
hanneshdc
8364e9a146 Add typescript and typescript loader 2017-11-10 21:03:58 +13:00
hanneshdc
ab89f5f93f Remove from code from makefile that is now handled by webpack 2017-11-10 20:45:31 +13:00
hanneshdc
29bcf75470 Install webpack and create a webpack config 2017-11-10 20:38:47 +13:00
hanneshdc
fcddbf85b0 Add package.json and gitignore 2017-11-10 19:56:24 +13:00
105 changed files with 5431 additions and 9091 deletions

8
.gitignore vendored
View File

@ -1,6 +1,2 @@
nmdc-webfrontend clientpack/**
nmdc-webfrontend.exe node_modules/**
nmdc-webfrontend.conf
clientpack/
_dist/
node_modules/

12
.hgignore Normal file
View File

@ -0,0 +1,12 @@
mode:regex
\.exe$
^nmdc-webfrontend\.conf$
^clientpack/
^node_modules/
^_dist/
^vendor/
^nmdc-webfrontend$
^nmdc-webfrontend\.exe$

11
.hgtags Normal file
View File

@ -0,0 +1,11 @@
769fad81e3f8db8f7e5f5c164656a382a169d735 v1.0.0
9ed95938d809a8226aca529e34b655e6d8c8c379 v1.0.1
46fe533682419c8a519836ac95b5575053aa0fa8 v1.0.2
a2c92b262f339f82eb01c8d92dda252a27432255 v1.1.0
d14041daa7bbbd37ea2ff47aa978b9595af67ca3 v1.1.1
7278eb0d067d8ed2a653de6a1feeeb7f76fb9891 v1.1.2
6cbd9d59630372c0dff430e3dc6c1fbd7dcee734 v1.1.3
76c178b8f27ec894e79b8f73649fcb3e45a73729 v1.1.4
0eeab5594ba4d683e6a268ef971675c73b226bd0 v1.2.0
c8cd84947e4516215e50639914adfaecf350fe91 v1.2.1
0c6b957de43252f89688ba73c4857f6d912b2912 v1.2.2

23
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,23 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "0.1.0",
"command": "npm",
"isShellCommand": true,
"showOutput": "always",
"suppressTaskName": true,
"tasks": [
{
"taskName": "install",
"args": ["install"]
},
{
"taskName": "update",
"args": ["update"]
},
{
"taskName": "test",
"args": ["run", "test"]
}
]
}

View File

@ -17,7 +17,7 @@ type Config struct {
Hub struct { Hub struct {
Address string `json:"address"` Address string `json:"address"`
Port int `json:"port,omitempty"` Port int `json:"port"`
Tag string `json:"tag"` Tag string `json:"tag"`
} }
} }

33
Gopkg.lock generated Normal file
View File

@ -0,0 +1,33 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "code.ivysaur.me/libnmdc"
packages = ["."]
revision = "21847db9fdc1b983285de44e789cd9e921bdf1bb"
version = "v0.14.0"
[[projects]]
branch = "master"
name = "github.com/googollee/go-engine.io"
packages = [".","message","parser","polling","transport","websocket"]
revision = "80ae0e43aca17b4c5a6834999d0f2eaa16b9afda"
[[projects]]
branch = "master"
name = "github.com/googollee/go-socket.io"
packages = ["."]
revision = "5447e71f36d394766bf855d5714a487596809f0d"
[[projects]]
name = "github.com/gorilla/websocket"
packages = ["."]
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
version = "v1.2.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "928468c40a3a664870459e1d5a1fa97fcbc45802fdd498ebf89bcffe0b44ec6e"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -21,6 +21,10 @@
# version = "2.4.0" # version = "2.4.0"
[[constraint]]
name = "code.ivysaur.me/libnmdc"
version = "0.14.0"
[[constraint]] [[constraint]]
branch = "master" branch = "master"
name = "github.com/cxmcc/tiger" name = "github.com/googollee/go-socket.io"

View File

@ -2,22 +2,24 @@
BINNAME=nmdc-webfrontend BINNAME=nmdc-webfrontend
VERSION=1.3.1 VERSION=1.2.3
GOFLAGS=-a \ GOFLAGS=-a \
-ldflags "-s -w -X main.VERSION=$(BINNAME)/$(VERSION)" \ -ldflags "-s -w -X main.VERSION=$(BINNAME)/$(VERSION)" \
-gcflags "-trimpath ${GOPATH}" \ -gcflags "-trimpath ${GOPATH}" \
-asmflags "-trimpath ${GOPATH}" -asmflags "-trimpath ${GOPATH}"
SOURCES=client/ go.mod go.sum Makefile Config.go main.go nmdc-webfrontend.conf.SAMPLE .PHONY: all dist deps clean clean-deps
.PHONY: all deps clean all: $(BINNAME) $(BINNAME).exe
all: $(BINNAME)-$(VERSION)-win32.7z $(BINNAME)-$(VERSION)-linux64.tar.xz $(BINNAME)-$(VERSION)-src.tar.xz dist: $(BINNAME)-$(VERSION)-win32.7z $(BINNAME)-$(VERSION)-linux64.tar.xz $(BINNAME)-$(VERSION)-src.zip
deps: deps:
npm install -g less uglify-js less-plugin-clean-css html-minifier npm i
go get -u github.com/jteeuwen/go-bindata/... go get -u github.com/jteeuwen/go-bindata/...
go get -u github.com/golang/dep/cmd/dep
$(GOPATH)/bin/dep ensure
clean: clean:
rm -f ./$(BINNAME) rm -f ./$(BINNAME)
@ -25,22 +27,21 @@ clean:
rm -fr ./clientpack rm -fr ./clientpack
rm -f ./bindata.go rm -f ./bindata.go
clean-deps:
rm -fr ./vendor
rm -fr ./node_modules
bindata.go: client client/* bindata.go: client client/*
rm -fr ./clientpack mkdir -p clientpack
cp -r ./client ./clientpack cp client/favicon.ico client/apple-touch-icon.png clientpack/
( echo ';(function() {' ; cat clientpack/dcwebui.js ; echo '})();' ) | uglifyjs -o clientpack/dcwebui.min.js -c -m --ie8 npm run webpack
lessc --clean-css clientpack/dcwebui.css clientpack/dcwebui.min.css cat client/index.htm \
cat clientpack/index.htm \ | sed -e '/bundle.js/{i <script>' -e 'r clientpack/bundle.min.js' -e 'a </script>' -e 'd}' \
| sed -e '/dcwebui.css/{i <style>' -e 'r clientpack/dcwebui.min.css' -e 'a </style>' -e 'd}' \ > clientpack/index.htm
| sed -e '/dcwebui.js/{i <script>' -e 'r clientpack/dcwebui.min.js' -e 'a </script>' -e 'd}' \
| sed -e '/socket.io-1.7.2.js/{i <script>' -e 'r clientpack/socket.io-1.7.2.js' -e 'a </script>' -e 'd}' \
> clientpack/index.packed.htm
mv clientpack/index.packed.htm clientpack/index.htm
html-minifier --collapse-whitespace -o clientpack/index.min.htm clientpack/index.htm html-minifier --collapse-whitespace -o clientpack/index.min.htm clientpack/index.htm
mv clientpack/index.min.htm clientpack/index.htm mv clientpack/index.min.htm clientpack/index.htm
rm ./clientpack/*.js rm clientpack/bundle.min.js
rm ./clientpack/*.css $(GOPATH)/bin/go-bindata -nomemcopy -nometadata -prefix clientpack clientpack
go-bindata -nomemcopy -nometadata -prefix clientpack clientpack
$(BINNAME).exe: bindata.go *.go $(BINNAME).exe: bindata.go *.go
GOARCH=386 GOOS=windows go build $(GOFLAGS) -o $(BINNAME).exe GOARCH=386 GOOS=windows go build $(GOFLAGS) -o $(BINNAME).exe
@ -54,5 +55,5 @@ $(BINNAME)-$(VERSION)-win32.7z: $(BINNAME).exe nmdc-webfrontend.conf.SAMPLE
$(BINNAME)-$(VERSION)-linux64.tar.xz: $(BINNAME) nmdc-webfrontend.conf.SAMPLE $(BINNAME)-$(VERSION)-linux64.tar.xz: $(BINNAME) nmdc-webfrontend.conf.SAMPLE
XZ_OPT='-9' tar caf "$(BINNAME)-$(VERSION)-linux64.tar.xz" $(BINNAME) nmdc-webfrontend.conf.SAMPLE --owner=0 --group=0 XZ_OPT='-9' tar caf "$(BINNAME)-$(VERSION)-linux64.tar.xz" $(BINNAME) nmdc-webfrontend.conf.SAMPLE --owner=0 --group=0
$(BINNAME)-$(VERSION)-src.tar.xz: $(SOURCES) $(BINNAME)-$(VERSION)-src.zip:
XZ_OPT='-9' tar caf "$(BINNAME)-$(VERSION)-src.tar.xz" $(SOURCES) --owner=0 --group=0 hg archive "$(BINNAME)-$(VERSION)-src.zip" || git archive -o "$(BINNAME)-$(VERSION)-src.zip" HEAD

165
README.md
View File

@ -1,165 +0,0 @@
# nmdc-webfrontend
A web interface to an NMDC/ADC hub.
Chat in real-time on your NMDC/ADC hub using a web browser.
## Relationship to other projects
This project forks and deprecates my earlier `dcwebui2` project since Go seems to use less memory than node.
This project supercedes [dcwebui](https://code.ivysaur.me/dcwebui/), [flexdc](https://code.ivysaur.me/flexdc/), and [dcwebui2](https://code.ivysaur.me/dcwebui2/).
### Upgrading from `dcwebui2`
- The configuration file content is identical between `nmdc-webfrontend` 1.0.0 and `dcwebui2` 1.3.0, but please now ensure it's valid JSON instead of arbitrary javascript. This means no assignment, use double-quoted strings, and no comments.
- Future changes to the configuration file since `nmdc-webfrontend` 1.0.0 are backward compatible (see the changelog for more details).
## Developing
1. Install Go (>= 1.11), Node.js, NPM, and 7-Zip (`p7zip-full` on Debian)
2. Download the source code
- `git clone https://git.ivysaur.me/code.ivysaur.me/nmdc-webfrontend.git `; or
- `go get code.ivysaur.me/nmdc-webfrontend` ; or
- download and extract a source archive
3. Install dependencies: `sudo make deps`
4. Build: `make`
5. Optional: Set `web.external_webroot: true` in the config file for unminified development
## Changelog
2017-11-26 1.3.0
- Update libnmdc to 0.16 (adds ADC hub support)
- Configuration: The `hub.port` property is now optional. You can specify a full URI in the `hub.address` property instead.
- [⬇️ nmdc-webfrontend-1.3.0-win32.7z](https://git.ivysaur.me/attachments/76e6b425-d34d-4451-815b-98a6e0685a19) *(1.44 MiB)*
- [⬇️ nmdc-webfrontend-1.3.0-src.tar.xz](https://git.ivysaur.me/attachments/a362dd86-2999-4753-adac-830f1a1a64ac) *(64.81 KiB)*
- [⬇️ nmdc-webfrontend-1.3.0-linux64.tar.xz](https://git.ivysaur.me/attachments/877fc7c9-0df8-4514-9147-7f102f221d49) *(1.69 MiB)*
2017-11-14 1.2.3
- Update libnmdc to 0.15
- [⬇️ nmdc-webfrontend-1.2.3-win32.7z](https://git.ivysaur.me/attachments/02c33e38-89ef-4cbd-b8a2-80cbd6077520) *(1.41 MiB)*
- [⬇️ nmdc-webfrontend-1.2.3-src.tar.xz](https://git.ivysaur.me/attachments/e3e7599d-4805-4e39-a45e-d7f68cc32f09) *(64.72 KiB)*
- [⬇️ nmdc-webfrontend-1.2.3-linux64.tar.xz](https://git.ivysaur.me/attachments/c5a06c20-e14a-4cfd-b830-7b41a52074c1) *(1.66 MiB)*
2017-10-28 1.2.2
- Enhancement: Simplify build process
- Fix an issue with closing PM tabs
- [⬇️ nmdc-webfrontend-1.2.2-win32.7z](https://git.ivysaur.me/attachments/37f3a7f4-637b-44d7-9575-5a721a504ce5) *(1.41 MiB)*
- [⬇️ nmdc-webfrontend-1.2.2-src.tar.xz](https://git.ivysaur.me/attachments/265ed79c-901f-43e7-9746-99b45a28311b) *(64.72 KiB)*
- [⬇️ nmdc-webfrontend-1.2.2-linux64.tar.xz](https://git.ivysaur.me/attachments/74ebeaaa-a2ef-4b9c-948f-8c11522d5ab0) *(1.66 MiB)*
2017-10-16 1.2.1
- Enhancement: Increase scrollback buffer size
- Fix an issue with missing `contented` upload link once logged in
- Fix a cosmetic issue with `(0)` appearing in page title
- [⬇️ nmdc-webfrontend-1.2.1-win32.7z](https://git.ivysaur.me/attachments/a99d5d16-968f-4b02-954a-e22314ae9d64) *(1.39 MiB)*
- [⬇️ nmdc-webfrontend-1.2.1-src.tar.xz](https://git.ivysaur.me/attachments/e9c3dd9f-c0b1-4913-8315-3a39c809155c) *(65.74 KiB)*
- [⬇️ nmdc-webfrontend-1.2.1-linux64.tar.xz](https://git.ivysaur.me/attachments/bf3dc821-182b-4911-a46f-72efbc4310f8) *(1.64 MiB)*
2017-10-15 1.2.0
- Feature: Add `contented` integration (set `app.contented_server` in config file)
- Fix a cosmetic issue with the menu icon on devices without unicode font coverage
- [⬇️ nmdc-webfrontend-1.2.0-win32.7z](https://git.ivysaur.me/attachments/48c194e1-6463-4449-a8d4-e2b1cef2a522) *(1.39 MiB)*
- [⬇️ nmdc-webfrontend-1.2.0-src.tar.xz](https://git.ivysaur.me/attachments/ceba8731-b0e6-483e-8282-360a0a584304) *(65.69 KiB)*
- [⬇️ nmdc-webfrontend-1.2.0-linux64.tar.xz](https://git.ivysaur.me/attachments/6082cb13-0e62-4fa8-95c6-1b7d7f7a3272) *(1.64 MiB)*
2017-02-11 1.1.4
- Update libnmdc to 0.14
- [⬇️ nmdc-webfrontend-1.1.4-win64.7z](https://git.ivysaur.me/attachments/b104b02c-9469-4b8d-be9f-3fbafecb4edc) *(1.36 MiB)*
- [⬇️ nmdc-webfrontend-1.1.4-win32.7z](https://git.ivysaur.me/attachments/dd0a9bab-6da6-45d4-a48f-51c9621e9c55) *(1.25 MiB)*
- [⬇️ nmdc-webfrontend-1.1.4-src.tar.xz](https://git.ivysaur.me/attachments/cc0be981-bad7-401e-aace-ac6d8d9a56dd) *(65.53 KiB)*
- [⬇️ nmdc-webfrontend-1.1.4-linux64.tar.xz](https://git.ivysaur.me/attachments/7cd26e52-9ea7-4dcc-91b3-273bc0a3eda4) *(1.50 MiB)*
- [⬇️ nmdc-webfrontend-1.1.4-linux32.tar.xz](https://git.ivysaur.me/attachments/15895b23-a582-4d59-91da-36f6e6145dfe) *(1.41 MiB)*
2017-02-11 1.1.3
- Feature: Display user IP address on hover, if available
- Enhancement: Allow clicking on popup notifications
- Enhancement: Only show 'popups enabled' notification when enabling for the first time, not persistent page load
- Update libnmdc to 0.13
- Fix a cosmetic issue with not displaying non-zero user share sizes
- [⬇️ nmdc-webfrontend-1.1.3-win64.7z](https://git.ivysaur.me/attachments/d7b551fb-0dd4-4c38-bbe2-513c67311752) *(1.36 MiB)*
- [⬇️ nmdc-webfrontend-1.1.3-win32.7z](https://git.ivysaur.me/attachments/33b90056-bff3-488c-bf01-bcaf7f114176) *(1.25 MiB)*
- [⬇️ nmdc-webfrontend-1.1.3-src.tar.xz](https://git.ivysaur.me/attachments/0390f266-3663-48b5-9216-4a3e41623017) *(65.50 KiB)*
- [⬇️ nmdc-webfrontend-1.1.3-linux64.tar.xz](https://git.ivysaur.me/attachments/8ab5ac7c-e8d2-47c0-886a-90bfaf011efd) *(1.50 MiB)*
- [⬇️ nmdc-webfrontend-1.1.3-linux32.tar.xz](https://git.ivysaur.me/attachments/229ef410-19a1-4fa8-8625-473bf486b40f) *(1.41 MiB)*
2017-02-06 1.1.2
- Autodetect 'extern', no need to include it in config files
- Enhancement: Remove redundant request, for a faster page load
- Display server version number in log file and in response headers
- Fix a cosmetic issue with spacing around user count in page title
- [⬇️ nmdc-webfrontend-1.1.2-win64.7z](https://git.ivysaur.me/attachments/96c0305e-dc3e-45c6-9567-5eaa461551ec) *(1.36 MiB)*
- [⬇️ nmdc-webfrontend-1.1.2-win32.7z](https://git.ivysaur.me/attachments/27095754-735c-4e24-8cff-60bdbbe60b34) *(1.25 MiB)*
- [⬇️ nmdc-webfrontend-1.1.2-src.tar.xz](https://git.ivysaur.me/attachments/5b9d177e-f79d-4afd-a97c-6d563ba0e7af) *(65.42 KiB)*
- [⬇️ nmdc-webfrontend-1.1.2-linux64.tar.xz](https://git.ivysaur.me/attachments/a2325dc5-992e-4d45-a622-c516e41a3187) *(1.50 MiB)*
- [⬇️ nmdc-webfrontend-1.1.2-linux32.tar.xz](https://git.ivysaur.me/attachments/e2dee266-1b9d-4837-9536-98b1922eefa0) *(1.41 MiB)*
2017-02-06 1.1.1
- Fix an issue with malformed content in minified build
- [⬇️ nmdc-webfrontend-1.1.1-win64.7z](https://git.ivysaur.me/attachments/412f04e2-08f5-4a12-b933-43ea00164fb2) *(1.36 MiB)*
- [⬇️ nmdc-webfrontend-1.1.1-win32.7z](https://git.ivysaur.me/attachments/52753dca-3dd6-4668-a785-2d5807d27e63) *(1.25 MiB)*
- [⬇️ nmdc-webfrontend-1.1.1-src.tar.xz](https://git.ivysaur.me/attachments/e9beee17-8368-4d21-b33f-9e3fa2e11c74) *(65.39 KiB)*
- [⬇️ nmdc-webfrontend-1.1.1-linux64.tar.xz](https://git.ivysaur.me/attachments/e93a09fc-7d0c-4b2a-a706-dddd51d9426d) *(1.50 MiB)*
- [⬇️ nmdc-webfrontend-1.1.1-linux32.tar.xz](https://git.ivysaur.me/attachments/bb9daa45-1bdb-492d-9d55-5fc39a046433) *(1.41 MiB)*
2017-02-06 1.1.0
- Feature: Remember last username/password for login; remember last "show joins/parts" status
- Feature: Display user details on hover (description, email, client tag, share size)
- Feature: Optional desktop notifications for background PMs (not possible in incognito)
- Feature: Automatically reconnect with the same username/password if connection was lost
- Feature: Re-enter last message (Ctrl+Up, Ctrl+Down)
- Feature: Set custom date/time format (Minutes, Seconds, Full), remembered for next session
- Feature: Clickable magnet links
- Feature: Display unread main-chat message count in the page title if the window is inactive
- Feature: Add warning message when closing tab while still connected (optional preference, disabled by default, will be remembered).
- Feature: Admin option to load a custom favicon (set `web.custom_favicon=true` and place a `favicon.ico` in the current directory)
- Feature: Admin option to use external web resources (set `web.external_webroot=true` and use the /client/ directory)
- Enhancement: Higher resolution favicon
- Enhancement: Display operators in green in the user list
- Enhancement: Enable spellcheck for text input once logged in
- Enhancement: Prevent sending referrer to remote URLs
- Enhancement: Display joins/parts and connection/disconnection messages in PM tabs
- Enhancement: Support Shift+Tab to autocomplete backward
- Enhancement: Support unread status for the main tab
- Enhancement: Improve page load time via minification
- Remove unused options from the config file
- Update socket.io to 1.7.2
- Update libnmdc to 0.12
- Add margin between bottom of the text area and the text input box
- Fix a cosmetic issue with collapsing consecutive spaces in posted messages
- Fix a cosmetic issue with text size adjustment on mobile devices
- Fix a cosmetic issue with clearing the screen on reconnection
- Fix a cosmetic issue with not clearing the userlist on certain types of network error
- Fix a cosmetic issue with closed PM tabs reappearing in some cases
- Fix a cosmetic issue with marking all PM tabs as read when switching to a single one
- [⬇️ nmdc-webfrontend-1.1.0-win64.7z](https://git.ivysaur.me/attachments/a88be3f1-a20f-4ae8-8723-fb538e321a73) *(1.36 MiB)*
- [⬇️ nmdc-webfrontend-1.1.0-win32.7z](https://git.ivysaur.me/attachments/09326166-ef9e-4901-91f9-ad3b0fff156f) *(1.25 MiB)*
- [⬇️ nmdc-webfrontend-1.1.0-src.tar.xz](https://git.ivysaur.me/attachments/6d83cf30-3436-4c5d-a6e4-715703af7ce7) *(65.39 KiB)*
- [⬇️ nmdc-webfrontend-1.1.0-linux64.tar.xz](https://git.ivysaur.me/attachments/a9baf26c-1756-49d9-a8aa-720f01d72194) *(1.50 MiB)*
- [⬇️ nmdc-webfrontend-1.1.0-linux32.tar.xz](https://git.ivysaur.me/attachments/00e5a0a2-4b3f-4276-aefe-56046707a14b) *(1.41 MiB)*
2016-11-29 1.0.2
- Rebuild with libnmdc 0.11
- Fix an issue with not setting a version in the client tag
- [⬇️ nmdc-webfrontend-1.0.2-win64.7z](https://git.ivysaur.me/attachments/6b84047d-9a4c-45fe-985b-5321f629f806) *(1.34 MiB)*
- [⬇️ nmdc-webfrontend-1.0.2-win32.7z](https://git.ivysaur.me/attachments/1505b8fa-83e1-469d-8c77-acee3711e26f) *(1.23 MiB)*
- [⬇️ nmdc-webfrontend-1.0.2-src.tar.xz](https://git.ivysaur.me/attachments/faff2112-d191-4ee8-a572-553bc4a25be8) *(54.28 KiB)*
- [⬇️ nmdc-webfrontend-1.0.2-linux64.tar.xz](https://git.ivysaur.me/attachments/c319c140-8bed-4225-a953-e99940ba9732) *(1.48 MiB)*
- [⬇️ nmdc-webfrontend-1.0.2-linux32.tar.xz](https://git.ivysaur.me/attachments/a55fc471-5d80-49ad-8670-67cfb8c6175c) *(1.39 MiB)*
2016-10-08 1.0.1
- Fix an issue with backward compatibility with `dcwebui2` configuration file format
- [⬇️ nmdc-webfrontend-1.0.1-win64.7z](https://git.ivysaur.me/attachments/d5ed658d-1ef3-4bd8-b2de-3d9415322f57) *(1.35 MiB)*
- [⬇️ nmdc-webfrontend-1.0.1-win32.7z](https://git.ivysaur.me/attachments/546bd5f9-3742-498f-a1cb-85690c84ddc2) *(1.23 MiB)*
- [⬇️ nmdc-webfrontend-1.0.1-src.tar.xz](https://git.ivysaur.me/attachments/5ec729d7-d914-4b7e-ade3-21988c203652) *(54.25 KiB)*
- [⬇️ nmdc-webfrontend-1.0.1-linux64.tar.xz](https://git.ivysaur.me/attachments/d7bf1c9c-ab2c-4c61-b7d8-7b6233ae059f) *(1.48 MiB)*
- [⬇️ nmdc-webfrontend-1.0.1-linux32.tar.xz](https://git.ivysaur.me/attachments/d5265319-81e5-4f3d-95f7-a6e46c607490) *(1.39 MiB)*
2016-10-08 1.0.0
- Port `dcwebui2` from Node.js (Javascript) to Go
- Fix a cosmetic issue with not clearing userlist on disconnection
- [⬇️ nmdc-webfrontend-1.0.0-win64.7z](https://git.ivysaur.me/attachments/3b7386c8-1445-4590-9779-af9870ea542a) *(1.34 MiB)*
- [⬇️ nmdc-webfrontend-1.0.0-win32.7z](https://git.ivysaur.me/attachments/9ad9bd21-4152-462c-9ddc-b584917058e5) *(1.23 MiB)*
- [⬇️ nmdc-webfrontend-1.0.0-src.tar.xz](https://git.ivysaur.me/attachments/d2745543-671c-42b5-b334-2dba1ee8726c) *(54.23 KiB)*
- [⬇️ nmdc-webfrontend-1.0.0-linux64.tar.xz](https://git.ivysaur.me/attachments/6102edf8-471b-4793-8877-c747f5ac5680) *(1.48 MiB)*
- [⬇️ nmdc-webfrontend-1.0.0-linux32.tar.xz](https://git.ivysaur.me/attachments/5a4bcb39-41e4-4db9-80ff-c0ea04912b06) *(1.39 MiB)*

104
_dist/README.txt Normal file
View File

@ -0,0 +1,104 @@
A web interface to an NMDC hub.
Chat in real-time on your NMDC hub using a web browser. This project forks and deprecates my earlier `dcwebui2` project since Go seems to use less memory than node.
Written in Golang
Tags: nmdc
This project supercedes [entry=dcwebui]dcwebui[/entry], [entry=flexdc]flexdc[/entry], and [entry=dcwebui2]dcwebui2[/entry].
=UPGRADING FROM DCWEBUI2=
- The configuration file content is identical between nmdc-webfrontend 1.0.0 and dcwebui2 1.3.0, but please now ensure it's valid JSON instead of arbitrary javascript. This means no assignment, use double-quoted strings, and no comments.
- Future changes to the configuration file since nmdc-webfrontend 1.0.0 are backward compatible (see the changelog for more details).
=DEVELOPING=
[go-get]code.ivysaur.me/nmdc-webfrontend git https://git.ivysaur.me/code.ivysaur.me/nmdc-webfrontend.git[/go-get]
1. Install Go, Node.js, NPM, and 7-Zip (`p7zip-full` on Debian)
2. Set up your `$GOPATH`
3. Download the source code
    - `git clone https://git.ivysaur.me/code.ivysaur.me/nmdc-webfrontend.git `; or
    - `go get code.ivysaur.me/nmdc-webfrontend` ; or
    - download and extract a source archive
4. Install dependencies: `sudo make deps`
5. Build: `make`
6. Optional: Set `web.external_webroot: true` in the config file for unminified development
=CHANGELOG=
2017-10-28 1.2.2
- Enhancement: Simplify build process
- Fix an issue with closing PM tabs
2017-10-16 1.2.1
- Enhancement: Increase scrollback buffer size
- Fix an issue with missing `contented` upload link once logged in
- Fix a cosmetic issue with `(0)` appearing in page title
2017-10-15 1.2.0
- Feature: Add `contented` integration (set `app.contented_server` in config file)
- Fix a cosmetic issue with the menu icon on devices without unicode font coverage
2017-02-11 1.1.4
- Update libnmdc to 0.14
2017-02-11 1.1.3
- Feature: Display user IP address on hover, if available
- Enhancement: Allow clicking on popup notifications
- Enhancement: Only show 'popups enabled' notification when enabling for the first time, not persistent page load
- Update libnmdc to 0.13
- Fix a cosmetic issue with not displaying non-zero user share sizes
2017-02-06 1.1.2
- Autodetect 'extern', no need to include it in config files
- Enhancement: Remove redundant request, for a faster page load
- Display server version number in log file and in response headers
- Fix a cosmetic issue with spacing around user count in page title
2017-02-06 1.1.1
- Fix an issue with malformed content in minified build
2017-02-06 1.1.0
- Feature: Remember last username/password for login; remember last "show joins/parts" status
- Feature: Display user details on hover (description, email, client tag, share size)
- Feature: Optional desktop notifications for background PMs (not possible in incognito)
- Feature: Automatically reconnect with the same username/password if connection was lost
- Feature: Re-enter last message (Ctrl+Up, Ctrl+Down)
- Feature: Set custom date/time format (Minutes, Seconds, Full), remembered for next session
- Feature: Clickable magnet links
- Feature: Display unread main-chat message count in the page title if the window is inactive
- Feature: Add warning message when closing tab while still connected (optional preference, disabled by default, will be remembered).
- Feature: Admin option to load a custom favicon (set `web.custom_favicon=true` and place a `favicon.ico` in the current directory)
- Feature: Admin option to use external web resources (set `web.external_webroot=true` and use the /client/ directory)
- Enhancement: Higher resolution favicon
- Enhancement: Display operators in green in the user list
- Enhancement: Enable spellcheck for text input once logged in
- Enhancement: Prevent sending referrer to remote URLs
- Enhancement: Display joins/parts and connection/disconnection messages in PM tabs
- Enhancement: Support Shift+Tab to autocomplete backward
- Enhancement: Support unread status for the main tab
- Enhancement: Improve page load time via minification
- Remove unused options from the config file
- Update socket.io to 1.7.2
- Update libnmdc to 0.12
- Add margin between bottom of the text area and the text input box
- Fix a cosmetic issue with collapsing consecutive spaces in posted messages
- Fix a cosmetic issue with text size adjustment on mobile devices
- Fix a cosmetic issue with clearing the screen on reconnection
- Fix a cosmetic issue with not clearing the userlist on certain types of network error
- Fix a cosmetic issue with closed PM tabs reappearing in some cases
- Fix a cosmetic issue with marking all PM tabs as read when switching to a single one
2016-11-29 1.0.2
- Rebuild with libnmdc 0.11
- Fix an issue with not setting a version in the client tag
2016-10-08 1.0.1
- Fix an issue with backward compatibility with `dcwebui2` configuration file format
2016-10-08 1.0.0
- Port `dcwebui2` from Node.js (Javascript) to Go
- Fix a cosmetic issue with not clearing userlist on disconnection

File diff suppressed because one or more lines are too long

View File

@ -51,14 +51,16 @@ html,body {
} }
/* WiiU placements */ /* WiiU placements */
.navigator-wiiu .placement-mid { .navigator-wiiu {
bottom:120px; .placement-mid {
} bottom:120px;
.navigator-wiiu .placement-bottom { }
height:120px; .placement-bottom {
} height:120px;
.navigator-wiiu .placement-panel { }
bottom:120px; .placement-panel {
bottom:120px;
}
} }
/* Menu button */ /* Menu button */
@ -75,14 +77,17 @@ html,body {
height:18px; height:18px;
width:18px; width:18px;
text-align:center; text-align:center;
svg {
width: 18px;
height: 18px;
}
&:hover {
background:white;
}
} }
.menubutton svg {
width: 18px;
height: 18px;
}
.menubutton:hover {
background:white;
}
/* Menu list */ /* Menu list */
.menu { .menu {
background:#DDD; background:#DDD;
@ -97,20 +102,24 @@ html,body {
z-index:10; /* above placement-panel */ z-index:10; /* above placement-panel */
overflow-y:auto; overflow-y:auto;
ul {
margin:0;
padding:0;
}
li {
list-style-type:none;
padding:6px;
cursor:pointer;
&:hover {
background:#333;
color:white;
}
}
} }
.menu ul {
margin:0;
padding:0;
}
.menu li {
list-style-type:none;
padding:6px;
cursor:pointer;
}
.menu li:hover {
background:#333;
color:white;
}
/* Tabs */ /* Tabs */
.tabbar { .tabbar {
position:relative; position:relative;
@ -132,16 +141,20 @@ html,body {
border-right:1px solid lightgrey; border-right:1px solid lightgrey;
margin-top:2px; margin-top:2px;
padding:0 8px; padding:0 8px;
&:hover {
border-top:2px solid #EEE;
}
&.selected {
border-top:2px solid darkgreen;
}
&.unread {
background:lightyellow;
}
} }
.tabitem:hover {
border-top:2px solid #EEE;
}
.tabitem.selected {
border-top:2px solid darkgreen;
}
.tabitem.unread {
background:lightyellow;
}
.tab-label { .tab-label {
cursor:pointer; cursor:pointer;
} }
@ -170,12 +183,14 @@ html,body {
-webkit-transition:0.1s linear all; -webkit-transition:0.1s linear all;
-moz-transition:0.1s linear all; -moz-transition:0.1s linear all;
transition:0.1s linear all; transition:0.1s linear all;
&:hover {
background:#FFDDDD;
border:1px solid red;
color:black;
}
} }
.tab-closer:hover {
background:#FFDDDD;
border:1px solid red;
color:black;
}
/* */ /* */
.content { .content {
display:block; display:block;
@ -241,7 +256,10 @@ html,body {
} }
#submit-container { #submit-container {
position:absolute; position:absolute;
top:0;bottom:0;right:0;width:32px; top:0;
bottom:0;
right:0;
width:32px;
} }
#btsubmit { #btsubmit {
width:100%; width:100%;
@ -265,42 +283,42 @@ html,body {
-webkit-border-radius: 12px; -webkit-border-radius: 12px;
-moz-border-radius: 12px; -moz-border-radius: 12px;
border-radius: 12px; border-radius: 12px;
}
.ul-large li {
color: #333;
font: bold 15px Geneva, Arial, Helvetica, sans-serif;
vertical-align: middle;
border-bottom: 1px solid #ccc;
list-style-type: none;
text-align: left;
white-space: nowrap;
overflow: hidden;
height: 30px;
list-style-type: none;
display: block;
padding: 15px 5px 5px 10px;
}
.ul-large li:first-child {
border-top: 0;
-webkit-border-top-left-radius: 12px;
-webkit-border-top-right-radius: 12px;
-moz-border-top-left-radius: 12px;
-moz-border-top-right-radius: 12px;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
.ul-large li:last-child {
border-bottom: 0;
-webkit-border-bottom-left-radius: 12px;
-webkit-border-bottom-right-radius: 12px;
-moz-border-bottom-left-radius: 12px;
-moz-border-bottom-right-radius: 12px;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
}
.ul-large li { li {
cursor:pointer; color: #333;
font: bold 15px Geneva, Arial, Helvetica, sans-serif;
vertical-align: middle;
border-bottom: 1px solid #ccc;
list-style-type: none;
text-align: left;
white-space: nowrap;
overflow: hidden;
height: 30px;
list-style-type: none;
display: block;
padding: 15px 5px 5px 10px;
cursor:pointer;
&:first-child {
border-top: 0;
-webkit-border-top-left-radius: 12px;
-webkit-border-top-right-radius: 12px;
-moz-border-top-left-radius: 12px;
-moz-border-top-right-radius: 12px;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
&:last-child {
border-bottom: 0;
-webkit-border-bottom-left-radius: 12px;
-webkit-border-bottom-right-radius: 12px;
-moz-border-bottom-left-radius: 12px;
-moz-border-bottom-right-radius: 12px;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
}
}
} }
/* Text */ /* Text */
@ -368,19 +386,21 @@ html,body {
.ul-mini { .ul-mini {
padding-left: 4px; padding-left: 4px;
margin-top:4px; margin-top:4px;
}
.ul-mini li {
list-style-type:none;
min-height:13px;
line-height:13px;
padding-left:14px;
margin-bottom:5px;
cursor:pointer;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAANCAYAAACQN/8FAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABh0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzT7MfTgAAAZJJREFUKJFtz71rE2EAgPHnrknbXBKkLWhx0DhoWsEOxk0taMGlQ7cimElK5uroH5DFSUQ3KQpV2qlQEDJ4mKFQ0MVS6kfN1dQP2ss1ydW79y6Xu3udBG36zL/lUaSUVKtVqes6hmGgaRqlUulUoVAw+ScV4P2bFfrEPhnF4+v2NuVy+QFHSjiNXWV2cgzh+dRqX1hsNflcr5/tgcJ14larSdDxCYIOuWEV0c3+7oEAxk4NANu2GUnFFIt3FnvgydxFZb9hSRmFCCGwmjb3i3OVo1AFyF2bZm/PxDRN4rDDo4WHt4+F77RfmKNZ1MQA9ukTPPeXXt14OvXita5f+guVJ+uPZ1brlZUJI08qG/F26AP9chDXO8RpeyzNvMyMnc+7fembmU9WYJFXL7M1YtBNhqRIEYURh1Gb/u+DH69fubqRaLQOcAccHDy++TW0pEaATzcOkFHMxs/NCQD13oV50laaH/EuQdhBhAIRClzF/W9GkVIC8Gx54dZafa3SDmxc1SEdZxgfHmdu+m7y3Jlc+AextbuG6q1RSQAAAABJRU5ErkJggg==') no-repeat 0 0; li {
} list-style-type:none;
.ul-mini li:hover { min-height:13px;
background-color:#EEE; line-height:13px;
padding-left:14px;
margin-bottom:5px;
cursor:pointer;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAANCAYAAACQN/8FAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABh0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzT7MfTgAAAZJJREFUKJFtz71rE2EAgPHnrknbXBKkLWhx0DhoWsEOxk0taMGlQ7cimElK5uroH5DFSUQ3KQpV2qlQEDJ4mKFQ0MVS6kfN1dQP2ss1ydW79y6Xu3udBG36zL/lUaSUVKtVqes6hmGgaRqlUulUoVAw+ScV4P2bFfrEPhnF4+v2NuVy+QFHSjiNXWV2cgzh+dRqX1hsNflcr5/tgcJ14larSdDxCYIOuWEV0c3+7oEAxk4NANu2GUnFFIt3FnvgydxFZb9hSRmFCCGwmjb3i3OVo1AFyF2bZm/PxDRN4rDDo4WHt4+F77RfmKNZ1MQA9ukTPPeXXt14OvXita5f+guVJ+uPZ1brlZUJI08qG/F26AP9chDXO8RpeyzNvMyMnc+7fembmU9WYJFXL7M1YtBNhqRIEYURh1Gb/u+DH69fubqRaLQOcAccHDy++TW0pEaATzcOkFHMxs/NCQD13oV50laaH/EuQdhBhAIRClzF/W9GkVIC8Gx54dZafa3SDmxc1SEdZxgfHmdu+m7y3Jlc+AextbuG6q1RSQAAAABJRU5ErkJggg==') no-repeat 0 0;
&:hover {
background-color:#EEE;
}
}
} }
/* Display userlist alongside main chat for wide screens */ /* Display userlist alongside main chat for wide screens */

View File

@ -1,29 +1,37 @@
/* dcwebui.js */ /* dcwebui.js */
import "./dcwebui.less"; // for webpack
import * as io from 'socket.io-client'
"use strict"; "use strict";
/**
* Display value when loading a saved password
*/
var SENTINEL_PASSWORD = "************"; var SENTINEL_PASSWORD = "************";
var CHAT_SCROLLBACK_LIMIT = 200; // Once over 2x $limit, the first $limit will be trimmed off the list
/**
* Number of lines of chat to keep in the scroll area.
* Once there are over 2x $limit lines, the first $limit lines will be trimmed off the list
*/
var CHAT_SCROLLBACK_LIMIT = 200;
/**
* Our externally-accessible URL
*/
var EXTERN_ROOT = window.location.protocol + "//" + window.location.host + "/"; var EXTERN_ROOT = window.location.protocol + "//" + window.location.host + "/";
var el = function(s) { // Help out the braindead minifier, use these functions instead
// There used to be a querySelectorAll implementation, but, better that we don't have var document_getElementById = function(x) { return document.getElementById(x); }
// potentially-incompatible implementations if this one does actually work. var document_getElementsByClassName = function(x) { return document.getElementsByClassName(x); }
// i'm not writing a selector engine... var document_createElement = function(x) { return document.createElement(x); }
if (! s.length) {
return [];
}
if (s[0] === '#') { /**
return document.getElementById(s.slice(1)); // single element * Encode a string for NMDC
} else if (s[0] === '.') { *
return document.getElementsByClassName(s.slice(1)); // multiple elements * @param str
} else { */
return document.getElementsByTagName(s); // multiple elements var nmdc_escape = function(str: string): string {
}
};
var nmdc_escape = function(str) {
return ( return (
(''+str).length (''+str).length
? (''+str).replace(/&/g,'&amp;').replace(/\|/g,'&#124;').replace(/\$/g,'&#36;') ? (''+str).replace(/&/g,'&amp;').replace(/\|/g,'&#124;').replace(/\$/g,'&#36;')
@ -31,14 +39,24 @@ var nmdc_escape = function(str) {
); );
}; };
var hesc = function(s) { /**
* Encode a string for HTML
*
* @param s
*/
var hesc = function(s: string): string {
var filter = { var filter = {
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&#34;', '\'': '&#39;' '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&#34;', '\'': '&#39;'
}; };
return s.toString().replace(/[&<>'"]/g, function(s) { return filter[s]; }); return s.toString().replace(/[&<>'"]/g, function(s) { return filter[s]; });
}; };
var fmtBytes = function(b) { /**
* Format a number of bytes as a human-readable string
*
* @param b
*/
var fmtBytes = function(b: number): string {
if (b == 0) { if (b == 0) {
return '(nothing)'; return '(nothing)';
} }
@ -49,12 +67,21 @@ var fmtBytes = function(b) {
return parseFloat((b / Math.pow(k, i)).toFixed(3)) + sizes[i]; return parseFloat((b / Math.pow(k, i)).toFixed(3)) + sizes[i];
}; };
/**
var urldesc = function(s) { * Decode a string that was previously encoded in raw-url format.
*
* @param s
*/
var urldesc = function(s: string):string {
return decodeURIComponent(s.replace(/\+/g, " ")); return decodeURIComponent(s.replace(/\+/g, " "));
} }
var linkify = function(str) { /**
* Enhance an HTML string by automatically making links clickable, etc.
*
* @param str An HTML-safe string
*/
var linkify = function(str : string):string {
// n.b. str is already hesced // n.b. str is already hesced
return (str return (str
.replace( .replace(
@ -68,17 +95,39 @@ var linkify = function(str) {
); );
}; };
var sanitise = function(s) { /**
* Convert a plain-text string into an enhanced, HTML-safe string.
*
* @param s
*/
var sanitise = function(s:string): string {
return linkify(hesc(s)); return linkify(hesc(s));
}; };
var textContent = function($el) { /**
if ($el.textContent) return $el.textContent; * Retrieve the plain-text content from an HTML element in a browser-compatible way.
if ($el.innerText) return $el.innerText; *
* @param $el
*/
var textContent = function($el : HTMLElement) : string {
if ($el.textContent) {
return $el.textContent;
}
if ($el.innerText) {
return $el.innerText;
}
return ""; return "";
}; };
var negmod = function(l, r) { /**
* Calculate the positive modulo of (l % r).
*
* @param l
* @param r
*/
var negmod = function(l:number, r:number):number {
var ret = l % r; var ret = l % r;
if (l < 0) { if (l < 0) {
return ret + r; return ret + r;
@ -87,16 +136,27 @@ var negmod = function(l, r) {
} }
}; };
// @ref https://developer.mozilla.org/en/docs/Web/API/WindowBase64/Base64_encoding_and_decoding /**
var b64 = function(str) { * Encode a string to base64 in a UTF8-safe way.
*
* @ref https://developer.mozilla.org/en/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
* @param str
*/
var b64 = function(str : string):string {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) { return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
return String.fromCharCode('0x' + p1); return String.fromCharCode(parseInt('0x' + p1));
})).replace(/=/g, ''); })).replace(/=/g, '');
} }
// @ref https://gist.github.com/eligrey/1276030 /**
var appendInnerHTML = function($el, html) { * Append content to an HTML element in a browser-compatible way.
var child = document.createElement("span"); *
* @ref https://gist.github.com/eligrey/1276030
* @param $el
* @param html
*/
var appendInnerHTML = function($el: HTMLElement, html:string) {
var child = document_createElement("span");
child.innerHTML = html; child.innerHTML = html;
var node; var node;
@ -105,30 +165,48 @@ var appendInnerHTML = function($el, html) {
} }
}; };
// @ref http://stackoverflow.com/a/5598797 /**
function getOffsetLeft( elem ) { * Retrieve the left offset of a DOM element relative to the document.
*
* @ref http://stackoverflow.com/a/5598797
* @param elem
*/
function getOffsetLeft( elem: HTMLElement ):number {
var offsetLeft = 0; var offsetLeft = 0;
do { do {
if (!isNaN(elem.offsetLeft)) { if (!isNaN(elem.offsetLeft)) {
offsetLeft += elem.offsetLeft; offsetLeft += elem.offsetLeft;
} }
} while (elem = elem.offsetParent); } while (elem = <HTMLElement>elem.offsetParent);
return offsetLeft; return offsetLeft;
} }
function getOffsetTop( elem ) { /**
* Retrieve the top offset for a DOM element relative to the document.
*
* @ref http://stackoverflow.com/a/5598797
* @param elem
*/
function getOffsetTop( elem: HTMLElement ):number {
var offsetTop = 0; var offsetTop = 0;
do { do {
if (!isNaN(elem.offsetTop)) { if (!isNaN(elem.offsetTop)) {
offsetTop += elem.offsetTop; offsetTop += elem.offsetTop;
} }
} while (elem = elem.offsetParent); } while (elem = <HTMLElement>elem.offsetParent);
return offsetTop; return offsetTop;
} }
/* */ /* */
var date_format = function(d, format) { /**
* Format a string in a date format, analgous to strftime().
*
* @param d
* @param format Formatting string, supporting HisYmd character specifiers
* @return Plain text string
*/
var date_format = function(d:Date, format:string):string {
var pad = function(s) { var pad = function(s) {
return (s < 10) ? '0'+s : ''+s ; return (s < 10) ? '0'+s : ''+s ;
}; };
@ -137,20 +215,25 @@ var date_format = function(d, format) {
ret = ret.replace(/H/g, pad(d.getHours())); ret = ret.replace(/H/g, pad(d.getHours()));
ret = ret.replace(/i/g, pad(d.getMinutes())); ret = ret.replace(/i/g, pad(d.getMinutes()));
ret = ret.replace(/s/g, pad(d.getSeconds())); ret = ret.replace(/s/g, pad(d.getSeconds()));
ret = ret.replace(/Y/g, d.getFullYear()); ret = ret.replace(/Y/g, "" + d.getFullYear());
ret = ret.replace(/m/g, pad(d.getMonth() + 1)); ret = ret.replace(/m/g, pad(d.getMonth() + 1));
ret = ret.replace(/d/g, pad(d.getDate())); ret = ret.replace(/d/g, pad(d.getDate()));
return ret; return ret;
}; };
/* */ /**
* Emit an HTML5 notification.
var notify = function(title, body, tab) { *
* @param title
* @param body
* @param tab
*/
var notify = function(title:string, body:string, tab:string) {
if (!("Notification" in window)) { if (!("Notification" in window)) {
return; // not supported by browser return; // not supported by browser
} }
switch (window.Notification.permission) { switch ( (window as any).Notification.permission) {
case "granted": { case "granted": {
var n = new Notification(title, { var n = new Notification(title, {
body: body, body: body,
@ -169,7 +252,7 @@ var notify = function(title, body, tab) {
default: { default: {
// Clarify permission and retry // Clarify permission and retry
Notification.requestPermission(function(permission) { Notification.requestPermission(function(permission) {
notify(title, body); notify(title, body, "tab-main");
}); });
} break; } break;
} }
@ -178,7 +261,7 @@ var notify = function(title, body, tab) {
/* Tab writers */ /* Tab writers */
var write = function(tab) { var write = function(tab) {
var $tab = el('#inner-'+tab); var $tab = document_getElementById("inner-"+tab);
return { return {
'cls': function() { 'cls': function() {
$tab.innerHTML = ''; $tab.innerHTML = '';
@ -254,11 +337,11 @@ var userlist = {
'add': function(u) { 'add': function(u) {
if (this.has(u)) return; if (this.has(u)) return;
var userlists = el(".userlist"); var userlists = document_getElementsByClassName("userlist");
for (var l = 0, e = userlists.length; l !== e; ++l) { for (var l = 0, e = userlists.length; l !== e; ++l) {
var userlist = userlists[l]; var userlist = userlists[l];
var to_add = document.createElement('li'); var to_add = document_createElement('li');
to_add.className = "user-" + b64(u); to_add.className = "user-" + b64(u);
to_add.innerHTML = hesc(u); to_add.innerHTML = hesc(u);
@ -283,7 +366,7 @@ var userlist = {
return this; return this;
}, },
'del': function(u) { 'del': function(u) {
var userlists = el(".userlist"); var userlists = document_getElementsByClassName("userlist");
for (var l = 0, e = userlists.length; l !== e; ++l) { for (var l = 0, e = userlists.length; l !== e; ++l) {
if (! userlists[l].children) continue; if (! userlists[l].children) continue;
var userlist = userlists[l]; var userlist = userlists[l];
@ -300,7 +383,7 @@ var userlist = {
return this; return this;
}, },
'clear': function() { 'clear': function() {
var userlists = el(".userlist"); var userlists = document_getElementsByClassName("userlist");
for (var i in userlists) { for (var i in userlists) {
if (! userlists[i].children) continue; if (! userlists[i].children) continue;
var userlist = userlists[i]; var userlist = userlists[i];
@ -313,22 +396,22 @@ var userlist = {
return this; return this;
}, },
'names': function() { 'names': function() {
var userlist = el(".userlist")[0].children; var userlist = document_getElementsByClassName("userlist")[0].children;
var ret = []; var ret = [];
for (var i = 0, e = userlist.length; i < e; ++i) { for (var i = 0, e = userlist.length; i < e; ++i) {
ret.push( textContent(userlist[i]) ); ret.push( textContent((<HTMLElement>userlist[i])) );
} }
return ret; return ret;
}, },
'has': function(u) { 'has': function(u) {
return el(".user-" + b64(u)).length !== 0; /* there are two - large and non-large */ return document_getElementsByClassName("user-" + b64(u)).length !== 0; /* there are two - large and non-large */
}, },
'count': function() { 'count': function() {
return el(".userlist")[0].children.length; return document_getElementsByClassName("userlist")[0].children.length;
}, },
'setInfo': function(nick, props) { 'setInfo': function(nick, props) {
var baseClass = "user-" + b64(nick); var baseClass = "user-" + b64(nick);
var $el = el("." + baseClass); var $el = document_getElementsByClassName("" + baseClass);
var prop_str = []; var prop_str = [];
if (props.Description.length > 0) { if (props.Description.length > 0) {
prop_str.push(props.Description); prop_str.push(props.Description);
@ -345,7 +428,7 @@ var userlist = {
prop_str.push("Sharing " + fmtBytes(props.ShareSize)); prop_str.push("Sharing " + fmtBytes(props.ShareSize));
for (var i = 0; i < $el.length; ++i) { for (var i = 0; i < $el.length; ++i) {
$el[i].title = prop_str.join("\n"); (<HTMLElement> $el[i]).title = prop_str.join("\n");
if (props.IsOperator) { if (props.IsOperator) {
$el[i].className = baseClass + " user-is-operator"; $el[i].className = baseClass + " user-is-operator";
@ -357,7 +440,7 @@ var userlist = {
}; };
var submit = function() { var submit = function() {
var str = el("#chatbox").value; var str = (<HTMLInputElement>document_getElementById("chatbox")).value;
if (! str.length) return; if (! str.length) return;
if (hub_state === STATE_READY_FOR_LOGIN) { if (hub_state === STATE_READY_FOR_LOGIN) {
@ -383,7 +466,7 @@ var submit = function() {
write("tab-main").system("Connecting..."); write("tab-main").system("Connecting...");
} else if (hub_state === STATE_ACTIVE) { } else if (hub_state === STATE_ACTIVE) {
if (pm_target !== false) { if (pm_target !== PM_TARGET_NONE) {
sock.emit('priv', {'user': pm_target, 'message': str}); sock.emit('priv', {'user': pm_target, 'message': str});
writerFor(pm_target).pub(hub_last_nick, str ); writerFor(pm_target).pub(hub_last_nick, str );
} else { } else {
@ -400,7 +483,7 @@ var submit = function() {
write("tab-main").system("Invalid internal state."); write("tab-main").system("Invalid internal state.");
} }
el("#chatbox").value = ''; (<HTMLInputElement>document_getElementById("chatbox")).value = '';
}; };
/* page visibility */ /* page visibility */
@ -413,10 +496,10 @@ var pagevis_setup = function(fnActive, fnInactive) {
if (typeof document.hidden !== "undefined") { if (typeof document.hidden !== "undefined") {
h = "hidden"; h = "hidden";
vc = "visibilitychange"; vc = "visibilitychange";
} else if (typeof document.msHidden !== "undefined") { } else if (typeof (document as any).msHidden !== "undefined") {
h = "msHidden"; h = "msHidden";
vc = "msvisibilitychange"; vc = "msvisibilitychange";
} else if (typeof document.webkitHidden !== "undefined") { } else if (typeof (document as any).webkitHidden !== "undefined") {
h = "webkitHidden"; h = "webkitHidden";
vc = "webkitvisibilitychange"; vc = "webkitvisibilitychange";
} }
@ -447,13 +530,13 @@ var pagevis_setup = function(fnActive, fnInactive) {
*/ */
var tab_set = function(tab) { var tab_set = function(tab) {
var tabs = el(".tabpane"); var tabs = document_getElementsByClassName("tabpane");
for (var i in tabs) { for (var i in tabs) {
try { try {
tabs[i].style.display = (tabs[i].id === tab ? 'block' : 'none'); (<HTMLElement> tabs[i]).style.display = (tabs[i].id === tab ? 'block' : 'none');
} catch (e) {}; } catch (e) {};
} }
var tabitems = el(".tabitem"); var tabitems = document_getElementsByClassName("tabitem");
for (var i in tabitems) { for (var i in tabitems) {
try { try {
// Update UNREAD/SELECTED flags for the target // Update UNREAD/SELECTED flags for the target
@ -466,7 +549,7 @@ var tab_set = function(tab) {
} catch (e) {}; } catch (e) {};
} }
pm_target = false; pm_target = PM_TARGET_NONE;
for (var i in pm_tabs) { for (var i in pm_tabs) {
if (pm_tabs[i] === tab) { if (pm_tabs[i] === tab) {
pm_target = i; pm_target = i;
@ -481,13 +564,13 @@ var tab_set = function(tab) {
updateTitle(); updateTitle();
write(tab).scroll(); write(tab).scroll();
el("#chatbox").focus(); document_getElementById("chatbox").focus();
last_tab = tab; last_tab = tab;
}; };
var tab_new = function(id, name) { var tab_new = function(id, name) {
appendInnerHTML(el("#bar"), appendInnerHTML(document_getElementById("bar"),
' <div class="tabitem" data-tab="tab-ext-'+id+'" id="tabitem-tab-ext-'+id+'">'+ ' <div class="tabitem" data-tab="tab-ext-'+id+'" id="tabitem-tab-ext-'+id+'">'+
'<span class="tab-label">'+ '<span class="tab-label">'+
hesc(name)+ hesc(name)+
@ -495,7 +578,7 @@ var tab_new = function(id, name) {
'<a class="tab-closer" data-tab="tab-ext-'+id+'">&times;</a>'+ '<a class="tab-closer" data-tab="tab-ext-'+id+'">&times;</a>'+
'</div> ' '</div> '
); );
appendInnerHTML(el("#extratabs"), appendInnerHTML(document_getElementById("extratabs"),
' <div class="tabpane content placement-mid" id="tab-ext-'+id+'" style="display:none;">'+ ' <div class="tabpane content placement-mid" id="tab-ext-'+id+'" style="display:none;">'+
'<div class="content-inner" id="inner-tab-ext-'+id+'"></div>'+ '<div class="content-inner" id="inner-tab-ext-'+id+'"></div>'+
'</div>' '</div>'
@ -508,10 +591,10 @@ var tab_free = function(id) {
if (id === "tab-main") return; if (id === "tab-main") return;
// remove tab item and body // remove tab item and body
var $el = el("#tabitem-"+id); var $el = document_getElementById("tabitem-"+id);
$el.parentNode.removeChild($el); $el.parentNode.removeChild($el);
$el = el("#"+id); $el = document_getElementById(""+id);
$el.parentNode.removeChild($el); $el.parentNode.removeChild($el);
// clear from PM tabs // clear from PM tabs
@ -546,22 +629,26 @@ var noprop = function(ev) {
} }
var tab_addHandlers = function() { var tab_addHandlers = function() {
var tabitems = el(".tabitem"); var tabitems = document_getElementsByClassName("tabitem");
for (var i = 0; i < tabitems.length; i++) { for (var i = 0; i < tabitems.length; i++) {
if (! tabitems[i]) continue; if (! tabitems[i]) {
continue;
}
tabitems[i].onclick = function(ev) { (<HTMLElement> tabitems[i]).onclick = function(ev) {
tab_set( this.getAttribute('data-tab') ); tab_set( this.getAttribute('data-tab') );
return noprop(ev); return noprop(ev);
}; };
} }
var tabclosers = el(".tab-closer"); var tabclosers = document_getElementsByClassName("tab-closer");
for (var i = 0; i < tabclosers.length; i++) { for (var i = 0; i < tabclosers.length; i++) {
if (! tabclosers[i]) continue; if (! tabclosers[i]) {
continue;
}
tabclosers[i].onclick = function(ev) { (<HTMLElement> tabclosers[i]).onclick = function(ev) {
tab_free( this.getAttribute('data-tab') ); tab_free( this.getAttribute('data-tab') );
return noprop(ev); return noprop(ev);
@ -571,8 +658,8 @@ var tab_addHandlers = function() {
/* */ /* */
var maybeWriterFor = function(username) { var maybeWriterFor = function(username: string) {
if (! username in pm_tabs || ! pm_tabs[username]) { if (! (username in pm_tabs) || ! pm_tabs[username]) {
return null; return null;
} }
@ -597,7 +684,7 @@ var tabcomplete_state = '';
var tabcompletion_start = function(direction) { var tabcompletion_start = function(direction) {
var cursor = el("#chatbox").value.replace(/^.*\s([^\s]+)$/, '$1'); var cursor = (<HTMLInputElement>document_getElementById("chatbox")).value.replace(/^.*\s([^\s]+)$/, '$1');
if (tabcomplete_state === '') { if (tabcomplete_state === '') {
// new tab completion // new tab completion
@ -633,10 +720,11 @@ var tabcompletion_start = function(direction) {
// Replace in textbox // Replace in textbox
var chatprefix = el("#chatbox").value.substr(0, el("#chatbox").value.length - cursor.length); var $chatbox = (<HTMLInputElement>document_getElementById("chatbox"));
var chatprefix = $chatbox.value.substr(0, $chatbox.value.length - cursor.length);
el("#chatbox").value = chatprefix + targetName; $chatbox.value = chatprefix + targetName;
el("#chatbox").focus(); $chatbox.focus();
}; };
var tabcompletion_inactive = function() { var tabcompletion_inactive = function() {
@ -648,11 +736,11 @@ var tabcompletion_inactive = function() {
var MenuList = function(el) { var MenuList = function(el) {
this.el = el; this.el = el;
this.div = document.createElement("div"); this.div = document_createElement("div");
this.div.classList.add("menu"); this.div.classList.add("menu");
this.div.style.position = "absolute"; this.div.style.position = "absolute";
this.ul = document.createElement("ul"); this.ul = document_createElement("ul");
this.div.appendChild(this.ul); this.div.appendChild(this.ul);
document.body.appendChild(this.div); document.body.appendChild(this.div);
@ -664,7 +752,7 @@ MenuList.prototype.clear = function() {
} }
}; };
MenuList.prototype.add = function(txt, cb) { MenuList.prototype.add = function(txt, cb) {
var li = document.createElement("li"); var li = document_createElement("li");
li.innerHTML = txt; li.innerHTML = txt;
li.onclick = cb; li.onclick = cb;
this.ul.appendChild(li); this.ul.appendChild(li);
@ -685,7 +773,7 @@ MenuList.prototype.toggle = function() {
/* */ /* */
var menu = new MenuList(el("#menubutton")); var menu = new MenuList(document_getElementById("menubutton"));
menu.reset = function() { menu.reset = function() {
this.clear(); this.clear();
@ -788,7 +876,7 @@ var toggle_joinparts = function(ev) {
var updateTitle = function() { var updateTitle = function() {
var prefix = ""; var prefix = "";
var unrTabs = el(".unread"); var unrTabs = document_getElementsByClassName("unread");
if (unrTabs.length === 1 && unrTabs[0].getAttribute('data-tab') == "tab-main") { if (unrTabs.length === 1 && unrTabs[0].getAttribute('data-tab') == "tab-main") {
prefix = "[" + mainchat_unread_count + " NEW] " prefix = "[" + mainchat_unread_count + " NEW] "
} else if (unrTabs.length > 0) { } else if (unrTabs.length > 0) {
@ -802,14 +890,15 @@ var updateTitle = function() {
document.title = prefix + hub_hubname + suffix; document.title = prefix + hub_hubname + suffix;
}; };
var sock = {}; var sock:SocketIOClient.Socket = null;
var hub_state = 0; // [disconnected, sent-nick, connected] var hub_state = 0; // [disconnected, sent-nick, connected]
var hub_last_nick = ''; var hub_last_nick = '';
var hub_hubname = "Loading..."; var hub_hubname = "Loading...";
var pm_tabs = {}; // nick => tabid var pm_tabs = {}; // nick => tabid
var next_tabid = 1; var next_tabid = 1;
var pm_target = false; const PM_TARGET_NONE = "";
var pm_target = PM_TARGET_NONE;
var last_tab = "tab-main"; var last_tab = "tab-main";
@ -893,7 +982,7 @@ var scrollback_move = function(delta) {
} }
} }
el("#chatbox").value = chat_scrollback[chat_scrollback_index]; (<HTMLInputElement>document_getElementById("chatbox")).value = chat_scrollback[chat_scrollback_index];
}; };
/* */ /* */
@ -914,29 +1003,30 @@ var persistence_get = function(key, fallback) {
var transition = function(new_state) { var transition = function(new_state) {
hub_state = new_state; hub_state = new_state;
var $chatbox = (<HTMLInputElement>document_getElementById("chatbox"));
switch(new_state) { switch(new_state) {
case STATE_DISCONNECTED: { case STATE_DISCONNECTED: {
userlist.clear(); userlist.clear();
el("#chatbox").disabled = true; $chatbox.disabled = true;
el("#chatbox").value = ''; // clear $chatbox.value = ''; // clear
} break; } break;
case STATE_READY_FOR_LOGIN: { case STATE_READY_FOR_LOGIN: {
userlist.clear(); userlist.clear();
el("#chatbox").spellcheck = false; $chatbox.spellcheck = false;
el("#chatbox").disabled = false; $chatbox.disabled = false;
el("#chatbox").value = ''; // clear $chatbox.value = ''; // clear
} break; } break;
case STATE_CONNECTING: { case STATE_CONNECTING: {
el("#chatbox").disabled = true; $chatbox.disabled = true;
} break; } break;
case STATE_ACTIVE: { case STATE_ACTIVE: {
write("tab-main").system("Now talking on "+hub_hubname); write("tab-main").system("Now talking on "+hub_hubname);
el("#chatbox").disabled = false; $chatbox.disabled = false;
el("#chatbox").spellcheck = true; $chatbox.spellcheck = true;
} break; } break;
} }
}; };
@ -946,14 +1036,15 @@ var tab_is_visible = function(tabref) {
} }
var tab_mark_unread = function(tabref) { var tab_mark_unread = function(tabref) {
if (el("#tabitem-"+tabref).className.indexOf('unread') === -1) { if (document_getElementById("tabitem-"+tabref).className.indexOf('unread') === -1) {
el("#tabitem-"+tabref).className += " unread"; document_getElementById("tabitem-"+tabref).className += " unread";
updateTitle(); updateTitle();
} }
} }
// //
declare var contented: any;
var contented_url = ""; var contented_url = "";
var contented_loaded_sdk = false; var contented_loaded_sdk = false;
var contented_load = function() { var contented_load = function() {
@ -963,21 +1054,21 @@ var contented_load = function() {
var onceSDKLoaded = function() { var onceSDKLoaded = function() {
contented.init("#inner-tab-main", function(items) { contented.init("#inner-tab-main", function(items) {
var val = el("#chatbox").value; var val = (<HTMLInputElement> document_getElementById("chatbox")).value;
for (var i = 0; i < items.length; ++i) { for (var i = 0; i < items.length; ++i) {
if (val.length > 0) { if (val.length > 0) {
val += " "; val += " ";
} }
val += contented.getPreviewURL(items[i]); val += contented.getPreviewURL(items[i]);
} }
el("#chatbox").value = val; (<HTMLInputElement> document_getElementById("chatbox")).value = val;
}); });
}; };
if (contented_loaded_sdk) { if (contented_loaded_sdk) {
onceSDKLoaded(); onceSDKLoaded();
} else { } else {
var scriptElement = document.createElement('script'); var scriptElement = document_createElement('script');
scriptElement.onload = function() { scriptElement.onload = function() {
contented_loaded_sdk = true; contented_loaded_sdk = true;
onceSDKLoaded(); onceSDKLoaded();
@ -998,13 +1089,13 @@ window.onload = function() {
// HTML event handlers // HTML event handlers
el("#form-none").onsubmit = function(ev) { document_getElementById("form-none").onsubmit = function(ev) {
submit(); submit();
return noprop(ev); // don't submit form return noprop(ev); // don't submit form
}; };
el("#chatbox").onkeydown = function(ev) { document_getElementById("chatbox").onkeydown = function(ev) {
if (ev.keyCode === 9 /* Tab */) { if (ev.keyCode === 9 /* Tab */) {
tabcompletion_start( ev.shiftKey ? -1 : 1 ); tabcompletion_start( ev.shiftKey ? -1 : 1 );
return noprop(ev); return noprop(ev);
@ -1032,7 +1123,7 @@ window.onload = function() {
usermenu.hide(); usermenu.hide();
}; };
el("#menubutton").onclick = function(ev) { document_getElementById("menubutton").onclick = function(ev) {
menu.toggle(); menu.toggle();
return noprop(ev); return noprop(ev);
}; };
@ -1090,7 +1181,7 @@ window.onload = function() {
if (pre_login.indexOf(":") !== -1) { if (pre_login.indexOf(":") !== -1) {
pre_login = pre_login.substr(0, pre_login.indexOf(":")) + ":" + SENTINEL_PASSWORD; pre_login = pre_login.substr(0, pre_login.indexOf(":")) + ":" + SENTINEL_PASSWORD;
} }
el("#chatbox").value = pre_login; (<HTMLInputElement>document_getElementById("chatbox")).value = pre_login;
if (have_cleared_once) { if (have_cleared_once) {
// re-log-in automatically // re-log-in automatically

View File

@ -5,8 +5,7 @@
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0"> <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" href="/apple-touch-icon.png"> <link rel="apple-touch-icon" href="./apple-touch-icon.png">
<link rel="stylesheet" href="/dcwebui.css">
<title>Loading...</title> <title>Loading...</title>
</head> </head>
<body> <body>
@ -56,7 +55,6 @@
</form> </form>
</div> </div>
<script type="text/javascript" src="/socket.io-1.7.2.js"></script> <script type="text/javascript" src="/bundle.js"></script>
<script type="text/javascript" src="/dcwebui.js"></script>
</body> </body>
</html> </html>

File diff suppressed because one or more lines are too long

9
go.mod
View File

@ -1,9 +0,0 @@
module code.ivysaur.me/nmdc-webfrontend
require (
code.ivysaur.me/libnmdc v0.16.0
github.com/cxmcc/tiger v0.0.0-20170524142333-bde35e2713d7 // indirect
github.com/googollee/go-engine.io v0.0.0-20170224222511-80ae0e43aca1 // indirect
github.com/googollee/go-socket.io v0.0.0-20170525141029-5447e71f36d3
github.com/gorilla/websocket v1.2.0 // indirect
)

10
go.sum
View File

@ -1,10 +0,0 @@
code.ivysaur.me/libnmdc v0.16.0 h1:/y4Olm/DoZn83vE/QV1NyjQ2MF20opEJ+sxwDlDTIoE=
code.ivysaur.me/libnmdc v0.16.0/go.mod h1:ZHCjIX/zm29hd2H8YtzOBHiSyowZNOHvgoMRjKbBLLg=
github.com/cxmcc/tiger v0.0.0-20170524142333-bde35e2713d7 h1:jBEtq1t2gpn2kEzvRlCUxvvrxl5aSWkXNPwe/hwvSNQ=
github.com/cxmcc/tiger v0.0.0-20170524142333-bde35e2713d7/go.mod h1:ruCYvt9rtYymAr4rNmfYJrl1dz8HSXUFP7cufqKOsDI=
github.com/googollee/go-engine.io v0.0.0-20170224222511-80ae0e43aca1 h1:EYruZXoG1VD2n22p93st1miPfLNB7KiTuyA7qit4gnU=
github.com/googollee/go-engine.io v0.0.0-20170224222511-80ae0e43aca1/go.mod h1:MBpz1MS3P4HtRcBpQU4HcjvWXZ9q+JWacMEh2/BFYbg=
github.com/googollee/go-socket.io v0.0.0-20170525141029-5447e71f36d3 h1:J+S/ZG2A5qZPZM4+WwpOA3cc5tVL8OTKf2mWEO2dXN0=
github.com/googollee/go-socket.io v0.0.0-20170525141029-5447e71f36d3/go.mod h1:ftBGBMhSYToR5oV4ImIPKvAIsNaTkLC+tTvoNafqxlQ=
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=

17
main.go
View File

@ -48,19 +48,14 @@ func (this *App) HubWorker(Nick, Pass string, so socketio.Socket, done chan stru
selfUser.ClientTag = this.cfg.Hub.Tag selfUser.ClientTag = this.cfg.Hub.Tag
selfUser.ClientVersion = libnmdc.DEFAULT_CLIENT_VERSION selfUser.ClientVersion = libnmdc.DEFAULT_CLIENT_VERSION
url := this.cfg.Hub.Address
if this.cfg.Hub.Port == 0 {
url = fmt.Sprintf("%s:%d", this.cfg.Hub.Address, this.cfg.Hub.Port)
}
hco := libnmdc.HubConnectionOptions{ hco := libnmdc.HubConnectionOptions{
Address: libnmdc.HubAddress(url), Address: libnmdc.HubAddress(fmt.Sprintf("%s:%d", this.cfg.Hub.Address, this.cfg.Hub.Port)),
Self: selfUser, Self: *selfUser,
NickPassword: Pass, NickPassword: Pass,
NumEventsToBuffer: 0,
} }
hubEvents := make(chan libnmdc.HubEvent, 10) hub := hco.Connect()
hub := libnmdc.ConnectAsync(&hco, hubEvents)
defer func() { defer func() {
hub.Disconnect() hub.Disconnect()
@ -110,7 +105,7 @@ func (this *App) HubWorker(Nick, Pass string, so socketio.Socket, done chan stru
for { for {
select { select {
case hev, ok := <-hubEvents: case hev, ok := <-hub.OnEvent:
if !ok { if !ok {
log.Printf("[%s] hub chan closed\n", so.Id()) log.Printf("[%s] hub chan closed\n", so.Id())
return // abandon return // abandon

View File

@ -12,7 +12,8 @@
}, },
"hub": { "hub": {
"address": "nmdc://127.0.0.1:411", "address": "127.0.0.1",
"port": 411,
"tag": "nmdc-webfrontend" "tag": "nmdc-webfrontend"
} }

4802
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "nmdc-webfrontend",
"version": "1.0.0",
"description": "A web interface to an NMDC hub",
"homepage": "https://code.ivysaur.me/nmdc-webfrontend/",
"main": "s",
"scripts": {
"webpack": "webpack"
},
"repository": {
"type": "git",
"url": "https://git.ivysaur.me/code.ivysaur.me/nmdc-webfrontend"
},
"author": "",
"license": "ISC",
"dependencies": {
"@types/socket.io-client": "^1.4.31",
"awesome-typescript-loader": "^3.3.0",
"css-loader": "^0.28.7",
"html-minifier": "^3.5.6",
"less": "^2.7.3",
"less-loader": "^4.0.5",
"less-plugin-clean-css": "^1.5.1",
"socket.io-client": "^2.0.4",
"style-loader": "^0.19.0",
"ts-loader": "^3.1.1",
"typescript": "^2.6.1",
"uglify-js": "^3.1.8",
"webpack": "^3.8.1"
},
"devDependencies": {}
}

9
tsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "es3",
"removeComments": true
},
"include": [
"client/*"
]
}

View File

@ -1,14 +0,0 @@
mode:regex
# Compilation output
\.(?:exe|a)$
^pkg/
# Dependencies
^src/(?:github.com|gopkg.in|golang.org)/
# Scratch space
^src/nmdc/
# Binary release artefacts
/?__dist/

View File

@ -1,20 +0,0 @@
945ab4b16d05aa084f71bf5da9a3f687e0ec8bbd v0.1.0
02a360e95480b97ddad83add5db48b2766339a99 nmdc-log-service-1.0.0
137c1b65039e03c80379826a6efdfd808f6fbc8f v0.2.0
d8b64d5527c2a5e4d76872e5bc3d69f7646135c6 v0.3.0
fca41372e400853775b02e951f9db91d87f41adb nmdc-log-service-1.0.1
050b424a7c5d5a27c9323c8810f3afbead1f5b96 v0.4.0
da9f123633f9c28be6435ed7898139665d4c39d9 nmdc-log-service-1.0.2
75a78f6a78f249a2cd8aa3d29f7e5e6319b4e03b v0.5.0
4116422bb10229d887f9296970a166fa1ef8c5fd nmdc-log-service-1.0.3
cb86f3a40115cc46f450c0c83fd9b9d3b740e820 nmdc-log-service-1.0.4
cb86f3a40115cc46f450c0c83fd9b9d3b740e820 v0.6.0
71343a2c641a438206d30ea7e75dc89a11dbef00 v0.7.0
b0e57a5fcffdf4102d669db51a3648ddf66a0792 v0.8.0
e7c2c71ef24b386add728fad35fff4a996fccbac v0.9.0
3ecc037cf2d7080572fe87c2e39ecd153fb0e947 v0.10.0
5149ffe70ea8475e480b682345b31aa45a3352db v0.11.0
22b156a6fc2f6161765317f4ec9ab3731a26e0e2 v0.12.0
3ee0f4ea5142d66079a9500bdcd48a53bdcf362f v0.13.0
6422ed687cd308c339b6dc188bbe1034ed93f893 v0.14.0
84fb191007017862ffc37af68dcdace5d8c06eee v0.15.0

View File

@ -1,711 +0,0 @@
package libnmdc
import (
"encoding/base32"
"fmt"
"regexp"
"strconv"
"strings"
)
type adcState int
const (
adcStateProtocol adcState = 0
adcStateIdentify adcState = 1
adcStateVerify adcState = 2
adcStateNormal adcState = 3
adcStateData adcState = 4
)
type AdcProtocol struct {
hc *HubConnection
state adcState
sid, pid, cid string // all in base32 encoding
supports map[string]struct{}
}
const (
// extra extensions that aren't flagged in SUPPORTS
adcSeparateApVe string = "SEPARATE_AP_VE" // we invented this string
)
func NewAdcProtocol(hc *HubConnection) Protocol {
proto := AdcProtocol{
hc: hc,
state: adcStateProtocol,
supports: make(map[string]struct{}),
}
rxPid := regexp.MustCompile("^[A-Z2-7]{39}$")
if !rxPid.MatchString(hc.Hco.AdcPID) {
hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Invalid custom PID, regenerating"})
hc.Hco.AdcPID = NewPID()
}
pid_base32 := hc.Hco.AdcPID
cid_base32, err := proto.pid2cid(pid_base32)
if err != nil {
panic(err)
}
proto.cid = cid_base32
proto.pid = pid_base32
// Start logging in
hc.SayRaw("HSUP ADBASE ADTIGR ADUCMD\n")
return &proto
}
func (this *AdcProtocol) pid2cid(pid_base32 string) (string, error) {
pid_raw, err := base32.StdEncoding.DecodeString(pid_base32 + "=")
if err != nil {
return "", err
}
cid_raw := Tiger(string(pid_raw))
cid_base32 := Base32(cid_raw)
return cid_base32, nil
}
func (this *AdcProtocol) SID2Nick(sid string) (string, bool) {
this.hc.usersMut.Lock()
defer this.hc.usersMut.Unlock()
nick, ok := this.hc.userSIDs[sid]
return nick, ok
}
func (this *AdcProtocol) Nick2SID(targetNick string) (string, bool) {
this.hc.usersMut.Lock()
defer this.hc.usersMut.Unlock()
for sid, nick := range this.hc.userSIDs {
if nick == targetNick {
return sid, true
}
}
return "", false
}
func (this *AdcProtocol) ProcessCommand(msg string) {
if len(msg) == 0 {
return
}
this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: msg})
parts := strings.Split(msg, " ")
switch parts[0] {
case "ISUP":
if !(this.state == adcStateProtocol || this.state == adcStateNormal) {
this.malformed(parts)
return
}
for _, supportflag := range parts[1:] {
if len(supportflag) < 2 {
this.malformed(parts)
return
}
if supportflag[0:2] == "AD" {
this.supports[supportflag[2:]] = struct{}{}
} else if supportflag[0:2] == "RM" {
delete(this.supports, supportflag[2:])
} else {
this.malformed(parts)
return
}
}
if this.state == adcStateProtocol {
this.state = adcStateIdentify
}
case "ISID":
if this.state != adcStateIdentify {
this.malformed(parts)
return
}
this.sid = parts[1]
// State transition IDENTIFY --> VERIFY and send our own info
this.hc.SayRaw("BINF " + this.escape(this.sid) + " " + this.ourINFO(true) + "\n")
this.state = adcStateVerify
case "IINF":
// Hub telling information about itself
// ADCH++ sends this once we are successfully logged in
flags, err := this.parts2flags(parts[1:])
if err != nil {
this.logError(err)
return
}
if flags["CT"] != "32" {
this.malformed(parts)
return
}
err = this.handleHubInfo(flags)
if err != nil {
this.logError(err)
return
}
if this.state != adcStateNormal {
this.enterNormalState() // successful login
}
case "BINF":
if this.state != adcStateNormal {
this.enterNormalState() // successful login
}
sid := parts[1]
flags, err := this.parts2flags(parts[2:])
if err != nil {
this.logError(err)
return
}
// Log this user in, and associate this SID with this user
this.hc.usersMut.Lock()
defer this.hc.usersMut.Unlock()
oldNick, sidExists := this.hc.userSIDs[sid]
uinfo := UserInfo{}
if sidExists {
uinfo_lookup, ok := this.hc.users[oldNick]
if !ok {
// Shouldn't happen
this.hc.processEvent(HubEvent{
EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN,
Message: fmt.Sprintf("Hub connection corrupted (missing info for SID='%s' nick='%s'), disconnecting", sid, oldNick),
})
this.hc.Disconnect()
return
}
uinfo = uinfo_lookup
}
this.updateUserInfo(&uinfo, flags)
newNick := uinfo.Nick
if len(newNick) == 0 {
this.logError(fmt.Errorf("Zero-length nick for user (SID='%s')", sid))
}
shouldHandleNewUser := false
if sidExists && oldNick != newNick {
// Nick change = delete all trace of this user first, treat as new
delete(this.hc.users, oldNick)
delete(this.hc.userSIDs, sid)
this.hc.processEvent(HubEvent{EventType: EVENT_USER_PART, Nick: oldNick})
shouldHandleNewUser = true
} else if sidExists && oldNick == newNick {
// Updating existing user
this.hc.users[newNick] = uinfo
this.hc.processEvent(HubEvent{EventType: EVENT_USER_UPDATED_INFO, Nick: newNick})
} else if !sidExists {
// User joined
shouldHandleNewUser = true
}
//
if shouldHandleNewUser {
// Install this SID as pointing to this nick
this.hc.userSIDs[sid] = uinfo.Nick
// Check if this nick was in use by any other SID already
for otherSid, otherSidNick := range this.hc.userSIDs {
if otherSidNick == newNick && otherSid != sid {
this.hc.processEvent(HubEvent{
EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN,
Message: fmt.Sprintf("Hub connection corrupted (duplicate SIDs '%s' and '%s' for nick '%s'), disconnecting", sid, otherSid, newNick),
})
this.hc.Disconnect()
return
}
}
// Notifications
this.hc.users[newNick] = uinfo
this.hc.processEvent(HubEvent{EventType: EVENT_USER_JOINED, Nick: newNick})
}
case "IMSG":
// General message from the hub
if len(parts) < 2 {
this.malformed(parts)
return
}
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Message: this.unescape(parts[1])})
case "ISTA":
// Error message from the hub
if len(parts) < 3 {
this.malformed(parts)
return
}
code, _ := strconv.Atoi(parts[1])
msg := this.unescape(parts[2])
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Message: this.ErrorMessage(code, msg)})
case "IQUI":
// Error message from the hub
// IQUI V3M6 DI1 MSNick\staken,\splease\spick\sanother\sone TL-1
if len(parts) < 2 {
this.malformed(parts)
return
}
sid := parts[1]
flags, err := this.parts2flags(parts[2:])
if err != nil {
return
}
if sid == this.sid {
if msg, ok := flags["MS"]; ok {
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Message: "The hub is closing our connection because: " + this.unescape(msg)})
} else {
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Message: "The hub is closing our connection"})
}
} else {
this.hc.usersMut.Lock()
defer this.hc.usersMut.Unlock()
otherSidNick, ok := this.hc.userSIDs[sid]
if ok {
delete(this.hc.userSIDs, sid)
delete(this.hc.users, otherSidNick)
this.hc.processEvent(HubEvent{EventType: EVENT_USER_PART, Nick: otherSidNick})
} else {
// ??
this.logError(fmt.Errorf("An unknown user quit the hub (SID=%s)", sid))
}
}
case "BMSG":
// Message from a user
// BMSG ZVF4 hi
if len(parts) < 3 {
this.malformed(parts)
return
}
sid := this.unescape(parts[1])
msg := this.unescape(parts[2])
this.hc.usersMut.Lock()
defer this.hc.usersMut.Unlock()
nick, ok := this.hc.userSIDs[sid]
if !ok {
this.logError(fmt.Errorf("Recieved message from unknown SID '%s'", sid))
return
}
this.hc.processEvent(HubEvent{EventType: EVENT_PUBLIC, Nick: nick, Message: msg})
case "IGPA":
// Password is needed
// IGPA 7EIAAAECLMAAAPJQAAADQQYAAAWAYAAAKVFQAAF6EAAAAAYFAAAA
// HPAS LZDIJOTZDPWHINHGPT5RHT6WLU7DRME7DQO2O3Q
if len(parts) < 2 {
this.malformed(parts)
return
}
/*
For GPA/PAS, assuming that '12345' is the random data supplied in GPA, then;
PAS = Base32( Hash( password + '12345' ) )
GPA: The data parameter is at least 24 random bytes (base32 encoded).
*/
data_base32 := parts[1]
if len(data_base32)%8 != 0 {
data_base32 += strings.Repeat("=", 8-(len(data_base32)%8))
}
data_raw, err := base32.StdEncoding.DecodeString(data_base32)
if err != nil {
this.logError(err)
return
}
resp := Base32(Tiger(this.hc.Hco.NickPassword + string(data_raw)))
this.hc.SayRaw("HPAS " + resp + "\n")
case "EMSG":
// Private message from other user
// EMSG I5RO FMWH test\spm PMI5RO
// EMSG sender recip==us message [flags...]
if len(parts) < 4 {
this.malformed(parts)
return
}
if parts[2] != this.sid {
this.logError(fmt.Errorf("Recieved a PM intended for someone else (got SID=%s expected SID=%s)", parts[2], this.sid))
return
}
senderSid := parts[1]
senderNick, ok := this.SID2Nick(parts[1])
if !ok {
this.logError(fmt.Errorf("Recieved a PM from an unknown user (SID=%s)", senderSid))
return
}
msg := this.unescape(parts[3])
this.hc.processEvent(HubEvent{EventType: EVENT_PRIVATE, Nick: senderNick, Message: msg})
case "ICMD":
// Usercommand
// ICMD ADCH++/About\sthis\shub TTHMSG\s+about\n CT3
if len(parts) < 2 {
this.malformed(parts)
return
}
uc := UserCommand{
Message: this.unescape(parts[1]),
Type: USERCOMMAND_TYPE_RAW, // default
}
flags, err := this.parts2flags(parts[2:])
if err != nil {
this.malformed(parts)
return
}
if ct, ok := flags["CT"]; ok {
ct64, _ := strconv.ParseUint(ct, 10, 64)
uc.Context = UserCommandContext(ct64)
}
if tt, ok := flags["TT"]; ok {
uc.Command = tt
}
if sp, ok := flags["SP"]; ok && sp == "1" {
uc.Type = USERCOMMAND_TYPE_SEPARATOR
}
if co, ok := flags["CO"]; ok && co == "1" {
uc.Type = USERCOMMAND_TYPE_NICKLIMITED // "Constrained" in ADC parlance
}
if rm, ok := flags["RM"]; ok && rm == "1" {
uc.RemoveThis = true
}
this.hc.processEvent(HubEvent{EventType: EVENT_USERCOMMAND, UserCommand: &uc})
// Ignored messages
// ````````````````
case "DCTM": // Client-client ConnectToMe
case "BSCH": // Search
default:
this.malformed(parts)
}
}
func (this *AdcProtocol) infoFlagsFor(u *UserInfo) map[string]string {
parts := map[string]string{
"NI": u.Nick,
"SS": fmt.Sprintf("%d", u.ShareSize),
"SF": fmt.Sprintf("%d", u.SharedFiles),
"US": fmt.Sprintf("%d", u.UploadSpeedBps),
"DS": fmt.Sprintf("%d", u.DownloadSpeedBps),
"SL": fmt.Sprintf("%d", u.Slots),
"HN": fmt.Sprintf("%d", u.HubsUnregistered),
"HR": fmt.Sprintf("%d", u.HubsRegistered),
"HO": fmt.Sprintf("%d", u.HubsOperator),
}
if _, ok := this.supports[adcSeparateApVe]; ok {
parts["AP"] = u.ClientTag
parts["VE"] = u.ClientVersion
} else {
parts["VE"] = fmt.Sprintf("%s %s", u.ClientTag, u.ClientVersion)
}
// Do not send the hub a CT (it decides what type we are)
return parts
}
func (this *AdcProtocol) ourINFO(includePid bool) string {
parts := this.infoFlagsFor(this.hc.Hco.Self)
parts["ID"] = this.cid
if includePid {
parts["PD"] = this.pid
}
ret := ""
for k, v := range parts {
ret += " " + k + this.escape(v)
}
return ret[1:]
}
func (this *AdcProtocol) parts2flags(parts []string) (map[string]string, error) {
flags := make(map[string]string, len(parts))
for _, flag := range parts {
if len(flag) < 2 {
return nil, fmt.Errorf("Malformed flag '%s'", flag)
}
flags[flag[0:2]] = this.unescape(flag[2:])
}
return flags, nil
}
func (this *AdcProtocol) handleHubInfo(flags map[string]string) error {
if flags["CT"] != "32" {
return fmt.Errorf("Expected CT==32")
}
// IINF DEADCH++\sTest\shub VE2.12.1\s(r"[unknown]")\sRelease HI1 NIADCH++ APADCH++ CT32
// AP: extension 3.24 "Application and version separation in INF"
// HI:
// Hub properties updated
// Special SUPPORT that is only indicated in IINF
if _, ok := flags["AP"]; ok {
this.supports[adcSeparateApVe] = struct{}{}
}
// Hub's name is in "NI", hub description in "DE"
hubName, ok := flags["NI"]
if ok {
if hubDesc, ok := flags["DE"]; ok && len(hubDesc) > 0 {
hubName += " - " + hubDesc
}
this.hc.HubName = hubName
this.hc.processEvent(HubEvent{EventType: EVENT_HUBNAME_CHANGED, Nick: this.hc.HubName})
}
return nil
}
func (this *AdcProtocol) updateUserInfo(u *UserInfo, flags map[string]string) {
// User MyINFO
// BINF GUPR IDFEARIFD33NTGC4YBEZ3UFQS5R4ZXXTFL2QN2GRY PDZMIFLG5EKZG3BDRRMIJPG7ARNA6KW3JVIH3DF7Q NIivysaur5 SL3 FS3 SS0 SF0 HN1 HR0 HO0 VEEiskaltDC++\s2.2.9 US2621440 KPSHA256/3UPRORG4BLJ4CG6TO6R3G75A67LXOGD437NALQALRWJF6XBOECTA I40.0.0.0 U418301
// BINF GUPR I4172.17.0.1 U418301 IDFEARIFD33NTGC4YBEZ3UFQS5R4ZXXTFL2QN2GRY VEEiskaltDC++\s2.2.9 SF0 NIivysaur5 SL3 HN1 HO0 KPSHA256/3UPRORG4BLJ4CG6TO6R3G75A67LXOGD437NALQALRWJF6XBOECTA HR0 FS3 SS0 US2621440 SUSEGA,ADC0,TCP4,UDP4
// Or maybe only incremental:
// BINF Z3BA HO1
// TODO
for prop, val := range flags {
switch prop {
case "ID":
u.CID = val
case "PD":
// ignore PID - it will only appear if we're talking about our own user
case "NI":
u.Nick = val
case "SL":
u.Slots, _ = strconv.ParseUint(val, 10, 64)
case "SS":
u.ShareSize, _ = strconv.ParseUint(val, 10, 64)
case "SF":
u.SharedFiles, _ = strconv.ParseUint(val, 10, 64)
case "HN":
u.HubsUnregistered, _ = strconv.ParseUint(val, 10, 64)
case "HR":
u.HubsRegistered, _ = strconv.ParseUint(val, 10, 64)
case "HO":
u.HubsOperator, _ = strconv.ParseUint(val, 10, 64)
case "US":
u.UploadSpeedBps, _ = strconv.ParseUint(val, 10, 64)
case "DS":
u.DownloadSpeedBps, _ = strconv.ParseUint(val, 10, 64)
case "KP":
u.Keyprint = val
case "I4":
u.IPv4Address = val
case "I6":
u.IPv6Address = val
case "U4":
u.IPv4UDPPort, _ = strconv.ParseUint(val, 10, 64)
case "U6":
u.IPv6UDPPort, _ = strconv.ParseUint(val, 10, 64)
case "SU":
u.SupportFlags = make(map[string]struct{})
for _, supportFlag := range strings.Split(val, ",") {
u.SupportFlags[supportFlag] = struct{}{}
}
}
}
// VE / AP
AP, hasAP := flags["AP"]
VE, hasVE := flags["VE"]
if hasAP && hasVE {
u.ClientTag = AP
u.ClientVersion = VE
} else if hasAP && !hasVE {
u.ClientTag, u.ClientVersion = this.getAPVEFromSingle(AP)
} else if !hasAP && hasVE {
u.ClientTag, u.ClientVersion = this.getAPVEFromSingle(VE)
}
}
func (this *AdcProtocol) getAPVEFromSingle(term string) (string, string) {
words := strings.Split(term, " ")
if len(words) > 1 {
return strings.Join(words[0:len(words)-1], " "), words[len(words)-1]
} else {
return term, "0"
}
}
func (this *AdcProtocol) enterNormalState() {
this.state = adcStateNormal
this.hc.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_CONNECTED})
this.hc.State = CONNECTIONSTATE_CONNECTED
}
func (this *AdcProtocol) malformed(parts []string) {
this.logError(fmt.Errorf("Ignoring malformed, unhandled, or out-of-state protocol command %v", parts))
}
func (this *AdcProtocol) logError(e error) {
this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Protocol error: " + e.Error()})
}
func (this *AdcProtocol) escape(plaintext string) string {
// The string "\s" escapes space, "\n" newline and "\\" backslash. This version of the protocol reserves all other escapes for future use; any message containing unknown escapes must be discarded.
v1 := strings.Replace(plaintext, `\`, `\\`, -1)
v2 := strings.Replace(v1, "\n", `\n`, -1)
return strings.Replace(v2, " ", `\s`, -1)
}
func (this *AdcProtocol) unescape(encoded string) string {
v1 := strings.Replace(encoded, `\s`, " ", -1)
v2 := strings.Replace(v1, `\n`, "\n", -1)
return strings.Replace(v2, `\\`, `\`, -1)
}
func (this *AdcProtocol) SayPublic(msg string) {
this.hc.SayRaw("BMSG " + this.sid + " " + this.escape(msg) + "\n")
}
func (this *AdcProtocol) SayPrivate(user, message string) {
if sid, ok := this.Nick2SID(user); ok {
this.hc.SayRaw("DMSG " + this.sid + " " + sid + " " + this.escape(message) + "\n")
} else {
this.logError(fmt.Errorf("Unknown user '%s'", user))
}
}
func (this *AdcProtocol) ProtoMessageSeparator() string {
return "\n"
}
func (this *AdcProtocol) ErrorMessage(code int, msg string) string {
severity := code / 100
category := (code % 100) / 10
cat_sub := (code % 100)
formatSeverity := func(severity int) string {
switch severity {
case 0:
return "OK"
case 1:
return "Warning"
case 2:
return "Error"
default:
return ""
}
}
formatCategory := func(category int) string {
switch category {
case 0:
return ""
case 1:
return "Hub not accepting users"
case 2:
return "Login failed"
case 3:
return "Access denied"
case 4:
return "Protocol error"
case 5:
return "Transfer error"
default:
return ""
}
}
formatCatSub := func(cat_sub int) string {
switch cat_sub {
case 11:
return "Hub is full"
case 12:
return "Hub is disabled"
case 21:
return "Invalid nick"
case 22:
return "Nick is already in use"
case 23:
return "Invalid password"
case 24:
return "CID already connected"
case 25:
return "Access denied"
case 26:
return "Registered users only"
case 27:
return "Invalid PID"
case 31:
return "Permanently banned"
case 32:
return "Temporarily banned"
default:
return ""
}
}
parts := make([]string, 0, 4)
if fs := formatSeverity(severity); len(fs) > 0 {
parts = append(parts, fs)
}
if fc := formatCategory(category); len(fc) > 0 {
parts = append(parts, fc)
}
if fcs := formatCatSub(cat_sub); len(fcs) > 0 {
parts = append(parts, fcs)
}
if len(msg) > 0 {
parts = append(parts, msg)
}
return strings.Join(parts, ": ") + fmt.Sprintf(" (code %d)", code)
}

View File

@ -1,80 +0,0 @@
package libnmdc
import (
"sync"
"time"
)
type AutodetectProtocol struct {
hc *HubConnection
realProtoMut sync.Mutex
realProto Protocol
}
func NewAutodetectProtocol(hc *HubConnection) Protocol {
proto := AutodetectProtocol{
hc: hc,
realProto: nil,
}
go proto.timeout()
return &proto
}
func (this *AutodetectProtocol) timeout() {
time.Sleep(AUTODETECT_ADC_NMDC_TIMEOUT)
this.realProtoMut.Lock()
defer this.realProtoMut.Unlock()
if this.realProto == nil {
this.realProto = NewAdcProtocol(this.hc)
this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Detected ADC protocol"})
}
}
func (this *AutodetectProtocol) ProcessCommand(msg string) {
this.realProtoMut.Lock()
defer this.realProtoMut.Unlock()
if this.realProto == nil {
// We actually got some data using $ as the separator?
// Upgrade to a full NMDC protocol
this.realProto = NewNmdcProtocol(this.hc)
this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Detected NMDC protocol"})
}
this.realProto.ProcessCommand(msg)
}
func (this *AutodetectProtocol) SayPublic(msg string) {
this.realProtoMut.Lock()
defer this.realProtoMut.Unlock()
if this.realProto == nil {
this.realProto = NewNmdcProtocol(this.hc)
}
this.realProto.SayPublic(msg)
}
func (this *AutodetectProtocol) SayPrivate(user, message string) {
this.realProtoMut.Lock()
defer this.realProtoMut.Unlock()
if this.realProto == nil {
this.realProto = NewNmdcProtocol(this.hc)
}
this.realProto.SayPrivate(user, message)
}
func (this *AutodetectProtocol) ProtoMessageSeparator() string {
this.realProtoMut.Lock()
defer this.realProtoMut.Unlock()
if this.realProto == nil {
return "|"
}
return this.realProto.ProtoMessageSeparator()
}

View File

@ -1,29 +0,0 @@
package libnmdc
import (
"fmt"
)
type ConnectionMode rune
const (
CONNECTIONMODE_ACTIVE ConnectionMode = 'A' // 65
CONNECTIONMODE_PASSIVE ConnectionMode = 'P' // 49
CONNECTIONMODE_SOCKS5 ConnectionMode = '5' // 53
)
func (this ConnectionMode) String() string {
switch this {
case CONNECTIONMODE_ACTIVE:
return "Active"
case CONNECTIONMODE_PASSIVE:
return "Passive"
case CONNECTIONMODE_SOCKS5:
return "SOCKS5"
default:
return fmt.Sprintf("ConnectionMode(\"%s\")", string(this))
}
}

View File

@ -1,40 +0,0 @@
package libnmdc
import (
"net"
)
type ConnectionState int
const (
CONNECTIONSTATE_DISCONNECTED = 1
CONNECTIONSTATE_CONNECTING = 2 // Handshake in progress
CONNECTIONSTATE_CONNECTED = 3
)
func (cs ConnectionState) String() string {
switch cs {
case CONNECTIONSTATE_DISCONNECTED:
return "Disconnected"
case CONNECTIONSTATE_CONNECTING:
return "Connecting"
case CONNECTIONSTATE_CONNECTED:
return "Connected"
default:
return "?"
}
}
func checkIsNetTimeout(err error) bool {
if err == nil {
return false
}
switch err.(type) {
case net.Error:
return err.(net.Error).Timeout()
default:
return false
}
}

View File

@ -1,15 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/cxmcc/tiger"
packages = ["."]
revision = "bde35e2713d7f674987c2ecb21a6b0fc33749516"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "c88ee670a5600b482019325b6d6633bb6b5fe789596dc29ef809aa7bb013927b"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -1,48 +0,0 @@
package libnmdc
import (
"net/url"
"strings"
)
type HubAddress string
func (this *HubAddress) parse() url.URL {
parsed, err := url.Parse(strings.ToLower(string(*this)))
if err != nil || len(parsed.Host) == 0 {
parsed = &url.URL{
Scheme: "",
Host: string(*this),
}
}
// Add default port if not specified
if !strings.ContainsRune(parsed.Host, ':') {
parsed.Host = parsed.Host + ":411"
}
return *parsed
}
func (this *HubAddress) IsSecure() bool {
parsed := this.parse()
return parsed.Scheme == "nmdcs" || parsed.Scheme == "dchubs" || parsed.Scheme == "adcs"
}
func (this *HubAddress) GetHostOnly() string {
return this.parse().Host
}
func (this *HubAddress) GetProtocol() HubProtocol {
parsed := this.parse()
switch parsed.Scheme {
case "nmdc", "dchub", "nmdcs", "dchubs":
return HubProtocolNmdc
case "adc", "adcs":
return HubProtocolAdc
default:
return HubProtocolAutodetect
}
}

View File

@ -1,229 +0,0 @@
package libnmdc
import (
"crypto/tls"
"fmt"
"net"
"regexp"
"sync"
"time"
)
type HubConnection struct {
// Supplied parameters
Hco *HubConnectionOptions
// Current remote status
HubName string
State ConnectionState
usersMut sync.RWMutex
users map[string]UserInfo
userSIDs map[string]string
proto Protocol
// Event callback
processEvent func(HubEvent)
// Private state
conn net.Conn // this is an interface
connValid bool
autoReconnect bool
lastDataRecieved time.Time
}
// Thread-safe user accessor.
func (this *HubConnection) Users(cb func(*map[string]UserInfo) error) error {
this.usersMut.Lock()
defer this.usersMut.Unlock()
return cb(&this.users)
}
func (this *HubConnection) SayPublic(message string) {
this.proto.SayPublic(message)
}
func (this *HubConnection) SayPrivate(recipient string, message string) {
this.proto.SayPrivate(recipient, message)
}
func (this *HubConnection) UserExists(nick string) bool {
this.usersMut.RLock()
defer this.usersMut.RUnlock()
_, already_existed := this.users[nick]
return already_existed
}
func (this *HubConnection) UserCount() int {
this.usersMut.RLock()
defer this.usersMut.RUnlock()
return len(this.users)
}
func (this *HubConnection) userJoined_NameOnly(nick string) {
if !this.UserExists(nick) {
this.usersMut.Lock()
this.users[nick] = *NewUserInfo(nick)
this.usersMut.Unlock() // Don't lock over a processEvent boundary
this.processEvent(HubEvent{EventType: EVENT_USER_JOINED, Nick: nick})
}
}
func (this *HubConnection) userJoined_Full(uinf *UserInfo) {
// n.b. also called when we get a replacement MyINFO for someone
this.usersMut.Lock()
_, userExisted := this.users[uinf.Nick] // don't use UserExists as it would deadlock the mutex
this.users[uinf.Nick] = *uinf
this.usersMut.Unlock() // Don't lock over a processEvent boundary
if !userExisted {
this.processEvent(HubEvent{EventType: EVENT_USER_JOINED, Nick: uinf.Nick})
} else {
this.processEvent(HubEvent{EventType: EVENT_USER_UPDATED_INFO, Nick: uinf.Nick})
}
}
// SayRaw sends raw bytes over the TCP socket. Callers should add the protocol
// terminating character themselves (e.g. `|` for NMDC).
// Note that protocol messages are transmitted on the caller thread, not from
// any internal libnmdc thread.
func (this *HubConnection) SayRaw(protocolCommand string) error {
if !this.connValid {
return ErrNotConnected
}
_, err := this.conn.Write([]byte(protocolCommand))
return err
}
func (this *HubConnection) SayKeepalive() error {
if !this.connValid {
return ErrNotConnected
}
return this.SayRaw(this.proto.ProtoMessageSeparator())
}
func (this *HubConnection) Disconnect() {
this.autoReconnect = false
if this.conn != nil {
this.conn.Close()
}
// A CONNECTIONSTATE_DISCONNECTED message will be emitted by the worker.
}
func (this *HubConnection) worker() {
var fullBuffer string
var err error = nil
var nbytes int = 0
for {
// If we're not connected, attempt reconnect
if this.conn == nil {
fullBuffer = "" // clear
if this.Hco.Address.IsSecure() {
this.conn, err = tls.Dial("tcp", this.Hco.Address.GetHostOnly(), &tls.Config{
InsecureSkipVerify: this.Hco.SkipVerifyTLS,
})
} else {
this.conn, err = net.Dial("tcp", this.Hco.Address.GetHostOnly())
}
if err != nil {
this.State = CONNECTIONSTATE_DISCONNECTED
this.connValid = false
this.proto = nil
} else {
this.State = CONNECTIONSTATE_CONNECTING
this.connValid = true
this.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_CONNECTING})
this.proto = this.Hco.Address.GetProtocol().Create(this)
}
}
// Read from socket into our local buffer (blocking)
if this.connValid {
readBuff := make([]byte, 1024)
this.conn.SetReadDeadline(time.Now().Add(SEND_KEEPALIVE_EVERY))
nbytes, err = this.conn.Read(readBuff)
if checkIsNetTimeout(err) {
// No data before read deadline
err = nil
if this.proto == nil {
// Autodetect: switch to ADC
this.proto = NewAdcProtocol(this)
} else {
// Normal
// Send KA packet
err = this.SayKeepalive()
}
}
if nbytes > 0 {
this.lastDataRecieved = time.Now()
fullBuffer += string(readBuff[0:nbytes])
}
}
if this.proto != nil {
rxSeparator := regexp.QuoteMeta(this.proto.ProtoMessageSeparator())
rxProtocolMessage := regexp.MustCompile(`(?ms)\A[^` + rxSeparator + `]*` + rxSeparator)
// Attempt to parse a message block
for len(fullBuffer) > 0 {
// FIXME nmdc
for len(fullBuffer) > 0 && fullBuffer[0] == '|' {
fullBuffer = fullBuffer[1:]
}
protocolMessage := rxProtocolMessage.FindString(fullBuffer)
if len(protocolMessage) > 0 {
this.proto.ProcessCommand(protocolMessage[:len(protocolMessage)-1])
fullBuffer = fullBuffer[len(protocolMessage):]
} else {
break
}
}
if err == nil && time.Now().Sub(this.lastDataRecieved) > RECONNECT_IF_NO_DATA_RECIEVED_IN {
err = fmt.Errorf("No packets recieved since %s, connection presumed lost", this.lastDataRecieved.Format(time.RFC3339))
}
}
// Maybe we disconnected
// Perform this check *last*, to ensure we've had a final shot at
// clearing out any queued messages
if err != nil {
this.State = CONNECTIONSTATE_DISCONNECTED
this.conn = nil
this.connValid = false
this.proto = nil
this.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_DISCONNECTED, Message: err.Error()})
if this.autoReconnect {
time.Sleep(AUTO_RECONNECT_AFTER) // Wait before reconnect
continue
} else {
return // leave the worker for good
}
}
}
}

View File

@ -1,79 +0,0 @@
package libnmdc
import (
"crypto/rand"
)
type HubConnectionOptions struct {
Address HubAddress
SkipVerifyTLS bool // using a negative verb, because bools default to false
SkipAutoReconnect bool // as above
Self *UserInfo
NickPassword string
AdcPID string // blank: autogenerate
}
func NewPID() string {
pidBytes := make([]byte, 24)
n, err := rand.Read(pidBytes)
if err != nil {
panic(err) // Insufficient cryptographic randomness
}
if n != 24 {
panic("Insufficient cryptographic randomness")
}
return Base32(pidBytes)
}
func (this *HubConnectionOptions) prepareConnection() *HubConnection {
if this.Self.ClientTag == "" {
this.Self.ClientTag = DEFAULT_CLIENT_TAG
this.Self.ClientVersion = DEFAULT_CLIENT_VERSION
}
// Shouldn't be blank either
if this.Self.ClientVersion == "" {
this.Self.ClientVersion = "0"
}
if this.AdcPID == "" {
this.AdcPID = NewPID()
}
hc := HubConnection{
Hco: this,
HubName: DEFAULT_HUB_NAME,
State: CONNECTIONSTATE_DISCONNECTED,
users: make(map[string]UserInfo),
userSIDs: make(map[string]string),
autoReconnect: !this.SkipAutoReconnect,
}
return &hc
}
// ConnectAsync connects to a hub server, and spawns a background goroutine to handle
// protocol messages. Events will be sent by channel to the supplied onEvent channel,
// the client is responsible for selecting off this.
func ConnectAsync(opts *HubConnectionOptions, onEvent chan HubEvent) *HubConnection {
hc := opts.prepareConnection()
hc.processEvent = func(ev HubEvent) {
onEvent <- ev
}
go hc.worker()
return hc
}
// ConnectSync connects to a hub server, and blocks forever to handle protocol messages.
// Client code should supply an event handling function as hco.OnEventSync.
func ConnectSync(opts *HubConnectionOptions, onEvent func(hub *HubConnection, ev HubEvent)) {
hc := opts.prepareConnection()
hc.processEvent = func(ev HubEvent) {
onEvent(hc, ev)
}
hc.worker()
}

View File

@ -1,25 +0,0 @@
package libnmdc
type HubEventType int
const (
EVENT_PUBLIC HubEventType = 1
EVENT_PRIVATE HubEventType = 2
EVENT_SYSTEM_MESSAGE_FROM_HUB HubEventType = 3
EVENT_SYSTEM_MESSAGE_FROM_CONN HubEventType = 4
EVENT_USER_JOINED HubEventType = 5
EVENT_USER_PART HubEventType = 6
EVENT_USER_UPDATED_INFO HubEventType = 7
EVENT_CONNECTION_STATE_CHANGED HubEventType = 8
EVENT_HUBNAME_CHANGED HubEventType = 9
EVENT_DEBUG_MESSAGE HubEventType = 10
EVENT_USERCOMMAND HubEventType = 11
)
type HubEvent struct {
EventType HubEventType
Nick string
Message string
StateChange ConnectionState
UserCommand *UserCommand
}

View File

@ -1,407 +0,0 @@
package libnmdc
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
)
type NmdcProtocol struct {
hc *HubConnection
sentOurHello bool
supports map[string]struct{}
rxPublicChat *regexp.Regexp
rxIncomingTo *regexp.Regexp
rxUserCommand *regexp.Regexp
rxMyInfo *regexp.Regexp
rxMyInfoNoTag *regexp.Regexp
}
func NewNmdcProtocol(hc *HubConnection) Protocol {
proto := NmdcProtocol{}
proto.hc = hc
// With the `m` flag, use \A instead of ^ to anchor to start
// This fixes accidentally finding a better match in the middle of a multi-line message
proto.rxPublicChat = regexp.MustCompile(`(?ms)\A<([^>]*)> (.*)$`)
proto.rxIncomingTo = regexp.MustCompile(`(?ms)\A([^ ]+) From: ([^ ]+) \$<([^>]*)> (.*)`)
proto.rxUserCommand = regexp.MustCompile(`(?ms)\A(\d+) (\d+)\s?([^\$]*)\$?(.*)`)
// Format: $ALL <nick> <description>$ $<connection><flag>$<e-mail>$<sharesize>$
HEAD := `(?ms)^\$ALL ([^ ]+) `
FOOT := `\$.\$([^$]+)\$([^$]*)\$([0-9]*)\$$`
proto.rxMyInfo = regexp.MustCompile(HEAD + `([^<]*)<(.+?) V:([^,]+),M:(.),H:([0-9]+)/([0-9]+)/([0-9]+),S:([0-9]+)>` + FOOT)
proto.rxMyInfoNoTag = regexp.MustCompile(HEAD + `([^$]*)` + FOOT) // Fallback for no tag
// Done
return &proto
}
func (this *NmdcProtocol) ProcessCommand(message string) {
// Zero-length protocol message
// ````````````````````````````
if len(message) == 0 {
return
}
// Public chat
// ```````````
if this.rxPublicChat.MatchString(message) {
pubchat_parts := this.rxPublicChat.FindStringSubmatch(message)
this.hc.processEvent(HubEvent{EventType: EVENT_PUBLIC, Nick: pubchat_parts[1], Message: this.unescape(pubchat_parts[2])})
return
}
// System messages
// ```````````````
if message[0] != '$' {
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Nick: this.hc.HubName, Message: this.unescape(message)})
return
}
// Protocol messages
// `````````````````
commandParts := strings.SplitN(message, " ", 2)
switch commandParts[0] {
case "$Lock":
this.hc.SayRaw("$Supports NoHello NoGetINFO UserCommand UserIP2 QuickList ChatOnly|" +
"$Key " + this.unlock([]byte(commandParts[1])) + "|")
this.sentOurHello = false
case "$Hello":
if commandParts[1] == this.hc.Hco.Self.Nick && !this.sentOurHello {
this.hc.SayRaw("$Version 1,0091|")
this.hc.SayRaw("$GetNickList|")
this.sayInfo()
this.sentOurHello = true
} else {
this.hc.userJoined_NameOnly(commandParts[1])
}
case "$HubName":
this.hc.HubName = commandParts[1]
this.hc.processEvent(HubEvent{EventType: EVENT_HUBNAME_CHANGED, Nick: commandParts[1]})
case "$ValidateDenide": // sic
if len(this.hc.Hco.NickPassword) > 0 {
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Incorrect password."})
} else {
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Nick already in use."})
}
case "$HubIsFull":
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Hub is full."})
case "$BadPass":
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Incorrect password."})
case "$GetPass":
if len(this.hc.Hco.NickPassword) == 0 {
// We've got a problem. MyPass with no arguments is a syntax error with no message = instant close
// Just drop the connection
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "This account is passworded."})
this.hc.Disconnect()
} else {
this.hc.SayRaw("$MyPass " + this.escape(this.hc.Hco.NickPassword) + "|")
}
case "$Quit":
this.hc.usersMut.Lock()
delete(this.hc.users, commandParts[1])
this.hc.usersMut.Unlock() // Don't lock over a processEvent boundary
this.hc.processEvent(HubEvent{EventType: EVENT_USER_PART, Nick: commandParts[1]})
case "$MyINFO":
u, err := this.parseMyINFO(commandParts[1])
if err != nil {
this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: err.Error()})
return
}
this.hc.userJoined_Full(u)
case "$NickList":
nicklist := strings.Split(commandParts[1], "$$")
for _, nick := range nicklist {
if len(nick) > 0 {
this.hc.userJoined_NameOnly(nick)
}
}
case "$OpList":
oplist := strings.Split(commandParts[1], "$$")
opmap := map[string]struct{}{}
// Organise/sort the list, and ensure we're not meeting an operator for
// the first time
for _, nick := range oplist {
if len(nick) > 0 {
opmap[nick] = struct{}{}
this.hc.userJoined_NameOnly(nick) // assert existence; noop otherwise
}
}
// Mark all mentioned nicks as being operators, and all unmentioned nicks
// as being /not/ an operator. (second pass minimises RW mutex use)
func() {
this.hc.usersMut.Lock()
defer this.hc.usersMut.Unlock()
for nick, userinfo := range this.hc.users {
_, isop := opmap[nick]
userinfo.IsOperator = isop
this.hc.users[nick] = userinfo
}
}()
case "$To:":
valid := false
if this.rxIncomingTo.MatchString(commandParts[1]) {
txparts := this.rxIncomingTo.FindStringSubmatch(commandParts[1])
if txparts[1] == this.hc.Hco.Self.Nick && txparts[2] == txparts[3] {
this.hc.processEvent(HubEvent{EventType: EVENT_PRIVATE, Nick: txparts[2], Message: this.unescape(txparts[4])})
valid = true
}
}
if !valid {
this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Malformed private message '" + commandParts[1] + "'"})
}
case "$UserIP":
this.hc.usersMut.Lock()
pairs := strings.Split(commandParts[1], "$$")
notifyOfUpdate := make([]string, 0, len(pairs))
nextIPPair:
for _, pair := range pairs {
parts := strings.SplitN(pair, " ", 2)
if len(parts) != 2 {
// ????
continue nextIPPair
}
ip2nick := parts[0]
ip2addr := parts[1]
uinfo, ok := this.hc.users[ip2nick]
if !ok {
this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Recieved IP '" + ip2addr + "' for unknown user '" + ip2nick + "'"})
continue nextIPPair
}
if uinfo.IPAddress != ip2addr {
uinfo.IPAddress = ip2addr
notifyOfUpdate = append(notifyOfUpdate, ip2nick)
this.hc.users[ip2nick] = uinfo
}
}
this.hc.usersMut.Unlock()
for _, nick := range notifyOfUpdate {
this.hc.processEvent(HubEvent{EventType: EVENT_USER_UPDATED_INFO, Nick: nick})
}
case "$ForceMove":
this.hc.Hco.Address = HubAddress(commandParts[1])
this.hc.conn.Close() // we'll reconnect onto the new address
case "$UserCommand":
// $UserCommand 1 1 Group chat\New group chat$<%[mynick]> !groupchat_new&#124;|
if this.rxUserCommand.MatchString(commandParts[1]) {
usc := this.rxUserCommand.FindStringSubmatch(commandParts[1])
typeInt, _ := strconv.Atoi(usc[1])
contextInt, _ := strconv.Atoi(usc[2])
uscStruct := UserCommand{
Type: UserCommandType(typeInt),
Context: UserCommandContext(contextInt),
Message: usc[3],
Command: this.unescape(usc[4]),
}
this.hc.processEvent(HubEvent{EventType: EVENT_USERCOMMAND, UserCommand: &uscStruct})
} else {
this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Malformed usercommand '" + commandParts[1] + "'"})
}
case "$Supports":
this.supports = make(map[string]struct{})
for _, s := range strings.Split(commandParts[1], " ") {
this.supports[s] = struct{}{}
}
if !this.sentOurHello {
// Need to log in.
// If the hub supports QuickList, we can skip one network roundtrip
if _, ok := this.supports["QuickList"]; ok {
this.sayInfo()
this.hc.SayRaw("$GetNickList|")
} else {
this.hc.SayRaw("$ValidateNick " + this.escape(this.hc.Hco.Self.Nick) + "|")
}
// This also counts as the end of the handshake from our POV. Consider
// ourselves logged in
this.sentOurHello = true
if this.hc.State != CONNECTIONSTATE_CONNECTED {
this.hc.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_CONNECTED})
this.hc.State = CONNECTIONSTATE_CONNECTED
}
}
// IGNORABLE COMMANDS
case "$HubTopic":
case "$Search":
case "$ConnectToMe":
default:
this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Unhandled protocol command '" + commandParts[0] + "'"})
}
}
func (this *NmdcProtocol) escape(plaintext string) string {
v1 := strings.Replace(plaintext, "&", "&amp;", -1)
v2 := strings.Replace(v1, "|", "&#124;", -1)
return strings.Replace(v2, "$", "&#36;", -1)
}
func (this *NmdcProtocol) unescape(encoded string) string {
v1 := strings.Replace(encoded, "&#36;", "$", -1)
v2 := strings.Replace(v1, "&#124;", "|", -1)
return strings.Replace(v2, "&amp;", "&", -1)
}
func (this *NmdcProtocol) SayPublic(message string) {
this.hc.SayRaw("<" + this.hc.Hco.Self.Nick + "> " + this.escape(message) + "|")
}
func (this *NmdcProtocol) SayPrivate(recipient, message string) {
this.hc.SayRaw("$To: " + recipient + " From: " + this.hc.Hco.Self.Nick + " $<" + this.hc.Hco.Self.Nick + "> " + this.escape(message) + "|")
}
func (this *NmdcProtocol) sayInfo() {
this.hc.SayRaw(this.getUserMyINFO(this.hc.Hco.Self) + "|")
}
func (this *NmdcProtocol) parseMyINFO(protomsg string) (*UserInfo, error) {
ret := UserInfo{}
// Normal format (with tag in exact V/M/H/S order)
matches := this.rxMyInfo.FindStringSubmatch(protomsg)
if matches != nil {
ret.Nick = matches[1]
ret.Description = this.unescape(matches[2])
ret.ClientTag = this.unescape(matches[3])
ret.ClientVersion = matches[4]
ret.ConnectionMode = ConnectionMode(matches[5][0])
maybeParse(matches[6], &ret.HubsUnregistered, 0)
maybeParse(matches[7], &ret.HubsRegistered, 0)
maybeParse(matches[8], &ret.HubsOperator, 0)
maybeParse(matches[9], &ret.Slots, 0)
if len(matches[10]) > 1 {
ret.Speed = matches[10][:len(matches[10])-2]
} else {
ret.Speed = ""
}
ret.Flag = UserFlag(matches[10][len(matches[10])-1])
ret.Email = this.unescape(matches[11])
maybeParse(matches[12], &ret.ShareSize, 0)
return &ret, nil
}
// No-tag format, used in early connection
matches = this.rxMyInfoNoTag.FindStringSubmatch(protomsg)
if matches != nil {
ret.Nick = matches[1]
ret.Description = this.unescape(matches[2])
ret.ClientTag = ""
ret.ClientVersion = "0"
ret.ConnectionMode = CONNECTIONMODE_PASSIVE
ret.HubsUnregistered = 0
ret.HubsRegistered = 0
ret.HubsOperator = 0
ret.Slots = 0
if len(matches[3]) > 1 {
ret.Speed = matches[3][:len(matches[3])-2]
} else {
ret.Speed = ""
}
ret.Flag = UserFlag(matches[3][len(matches[3])-1])
ret.Email = this.unescape(matches[4])
maybeParse(matches[5], &ret.ShareSize, 0)
return &ret, nil
}
// Couldn't get anything out of it...
return nil, errors.New("Malformed MyINFO")
}
// Returns the MyINFO command, WITH leading $MyINFO, and WITHOUT trailing pipe
func (this *NmdcProtocol) getUserMyINFO(u *UserInfo) string {
return fmt.Sprintf(
"$MyINFO $ALL %s %s<%s V:%s,M:%c,H:%d/%d/%d,S:%d>$ $%s%c$%s$%d$",
u.Nick,
u.Description,
u.ClientTag,
strings.Replace(u.ClientVersion, ",", "-", -1), // just in case
u.ConnectionMode,
u.HubsUnregistered,
u.HubsRegistered,
u.HubsOperator,
u.Slots,
u.Speed,
u.Flag,
u.Email,
u.ShareSize,
)
}
func (this *NmdcProtocol) ProtoMessageSeparator() string {
return "|"
}
func (this *NmdcProtocol) unlock(lock []byte) string {
nibble_swap := func(b byte) byte {
return ((b << 4) & 0xF0) | ((b >> 4) & 0x0F)
}
chr := func(b byte) string {
if b == 0 || b == 5 || b == 36 || b == 96 || b == 124 || b == 126 {
return fmt.Sprintf("/%%DCN%04d%%/", b)
} else {
return string(b)
}
}
key := chr(nibble_swap(lock[0] ^ lock[len(lock)-2] ^ lock[len(lock)-3] ^ 5))
for i := 1; i < len(lock); i += 1 {
key += chr(nibble_swap(lock[i] ^ lock[i-1]))
}
return key
}

View File

@ -1,31 +0,0 @@
package libnmdc
type Protocol interface {
ProcessCommand(msg string)
SayPublic(string)
SayPrivate(user, message string)
ProtoMessageSeparator() string
}
type HubProtocol int
const (
HubProtocolAutodetect HubProtocol = 0
HubProtocolNmdc HubProtocol = 1
HubProtocolAdc HubProtocol = 2
)
func (hp HubProtocol) Create(hc *HubConnection) Protocol {
if hp == HubProtocolNmdc {
return NewNmdcProtocol(hc)
} else if hp == HubProtocolAdc {
return NewAdcProtocol(hc)
} else {
return NewAutodetectProtocol(hc)
}
}

View File

@ -1,6 +0,0 @@
NMDC:
- Implement ZPipe ($ZOn)
ADC:
- Usercommands
- ???

View File

@ -1,27 +0,0 @@
package libnmdc
type UserCommandType uint8
const (
USERCOMMAND_TYPE_SEPARATOR UserCommandType = 0
USERCOMMAND_TYPE_RAW UserCommandType = 1
USERCOMMAND_TYPE_NICKLIMITED UserCommandType = 2
USERCOMMAND_TYPE_CLEARALL UserCommandType = 255
)
type UserCommandContext uint8
const (
USERCOMMAND_CONTEXT_HUB UserCommandContext = 1
USERCOMMAND_CONTEXT_USER UserCommandContext = 2
USERCOMMAND_CONTEXT_SEARCH UserCommandContext = 4
USERCOMMAND_CONTEXT_FILELIST UserCommandContext = 8
)
type UserCommand struct {
Type UserCommandType
Context UserCommandContext
Message string
Command string
RemoveThis bool // Currently only set by ADC hubs
}

View File

@ -1,17 +0,0 @@
package libnmdc
type UserFlag byte
const (
FLAG_NORMAL UserFlag = 1
FLAG_AWAY_1 UserFlag = 2
FLAG_AWAY_2 UserFlag = 3
FLAG_SERVER_1 UserFlag = 4
FLAG_SERVER_2 UserFlag = 5
FLAG_SERVER_AWAY_1 UserFlag = 6
FLAG_SERVER_AWAY_2 UserFlag = 7
FLAG_FIREBALL_1 UserFlag = 8
FLAG_FIREBALL_2 UserFlag = 9
FLAG_FIREBALL_AWAY_1 UserFlag = 10
FLAG_FIREBALL_AWAY_2 UserFlag = 11
)

View File

@ -1,53 +0,0 @@
package libnmdc
// This structure represents a user connected to a hub.
type UserInfo struct {
Nick string
Description string
ClientTag string
ClientVersion string
Email string
ShareSize uint64
Flag UserFlag
Slots uint64
HubsUnregistered uint64
HubsRegistered uint64
HubsOperator uint64
IsOperator bool
UserInfo_NMDCOnly
UserInfo_ADCOnly
}
type UserInfo_NMDCOnly struct {
Speed string
IPAddress string
ConnectionMode ConnectionMode
}
type UserInfo_ADCOnly struct {
SharedFiles uint64
UploadSpeedBps uint64
DownloadSpeedBps uint64
IsBot bool
IsRegistered bool
IsSuperUser bool
IsHubOwner bool
IPv4Address string // Passive <==> these fields are not set
IPv6Address string
IPv4UDPPort uint64
IPv6UDPPort uint64
Keyprint string
CID string
SupportFlags map[string]struct{}
}
func NewUserInfo(username string) *UserInfo {
return &UserInfo{
Nick: username,
HubsUnregistered: 1,
UserInfo_NMDCOnly: UserInfo_NMDCOnly{
ConnectionMode: CONNECTIONMODE_PASSIVE,
},
}
}

View File

@ -1,28 +0,0 @@
package libnmdc
import (
"errors"
"strconv"
"time"
)
const (
DEFAULT_CLIENT_TAG string = "libnmdc.go"
DEFAULT_CLIENT_VERSION string = "0.16"
DEFAULT_HUB_NAME string = "(unknown)"
SEND_KEEPALIVE_EVERY time.Duration = 29 * time.Second
AUTO_RECONNECT_AFTER time.Duration = 30 * time.Second
RECONNECT_IF_NO_DATA_RECIEVED_IN time.Duration = 24 * time.Hour // we expect keepalives wayyyy more frequently than this
AUTODETECT_ADC_NMDC_TIMEOUT time.Duration = 3 * time.Second
)
var ErrNotConnected error = errors.New("Not connected")
func maybeParse(str string, dest *uint64, default_val uint64) {
sz, err := strconv.ParseUint(str, 10, 64)
if err == nil {
*dest = sz
} else {
*dest = default_val
}
}

View File

@ -1,41 +0,0 @@
package libnmdc
import (
"encoding/base32"
"errors"
"strings"
"github.com/cxmcc/tiger"
)
// Base32 encodes the input slice in BASE32 string format without any trailing
// padding equals characters.
func Base32(input []byte) string {
return strings.TrimRight(base32.StdEncoding.EncodeToString(input), "=")
}
// TTH returns the TTH hash of a string in raw byte format. Use the Base32()
// function to convert it to normal string format.
// This is a basic implementation that only supports content up to 1024 bytes in length.
func TTH(input string) ([]byte, error) {
// Short segments do not need to be padded.
// Content above 1024 bytes needs tree handling (0x00 prefix for leaf nodes,
// 0x01 prefix for hash nodes) but for content less than 1024 bytes, just
// return the leaf hash
// @ref http://adc.sourceforge.net/draft-jchapweske-thex-02.html
if len(input) > 1024 {
return nil, errors.New("TTH content exceeded 1024 bytes")
}
// Single leaf hash only
leafHash := tiger.New()
leafHash.Write([]byte("\x00" + input))
return leafHash.Sum(nil), nil
}
func Tiger(input string) []byte {
leafHash := tiger.New()
leafHash.Write([]byte(input))
return leafHash.Sum(nil)
}

View File

@ -1,22 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe

View File

@ -1,7 +0,0 @@
language: go
go:
- 1.x
- 1.6
- 1.7.x
- master

View File

@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright (c) 2013 Xiuming Chen
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.

View File

@ -1,48 +0,0 @@
Tiger cryptographic hash function for Go
-----
[![Build Status](https://travis-ci.org/cxmcc/tiger.svg?branch=master)](https://travis-ci.org/cxmcc/tiger)
[![GoDoc](http://godoc.org/github.com/cxmcc/tiger?status.png)](http://godoc.org/github.com/cxmcc/tiger)
### About Tiger
* Tiger cryptographic hash function is designed by Ross Anderson and Eli Biham in 1995.
* The size of a Tiger hash value is 192 bits. Truncated versions (Tiger/128, Tiger/160) are simply prefixes of Tiger/192.
* Tiger2 is a variant where the message is padded by first appending a byte 0x80, rather than 0x01 as in the case of Tiger.
* Links: [paper](http://www.cs.technion.ac.il/~biham/Reports/Tiger/), [wikipedia](http://en.wikipedia.org/wiki/Tiger_\(cryptography\))
### API Documentation
Implementing [hash.Hash](http://golang.org/pkg/hash/#Hash). Usage is pretty much the same as other stanard hashing libraries.
Documentation currently available at Godoc: [http://godoc.org/github.com/cxmcc/tiger](http://godoc.org/github.com/cxmcc/tiger)
### Installing
~~~
go get github.com/cxmcc/tiger
~~~
### Example
~~~ go
package main
import (
"fmt"
"io"
"github.com/cxmcc/tiger"
)
func main() {
h := tiger.New()
io.WriteString(h, "Example for tiger")
fmt.Printf("Output: %x\n", h.Sum(nil))
// Output: 82bd060e19f945014f0063e8f0e6d7decfa9edfd97e76743
}
~~~
### License
It's MIT License

View File

@ -1,96 +0,0 @@
package tiger
import (
"encoding/binary"
"unsafe"
)
var littleEndian bool
func init() {
x := uint32(0x04030201)
y := [4]byte{0x1, 0x2, 0x3, 0x4}
littleEndian = *(*[4]byte)(unsafe.Pointer(&x)) == y
}
func pass(a, b, c uint64, x []uint64, mul uint64) (uint64, uint64, uint64) {
a, b, c = round(a, b, c, x[0], mul)
b, c, a = round(b, c, a, x[1], mul)
c, a, b = round(c, a, b, x[2], mul)
a, b, c = round(a, b, c, x[3], mul)
b, c, a = round(b, c, a, x[4], mul)
c, a, b = round(c, a, b, x[5], mul)
a, b, c = round(a, b, c, x[6], mul)
b, c, a = round(b, c, a, x[7], mul)
return a, b, c
}
func round(a, b, c, x, mul uint64) (uint64, uint64, uint64) {
c ^= x
a -= t1[c&0xff] ^ t2[(c>>16)&0xff] ^ t3[(c>>32)&0xff] ^ t4[(c>>48)&0xff]
b += t4[(c>>8)&0xff] ^ t3[(c>>24)&0xff] ^ t2[(c>>40)&0xff] ^ t1[(c>>56)&0xff]
b *= mul
return a, b, c
}
func keySchedule(x []uint64) {
x[0] -= x[7] ^ 0xa5a5a5a5a5a5a5a5
x[1] ^= x[0]
x[2] += x[1]
x[3] -= x[2] ^ ((^x[1]) << 19)
x[4] ^= x[3]
x[5] += x[4]
x[6] -= x[5] ^ ((^x[4]) >> 23)
x[7] ^= x[6]
x[0] += x[7]
x[1] -= x[0] ^ ((^x[7]) << 19)
x[2] ^= x[1]
x[3] += x[2]
x[4] -= x[3] ^ ((^x[2]) >> 23)
x[5] ^= x[4]
x[6] += x[5]
x[7] -= x[6] ^ 0x0123456789abcdef
}
func (d *digest) compress(data []byte) {
// save_abc
a := d.a
b := d.b
c := d.c
var x []uint64
if littleEndian {
x = []uint64{
binary.LittleEndian.Uint64(data[0:8]),
binary.LittleEndian.Uint64(data[8:16]),
binary.LittleEndian.Uint64(data[16:24]),
binary.LittleEndian.Uint64(data[24:32]),
binary.LittleEndian.Uint64(data[32:40]),
binary.LittleEndian.Uint64(data[40:48]),
binary.LittleEndian.Uint64(data[48:56]),
binary.LittleEndian.Uint64(data[56:64]),
}
} else {
x = []uint64{
binary.BigEndian.Uint64(data[0:8]),
binary.BigEndian.Uint64(data[8:16]),
binary.BigEndian.Uint64(data[16:24]),
binary.BigEndian.Uint64(data[24:32]),
binary.BigEndian.Uint64(data[32:40]),
binary.BigEndian.Uint64(data[40:48]),
binary.BigEndian.Uint64(data[48:56]),
binary.BigEndian.Uint64(data[56:64]),
}
}
d.a, d.b, d.c = pass(d.a, d.b, d.c, x, 5)
keySchedule(x)
d.c, d.a, d.b = pass(d.c, d.a, d.b, x, 7)
keySchedule(x)
d.b, d.c, d.a = pass(d.b, d.c, d.a, x, 9)
// feedforward
d.a ^= a
d.b -= b
d.c += c
}

View File

@ -1,269 +0,0 @@
package tiger
var t1 = [...]uint64{
0x02aab17cf7e90c5e, 0xac424b03e243a8ec, 0x72cd5be30dd5fcd3, 0x6d019b93f6f97f3a,
0xcd9978ffd21f9193, 0x7573a1c9708029e2, 0xb164326b922a83c3, 0x46883eee04915870,
0xeaace3057103ece6, 0xc54169b808a3535c, 0x4ce754918ddec47c, 0x0aa2f4dfdc0df40c,
0x10b76f18a74dbefa, 0xc6ccb6235ad1ab6a, 0x13726121572fe2ff, 0x1a488c6f199d921e,
0x4bc9f9f4da0007ca, 0x26f5e6f6e85241c7, 0x859079dbea5947b6, 0x4f1885c5c99e8c92,
0xd78e761ea96f864b, 0x8e36428c52b5c17d, 0x69cf6827373063c1, 0xb607c93d9bb4c56e,
0x7d820e760e76b5ea, 0x645c9cc6f07fdc42, 0xbf38a078243342e0, 0x5f6b343c9d2e7d04,
0xf2c28aeb600b0ec6, 0x6c0ed85f7254bcac, 0x71592281a4db4fe5, 0x1967fa69ce0fed9f,
0xfd5293f8b96545db, 0xc879e9d7f2a7600b, 0x860248920193194e, 0xa4f9533b2d9cc0b3,
0x9053836c15957613, 0xdb6dcf8afc357bf1, 0x18beea7a7a370f57, 0x037117ca50b99066,
0x6ab30a9774424a35, 0xf4e92f02e325249b, 0x7739db07061ccae1, 0xd8f3b49ceca42a05,
0xbd56be3f51382f73, 0x45faed5843b0bb28, 0x1c813d5c11bf1f83, 0x8af0e4b6d75fa169,
0x33ee18a487ad9999, 0x3c26e8eab1c94410, 0xb510102bc0a822f9, 0x141eef310ce6123b,
0xfc65b90059ddb154, 0xe0158640c5e0e607, 0x884e079826c3a3cf, 0x930d0d9523c535fd,
0x35638d754e9a2b00, 0x4085fccf40469dd5, 0xc4b17ad28be23a4c, 0xcab2f0fc6a3e6a2e,
0x2860971a6b943fcd, 0x3dde6ee212e30446, 0x6222f32ae01765ae, 0x5d550bb5478308fe,
0xa9efa98da0eda22a, 0xc351a71686c40da7, 0x1105586d9c867c84, 0xdcffee85fda22853,
0xccfbd0262c5eef76, 0xbaf294cb8990d201, 0xe69464f52afad975, 0x94b013afdf133e14,
0x06a7d1a32823c958, 0x6f95fe5130f61119, 0xd92ab34e462c06c0, 0xed7bde33887c71d2,
0x79746d6e6518393e, 0x5ba419385d713329, 0x7c1ba6b948a97564, 0x31987c197bfdac67,
0xde6c23c44b053d02, 0x581c49fed002d64d, 0xdd474d6338261571, 0xaa4546c3e473d062,
0x928fce349455f860, 0x48161bbacaab94d9, 0x63912430770e6f68, 0x6ec8a5e602c6641c,
0x87282515337ddd2b, 0x2cda6b42034b701b, 0xb03d37c181cb096d, 0xe108438266c71c6f,
0x2b3180c7eb51b255, 0xdf92b82f96c08bbc, 0x5c68c8c0a632f3ba, 0x5504cc861c3d0556,
0xabbfa4e55fb26b8f, 0x41848b0ab3baceb4, 0xb334a273aa445d32, 0xbca696f0a85ad881,
0x24f6ec65b528d56c, 0x0ce1512e90f4524a, 0x4e9dd79d5506d35a, 0x258905fac6ce9779,
0x2019295b3e109b33, 0xf8a9478b73a054cc, 0x2924f2f934417eb0, 0x3993357d536d1bc4,
0x38a81ac21db6ff8b, 0x47c4fbf17d6016bf, 0x1e0faadd7667e3f5, 0x7abcff62938beb96,
0xa78dad948fc179c9, 0x8f1f98b72911e50d, 0x61e48eae27121a91, 0x4d62f7ad31859808,
0xeceba345ef5ceaeb, 0xf5ceb25ebc9684ce, 0xf633e20cb7f76221, 0xa32cdf06ab8293e4,
0x985a202ca5ee2ca4, 0xcf0b8447cc8a8fb1, 0x9f765244979859a3, 0xa8d516b1a1240017,
0x0bd7ba3ebb5dc726, 0xe54bca55b86adb39, 0x1d7a3afd6c478063, 0x519ec608e7669edd,
0x0e5715a2d149aa23, 0x177d4571848ff194, 0xeeb55f3241014c22, 0x0f5e5ca13a6e2ec2,
0x8029927b75f5c361, 0xad139fabc3d6e436, 0x0d5df1a94ccf402f, 0x3e8bd948bea5dfc8,
0xa5a0d357bd3ff77e, 0xa2d12e251f74f645, 0x66fd9e525e81a082, 0x2e0c90ce7f687a49,
0xc2e8bcbeba973bc5, 0x000001bce509745f, 0x423777bbe6dab3d6, 0xd1661c7eaef06eb5,
0xa1781f354daacfd8, 0x2d11284a2b16affc, 0xf1fc4f67fa891d1f, 0x73ecc25dcb920ada,
0xae610c22c2a12651, 0x96e0a810d356b78a, 0x5a9a381f2fe7870f, 0xd5ad62ede94e5530,
0xd225e5e8368d1427, 0x65977b70c7af4631, 0x99f889b2de39d74f, 0x233f30bf54e1d143,
0x9a9675d3d9a63c97, 0x5470554ff334f9a8, 0x166acb744a4f5688, 0x70c74caab2e4aead,
0xf0d091646f294d12, 0x57b82a89684031d1, 0xefd95a5a61be0b6b, 0x2fbd12e969f2f29a,
0x9bd37013feff9fe8, 0x3f9b0404d6085a06, 0x4940c1f3166cfe15, 0x09542c4dcdf3defb,
0xb4c5218385cd5ce3, 0xc935b7dc4462a641, 0x3417f8a68ed3b63f, 0xb80959295b215b40,
0xf99cdaef3b8c8572, 0x018c0614f8fcb95d, 0x1b14accd1a3acdf3, 0x84d471f200bb732d,
0xc1a3110e95e8da16, 0x430a7220bf1a82b8, 0xb77e090d39df210e, 0x5ef4bd9f3cd05e9d,
0x9d4ff6da7e57a444, 0xda1d60e183d4a5f8, 0xb287c38417998e47, 0xfe3edc121bb31886,
0xc7fe3ccc980ccbef, 0xe46fb590189bfd03, 0x3732fd469a4c57dc, 0x7ef700a07cf1ad65,
0x59c64468a31d8859, 0x762fb0b4d45b61f6, 0x155baed099047718, 0x68755e4c3d50baa6,
0xe9214e7f22d8b4df, 0x2addbf532eac95f4, 0x32ae3909b4bd0109, 0x834df537b08e3450,
0xfa209da84220728d, 0x9e691d9b9efe23f7, 0x0446d288c4ae8d7f, 0x7b4cc524e169785b,
0x21d87f0135ca1385, 0xcebb400f137b8aa5, 0x272e2b66580796be, 0x3612264125c2b0de,
0x057702bdad1efbb2, 0xd4babb8eacf84be9, 0x91583139641bc67b, 0x8bdc2de08036e024,
0x603c8156f49f68ed, 0xf7d236f7dbef5111, 0x9727c4598ad21e80, 0xa08a0896670a5fd7,
0xcb4a8f4309eba9cb, 0x81af564b0f7036a1, 0xc0b99aa778199abd, 0x959f1ec83fc8e952,
0x8c505077794a81b9, 0x3acaaf8f056338f0, 0x07b43f50627a6778, 0x4a44ab49f5eccc77,
0x3bc3d6e4b679ee98, 0x9cc0d4d1cf14108c, 0x4406c00b206bc8a0, 0x82a18854c8d72d89,
0x67e366b35c3c432c, 0xb923dd61102b37f2, 0x56ab2779d884271d, 0xbe83e1b0ff1525af,
0xfb7c65d4217e49a9, 0x6bdbe0e76d48e7d4, 0x08df828745d9179e, 0x22ea6a9add53bd34,
0xe36e141c5622200a, 0x7f805d1b8cb750ee, 0xafe5c7a59f58e837, 0xe27f996a4fb1c23c,
0xd3867dfb0775f0d0, 0xd0e673de6e88891a, 0x123aeb9eafb86c25, 0x30f1d5d5c145b895,
0xbb434a2dee7269e7, 0x78cb67ecf931fa38, 0xf33b0372323bbf9c, 0x52d66336fb279c74,
0x505f33ac0afb4eaa, 0xe8a5cd99a2cce187, 0x534974801e2d30bb, 0x8d2d5711d5876d90,
0x1f1a412891bc038e, 0xd6e2e71d82e56648, 0x74036c3a497732b7, 0x89b67ed96361f5ab,
0xffed95d8f1ea02a2, 0xe72b3bd61464d43d, 0xa6300f170bdc4820, 0xebc18760ed78a77a,
}
var t2 = [...]uint64{
0xe6a6be5a05a12138, 0xb5a122a5b4f87c98, 0x563c6089140b6990, 0x4c46cb2e391f5dd5,
0xd932addbc9b79434, 0x08ea70e42015aff5, 0xd765a6673e478cf1, 0xc4fb757eab278d99,
0xdf11c6862d6e0692, 0xddeb84f10d7f3b16, 0x6f2ef604a665ea04, 0x4a8e0f0ff0e0dfb3,
0xa5edeef83dbcba51, 0xfc4f0a2a0ea4371e, 0xe83e1da85cb38429, 0xdc8ff882ba1b1ce2,
0xcd45505e8353e80d, 0x18d19a00d4db0717, 0x34a0cfeda5f38101, 0x0be77e518887caf2,
0x1e341438b3c45136, 0xe05797f49089ccf9, 0xffd23f9df2591d14, 0x543dda228595c5cd,
0x661f81fd99052a33, 0x8736e641db0f7b76, 0x15227725418e5307, 0xe25f7f46162eb2fa,
0x48a8b2126c13d9fe, 0xafdc541792e76eea, 0x03d912bfc6d1898f, 0x31b1aafa1b83f51b,
0xf1ac2796e42ab7d9, 0x40a3a7d7fcd2ebac, 0x1056136d0afbbcc5, 0x7889e1dd9a6d0c85,
0xd33525782a7974aa, 0xa7e25d09078ac09b, 0xbd4138b3eac6edd0, 0x920abfbe71eb9e70,
0xa2a5d0f54fc2625c, 0xc054e36b0b1290a3, 0xf6dd59ff62fe932b, 0x3537354511a8ac7d,
0xca845e9172fadcd4, 0x84f82b60329d20dc, 0x79c62ce1cd672f18, 0x8b09a2add124642c,
0xd0c1e96a19d9e726, 0x5a786a9b4ba9500c, 0x0e020336634c43f3, 0xc17b474aeb66d822,
0x6a731ae3ec9baac2, 0x8226667ae0840258, 0x67d4567691caeca5, 0x1d94155c4875adb5,
0x6d00fd985b813fdf, 0x51286efcb774cd06, 0x5e8834471fa744af, 0xf72ca0aee761ae2e,
0xbe40e4cdaee8e09a, 0xe9970bbb5118f665, 0x726e4beb33df1964, 0x703b000729199762,
0x4631d816f5ef30a7, 0xb880b5b51504a6be, 0x641793c37ed84b6c, 0x7b21ed77f6e97d96,
0x776306312ef96b73, 0xae528948e86ff3f4, 0x53dbd7f286a3f8f8, 0x16cadce74cfc1063,
0x005c19bdfa52c6dd, 0x68868f5d64d46ad3, 0x3a9d512ccf1e186a, 0x367e62c2385660ae,
0xe359e7ea77dcb1d7, 0x526c0773749abe6e, 0x735ae5f9d09f734b, 0x493fc7cc8a558ba8,
0xb0b9c1533041ab45, 0x321958ba470a59bd, 0x852db00b5f46c393, 0x91209b2bd336b0e5,
0x6e604f7d659ef19f, 0xb99a8ae2782ccb24, 0xccf52ab6c814c4c7, 0x4727d9afbe11727b,
0x7e950d0c0121b34d, 0x756f435670ad471f, 0xf5add442615a6849, 0x4e87e09980b9957a,
0x2acfa1df50aee355, 0xd898263afd2fd556, 0xc8f4924dd80c8fd6, 0xcf99ca3d754a173a,
0xfe477bacaf91bf3c, 0xed5371f6d690c12d, 0x831a5c285e687094, 0xc5d3c90a3708a0a4,
0x0f7f903717d06580, 0x19f9bb13b8fdf27f, 0xb1bd6f1b4d502843, 0x1c761ba38fff4012,
0x0d1530c4e2e21f3b, 0x8943ce69a7372c8a, 0xe5184e11feb5ce66, 0x618bdb80bd736621,
0x7d29bad68b574d0b, 0x81bb613e25e6fe5b, 0x071c9c10bc07913f, 0xc7beeb7909ac2d97,
0xc3e58d353bc5d757, 0xeb017892f38f61e8, 0xd4effb9c9b1cc21a, 0x99727d26f494f7ab,
0xa3e063a2956b3e03, 0x9d4a8b9a4aa09c30, 0x3f6ab7d500090fb4, 0x9cc0f2a057268ac0,
0x3dee9d2dedbf42d1, 0x330f49c87960a972, 0xc6b2720287421b41, 0x0ac59ec07c00369c,
0xef4eac49cb353425, 0xf450244eef0129d8, 0x8acc46e5caf4deb6, 0x2ffeab63989263f7,
0x8f7cb9fe5d7a4578, 0x5bd8f7644e634635, 0x427a7315bf2dc900, 0x17d0c4aa2125261c,
0x3992486c93518e50, 0xb4cbfee0a2d7d4c3, 0x7c75d6202c5ddd8d, 0xdbc295d8e35b6c61,
0x60b369d302032b19, 0xce42685fdce44132, 0x06f3ddb9ddf65610, 0x8ea4d21db5e148f0,
0x20b0fce62fcd496f, 0x2c1b912358b0ee31, 0xb28317b818f5a308, 0xa89c1e189ca6d2cf,
0x0c6b18576aaadbc8, 0xb65deaa91299fae3, 0xfb2b794b7f1027e7, 0x04e4317f443b5beb,
0x4b852d325939d0a6, 0xd5ae6beefb207ffc, 0x309682b281c7d374, 0xbae309a194c3b475,
0x8cc3f97b13b49f05, 0x98a9422ff8293967, 0x244b16b01076ff7c, 0xf8bf571c663d67ee,
0x1f0d6758eee30da1, 0xc9b611d97adeb9b7, 0xb7afd5887b6c57a2, 0x6290ae846b984fe1,
0x94df4cdeacc1a5fd, 0x058a5bd1c5483aff, 0x63166cc142ba3c37, 0x8db8526eb2f76f40,
0xe10880036f0d6d4e, 0x9e0523c9971d311d, 0x45ec2824cc7cd691, 0x575b8359e62382c9,
0xfa9e400dc4889995, 0xd1823ecb45721568, 0xdafd983b8206082f, 0xaa7d29082386a8cb,
0x269fcd4403b87588, 0x1b91f5f728bdd1e0, 0xe4669f39040201f6, 0x7a1d7c218cf04ade,
0x65623c29d79ce5ce, 0x2368449096c00bb1, 0xab9bf1879da503ba, 0xbc23ecb1a458058e,
0x9a58df01bb401ecc, 0xa070e868a85f143d, 0x4ff188307df2239e, 0x14d565b41a641183,
0xee13337452701602, 0x950e3dcf3f285e09, 0x59930254b9c80953, 0x3bf299408930da6d,
0xa955943f53691387, 0xa15edecaa9cb8784, 0x29142127352be9a0, 0x76f0371fff4e7afb,
0x0239f450274f2228, 0xbb073af01d5e868b, 0xbfc80571c10e96c1, 0xd267088568222e23,
0x9671a3d48e80b5b0, 0x55b5d38ae193bb81, 0x693ae2d0a18b04b8, 0x5c48b4ecadd5335f,
0xfd743b194916a1ca, 0x2577018134be98c4, 0xe77987e83c54a4ad, 0x28e11014da33e1b9,
0x270cc59e226aa213, 0x71495f756d1a5f60, 0x9be853fb60afef77, 0xadc786a7f7443dbf,
0x0904456173b29a82, 0x58bc7a66c232bd5e, 0xf306558c673ac8b2, 0x41f639c6b6c9772a,
0x216defe99fda35da, 0x11640cc71c7be615, 0x93c43694565c5527, 0xea038e6246777839,
0xf9abf3ce5a3e2469, 0x741e768d0fd312d2, 0x0144b883ced652c6, 0xc20b5a5ba33f8552,
0x1ae69633c3435a9d, 0x97a28ca4088cfdec, 0x8824a43c1e96f420, 0x37612fa66eeea746,
0x6b4cb165f9cf0e5a, 0x43aa1c06a0abfb4a, 0x7f4dc26ff162796b, 0x6cbacc8e54ed9b0f,
0xa6b7ffefd2bb253e, 0x2e25bc95b0a29d4f, 0x86d6a58bdef1388c, 0xded74ac576b6f054,
0x8030bdbc2b45805d, 0x3c81af70e94d9289, 0x3eff6dda9e3100db, 0xb38dc39fdfcc8847,
0x123885528d17b87e, 0xf2da0ed240b1b642, 0x44cefadcd54bf9a9, 0x1312200e433c7ee6,
0x9ffcc84f3a78c748, 0xf0cd1f72248576bb, 0xec6974053638cfe4, 0x2ba7b67c0cec4e4c,
0xac2f4df3e5ce32ed, 0xcb33d14326ea4c11, 0xa4e9044cc77e58bc, 0x5f513293d934fcef,
0x5dc9645506e55444, 0x50de418f317de40a, 0x388cb31a69dde259, 0x2db4a83455820a86,
0x9010a91e84711ae9, 0x4df7f0b7b1498371, 0xd62a2eabc0977179, 0x22fac097aa8d5c0e,
}
var t3 = [...]uint64{
0xf49fcc2ff1daf39b, 0x487fd5c66ff29281, 0xe8a30667fcdca83f, 0x2c9b4be3d2fcce63,
0xda3ff74b93fbbbc2, 0x2fa165d2fe70ba66, 0xa103e279970e93d4, 0xbecdec77b0e45e71,
0xcfb41e723985e497, 0xb70aaa025ef75017, 0xd42309f03840b8e0, 0x8efc1ad035898579,
0x96c6920be2b2abc5, 0x66af4163375a9172, 0x2174abdcca7127fb, 0xb33ccea64a72ff41,
0xf04a4933083066a5, 0x8d970acdd7289af5, 0x8f96e8e031c8c25e, 0xf3fec02276875d47,
0xec7bf310056190dd, 0xf5adb0aebb0f1491, 0x9b50f8850fd58892, 0x4975488358b74de8,
0xa3354ff691531c61, 0x0702bbe481d2c6ee, 0x89fb24057deded98, 0xac3075138596e902,
0x1d2d3580172772ed, 0xeb738fc28e6bc30d, 0x5854ef8f63044326, 0x9e5c52325add3bbe,
0x90aa53cf325c4623, 0xc1d24d51349dd067, 0x2051cfeea69ea624, 0x13220f0a862e7e4f,
0xce39399404e04864, 0xd9c42ca47086fcb7, 0x685ad2238a03e7cc, 0x066484b2ab2ff1db,
0xfe9d5d70efbf79ec, 0x5b13b9dd9c481854, 0x15f0d475ed1509ad, 0x0bebcd060ec79851,
0xd58c6791183ab7f8, 0xd1187c5052f3eee4, 0xc95d1192e54e82ff, 0x86eea14cb9ac6ca2,
0x3485beb153677d5d, 0xdd191d781f8c492a, 0xf60866baa784ebf9, 0x518f643ba2d08c74,
0x8852e956e1087c22, 0xa768cb8dc410ae8d, 0x38047726bfec8e1a, 0xa67738b4cd3b45aa,
0xad16691cec0dde19, 0xc6d4319380462e07, 0xc5a5876d0ba61938, 0x16b9fa1fa58fd840,
0x188ab1173ca74f18, 0xabda2f98c99c021f, 0x3e0580ab134ae816, 0x5f3b05b773645abb,
0x2501a2be5575f2f6, 0x1b2f74004e7e8ba9, 0x1cd7580371e8d953, 0x7f6ed89562764e30,
0xb15926ff596f003d, 0x9f65293da8c5d6b9, 0x6ecef04dd690f84c, 0x4782275fff33af88,
0xe41433083f820801, 0xfd0dfe409a1af9b5, 0x4325a3342cdb396b, 0x8ae77e62b301b252,
0xc36f9e9f6655615a, 0x85455a2d92d32c09, 0xf2c7dea949477485, 0x63cfb4c133a39eba,
0x83b040cc6ebc5462, 0x3b9454c8fdb326b0, 0x56f56a9e87ffd78c, 0x2dc2940d99f42bc6,
0x98f7df096b096e2d, 0x19a6e01e3ad852bf, 0x42a99ccbdbd4b40b, 0xa59998af45e9c559,
0x366295e807d93186, 0x6b48181bfaa1f773, 0x1fec57e2157a0a1d, 0x4667446af6201ad5,
0xe615ebcacfb0f075, 0xb8f31f4f68290778, 0x22713ed6ce22d11e, 0x3057c1a72ec3c93b,
0xcb46acc37c3f1f2f, 0xdbb893fd02aaf50e, 0x331fd92e600b9fcf, 0xa498f96148ea3ad6,
0xa8d8426e8b6a83ea, 0xa089b274b7735cdc, 0x87f6b3731e524a11, 0x118808e5cbc96749,
0x9906e4c7b19bd394, 0xafed7f7e9b24a20c, 0x6509eadeeb3644a7, 0x6c1ef1d3e8ef0ede,
0xb9c97d43e9798fb4, 0xa2f2d784740c28a3, 0x7b8496476197566f, 0x7a5be3e6b65f069d,
0xf96330ed78be6f10, 0xeee60de77a076a15, 0x2b4bee4aa08b9bd0, 0x6a56a63ec7b8894e,
0x02121359ba34fef4, 0x4cbf99f8283703fc, 0x398071350caf30c8, 0xd0a77a89f017687a,
0xf1c1a9eb9e423569, 0x8c7976282dee8199, 0x5d1737a5dd1f7abd, 0x4f53433c09a9fa80,
0xfa8b0c53df7ca1d9, 0x3fd9dcbc886ccb77, 0xc040917ca91b4720, 0x7dd00142f9d1dcdf,
0x8476fc1d4f387b58, 0x23f8e7c5f3316503, 0x032a2244e7e37339, 0x5c87a5d750f5a74b,
0x082b4cc43698992e, 0xdf917becb858f63c, 0x3270b8fc5bf86dda, 0x10ae72bb29b5dd76,
0x576ac94e7700362b, 0x1ad112dac61efb8f, 0x691bc30ec5faa427, 0xff246311cc327143,
0x3142368e30e53206, 0x71380e31e02ca396, 0x958d5c960aad76f1, 0xf8d6f430c16da536,
0xc8ffd13f1be7e1d2, 0x7578ae66004ddbe1, 0x05833f01067be646, 0xbb34b5ad3bfe586d,
0x095f34c9a12b97f0, 0x247ab64525d60ca8, 0xdcdbc6f3017477d1, 0x4a2e14d4decad24d,
0xbdb5e6d9be0a1eeb, 0x2a7e70f7794301ab, 0xdef42d8a270540fd, 0x01078ec0a34c22c1,
0xe5de511af4c16387, 0x7ebb3a52bd9a330a, 0x77697857aa7d6435, 0x004e831603ae4c32,
0xe7a21020ad78e312, 0x9d41a70c6ab420f2, 0x28e06c18ea1141e6, 0xd2b28cbd984f6b28,
0x26b75f6c446e9d83, 0xba47568c4d418d7f, 0xd80badbfe6183d8e, 0x0e206d7f5f166044,
0xe258a43911cbca3e, 0x723a1746b21dc0bc, 0xc7caa854f5d7cdd3, 0x7cac32883d261d9c,
0x7690c26423ba942c, 0x17e55524478042b8, 0xe0be477656a2389f, 0x4d289b5e67ab2da0,
0x44862b9c8fbbfd31, 0xb47cc8049d141365, 0x822c1b362b91c793, 0x4eb14655fb13dfd8,
0x1ecbba0714e2a97b, 0x6143459d5cde5f14, 0x53a8fbf1d5f0ac89, 0x97ea04d81c5e5b00,
0x622181a8d4fdb3f3, 0xe9bcd341572a1208, 0x1411258643cce58a, 0x9144c5fea4c6e0a4,
0x0d33d06565cf620f, 0x54a48d489f219ca1, 0xc43e5eac6d63c821, 0xa9728b3a72770daf,
0xd7934e7b20df87ef, 0xe35503b61a3e86e5, 0xcae321fbc819d504, 0x129a50b3ac60bfa6,
0xcd5e68ea7e9fb6c3, 0xb01c90199483b1c7, 0x3de93cd5c295376c, 0xaed52edf2ab9ad13,
0x2e60f512c0a07884, 0xbc3d86a3e36210c9, 0x35269d9b163951ce, 0x0c7d6e2ad0cdb5fa,
0x59e86297d87f5733, 0x298ef221898db0e7, 0x55000029d1a5aa7e, 0x8bc08ae1b5061b45,
0xc2c31c2b6c92703a, 0x94cc596baf25ef42, 0x0a1d73db22540456, 0x04b6a0f9d9c4179a,
0xeffdafa2ae3d3c60, 0xf7c8075bb49496c4, 0x9cc5c7141d1cd4e3, 0x78bd1638218e5534,
0xb2f11568f850246a, 0xedfabcfa9502bc29, 0x796ce5f2da23051b, 0xaae128b0dc93537c,
0x3a493da0ee4b29ae, 0xb5df6b2c416895d7, 0xfcabbd25122d7f37, 0x70810b58105dc4b1,
0xe10fdd37f7882a90, 0x524dcab5518a3f5c, 0x3c9e85878451255b, 0x4029828119bd34e2,
0x74a05b6f5d3ceccb, 0xb610021542e13eca, 0x0ff979d12f59e2ac, 0x6037da27e4f9cc50,
0x5e92975a0df1847d, 0xd66de190d3e623fe, 0x5032d6b87b568048, 0x9a36b7ce8235216e,
0x80272a7a24f64b4a, 0x93efed8b8c6916f7, 0x37ddbff44cce1555, 0x4b95db5d4b99bd25,
0x92d3fda169812fc0, 0xfb1a4a9a90660bb6, 0x730c196946a4b9b2, 0x81e289aa7f49da68,
0x64669a0f83b1a05f, 0x27b3ff7d9644f48b, 0xcc6b615c8db675b3, 0x674f20b9bcebbe95,
0x6f31238275655982, 0x5ae488713e45cf05, 0xbf619f9954c21157, 0xeabac46040a8eae9,
0x454c6fe9f2c0c1cd, 0x419cf6496412691c, 0xd3dc3bef265b0f70, 0x6d0e60f5c3578a9e,
}
var t4 = [...]uint64{
0x5b0e608526323c55, 0x1a46c1a9fa1b59f5, 0xa9e245a17c4c8ffa, 0x65ca5159db2955d7,
0x05db0a76ce35afc2, 0x81eac77ea9113d45, 0x528ef88ab6ac0a0d, 0xa09ea253597be3ff,
0x430ddfb3ac48cd56, 0xc4b3a67af45ce46f, 0x4ececfd8fbe2d05e, 0x3ef56f10b39935f0,
0x0b22d6829cd619c6, 0x17fd460a74df2069, 0x6cf8cc8e8510ed40, 0xd6c824bf3a6ecaa7,
0x61243d581a817049, 0x048bacb6bbc163a2, 0xd9a38ac27d44cc32, 0x7fddff5baaf410ab,
0xad6d495aa804824b, 0xe1a6a74f2d8c9f94, 0xd4f7851235dee8e3, 0xfd4b7f886540d893,
0x247c20042aa4bfda, 0x096ea1c517d1327c, 0xd56966b4361a6685, 0x277da5c31221057d,
0x94d59893a43acff7, 0x64f0c51ccdc02281, 0x3d33bcc4ff6189db, 0xe005cb184ce66af1,
0xff5ccd1d1db99bea, 0xb0b854a7fe42980f, 0x7bd46a6a718d4b9f, 0xd10fa8cc22a5fd8c,
0xd31484952be4bd31, 0xc7fa975fcb243847, 0x4886ed1e5846c407, 0x28cddb791eb70b04,
0xc2b00be2f573417f, 0x5c9590452180f877, 0x7a6bddfff370eb00, 0xce509e38d6d9d6a4,
0xebeb0f00647fa702, 0x1dcc06cf76606f06, 0xe4d9f28ba286ff0a, 0xd85a305dc918c262,
0x475b1d8732225f54, 0x2d4fb51668ccb5fe, 0xa679b9d9d72bba20, 0x53841c0d912d43a5,
0x3b7eaa48bf12a4e8, 0x781e0e47f22f1ddf, 0xeff20ce60ab50973, 0x20d261d19dffb742,
0x16a12b03062a2e39, 0x1960eb2239650495, 0x251c16fed50eb8b8, 0x9ac0c330f826016e,
0xed152665953e7671, 0x02d63194a6369570, 0x5074f08394b1c987, 0x70ba598c90b25ce1,
0x794a15810b9742f6, 0x0d5925e9fcaf8c6c, 0x3067716cd868744e, 0x910ab077e8d7731b,
0x6a61bbdb5ac42f61, 0x93513efbf0851567, 0xf494724b9e83e9d5, 0xe887e1985c09648d,
0x34b1d3c675370cfd, 0xdc35e433bc0d255d, 0xd0aab84234131be0, 0x08042a50b48b7eaf,
0x9997c4ee44a3ab35, 0x829a7b49201799d0, 0x263b8307b7c54441, 0x752f95f4fd6a6ca6,
0x927217402c08c6e5, 0x2a8ab754a795d9ee, 0xa442f7552f72943d, 0x2c31334e19781208,
0x4fa98d7ceaee6291, 0x55c3862f665db309, 0xbd0610175d53b1f3, 0x46fe6cb840413f27,
0x3fe03792df0cfa59, 0xcfe700372eb85e8f, 0xa7be29e7adbce118, 0xe544ee5cde8431dd,
0x8a781b1b41f1873e, 0xa5c94c78a0d2f0e7, 0x39412e2877b60728, 0xa1265ef3afc9a62c,
0xbcc2770c6a2506c5, 0x3ab66dd5dce1ce12, 0xe65499d04a675b37, 0x7d8f523481bfd216,
0x0f6f64fcec15f389, 0x74efbe618b5b13c8, 0xacdc82b714273e1d, 0xdd40bfe003199d17,
0x37e99257e7e061f8, 0xfa52626904775aaa, 0x8bbbf63a463d56f9, 0xf0013f1543a26e64,
0xa8307e9f879ec898, 0xcc4c27a4150177cc, 0x1b432f2cca1d3348, 0xde1d1f8f9f6fa013,
0x606602a047a7ddd6, 0xd237ab64cc1cb2c7, 0x9b938e7225fcd1d3, 0xec4e03708e0ff476,
0xfeb2fbda3d03c12d, 0xae0bced2ee43889a, 0x22cb8923ebfb4f43, 0x69360d013cf7396d,
0x855e3602d2d4e022, 0x073805bad01f784c, 0x33e17a133852f546, 0xdf4874058ac7b638,
0xba92b29c678aa14a, 0x0ce89fc76cfaadcd, 0x5f9d4e0908339e34, 0xf1afe9291f5923b9,
0x6e3480f60f4a265f, 0xeebf3a2ab29b841c, 0xe21938a88f91b4ad, 0x57dfeff845c6d3c3,
0x2f006b0bf62caaf2, 0x62f479ef6f75ee78, 0x11a55ad41c8916a9, 0xf229d29084fed453,
0x42f1c27b16b000e6, 0x2b1f76749823c074, 0x4b76eca3c2745360, 0x8c98f463b91691bd,
0x14bcc93cf1ade66a, 0x8885213e6d458397, 0x8e177df0274d4711, 0xb49b73b5503f2951,
0x10168168c3f96b6b, 0x0e3d963b63cab0ae, 0x8dfc4b5655a1db14, 0xf789f1356e14de5c,
0x683e68af4e51dac1, 0xc9a84f9d8d4b0fd9, 0x3691e03f52a0f9d1, 0x5ed86e46e1878e80,
0x3c711a0e99d07150, 0x5a0865b20c4e9310, 0x56fbfc1fe4f0682e, 0xea8d5de3105edf9b,
0x71abfdb12379187a, 0x2eb99de1bee77b9c, 0x21ecc0ea33cf4523, 0x59a4d7521805c7a1,
0x3896f5eb56ae7c72, 0xaa638f3db18f75dc, 0x9f39358dabe9808e, 0xb7defa91c00b72ac,
0x6b5541fd62492d92, 0x6dc6dee8f92e4d5b, 0x353f57abc4beea7e, 0x735769d6da5690ce,
0x0a234aa642391484, 0xf6f9508028f80d9d, 0xb8e319a27ab3f215, 0x31ad9c1151341a4d,
0x773c22a57bef5805, 0x45c7561a07968633, 0xf913da9e249dbe36, 0xda652d9b78a64c68,
0x4c27a97f3bc334ef, 0x76621220e66b17f4, 0x967743899acd7d0b, 0xf3ee5bcae0ed6782,
0x409f753600c879fc, 0x06d09a39b5926db6, 0x6f83aeb0317ac588, 0x01e6ca4a86381f21,
0x66ff3462d19f3025, 0x72207c24ddfd3bfb, 0x4af6b6d3e2ece2eb, 0x9c994dbec7ea08de,
0x49ace597b09a8bc4, 0xb38c4766cf0797ba, 0x131b9373c57c2a75, 0xb1822cce61931e58,
0x9d7555b909ba1c0c, 0x127fafdd937d11d2, 0x29da3badc66d92e4, 0xa2c1d57154c2ecbc,
0x58c5134d82f6fe24, 0x1c3ae3515b62274f, 0xe907c82e01cb8126, 0xf8ed091913e37fcb,
0x3249d8f9c80046c9, 0x80cf9bede388fb63, 0x1881539a116cf19e, 0x5103f3f76bd52457,
0x15b7e6f5ae47f7a8, 0xdbd7c6ded47e9ccf, 0x44e55c410228bb1a, 0xb647d4255edb4e99,
0x5d11882bb8aafc30, 0xf5098bbb29d3212a, 0x8fb5ea14e90296b3, 0x677b942157dd025a,
0xfb58e7c0a390acb5, 0x89d3674c83bd4a01, 0x9e2da4df4bf3b93b, 0xfcc41e328cab4829,
0x03f38c96ba582c52, 0xcad1bdbd7fd85db2, 0xbbb442c16082ae83, 0xb95fe86ba5da9ab0,
0xb22e04673771a93f, 0x845358c9493152d8, 0xbe2a488697b4541e, 0x95a2dc2dd38e6966,
0xc02c11ac923c852b, 0x2388b1990df2a87b, 0x7c8008fa1b4f37be, 0x1f70d0c84d54e503,
0x5490adec7ece57d4, 0x002b3c27d9063a3a, 0x7eaea3848030a2bf, 0xc602326ded2003c0,
0x83a7287d69a94086, 0xc57a5fcb30f57a8a, 0xb56844e479ebe779, 0xa373b40f05dcbce9,
0xd71a786e88570ee2, 0x879cbacdbde8f6a0, 0x976ad1bcc164a32f, 0xab21e25e9666d78b,
0x901063aae5e5c33c, 0x9818b34448698d90, 0xe36487ae3e1e8abb, 0xafbdf931893bdcb4,
0x6345a0dc5fbbd519, 0x8628fe269b9465ca, 0x1e5d01603f9c51ec, 0x4de44006a15049b7,
0xbf6c70e5f776cbb1, 0x411218f2ef552bed, 0xcb0c0708705a36a3, 0xe74d14754f986044,
0xcd56d9430ea8280e, 0xc12591d7535f5065, 0xc83223f1720aef96, 0xc3a0396f7363a51f,
}

View File

@ -1,117 +0,0 @@
// Package tiger implements the Tiger hash algorithm
// https://github.com/cxmcc/tiger
package tiger
import "hash"
// The size of a Tiger hash value in bytes
const Size = 24
// The blocksize of Tiger hash function in bytes
const BlockSize = 64
const (
chunk = 64
initA = 0x0123456789abcdef
initB = 0xfedcba9876543210
initC = 0xf096a5b4c3b2e187
)
type digest struct {
a uint64
b uint64
c uint64
x [chunk]byte
nx int
length uint64
ver int
}
func (d *digest) Reset() {
d.a = initA
d.b = initB
d.c = initC
d.nx = 0
d.length = 0
}
// New returns a new hash.Hash computing the Tiger hash value
func New() hash.Hash {
d := new(digest)
d.Reset()
d.ver = 1
return d
}
// New returns a new hash.Hash computing the Tiger2 hash value
func New2() hash.Hash {
d := new(digest)
d.Reset()
d.ver = 2
return d
}
func (d *digest) BlockSize() int {
return BlockSize
}
func (d *digest) Size() int {
return Size
}
func (d *digest) Write(p []byte) (length int, err error) {
length = len(p)
d.length += uint64(length)
if d.nx > 0 {
n := len(p)
if n > chunk-d.nx {
n = chunk - d.nx
}
copy(d.x[d.nx:d.nx+n], p[:n])
d.nx += n
if d.nx == chunk {
d.compress(d.x[:chunk])
d.nx = 0
}
p = p[n:]
}
for len(p) >= chunk {
d.compress(p[:chunk])
p = p[chunk:]
}
if len(p) > 0 {
d.nx = copy(d.x[:], p)
}
return
}
func (d digest) Sum(in []byte) []byte {
length := d.length
var tmp [64]byte
if d.ver == 1 {
tmp[0] = 0x01
} else {
tmp[0] = 0x80
}
size := length & 0x3f
if size < 56 {
d.Write(tmp[:56-size])
} else {
d.Write(tmp[:64+56-size])
}
length <<= 3
for i := uint(0); i < 8; i++ {
tmp[i] = byte(length >> (8 * i))
}
d.Write(tmp[:8])
for i := uint(0); i < 8; i++ {
tmp[i] = byte(d.a >> (8 * i))
tmp[i+8] = byte(d.b >> (8 * i))
tmp[i+16] = byte(d.c >> (8 * i))
}
return append(in, tmp[:24]...)
}

View File

@ -1,7 +0,0 @@
language: go
go: 1.5
install:
- go get "github.com/smartystreets/goconvey/convey"
- go get -v .
script:
- go test -race -v ./...

View File

@ -1,23 +0,0 @@
Copyright (c) 2014-2014 Googol Lee <i@googol.im>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. 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.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.

View File

@ -1,78 +0,0 @@
# go-engine.io
[![GoDoc](http://godoc.org/github.com/googollee/go-engine.io?status.svg)](http://godoc.org/github.com/googollee/go-engine.io) [![Build Status](https://travis-ci.org/googollee/go-engine.io.svg)](https://travis-ci.org/googollee/go-engine.io)
go-engine.io is the implement of engine.io in golang, which is transport-based cross-browser/cross-device bi-directional communication layer for [go-socket.io](https://github.com/googollee/go-socket.io).
It is compatible with node.js implement, and supported long-polling and websocket transport.
## Install
Install the package with:
```bash
go get github.com/googollee/go-engine.io
```
Import it with:
```go
import "github.com/googollee/go-engine.io"
```
and use `engineio` as the package name inside the code.
## Example
Please check example folder for details.
```go
package main
import (
"encoding/hex"
"io/ioutil"
"log"
"net/http"
"github.com/googollee/go-engine.io"
)
func main() {
server, err := engineio.NewServer(nil)
if err != nil {
log.Fatal(err)
}
go func() {
for {
conn, _ := server.Accept()
go func() {
defer conn.Close()
for i := 0; i < 10; i++ {
t, r, _ := conn.NextReader()
b, _ := ioutil.ReadAll(r)
r.Close()
if t == engineio.MessageText {
log.Println(t, string(b))
} else {
log.Println(t, hex.EncodeToString(b))
}
w, _ := conn.NextWriter(t)
w.Write([]byte("pong"))
w.Close()
}
}()
}
}()
http.Handle("/engine.io/", server)
http.Handle("/", http.FileServer(http.Dir("./asset")))
log.Println("Serving at localhost:5000...")
log.Fatal(http.ListenAndServe(":5000", nil))
}
```
## License
The 3-clause BSD License - see LICENSE for more details

View File

@ -1,50 +0,0 @@
package engineio
import (
"github.com/googollee/go-engine.io/parser"
"io"
"sync"
)
type connReader struct {
*parser.PacketDecoder
closeChan chan struct{}
}
func newConnReader(d *parser.PacketDecoder, closeChan chan struct{}) *connReader {
return &connReader{
PacketDecoder: d,
closeChan: closeChan,
}
}
func (r *connReader) Close() error {
if r.closeChan == nil {
return nil
}
r.closeChan <- struct{}{}
r.closeChan = nil
return nil
}
type connWriter struct {
io.WriteCloser
locker *sync.Mutex
}
func newConnWriter(w io.WriteCloser, locker *sync.Mutex) *connWriter {
return &connWriter{
WriteCloser: w,
locker: locker,
}
}
func (w *connWriter) Close() error {
defer func() {
if w.locker != nil {
w.locker.Unlock()
w.locker = nil
}
}()
return w.WriteCloser.Close()
}

View File

@ -1,8 +0,0 @@
package message
type MessageType int
const (
MessageText MessageType = iota
MessageBinary
)

View File

@ -1,45 +0,0 @@
package parser
import (
"io"
)
type limitReader struct {
io.Reader
remain int
}
func newLimitReader(r io.Reader, limit int) *limitReader {
return &limitReader{
Reader: r,
remain: limit,
}
}
func (r *limitReader) Read(b []byte) (int, error) {
if r.remain == 0 {
return 0, io.EOF
}
if len(b) > r.remain {
b = b[:r.remain]
}
n, err := r.Reader.Read(b)
r.remain -= n
return n, err
}
func (r *limitReader) Close() error {
if r.remain > 0 {
b := make([]byte, 10240)
for {
_, err := r.Read(b)
if err == io.EOF {
break
}
if err != nil {
return err
}
}
}
return nil
}

View File

@ -1,191 +0,0 @@
package parser
import (
"encoding/base64"
"fmt"
"io"
"github.com/googollee/go-engine.io/message"
)
// PacketType is the type of packet
type PacketType string
const (
OPEN PacketType = "open"
CLOSE PacketType = "close"
PING PacketType = "ping"
PONG PacketType = "pong"
MESSAGE PacketType = "message"
UPGRADE PacketType = "upgrade"
NOOP PacketType = "noop"
)
func ByteToType(b byte) (PacketType, error) {
switch b {
case 0:
return OPEN, nil
case 1:
return CLOSE, nil
case 2:
return PING, nil
case 3:
return PONG, nil
case 4:
return MESSAGE, nil
case 5:
return UPGRADE, nil
case 6:
return NOOP, nil
}
return NOOP, fmt.Errorf("invalid byte 0x%x", b)
}
// Byte return the byte of type
func (t PacketType) Byte() byte {
switch t {
case OPEN:
return 0
case CLOSE:
return 1
case PING:
return 2
case PONG:
return 3
case MESSAGE:
return 4
case UPGRADE:
return 5
}
return 6
}
// packetEncoder is the encoder which encode the packet.
type PacketEncoder struct {
closer io.Closer
w io.Writer
}
// NewStringEncoder return the encoder which encode type t to writer w, as string.
func NewStringEncoder(w io.Writer, t PacketType) (*PacketEncoder, error) {
return newEncoder(w, t.Byte()+'0')
}
// NewBinaryEncoder return the encoder which encode type t to writer w, as binary.
func NewBinaryEncoder(w io.Writer, t PacketType) (*PacketEncoder, error) {
return newEncoder(w, t.Byte())
}
func newEncoder(w io.Writer, t byte) (*PacketEncoder, error) {
if _, err := w.Write([]byte{t}); err != nil {
return nil, err
}
closer, ok := w.(io.Closer)
if !ok {
closer = nil
}
return &PacketEncoder{
closer: closer,
w: w,
}, nil
}
// NewB64Encoder return the encoder which encode type t to writer w, as string. When write binary, it uses base64.
func NewB64Encoder(w io.Writer, t PacketType) (*PacketEncoder, error) {
_, err := w.Write([]byte{'b', t.Byte() + '0'})
if err != nil {
return nil, err
}
base := base64.NewEncoder(base64.StdEncoding, w)
return &PacketEncoder{
closer: base,
w: base,
}, nil
}
// Write writes bytes p.
func (e *PacketEncoder) Write(p []byte) (int, error) {
return e.w.Write(p)
}
// Close closes the encoder.
func (e *PacketEncoder) Close() error {
if e.closer != nil {
return e.closer.Close()
}
return nil
}
// packetDecoder is the decoder which decode data to packet.
type PacketDecoder struct {
closer io.Closer
r io.Reader
t PacketType
msgType message.MessageType
}
// NewDecoder return the decoder which decode from reader r.
func NewDecoder(r io.Reader) (*PacketDecoder, error) {
var closer io.Closer
if limit, ok := r.(*limitReader); ok {
closer = limit
}
defer func() {
if closer != nil {
closer.Close()
}
}()
b := []byte{0xff}
if _, err := r.Read(b); err != nil {
return nil, err
}
msgType := message.MessageText
if b[0] == 'b' {
if _, err := r.Read(b); err != nil {
return nil, err
}
r = base64.NewDecoder(base64.StdEncoding, r)
msgType = message.MessageBinary
}
if b[0] >= '0' {
b[0] = b[0] - '0'
} else {
msgType = message.MessageBinary
}
t, err := ByteToType(b[0])
if err != nil {
return nil, err
}
ret := &PacketDecoder{
closer: closer,
r: r,
t: t,
msgType: msgType,
}
closer = nil
return ret, nil
}
// Read reads packet data to bytes p.
func (d *PacketDecoder) Read(p []byte) (int, error) {
return d.r.Read(p)
}
// Type returns the type of packet.
func (d *PacketDecoder) Type() PacketType {
return d.t
}
// MessageType returns the type of message, binary or string.
func (d *PacketDecoder) MessageType() message.MessageType {
return d.msgType
}
// Close closes the decoder.
func (d *PacketDecoder) Close() error {
if d.closer != nil {
return d.closer.Close()
}
return nil
}

View File

@ -1,3 +0,0 @@
package parser
const Protocol = 3

View File

@ -1,170 +0,0 @@
package parser
import (
"bufio"
"bytes"
"fmt"
"io"
"strconv"
"sync"
)
// payloadEncoder is the encoder to encode packets as payload. It can be used in multi-thread.
type PayloadEncoder struct {
buffers [][]byte
locker sync.Mutex
isString bool
}
// NewStringPayloadEncoder returns the encoder which encode as string.
func NewStringPayloadEncoder() *PayloadEncoder {
return &PayloadEncoder{
isString: true,
}
}
// NewStringPayloadEncoder returns the encoder which encode as binary.
func NewBinaryPayloadEncoder() *PayloadEncoder {
return &PayloadEncoder{
isString: false,
}
}
type encoder struct {
*PacketEncoder
buf *bytes.Buffer
binaryPrefix string
payload *PayloadEncoder
}
func (e encoder) Close() error {
if err := e.PacketEncoder.Close(); err != nil {
return err
}
var buffer []byte
if e.payload.isString {
buffer = []byte(fmt.Sprintf("%d:%s", e.buf.Len(), e.buf.String()))
} else {
buffer = []byte(fmt.Sprintf("%s%d", e.binaryPrefix, e.buf.Len()))
for i, n := 0, len(buffer); i < n; i++ {
buffer[i] = buffer[i] - '0'
}
buffer = append(buffer, 0xff)
buffer = append(buffer, e.buf.Bytes()...)
}
e.payload.locker.Lock()
e.payload.buffers = append(e.payload.buffers, buffer)
e.payload.locker.Unlock()
return nil
}
// NextString returns the encoder with packet type t and encode as string.
func (e *PayloadEncoder) NextString(t PacketType) (io.WriteCloser, error) {
buf := bytes.NewBuffer(nil)
pEncoder, err := NewStringEncoder(buf, t)
if err != nil {
return nil, err
}
return encoder{
PacketEncoder: pEncoder,
buf: buf,
binaryPrefix: "0",
payload: e,
}, nil
}
// NextBinary returns the encoder with packet type t and encode as binary.
func (e *PayloadEncoder) NextBinary(t PacketType) (io.WriteCloser, error) {
buf := bytes.NewBuffer(nil)
var pEncoder *PacketEncoder
var err error
if e.isString {
pEncoder, err = NewB64Encoder(buf, t)
} else {
pEncoder, err = NewBinaryEncoder(buf, t)
}
if err != nil {
return nil, err
}
return encoder{
PacketEncoder: pEncoder,
buf: buf,
binaryPrefix: "1",
payload: e,
}, nil
}
// EncodeTo writes encoded payload to writer w. It will clear the buffer of encoder.
func (e *PayloadEncoder) EncodeTo(w io.Writer) error {
e.locker.Lock()
buffers := e.buffers
e.buffers = nil
e.locker.Unlock()
for _, b := range buffers {
for len(b) > 0 {
n, err := w.Write(b)
if err != nil {
return err
}
b = b[n:]
}
}
return nil
}
//IsString returns true if payload encode to string, otherwise returns false.
func (e *PayloadEncoder) IsString() bool {
return e.isString
}
// payloadDecoder is the decoder to decode payload.
type PayloadDecoder struct {
r *bufio.Reader
}
// NewPaylaodDecoder returns the payload decoder which read from reader r.
func NewPayloadDecoder(r io.Reader) *PayloadDecoder {
br, ok := r.(*bufio.Reader)
if !ok {
br = bufio.NewReader(r)
}
return &PayloadDecoder{
r: br,
}
}
// Next returns the packet decoder. Make sure it will be closed after used.
func (d *PayloadDecoder) Next() (*PacketDecoder, error) {
firstByte, err := d.r.Peek(1)
if err != nil {
return nil, err
}
isBinary := firstByte[0] < '0'
delim := byte(':')
if isBinary {
d.r.ReadByte()
delim = 0xff
}
line, err := d.r.ReadBytes(delim)
if err != nil {
return nil, err
}
l := len(line)
if l < 1 {
return nil, fmt.Errorf("invalid input")
}
lenByte := line[:l-1]
if isBinary {
for i, n := 0, l; i < n; i++ {
line[i] = line[i] + '0'
}
}
packetLen, err := strconv.ParseInt(string(lenByte), 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid input")
}
return NewDecoder(newLimitReader(d.r, int(packetLen)))
}

View File

@ -1,149 +0,0 @@
package polling
import (
"bytes"
"fmt"
"github.com/googollee/go-engine.io/message"
"io"
"io/ioutil"
"net/http"
"net/url"
"time"
"github.com/googollee/go-engine.io/parser"
"github.com/googollee/go-engine.io/transport"
)
type client struct {
req http.Request
url url.URL
seq uint
getResp *http.Response
postResp *http.Response
resp *http.Response
payloadDecoder *parser.PayloadDecoder
payloadEncoder *parser.PayloadEncoder
client *http.Client
state state
}
func NewClient(r *http.Request) (transport.Client, error) {
newEncoder := parser.NewBinaryPayloadEncoder
if _, ok := r.URL.Query()["b64"]; ok {
newEncoder = parser.NewStringPayloadEncoder
}
ret := &client{
req: *r,
url: *r.URL,
seq: 0,
payloadEncoder: newEncoder(),
client: http.DefaultClient,
state: stateNormal,
}
return ret, nil
}
func (c *client) Response() *http.Response {
return c.resp
}
func (c *client) NextReader() (*parser.PacketDecoder, error) {
if c.state != stateNormal {
return nil, io.EOF
}
if c.payloadDecoder != nil {
ret, err := c.payloadDecoder.Next()
if err != io.EOF {
return ret, err
}
c.getResp.Body.Close()
c.payloadDecoder = nil
}
req := c.getReq()
req.Method = "GET"
var err error
c.getResp, err = c.client.Do(req)
if err != nil {
return nil, err
}
if c.resp == nil {
c.resp = c.getResp
}
c.payloadDecoder = parser.NewPayloadDecoder(c.getResp.Body)
return c.payloadDecoder.Next()
}
func (c *client) NextWriter(messageType message.MessageType, packetType parser.PacketType) (io.WriteCloser, error) {
if c.state != stateNormal {
return nil, io.EOF
}
next := c.payloadEncoder.NextBinary
if messageType == message.MessageText {
next = c.payloadEncoder.NextString
}
w, err := next(packetType)
if err != nil {
return nil, err
}
return newClientWriter(c, w), nil
}
func (c *client) Close() error {
if c.state != stateNormal {
return nil
}
c.state = stateClosed
return nil
}
func (c *client) getReq() *http.Request {
req := c.req
url := c.url
req.URL = &url
query := req.URL.Query()
query.Set("t", fmt.Sprintf("%d-%d", time.Now().Unix()*1000, c.seq))
c.seq++
req.URL.RawQuery = query.Encode()
return &req
}
func (c *client) doPost() error {
if c.state != stateNormal {
return io.EOF
}
req := c.getReq()
req.Method = "POST"
buf := bytes.NewBuffer(nil)
if err := c.payloadEncoder.EncodeTo(buf); err != nil {
return err
}
req.Body = ioutil.NopCloser(buf)
var err error
c.postResp, err = c.client.Do(req)
if err != nil {
return err
}
if c.resp == nil {
c.resp = c.postResp
}
return nil
}
type clientWriter struct {
io.WriteCloser
client *client
}
func newClientWriter(c *client, w io.WriteCloser) io.WriteCloser {
return &clientWriter{
WriteCloser: w,
client: c,
}
}
func (w *clientWriter) Close() error {
if err := w.WriteCloser.Close(); err != nil {
return err
}
return w.client.doPost()
}

View File

@ -1,197 +0,0 @@
package polling
import (
"bytes"
"html/template"
"io"
"net/http"
"sync"
"github.com/googollee/go-engine.io/message"
"github.com/googollee/go-engine.io/parser"
"github.com/googollee/go-engine.io/transport"
)
type state int
const (
stateUnknow state = iota
stateNormal
stateClosing
stateClosed
)
type Polling struct {
sendChan chan bool
encoder *parser.PayloadEncoder
callback transport.Callback
getLocker *Locker
postLocker *Locker
state state
stateLocker sync.Mutex
}
func NewServer(w http.ResponseWriter, r *http.Request, callback transport.Callback) (transport.Server, error) {
newEncoder := parser.NewBinaryPayloadEncoder
if r.URL.Query()["b64"] != nil {
newEncoder = parser.NewStringPayloadEncoder
}
ret := &Polling{
sendChan: MakeSendChan(),
encoder: newEncoder(),
callback: callback,
getLocker: NewLocker(),
postLocker: NewLocker(),
state: stateNormal,
}
return ret, nil
}
func (p *Polling) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
p.get(w, r)
case "POST":
p.post(w, r)
}
}
func (p *Polling) Close() error {
if p.getState() != stateNormal {
return nil
}
close(p.sendChan)
p.setState(stateClosing)
if p.getLocker.TryLock() {
if p.postLocker.TryLock() {
p.callback.OnClose(p)
p.setState(stateClosed)
p.postLocker.Unlock()
}
p.getLocker.Unlock()
}
return nil
}
func (p *Polling) NextWriter(msgType message.MessageType, packetType parser.PacketType) (io.WriteCloser, error) {
if p.getState() != stateNormal {
return nil, io.EOF
}
var ret io.WriteCloser
var err error
switch msgType {
case message.MessageText:
ret, err = p.encoder.NextString(packetType)
case message.MessageBinary:
ret, err = p.encoder.NextBinary(packetType)
}
if err != nil {
return nil, err
}
return NewWriter(ret, p), nil
}
func (p *Polling) get(w http.ResponseWriter, r *http.Request) {
if !p.getLocker.TryLock() {
http.Error(w, "overlay get", http.StatusBadRequest)
return
}
if p.getState() != stateNormal {
http.Error(w, "closed", http.StatusBadRequest)
return
}
defer func() {
if p.getState() == stateClosing {
if p.postLocker.TryLock() {
p.setState(stateClosed)
p.callback.OnClose(p)
p.postLocker.Unlock()
}
}
p.getLocker.Unlock()
}()
<-p.sendChan
if j := r.URL.Query().Get("j"); j != "" {
// JSONP Polling
w.Header().Set("Content-Type", "text/javascript; charset=UTF-8")
tmp := bytes.Buffer{}
p.encoder.EncodeTo(&tmp)
pl := template.JSEscapeString(tmp.String())
w.Write([]byte("___eio[" + j + "](\""))
w.Write([]byte(pl))
w.Write([]byte("\");"))
} else {
// XHR Polling
if p.encoder.IsString() {
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
} else {
w.Header().Set("Content-Type", "application/octet-stream")
}
p.encoder.EncodeTo(w)
}
}
func (p *Polling) post(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
if !p.postLocker.TryLock() {
http.Error(w, "overlay post", http.StatusBadRequest)
return
}
if p.getState() != stateNormal {
http.Error(w, "closed", http.StatusBadRequest)
return
}
defer func() {
if p.getState() == stateClosing {
if p.getLocker.TryLock() {
p.setState(stateClosed)
p.callback.OnClose(p)
p.getLocker.Unlock()
}
}
p.postLocker.Unlock()
}()
var decoder *parser.PayloadDecoder
if j := r.URL.Query().Get("j"); j != "" {
// JSONP Polling
d := r.FormValue("d")
decoder = parser.NewPayloadDecoder(bytes.NewBufferString(d))
} else {
// XHR Polling
decoder = parser.NewPayloadDecoder(r.Body)
}
for {
d, err := decoder.Next()
if err == io.EOF {
break
}
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p.callback.OnPacket(d)
d.Close()
}
w.Write([]byte("ok"))
}
func (p *Polling) setState(s state) {
p.stateLocker.Lock()
defer p.stateLocker.Unlock()
p.state = s
}
func (p *Polling) getState() state {
p.stateLocker.Lock()
defer p.stateLocker.Unlock()
return p.state
}

View File

@ -1,28 +0,0 @@
package polling
type Locker struct {
locker chan struct{}
}
func NewLocker() *Locker {
return &Locker{
locker: make(chan struct{}, 1),
}
}
func (l *Locker) Lock() {
l.locker <- struct{}{}
}
func (l *Locker) TryLock() bool {
select {
case l.locker <- struct{}{}:
return true
default:
return false
}
}
func (l *Locker) Unlock() {
<-l.locker
}

View File

@ -1,33 +0,0 @@
package polling
import (
"errors"
"io"
)
func MakeSendChan() chan bool {
return make(chan bool, 1)
}
type Writer struct {
io.WriteCloser
server *Polling
}
func NewWriter(w io.WriteCloser, server *Polling) *Writer {
return &Writer{
WriteCloser: w,
server: server,
}
}
func (w *Writer) Close() error {
if w.server.getState() != stateNormal {
return errors.New("use of closed network connection")
}
select {
case w.server.sendChan <- true:
default:
}
return w.WriteCloser.Close()
}

View File

@ -1,12 +0,0 @@
package polling
import (
"github.com/googollee/go-engine.io/transport"
)
var Creater = transport.Creater{
Name: "polling",
Upgrading: false,
Server: NewServer,
Client: NewClient,
}

View File

@ -1,188 +0,0 @@
package engineio
import (
"bytes"
"crypto/md5"
"encoding/base64"
"fmt"
"net/http"
"sync/atomic"
"time"
"github.com/googollee/go-engine.io/polling"
"github.com/googollee/go-engine.io/websocket"
)
type config struct {
PingTimeout time.Duration
PingInterval time.Duration
MaxConnection int
AllowRequest func(*http.Request) error
AllowUpgrades bool
Cookie string
NewId func(r *http.Request) string
}
// Server is the server of engine.io.
type Server struct {
config config
socketChan chan Conn
serverSessions Sessions
creaters transportCreaters
currentConnection int32
}
// NewServer returns the server suppported given transports. If transports is nil, server will use ["polling", "websocket"] as default.
func NewServer(transports []string) (*Server, error) {
if transports == nil {
transports = []string{"polling", "websocket"}
}
creaters := make(transportCreaters)
for _, t := range transports {
switch t {
case "polling":
creaters[t] = polling.Creater
case "websocket":
creaters[t] = websocket.Creater
default:
return nil, InvalidError
}
}
return &Server{
config: config{
PingTimeout: 60000 * time.Millisecond,
PingInterval: 25000 * time.Millisecond,
MaxConnection: 1000,
AllowRequest: func(*http.Request) error { return nil },
AllowUpgrades: true,
Cookie: "io",
NewId: newId,
},
socketChan: make(chan Conn),
serverSessions: newServerSessions(),
creaters: creaters,
}, nil
}
// SetPingTimeout sets the timeout of ping. When time out, server will close connection. Default is 60s.
func (s *Server) SetPingTimeout(t time.Duration) {
s.config.PingTimeout = t
}
// SetPingInterval sets the interval of ping. Default is 25s.
func (s *Server) SetPingInterval(t time.Duration) {
s.config.PingInterval = t
}
// SetMaxConnection sets the max connetion. Default is 1000.
func (s *Server) SetMaxConnection(n int) {
s.config.MaxConnection = n
}
// GetMaxConnection returns the current max connection
func (s *Server) GetMaxConnection() int {
return s.config.MaxConnection
}
// Count returns a count of current number of active connections in session
func (s *Server) Count() int {
return int(atomic.LoadInt32(&s.currentConnection))
}
// SetAllowRequest sets the middleware function when establish connection. If it return non-nil, connection won't be established. Default will allow all request.
func (s *Server) SetAllowRequest(f func(*http.Request) error) {
s.config.AllowRequest = f
}
// SetAllowUpgrades sets whether server allows transport upgrade. Default is true.
func (s *Server) SetAllowUpgrades(allow bool) {
s.config.AllowUpgrades = allow
}
// SetCookie sets the name of cookie which used by engine.io. Default is "io".
func (s *Server) SetCookie(prefix string) {
s.config.Cookie = prefix
}
// SetNewId sets the callback func to generate new connection id. By default, id is generated from remote addr + current time stamp
func (s *Server) SetNewId(f func(*http.Request) string) {
s.config.NewId = f
}
// SetSessionManager sets the sessions as server's session manager. Default sessions is single process manager. You can custom it as load balance.
func (s *Server) SetSessionManager(sessions Sessions) {
s.serverSessions = sessions
}
// ServeHTTP handles http request.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
sid := r.URL.Query().Get("sid")
conn := s.serverSessions.Get(sid)
if conn == nil {
if sid != "" {
http.Error(w, "invalid sid", http.StatusBadRequest)
return
}
if err := s.config.AllowRequest(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
n := atomic.AddInt32(&s.currentConnection, 1)
if int(n) > s.config.MaxConnection {
atomic.AddInt32(&s.currentConnection, -1)
http.Error(w, "too many connections", http.StatusServiceUnavailable)
return
}
sid = s.config.NewId(r)
var err error
conn, err = newServerConn(sid, w, r, s)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.serverSessions.Set(sid, conn)
s.socketChan <- conn
}
http.SetCookie(w, &http.Cookie{
Name: s.config.Cookie,
Value: sid,
})
conn.(*serverConn).ServeHTTP(w, r)
}
// Accept returns Conn when client connect to server.
func (s *Server) Accept() (Conn, error) {
return <-s.socketChan, nil
}
func (s *Server) configure() config {
return s.config
}
func (s *Server) transports() transportCreaters {
return s.creaters
}
func (s *Server) onClose(id string) {
s.serverSessions.Remove(id)
atomic.AddInt32(&s.currentConnection, -1)
}
func newId(r *http.Request) string {
hash := fmt.Sprintf("%s %s", r.RemoteAddr, time.Now())
buf := bytes.NewBuffer(nil)
sum := md5.Sum([]byte(hash))
encoder := base64.NewEncoder(base64.URLEncoding, buf)
encoder.Write(sum[:])
encoder.Close()
return buf.String()[:20]
}

View File

@ -1,388 +0,0 @@
package engineio
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"sync"
"time"
"github.com/googollee/go-engine.io/message"
"github.com/googollee/go-engine.io/parser"
"github.com/googollee/go-engine.io/transport"
)
type MessageType message.MessageType
const (
MessageBinary MessageType = MessageType(message.MessageBinary)
MessageText MessageType = MessageType(message.MessageText)
)
// Conn is the connection object of engine.io.
type Conn interface {
// Id returns the session id of connection.
Id() string
// Request returns the first http request when established connection.
Request() *http.Request
// Close closes the connection.
Close() error
// NextReader returns the next message type, reader. If no message received, it will block.
NextReader() (MessageType, io.ReadCloser, error)
// NextWriter returns the next message writer with given message type.
NextWriter(messageType MessageType) (io.WriteCloser, error)
}
type transportCreaters map[string]transport.Creater
func (c transportCreaters) Get(name string) transport.Creater {
return c[name]
}
type serverCallback interface {
configure() config
transports() transportCreaters
onClose(sid string)
}
type state int
const (
stateUnknow state = iota
stateNormal
stateUpgrading
stateClosing
stateClosed
)
type serverConn struct {
id string
request *http.Request
callback serverCallback
writerLocker sync.Mutex
transportLocker sync.RWMutex
currentName string
current transport.Server
upgradingName string
upgrading transport.Server
state state
stateLocker sync.RWMutex
readerChan chan *connReader
pingTimeout time.Duration
pingInterval time.Duration
pingChan chan bool
pingLocker sync.Mutex
}
var InvalidError = errors.New("invalid transport")
func newServerConn(id string, w http.ResponseWriter, r *http.Request, callback serverCallback) (*serverConn, error) {
transportName := r.URL.Query().Get("transport")
creater := callback.transports().Get(transportName)
if creater.Name == "" {
return nil, InvalidError
}
ret := &serverConn{
id: id,
request: r,
callback: callback,
state: stateNormal,
readerChan: make(chan *connReader),
pingTimeout: callback.configure().PingTimeout,
pingInterval: callback.configure().PingInterval,
pingChan: make(chan bool),
}
transport, err := creater.Server(w, r, ret)
if err != nil {
return nil, err
}
ret.setCurrent(transportName, transport)
if err := ret.onOpen(); err != nil {
return nil, err
}
go ret.pingLoop()
return ret, nil
}
func (c *serverConn) Id() string {
return c.id
}
func (c *serverConn) Request() *http.Request {
return c.request
}
func (c *serverConn) NextReader() (MessageType, io.ReadCloser, error) {
if c.getState() == stateClosed {
return MessageBinary, nil, io.EOF
}
ret := <-c.readerChan
if ret == nil {
return MessageBinary, nil, io.EOF
}
return MessageType(ret.MessageType()), ret, nil
}
func (c *serverConn) NextWriter(t MessageType) (io.WriteCloser, error) {
switch c.getState() {
case stateUpgrading:
for i := 0; i < 30; i++ {
time.Sleep(50 * time.Millisecond)
if c.getState() != stateUpgrading {
break
}
}
if c.getState() == stateUpgrading {
return nil, fmt.Errorf("upgrading")
}
case stateNormal:
default:
return nil, io.EOF
}
c.writerLocker.Lock()
ret, err := c.getCurrent().NextWriter(message.MessageType(t), parser.MESSAGE)
if err != nil {
c.writerLocker.Unlock()
return ret, err
}
writer := newConnWriter(ret, &c.writerLocker)
return writer, err
}
func (c *serverConn) Close() error {
if c.getState() != stateNormal && c.getState() != stateUpgrading {
return nil
}
if c.upgrading != nil {
c.upgrading.Close()
}
c.writerLocker.Lock()
if w, err := c.getCurrent().NextWriter(message.MessageText, parser.CLOSE); err == nil {
writer := newConnWriter(w, &c.writerLocker)
writer.Close()
} else {
c.writerLocker.Unlock()
}
if err := c.getCurrent().Close(); err != nil {
return err
}
c.setState(stateClosing)
return nil
}
func (c *serverConn) ServeHTTP(w http.ResponseWriter, r *http.Request) {
transportName := r.URL.Query().Get("transport")
if c.currentName != transportName {
creater := c.callback.transports().Get(transportName)
if creater.Name == "" {
http.Error(w, fmt.Sprintf("invalid transport %s", transportName), http.StatusBadRequest)
return
}
u, err := creater.Server(w, r, c)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
c.setUpgrading(creater.Name, u)
return
}
c.current.ServeHTTP(w, r)
}
func (c *serverConn) OnPacket(r *parser.PacketDecoder) {
if s := c.getState(); s != stateNormal && s != stateUpgrading {
return
}
switch r.Type() {
case parser.OPEN:
case parser.CLOSE:
c.getCurrent().Close()
case parser.PING:
c.writerLocker.Lock()
t := c.getCurrent()
u := c.getUpgrade()
newWriter := t.NextWriter
if u != nil {
if w, _ := t.NextWriter(message.MessageText, parser.NOOP); w != nil {
w.Close()
}
newWriter = u.NextWriter
}
if w, _ := newWriter(message.MessageText, parser.PONG); w != nil {
io.Copy(w, r)
w.Close()
}
c.writerLocker.Unlock()
fallthrough
case parser.PONG:
c.pingLocker.Lock()
defer c.pingLocker.Unlock()
if s := c.getState(); s != stateNormal && s != stateUpgrading {
return
}
c.pingChan <- true
case parser.MESSAGE:
closeChan := make(chan struct{})
c.readerChan <- newConnReader(r, closeChan)
<-closeChan
close(closeChan)
r.Close()
case parser.UPGRADE:
c.upgraded()
case parser.NOOP:
}
}
func (c *serverConn) OnClose(server transport.Server) {
if t := c.getUpgrade(); server == t {
c.setUpgrading("", nil)
t.Close()
return
}
t := c.getCurrent()
if server != t {
return
}
t.Close()
if t := c.getUpgrade(); t != nil {
t.Close()
c.setUpgrading("", nil)
}
c.setState(stateClosed)
close(c.readerChan)
c.pingLocker.Lock()
close(c.pingChan)
c.pingLocker.Unlock()
c.callback.onClose(c.id)
}
func (s *serverConn) onOpen() error {
upgrades := []string{}
for name := range s.callback.transports() {
if name == s.currentName {
continue
}
upgrades = append(upgrades, name)
}
type connectionInfo struct {
Sid string `json:"sid"`
Upgrades []string `json:"upgrades"`
PingInterval time.Duration `json:"pingInterval"`
PingTimeout time.Duration `json:"pingTimeout"`
}
resp := connectionInfo{
Sid: s.Id(),
Upgrades: upgrades,
PingInterval: s.callback.configure().PingInterval / time.Millisecond,
PingTimeout: s.callback.configure().PingTimeout / time.Millisecond,
}
w, err := s.getCurrent().NextWriter(message.MessageText, parser.OPEN)
if err != nil {
return err
}
encoder := json.NewEncoder(w)
if err := encoder.Encode(resp); err != nil {
return err
}
if err := w.Close(); err != nil {
return err
}
return nil
}
func (c *serverConn) getCurrent() transport.Server {
c.transportLocker.RLock()
defer c.transportLocker.RUnlock()
return c.current
}
func (c *serverConn) getUpgrade() transport.Server {
c.transportLocker.RLock()
defer c.transportLocker.RUnlock()
return c.upgrading
}
func (c *serverConn) setCurrent(name string, s transport.Server) {
c.transportLocker.Lock()
defer c.transportLocker.Unlock()
c.currentName = name
c.current = s
}
func (c *serverConn) setUpgrading(name string, s transport.Server) {
c.transportLocker.Lock()
defer c.transportLocker.Unlock()
c.upgradingName = name
c.upgrading = s
c.setState(stateUpgrading)
}
func (c *serverConn) upgraded() {
c.transportLocker.Lock()
current := c.current
c.current = c.upgrading
c.currentName = c.upgradingName
c.upgrading = nil
c.upgradingName = ""
c.transportLocker.Unlock()
current.Close()
c.setState(stateNormal)
}
func (c *serverConn) getState() state {
c.stateLocker.RLock()
defer c.stateLocker.RUnlock()
return c.state
}
func (c *serverConn) setState(state state) {
c.stateLocker.Lock()
defer c.stateLocker.Unlock()
c.state = state
}
func (c *serverConn) pingLoop() {
lastPing := time.Now()
lastTry := lastPing
for {
now := time.Now()
pingDiff := now.Sub(lastPing)
tryDiff := now.Sub(lastTry)
select {
case ok := <-c.pingChan:
if !ok {
return
}
lastPing = time.Now()
lastTry = lastPing
case <-time.After(c.pingInterval - tryDiff):
c.writerLocker.Lock()
if w, _ := c.getCurrent().NextWriter(message.MessageText, parser.PING); w != nil {
writer := newConnWriter(w, &c.writerLocker)
writer.Close()
} else {
c.writerLocker.Unlock()
}
lastTry = time.Now()
case <-time.After(c.pingTimeout - pingDiff):
c.Close()
return
}
}
}

View File

@ -1,47 +0,0 @@
package engineio
import (
"sync"
)
type Sessions interface {
Get(id string) Conn
Set(id string, conn Conn)
Remove(id string)
}
type serverSessions struct {
sessions map[string]Conn
locker sync.RWMutex
}
func newServerSessions() *serverSessions {
return &serverSessions{
sessions: make(map[string]Conn),
}
}
func (s *serverSessions) Get(id string) Conn {
s.locker.RLock()
defer s.locker.RUnlock()
ret, ok := s.sessions[id]
if !ok {
return nil
}
return ret
}
func (s *serverSessions) Set(id string, conn Conn) {
s.locker.Lock()
defer s.locker.Unlock()
s.sessions[id] = conn
}
func (s *serverSessions) Remove(id string) {
s.locker.Lock()
defer s.locker.Unlock()
delete(s.sessions, id)
}

View File

@ -1,50 +0,0 @@
package transport
import (
"io"
"net/http"
"github.com/googollee/go-engine.io/message"
"github.com/googollee/go-engine.io/parser"
)
type Callback interface {
OnPacket(r *parser.PacketDecoder)
OnClose(server Server)
}
type Creater struct {
Name string
Upgrading bool
Server func(w http.ResponseWriter, r *http.Request, callback Callback) (Server, error)
Client func(r *http.Request) (Client, error)
}
// Server is a transport layer in server to connect client.
type Server interface {
// ServeHTTP handles the http request. It will call conn.onPacket when receive packet.
ServeHTTP(http.ResponseWriter, *http.Request)
// Close closes the transport.
Close() error
// NextWriter returns packet writer. This function call should be synced.
NextWriter(messageType message.MessageType, packetType parser.PacketType) (io.WriteCloser, error)
}
// Client is a transport layer in client to connect server.
type Client interface {
// Response returns the response of last http request.
Response() *http.Response
// NextReader returns packet decoder. This function call should be synced.
NextReader() (*parser.PacketDecoder, error)
// NextWriter returns packet writer. This function call should be synced.
NextWriter(messageType message.MessageType, packetType parser.PacketType) (io.WriteCloser, error)
// Close closes the transport.
Close() error
}

View File

@ -1,72 +0,0 @@
package websocket
import (
"io"
"net/http"
"github.com/googollee/go-engine.io/message"
"github.com/googollee/go-engine.io/parser"
"github.com/googollee/go-engine.io/transport"
"github.com/gorilla/websocket"
)
type client struct {
conn *websocket.Conn
resp *http.Response
}
func NewClient(r *http.Request) (transport.Client, error) {
dialer := websocket.DefaultDialer
conn, resp, err := dialer.Dial(r.URL.String(), r.Header)
if err != nil {
return nil, err
}
return &client{
conn: conn,
resp: resp,
}, nil
}
func (c *client) Response() *http.Response {
return c.resp
}
func (c *client) NextReader() (*parser.PacketDecoder, error) {
var reader io.Reader
for {
t, r, err := c.conn.NextReader()
if err != nil {
return nil, err
}
switch t {
case websocket.TextMessage:
fallthrough
case websocket.BinaryMessage:
reader = r
return parser.NewDecoder(reader)
}
}
}
func (c *client) NextWriter(msgType message.MessageType, packetType parser.PacketType) (io.WriteCloser, error) {
wsType, newEncoder := websocket.TextMessage, parser.NewStringEncoder
if msgType == message.MessageBinary {
wsType, newEncoder = websocket.BinaryMessage, parser.NewBinaryEncoder
}
w, err := c.conn.NextWriter(wsType)
if err != nil {
return nil, err
}
ret, err := newEncoder(w, packetType)
if err != nil {
return nil, err
}
return ret, nil
}
func (c *client) Close() error {
return c.conn.Close()
}

View File

@ -1,81 +0,0 @@
package websocket
import (
"io"
"net/http"
"github.com/googollee/go-engine.io/message"
"github.com/googollee/go-engine.io/parser"
"github.com/googollee/go-engine.io/transport"
"github.com/gorilla/websocket"
)
type Server struct {
callback transport.Callback
conn *websocket.Conn
}
func NewServer(w http.ResponseWriter, r *http.Request, callback transport.Callback) (transport.Server, error) {
conn, err := websocket.Upgrade(w, r, nil, 10240, 10240)
if err != nil {
return nil, err
}
ret := &Server{
callback: callback,
conn: conn,
}
go ret.serveHTTP(w, r)
return ret, nil
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
}
func (s *Server) NextWriter(msgType message.MessageType, packetType parser.PacketType) (io.WriteCloser, error) {
wsType, newEncoder := websocket.TextMessage, parser.NewStringEncoder
if msgType == message.MessageBinary {
wsType, newEncoder = websocket.BinaryMessage, parser.NewBinaryEncoder
}
w, err := s.conn.NextWriter(wsType)
if err != nil {
return nil, err
}
ret, err := newEncoder(w, packetType)
if err != nil {
return nil, err
}
return ret, nil
}
func (s *Server) Close() error {
return s.conn.Close()
}
func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
defer s.callback.OnClose(s)
for {
t, r, err := s.conn.NextReader()
if err != nil {
s.conn.Close()
return
}
switch t {
case websocket.TextMessage:
fallthrough
case websocket.BinaryMessage:
decoder, err := parser.NewDecoder(r)
if err != nil {
return
}
s.callback.OnPacket(decoder)
decoder.Close()
}
}
}

View File

@ -1,12 +0,0 @@
package websocket
import (
"github.com/googollee/go-engine.io/transport"
)
var Creater = transport.Creater{
Name: "websocket",
Upgrading: true,
Server: NewServer,
Client: NewClient,
}

View File

@ -1,5 +0,0 @@
language: go
go: 1.5
install:
- go get "github.com/smartystreets/goconvey/convey"
- go get -v .

View File

@ -1,23 +0,0 @@
Copyright (c) 2014-2014 Googol Lee <i@googol.im>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. 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.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.

View File

@ -1,127 +0,0 @@
# socket.io
[![GoDoc](http://godoc.org/github.com/googollee/go-socket.io?status.svg)](http://godoc.org/github.com/googollee/go-socket.io) [![Build Status](https://travis-ci.org/googollee/go-socket.io.svg)](https://travis-ci.org/googollee/go-socket.io)
**Please use v1.4 branch, or import "gopkg.in/googollee/go-socket.io.v1". I have no time to maintain master branch now**
go-socket.io is an implementation of [socket.io](http://socket.io) in golang, which is a realtime application framework.
It is compatible with latest implementation of socket.io in node.js, and supports room and namespace.
* for compatability with socket.io 0.9.x, please use branch 0.9.x *
## Install
Install the package with:
```bash
go get github.com/googollee/go-socket.io
```
Import it with:
```go
import "github.com/googollee/go-socket.io"
```
and use `socketio` as the package name inside the code.
## Example
Please check the example folder for details.
```go
package main
import (
"log"
"net/http"
"github.com/googollee/go-socket.io"
)
func main() {
server, err := socketio.NewServer(nil)
if err != nil {
log.Fatal(err)
}
server.On("connection", func(so socketio.Socket) {
log.Println("on connection")
so.Join("chat")
so.On("chat message", func(msg string) {
log.Println("emit:", so.Emit("chat message", msg))
so.BroadcastTo("chat", "chat message", msg)
})
so.On("disconnection", func() {
log.Println("on disconnect")
})
})
server.On("error", func(so socketio.Socket, err error) {
log.Println("error:", err)
})
http.Handle("/socket.io/", server)
http.Handle("/", http.FileServer(http.Dir("./asset")))
log.Println("Serving at localhost:5000...")
log.Fatal(http.ListenAndServe(":5000", nil))
}
```
## Acknowledgements in go-socket.io 1.X.X
[See documentation about acknowledgements](http://socket.io/docs/#sending-and-getting-data-(acknowledgements))
##### Sending ACK with data from SERVER to CLIENT
* Client-side
```javascript
//using client-side socket.io-1.X.X.js
socket.emit('some:event', JSON.stringify(someData), function(data){
console.log('ACK from server wtih data: ', data));
});
```
* Server-side
```go
// The return type may vary depending on whether you will return
// In golang implementation of socket.io don't used callbacks for acknowledgement,
// but used return value, which wrapped into ack package and returned to the client's callback in JavaScript
so.On("some:event", func(msg string) string {
return msg //Sending ack with data in msg back to client, using "return statement"
})
```
##### Sending ACK with data from CLIENT to SERVER
* Client-side
```javascript
//using client-side socket.io-1.X.X.js
//last parameter of "on" handler is callback for sending ack to server with data or without data
socket.on('some:event', function (msg, sendAckCb) {
//Sending ACK with data to server after receiving some:event from server
sendAckCb(JSON.stringify(data)); // for example used serializing to JSON
}
```
* Server-side
```go
//You can use Emit or BroadcastTo with last parameter as callback for handling ack from client
//Sending packet to room "room_name" and event "some:event"
so.BroadcastTo("room_name", "some:event", dataForClient, func (so socketio.Socket, data string) {
log.Println("Client ACK with data: ", data)
})
// Or
so.Emit("some:event", dataForClient, func (so socketio.Socket, data string) {
log.Println("Client ACK with data: ", data)
})
```
## License
The 3-clause BSD License - see LICENSE for more details

View File

@ -1,70 +0,0 @@
package socketio
import "sync"
// BroadcastAdaptor is the adaptor to handle broadcasts.
type BroadcastAdaptor interface {
// Join causes the socket to join a room.
Join(room string, socket Socket) error
// Leave causes the socket to leave a room.
Leave(room string, socket Socket) error
// Send will send an event with args to the room. If "ignore" is not nil, the event will be excluded from being sent to "ignore".
Send(ignore Socket, room, event string, args ...interface{}) error
}
var newBroadcast = newBroadcastDefault
type broadcast struct {
m map[string]map[string]Socket
sync.RWMutex
}
func newBroadcastDefault() BroadcastAdaptor {
return &broadcast{
m: make(map[string]map[string]Socket),
}
}
func (b *broadcast) Join(room string, socket Socket) error {
b.Lock()
sockets, ok := b.m[room]
if !ok {
sockets = make(map[string]Socket)
}
sockets[socket.Id()] = socket
b.m[room] = sockets
b.Unlock()
return nil
}
func (b *broadcast) Leave(room string, socket Socket) error {
b.Lock()
defer b.Unlock()
sockets, ok := b.m[room]
if !ok {
return nil
}
delete(sockets, socket.Id())
if len(sockets) == 0 {
delete(b.m, room)
return nil
}
b.m[room] = sockets
return nil
}
func (b *broadcast) Send(ignore Socket, room, event string, args ...interface{}) error {
b.RLock()
sockets := b.m[room]
for id, s := range sockets {
if ignore != nil && ignore.Id() == id {
continue
}
s.Emit(event, args...)
}
b.RUnlock()
return nil
}

View File

@ -1,168 +0,0 @@
package socketio
import (
"bytes"
"encoding/json"
"fmt"
"io"
"reflect"
)
// Attachment is an attachment handler used in emit args. All attachments will be sent as binary data in the transport layer. When using an attachment, make sure it is a pointer.
//
// For example:
//
// type Arg struct {
// Title string `json:"title"`
// File *Attachment `json:"file"`
// }
//
// f, _ := os.Open("./some_file")
// arg := Arg{
// Title: "some_file",
// File: &Attachment{
// Data: f,
// }
// }
//
// socket.Emit("send file", arg)
// socket.On("get file", func(so Socket, arg Arg) {
// b, _ := ioutil.ReadAll(arg.File.Data)
// })
type Attachment struct {
Data io.ReadWriter
num int
}
func encodeAttachments(v interface{}) []io.Reader {
index := 0
return encodeAttachmentValue(reflect.ValueOf(v), &index)
}
func encodeAttachmentValue(v reflect.Value, index *int) []io.Reader {
v = reflect.Indirect(v)
ret := []io.Reader{}
if !v.IsValid() {
return ret
}
switch v.Kind() {
case reflect.Struct:
if v.Type().Name() == "Attachment" {
a, ok := v.Addr().Interface().(*Attachment)
if !ok {
panic("can't convert")
}
a.num = *index
ret = append(ret, a.Data)
(*index)++
return ret
}
for i, n := 0, v.NumField(); i < n; i++ {
var r []io.Reader
r = encodeAttachmentValue(v.Field(i), index)
ret = append(ret, r...)
}
case reflect.Map:
if v.IsNil() {
return ret
}
for _, key := range v.MapKeys() {
var r []io.Reader
r = encodeAttachmentValue(v.MapIndex(key), index)
ret = append(ret, r...)
}
case reflect.Slice:
if v.IsNil() {
return ret
}
fallthrough
case reflect.Array:
for i, n := 0, v.Len(); i < n; i++ {
var r []io.Reader
r = encodeAttachmentValue(v.Index(i), index)
ret = append(ret, r...)
}
case reflect.Interface:
ret = encodeAttachmentValue(reflect.ValueOf(v.Interface()), index)
}
return ret
}
func decodeAttachments(v interface{}, binary [][]byte) error {
return decodeAttachmentValue(reflect.ValueOf(v), binary)
}
func decodeAttachmentValue(v reflect.Value, binary [][]byte) error {
v = reflect.Indirect(v)
if !v.IsValid() {
return fmt.Errorf("invalid value")
}
switch v.Kind() {
case reflect.Struct:
if v.Type().Name() == "Attachment" {
a, ok := v.Addr().Interface().(*Attachment)
if !ok {
panic("can't convert")
}
if a.num >= len(binary) || a.num < 0 {
return fmt.Errorf("out of range")
}
if a.Data == nil {
a.Data = bytes.NewBuffer(nil)
}
for b := binary[a.num]; len(b) > 0; {
n, err := a.Data.Write(b)
if err != nil {
return err
}
b = b[n:]
}
return nil
}
for i, n := 0, v.NumField(); i < n; i++ {
if err := decodeAttachmentValue(v.Field(i), binary); err != nil {
return err
}
}
case reflect.Map:
if v.IsNil() {
return nil
}
for _, key := range v.MapKeys() {
if err := decodeAttachmentValue(v.MapIndex(key), binary); err != nil {
return err
}
}
case reflect.Slice:
if v.IsNil() {
return nil
}
fallthrough
case reflect.Array:
for i, n := 0, v.Len(); i < n; i++ {
if err := decodeAttachmentValue(v.Index(i), binary); err != nil {
return err
}
}
case reflect.Interface:
if err := decodeAttachmentValue(reflect.ValueOf(v.Interface()), binary); err != nil {
return err
}
}
return nil
}
func (a Attachment) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("{\"_placeholder\":true,\"num\":%d}", a.num)), nil
}
func (a *Attachment) UnmarshalJSON(b []byte) error {
var v struct {
Num int `json:"num"`
}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
a.num = v.Num
return nil
}

View File

@ -1,82 +0,0 @@
package socketio
import (
"errors"
"fmt"
"reflect"
)
type caller struct {
Func reflect.Value
Args []reflect.Type
NeedSocket bool
}
func newCaller(f interface{}) (*caller, error) {
fv := reflect.ValueOf(f)
if fv.Kind() != reflect.Func {
return nil, fmt.Errorf("f is not func")
}
ft := fv.Type()
if ft.NumIn() == 0 {
return &caller{
Func: fv,
}, nil
}
args := make([]reflect.Type, ft.NumIn())
for i, n := 0, ft.NumIn(); i < n; i++ {
args[i] = ft.In(i)
}
needSocket := false
if args[0].Name() == "Socket" {
args = args[1:]
needSocket = true
}
return &caller{
Func: fv,
Args: args,
NeedSocket: needSocket,
}, nil
}
func (c *caller) GetArgs() []interface{} {
ret := make([]interface{}, len(c.Args))
for i, argT := range c.Args {
if argT.Kind() == reflect.Ptr {
argT = argT.Elem()
}
v := reflect.New(argT)
ret[i] = v.Interface()
}
return ret
}
func (c *caller) Call(so Socket, args []interface{}) []reflect.Value {
var a []reflect.Value
diff := 0
if c.NeedSocket {
diff = 1
a = make([]reflect.Value, len(args)+1)
a[0] = reflect.ValueOf(so)
} else {
a = make([]reflect.Value, len(args))
}
if len(args) != len(c.Args) {
return []reflect.Value{reflect.ValueOf([]interface{}{}), reflect.ValueOf(errors.New("Arguments do not match"))}
}
for i, arg := range args {
v := reflect.ValueOf(arg)
if c.Args[i].Kind() != reflect.Ptr {
if v.IsValid() {
v = v.Elem()
} else {
v = reflect.Zero(c.Args[i])
}
}
a[i+diff] = v
}
return c.Func.Call(a)
}

View File

@ -1,213 +0,0 @@
package socketio
import (
"fmt"
"reflect"
"sync"
)
type baseHandler struct {
events map[string]*caller
name string
broadcast BroadcastAdaptor
evMu sync.Mutex
}
func newBaseHandler(name string, broadcast BroadcastAdaptor) *baseHandler {
return &baseHandler{
events: make(map[string]*caller),
name: name,
broadcast: broadcast,
evMu: sync.Mutex{},
}
}
// On registers the function f to handle an event.
func (h *baseHandler) On(event string, f interface{}) error {
c, err := newCaller(f)
if err != nil {
return err
}
h.evMu.Lock()
h.events[event] = c
h.evMu.Unlock()
return nil
}
type socketHandler struct {
*baseHandler
acksmu sync.Mutex
acks map[int]*caller
socket *socket
rooms map[string]struct{}
}
func newSocketHandler(s *socket, base *baseHandler) *socketHandler {
events := make(map[string]*caller)
base.evMu.Lock()
for k, v := range base.events {
events[k] = v
}
base.evMu.Unlock()
return &socketHandler{
baseHandler: &baseHandler{
events: events,
broadcast: base.broadcast,
evMu: base.evMu,
},
acks: make(map[int]*caller),
socket: s,
rooms: make(map[string]struct{}),
}
}
func (h *socketHandler) Emit(event string, args ...interface{}) error {
var c *caller
if l := len(args); l > 0 {
fv := reflect.ValueOf(args[l-1])
if fv.Kind() == reflect.Func {
var err error
c, err = newCaller(args[l-1])
if err != nil {
return err
}
args = args[:l-1]
}
}
args = append([]interface{}{event}, args...)
if c != nil {
id, err := h.socket.sendId(args)
if err != nil {
return err
}
h.acksmu.Lock()
h.acks[id] = c
h.acksmu.Unlock()
return nil
}
return h.socket.send(args)
}
func (h *socketHandler) Rooms() []string {
ret := make([]string, len(h.rooms))
i := 0
for room := range h.rooms {
ret[i] = room
i++
}
return ret
}
func (h *socketHandler) Join(room string) error {
if err := h.baseHandler.broadcast.Join(h.broadcastName(room), h.socket); err != nil {
return err
}
h.rooms[room] = struct{}{}
return nil
}
func (h *socketHandler) Leave(room string) error {
if err := h.baseHandler.broadcast.Leave(h.broadcastName(room), h.socket); err != nil {
return err
}
delete(h.rooms, room)
return nil
}
func (h *socketHandler) LeaveAll() error {
for room := range h.rooms {
if err := h.baseHandler.broadcast.Leave(h.broadcastName(room), h.socket); err != nil {
return err
}
}
return nil
}
func (h *baseHandler) BroadcastTo(room, event string, args ...interface{}) error {
return h.broadcast.Send(nil, h.broadcastName(room), event, args...)
}
func (h *socketHandler) BroadcastTo(room, event string, args ...interface{}) error {
return h.baseHandler.broadcast.Send(h.socket, h.broadcastName(room), event, args...)
}
func (h *baseHandler) broadcastName(room string) string {
return fmt.Sprintf("%s:%s", h.name, room)
}
func (h *socketHandler) onPacket(decoder *decoder, packet *packet) ([]interface{}, error) {
var message string
switch packet.Type {
case _CONNECT:
message = "connection"
case _DISCONNECT:
message = "disconnection"
case _ERROR:
message = "error"
case _ACK:
fallthrough
case _BINARY_ACK:
return nil, h.onAck(packet.Id, decoder, packet)
default:
if decoder != nil {
message = decoder.Message()
}
}
h.evMu.Lock()
c, ok := h.events[message]
h.evMu.Unlock()
if !ok {
// If the message is not recognized by the server, the decoder.currentCloser
// needs to be closed otherwise the server will be stuck until the e
if decoder != nil {
decoder.Close()
}
return nil, nil
}
args := c.GetArgs()
olen := len(args)
if olen > 0 && decoder != nil {
packet.Data = &args
if err := decoder.DecodeData(packet); err != nil {
return nil, err
}
}
for i := len(args); i < olen; i++ {
args = append(args, nil)
}
retV := c.Call(h.socket, args)
if len(retV) == 0 {
return nil, nil
}
var err error
if last, ok := retV[len(retV)-1].Interface().(error); ok {
err = last
retV = retV[0 : len(retV)-1]
}
ret := make([]interface{}, len(retV))
for i, v := range retV {
ret[i] = v.Interface()
}
return ret, err
}
func (h *socketHandler) onAck(id int, decoder *decoder, packet *packet) error {
h.acksmu.Lock()
c, ok := h.acks[id]
if !ok {
h.acksmu.Unlock()
return nil
}
delete(h.acks, id)
h.acksmu.Unlock()
args := c.GetArgs()
packet.Data = &args
if err := decoder.DecodeData(packet); err != nil {
return err
}
c.Call(h.socket, args)
return nil
}

View File

@ -1,34 +0,0 @@
package socketio
import (
"io"
)
type writerHelper struct {
writer io.Writer
err error
}
func newWriterHelper(w io.Writer) *writerHelper {
return &writerHelper{
writer: w,
}
}
func (h *writerHelper) Write(p []byte) {
if h.err != nil {
return
}
for len(p) > 0 {
n, err := h.writer.Write(p)
if err != nil {
h.err = err
return
}
p = p[n:]
}
}
func (h *writerHelper) Error() error {
return h.err
}

View File

@ -1,6 +0,0 @@
/*
go-socket.io is a server implementation of socket.io in golang.
It is compatible with the official Node.js implementation.
*/
package socketio

View File

@ -1,60 +0,0 @@
package socketio
import (
"bufio"
)
type messageReader struct {
reader *bufio.Reader
message string
firstRead bool
}
func newMessageReader(bufr *bufio.Reader) (*messageReader, error) {
if _, err := bufr.ReadBytes('"'); err != nil {
return nil, err
}
msg, err := bufr.ReadBytes('"')
if err != nil {
return nil, err
}
for {
b, err := bufr.Peek(1)
if err != nil {
return nil, err
}
if b[0] == ',' {
bufr.ReadByte()
break
}
if b[0] != ' ' {
break
}
bufr.ReadByte()
}
return &messageReader{
reader: bufr,
message: string(msg[:len(msg)-1]),
firstRead: true,
}, nil
}
func (r *messageReader) Message() string {
return r.message
}
func (r *messageReader) Read(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
if r.firstRead {
r.firstRead = false
b[0] = '['
n, err := r.reader.Read(b[1:])
if err != nil {
return -1, err
}
return n + 1, err
}
return r.reader.Read(b)
}

View File

@ -1,47 +0,0 @@
package socketio
// Namespace is the name space of a socket.io handler.
type Namespace interface {
// Name returns the name of the namespace.
Name() string
// Of returns the namespace with given name.
Of(name string) Namespace
// On registers the function f to handle an event.
On(event string, f interface{}) error
}
type namespace struct {
*baseHandler
root map[string]Namespace
}
func newNamespace(broadcast BroadcastAdaptor) *namespace {
ret := &namespace{
baseHandler: newBaseHandler("", broadcast),
root: make(map[string]Namespace),
}
ret.root[ret.Name()] = ret
return ret
}
func (n *namespace) Name() string {
return n.baseHandler.name
}
func (n *namespace) Of(name string) Namespace {
if name == "/" {
name = ""
}
if ret, ok := n.root[name]; ok {
return ret
}
ret := &namespace{
baseHandler: newBaseHandler(name, n.baseHandler.broadcast),
root: n.root,
}
n.root[name] = ret
return ret
}

View File

@ -1,338 +0,0 @@
package socketio
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"strconv"
"github.com/googollee/go-engine.io"
)
const Protocol = 4
type packetType int
const (
_CONNECT packetType = iota
_DISCONNECT
_EVENT
_ACK
_ERROR
_BINARY_EVENT
_BINARY_ACK
)
func (t packetType) String() string {
switch t {
case _CONNECT:
return "connect"
case _DISCONNECT:
return "disconnect"
case _EVENT:
return "event"
case _ACK:
return "ack"
case _ERROR:
return "error"
case _BINARY_EVENT:
return "binary_event"
case _BINARY_ACK:
return "binary_ack"
}
return fmt.Sprintf("unknown(%d)", t)
}
type frameReader interface {
NextReader() (engineio.MessageType, io.ReadCloser, error)
}
type frameWriter interface {
NextWriter(engineio.MessageType) (io.WriteCloser, error)
}
type packet struct {
Type packetType
NSP string
Id int
Data interface{}
attachNumber int
}
type encoder struct {
w frameWriter
err error
}
func newEncoder(w frameWriter) *encoder {
return &encoder{
w: w,
}
}
func (e *encoder) Encode(v packet) error {
attachments := encodeAttachments(v.Data)
v.attachNumber = len(attachments)
if v.attachNumber > 0 {
v.Type += _BINARY_EVENT - _EVENT
}
if err := e.encodePacket(v); err != nil {
return err
}
for _, a := range attachments {
if err := e.writeBinary(a); err != nil {
return err
}
}
return nil
}
func (e *encoder) encodePacket(v packet) error {
writer, err := e.w.NextWriter(engineio.MessageText)
if err != nil {
return err
}
defer writer.Close()
w := newTrimWriter(writer, "\n")
wh := newWriterHelper(w)
wh.Write([]byte{byte(v.Type) + '0'})
if v.Type == _BINARY_EVENT || v.Type == _BINARY_ACK {
wh.Write([]byte(fmt.Sprintf("%d-", v.attachNumber)))
}
needEnd := false
if v.NSP != "" {
wh.Write([]byte(v.NSP))
needEnd = true
}
if v.Id >= 0 {
f := "%d"
if needEnd {
f = ",%d"
needEnd = false
}
wh.Write([]byte(fmt.Sprintf(f, v.Id)))
}
if v.Data != nil {
if needEnd {
wh.Write([]byte{','})
needEnd = false
}
if wh.Error() != nil {
return wh.Error()
}
encoder := json.NewEncoder(w)
return encoder.Encode(v.Data)
}
return wh.Error()
}
func (e *encoder) writeBinary(r io.Reader) error {
writer, err := e.w.NextWriter(engineio.MessageBinary)
if err != nil {
return err
}
defer writer.Close()
if _, err := io.Copy(writer, r); err != nil {
return err
}
return nil
}
type decoder struct {
reader frameReader
message string
current io.Reader
currentCloser io.Closer
}
func newDecoder(r frameReader) *decoder {
return &decoder{
reader: r,
}
}
func (d *decoder) Close() {
if d != nil && d.currentCloser != nil {
d.currentCloser.Close()
d.current = nil
d.currentCloser = nil
}
}
func (d *decoder) Decode(v *packet) error {
ty, r, err := d.reader.NextReader()
if err != nil {
return err
}
if d.current != nil {
d.Close()
}
defer func() {
if d.current == nil {
r.Close()
}
}()
if ty != engineio.MessageText {
return fmt.Errorf("need text package")
}
reader := bufio.NewReader(r)
v.Id = -1
t, err := reader.ReadByte()
if err != nil {
return err
}
v.Type = packetType(t - '0')
if v.Type == _BINARY_EVENT || v.Type == _BINARY_ACK {
num, err := reader.ReadBytes('-')
if err != nil {
return err
}
numLen := len(num)
if numLen == 0 {
return fmt.Errorf("invalid packet")
}
n, err := strconv.ParseInt(string(num[:numLen-1]), 10, 64)
if err != nil {
return fmt.Errorf("invalid packet")
}
v.attachNumber = int(n)
}
next, err := reader.Peek(1)
if err == io.EOF {
return nil
}
if err != nil {
return err
}
if len(next) == 0 {
return fmt.Errorf("invalid packet")
}
if next[0] == '/' {
path, err := reader.ReadBytes(',')
if err != nil && err != io.EOF {
return err
}
pathLen := len(path)
if pathLen == 0 {
return fmt.Errorf("invalid packet")
}
if err == nil {
path = path[:pathLen-1]
}
v.NSP = string(path)
if err == io.EOF {
return nil
}
}
id := bytes.NewBuffer(nil)
finish := false
for {
next, err := reader.Peek(1)
if err == io.EOF {
finish = true
break
}
if err != nil {
return err
}
if '0' <= next[0] && next[0] <= '9' {
if err := id.WriteByte(next[0]); err != nil {
return err
}
} else {
break
}
reader.ReadByte()
}
if id.Len() > 0 {
id, err := strconv.ParseInt(id.String(), 10, 64)
if err != nil {
return err
}
v.Id = int(id)
}
if finish {
return nil
}
switch v.Type {
case _EVENT:
fallthrough
case _BINARY_EVENT:
msgReader, err := newMessageReader(reader)
if err != nil {
return err
}
d.message = msgReader.Message()
d.current = msgReader
d.currentCloser = r
case _ACK:
fallthrough
case _BINARY_ACK:
d.current = reader
d.currentCloser = r
}
return nil
}
func (d *decoder) Message() string {
return d.message
}
func (d *decoder) DecodeData(v *packet) error {
if d.current == nil {
return nil
}
defer func() {
d.Close()
}()
decoder := json.NewDecoder(d.current)
if err := decoder.Decode(v.Data); err != nil {
return err
}
if v.Type == _BINARY_EVENT || v.Type == _BINARY_ACK {
binary, err := d.decodeBinary(v.attachNumber)
if err != nil {
return err
}
if err := decodeAttachments(v.Data, binary); err != nil {
return err
}
v.Type -= _BINARY_EVENT - _EVENT
}
return nil
}
func (d *decoder) decodeBinary(num int) ([][]byte, error) {
ret := make([][]byte, num)
for i := 0; i < num; i++ {
d.currentCloser.Close()
t, r, err := d.reader.NextReader()
if err != nil {
return nil, err
}
d.currentCloser = r
if t == engineio.MessageText {
return nil, fmt.Errorf("need binary")
}
b, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
ret[i] = b
}
return ret, nil
}

View File

@ -1,106 +0,0 @@
package socketio
import (
"github.com/googollee/go-engine.io"
"net/http"
"time"
)
// Server is the server of socket.io.
type Server struct {
*namespace
broadcast BroadcastAdaptor
eio *engineio.Server
}
// NewServer returns the server supported given transports. If transports is nil, the server will use ["polling", "websocket"] as default.
func NewServer(transportNames []string) (*Server, error) {
eio, err := engineio.NewServer(transportNames)
if err != nil {
return nil, err
}
ret := &Server{
namespace: newNamespace(newBroadcastDefault()),
eio: eio,
}
go ret.loop()
return ret, nil
}
// SetPingTimeout sets the timeout of a connection ping. When it times out, the server will close the connection with the client. Default is 60s.
func (s *Server) SetPingTimeout(t time.Duration) {
s.eio.SetPingTimeout(t)
}
// SetPingInterval sets the interval of pings. Default is 25s.
func (s *Server) SetPingInterval(t time.Duration) {
s.eio.SetPingInterval(t)
}
// SetMaxConnection sets the maximum number of connections with clients. Default is 1000.
func (s *Server) SetMaxConnection(n int) {
s.eio.SetMaxConnection(n)
}
// GetMaxConnection returns the current max connection
func (s *Server) GetMaxConnection() int {
return s.eio.GetMaxConnection()
}
// Count returns the current number of connected clients in session
func (s *Server) Count() int {
return s.eio.Count()
}
// SetAllowRequest sets the middleware function when a connection is established. If a non-nil value is returned, the connection won't be established. Default will allow all connections.
func (s *Server) SetAllowRequest(f func(*http.Request) error) {
s.eio.SetAllowRequest(f)
}
// SetAllowUpgrades sets whether server allows transport upgrades. Default is true.
func (s *Server) SetAllowUpgrades(allow bool) {
s.eio.SetAllowUpgrades(allow)
}
// SetCookie sets the name of the cookie used by engine.io. Default is "io".
func (s *Server) SetCookie(prefix string) {
s.eio.SetCookie(prefix)
}
// SetNewId sets the callback func to generate new connection id. By default, id is generated from remote address + current time stamp
func (s *Server) SetNewId(f func(*http.Request) string) {
s.eio.SetNewId(f)
}
// SetSessionsManager sets the sessions as server's session manager. Default sessions is a single process manager. You can customize it as a load balancer.
func (s *Server) SetSessionManager(sessions engineio.Sessions) {
s.eio.SetSessionManager(sessions)
}
// SetAdaptor sets the adaptor of broadcast. Default is an in-process broadcast implementation.
func (s *Server) SetAdaptor(adaptor BroadcastAdaptor) {
s.namespace = newNamespace(adaptor)
}
// ServeHTTP handles http requests.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.eio.ServeHTTP(w, r)
}
// BroadcastTo is a server level broadcast function.
func (s *Server) BroadcastTo(room, message string, args ...interface{}) {
s.namespace.BroadcastTo(room, message, args...)
}
func (s *Server) loop() {
for {
conn, err := s.eio.Accept()
if err != nil {
return
}
s := newSocket(conn, s.baseHandler)
go func(s *socket) {
s.loop()
}(s)
}
}

View File

@ -1,174 +0,0 @@
package socketio
import (
"net/http"
"sync"
"github.com/googollee/go-engine.io"
)
// Socket is the socket object of socket.io.
type Socket interface {
// Id returns the session id of socket.
Id() string
// Rooms returns the rooms name joined now.
Rooms() []string
// Request returns the first http request when established connection.
Request() *http.Request
// On registers the function f to handle an event.
On(event string, f interface{}) error
// Emit emits an event with given args.
Emit(event string, args ...interface{}) error
// Join joins the room.
Join(room string) error
// Leave leaves the room.
Leave(room string) error
// Disconnect disconnect the socket.
Disconnect()
// BroadcastTo broadcasts an event to the room with given args.
BroadcastTo(room, event string, args ...interface{}) error
}
type socket struct {
*socketHandler
conn engineio.Conn
namespace string
id int
mu sync.Mutex
}
func newSocket(conn engineio.Conn, base *baseHandler) *socket {
ret := &socket{
conn: conn,
}
ret.socketHandler = newSocketHandler(ret, base)
return ret
}
func (s *socket) Id() string {
return s.conn.Id()
}
func (s *socket) Request() *http.Request {
return s.conn.Request()
}
func (s *socket) Emit(event string, args ...interface{}) error {
if err := s.socketHandler.Emit(event, args...); err != nil {
return err
}
if event == "disconnect" {
s.conn.Close()
}
return nil
}
func (s *socket) Disconnect() {
s.conn.Close()
}
func (s *socket) send(args []interface{}) error {
packet := packet{
Type: _EVENT,
Id: -1,
NSP: s.namespace,
Data: args,
}
encoder := newEncoder(s.conn)
return encoder.Encode(packet)
}
func (s *socket) sendConnect() error {
packet := packet{
Type: _CONNECT,
Id: -1,
NSP: s.namespace,
}
encoder := newEncoder(s.conn)
return encoder.Encode(packet)
}
func (s *socket) sendId(args []interface{}) (int, error) {
s.mu.Lock()
packet := packet{
Type: _EVENT,
Id: s.id,
NSP: s.namespace,
Data: args,
}
s.id++
if s.id < 0 {
s.id = 0
}
s.mu.Unlock()
encoder := newEncoder(s.conn)
err := encoder.Encode(packet)
if err != nil {
return -1, nil
}
return packet.Id, nil
}
func (s *socket) loop() error {
defer func() {
s.LeaveAll()
p := packet{
Type: _DISCONNECT,
Id: -1,
}
s.socketHandler.onPacket(nil, &p)
}()
p := packet{
Type: _CONNECT,
Id: -1,
}
encoder := newEncoder(s.conn)
if err := encoder.Encode(p); err != nil {
return err
}
s.socketHandler.onPacket(nil, &p)
for {
decoder := newDecoder(s.conn)
var p packet
if err := decoder.Decode(&p); err != nil {
return err
}
ret, err := s.socketHandler.onPacket(decoder, &p)
if err != nil {
return err
}
switch p.Type {
case _CONNECT:
s.namespace = p.NSP
s.sendConnect()
case _BINARY_EVENT:
fallthrough
case _EVENT:
if p.Id >= 0 {
p := packet{
Type: _ACK,
Id: p.Id,
NSP: s.namespace,
Data: ret,
}
encoder := newEncoder(s.conn)
if err := encoder.Encode(p); err != nil {
return err
}
}
case _DISCONNECT:
return nil
}
}
}

View File

@ -1,45 +0,0 @@
package socketio
import (
"bytes"
"io"
)
type trimWriter struct {
trimChars string
trimBuf []byte
output io.Writer
}
func newTrimWriter(w io.Writer, trimChars string) *trimWriter {
return &trimWriter{
trimChars: trimChars,
output: w,
}
}
func (w *trimWriter) Write(p []byte) (int, error) {
out := bytes.TrimRight(p, w.trimChars)
buf := p[len(out):]
var written int
if (len(out) > 0) && (w.trimBuf != nil) {
var err error
if written, err = w.output.Write(w.trimBuf); err != nil {
return 0, err
}
w.trimBuf = nil
}
if w.trimBuf != nil {
w.trimBuf = append(w.trimBuf, buf...)
} else {
w.trimBuf = buf
}
if len(p) == 0 {
return written, nil
}
ret, err := w.output.Write(out)
if err != nil {
return 0, err
}
return written + ret, nil
}

View File

@ -1,25 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
.idea/
*.iml

View File

@ -1,19 +0,0 @@
language: go
sudo: false
matrix:
include:
- go: 1.4
- go: 1.5
- go: 1.6
- go: 1.7
- go: 1.8
- go: tip
allow_failures:
- go: tip
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d .)
- go vet $(go list ./... | grep -v /vendor/)
- go test -v -race ./...

View File

@ -1,8 +0,0 @@
# This is the official list of Gorilla WebSocket authors for copyright
# purposes.
#
# Please keep the list sorted.
Gary Burd <gary@beagledreams.com>
Joachim Bauch <mail@joachim-bauch.de>

View File

@ -1,22 +0,0 @@
Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,64 +0,0 @@
# Gorilla WebSocket
Gorilla WebSocket is a [Go](http://golang.org/) implementation of the
[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol.
[![Build Status](https://travis-ci.org/gorilla/websocket.svg?branch=master)](https://travis-ci.org/gorilla/websocket)
[![GoDoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](https://godoc.org/github.com/gorilla/websocket)
### 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><a href="https://tools.ietf.org/html/rfc7692">Compression Extensions</a></td><td>Experimental</td><td>No</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.

View File

@ -1,392 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"bufio"
"bytes"
"crypto/tls"
"encoding/base64"
"errors"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
"time"
)
// ErrBadHandshake is returned when the server response to opening handshake is
// invalid.
var ErrBadHandshake = errors.New("websocket: bad handshake")
var errInvalidCompression = errors.New("websocket: invalid compression negotiation")
// 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
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer
// size is zero, then a useful default size is used. The I/O buffer sizes
// do not limit the size of the messages that can be sent or received.
ReadBufferSize, WriteBufferSize int
// Subprotocols specifies the client's requested subprotocols.
Subprotocols []string
// EnableCompression specifies if the client should attempt to negotiate
// per message compression (RFC 7692). Setting this value to true does not
// guarantee that compression will be supported. Currently only "no context
// takeover" modes are supported.
EnableCompression bool
// Jar specifies the cookie jar.
// If Jar is nil, cookies are not sent in requests and ignored
// in responses.
Jar http.CookieJar
}
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 cookies present in the cookie jar of the dialer
if d.Jar != nil {
for _, cookie := range d.Jar.Cookies(u) {
req.AddCookie(cookie)
}
}
// 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-Extensions" ||
(k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0):
return nil, nil, errors.New("websocket: duplicate header not allowed: " + k)
default:
req.Header[k] = vs
}
}
if d.EnableCompression {
req.Header.Set("Sec-Websocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover")
}
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 := cloneTLSConfig(d.TLSClientConfig)
if cfg.ServerName == "" {
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 d.Jar != nil {
if rc := resp.Cookies(); len(rc) > 0 {
d.Jar.SetCookies(u, rc)
}
}
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
}
for _, ext := range parseExtensions(resp.Header) {
if ext[""] != "permessage-deflate" {
continue
}
_, snct := ext["server_no_context_takeover"]
_, cnct := ext["client_no_context_takeover"]
if !snct || !cnct {
return nil, resp, errInvalidCompression
}
conn.newCompressionWriter = compressNoContextTakeover
conn.newDecompressionReader = decompressNoContextTakeover
break
}
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
}

View File

@ -1,16 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.8
package websocket
import "crypto/tls"
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
if cfg == nil {
return &tls.Config{}
}
return cfg.Clone()
}

View File

@ -1,38 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !go1.8
package websocket
import "crypto/tls"
// cloneTLSConfig clones all public fields except the fields
// SessionTicketsDisabled and SessionTicketKey. This avoids copying the
// sync.Mutex in the sync.Once and makes it safe to call cloneTLSConfig on a
// config in active use.
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
if cfg == nil {
return &tls.Config{}
}
return &tls.Config{
Rand: cfg.Rand,
Time: cfg.Time,
Certificates: cfg.Certificates,
NameToCertificate: cfg.NameToCertificate,
GetCertificate: cfg.GetCertificate,
RootCAs: cfg.RootCAs,
NextProtos: cfg.NextProtos,
ServerName: cfg.ServerName,
ClientAuth: cfg.ClientAuth,
ClientCAs: cfg.ClientCAs,
InsecureSkipVerify: cfg.InsecureSkipVerify,
CipherSuites: cfg.CipherSuites,
PreferServerCipherSuites: cfg.PreferServerCipherSuites,
ClientSessionCache: cfg.ClientSessionCache,
MinVersion: cfg.MinVersion,
MaxVersion: cfg.MaxVersion,
CurvePreferences: cfg.CurvePreferences,
}
}

View File

@ -1,148 +0,0 @@
// Copyright 2017 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 (
"compress/flate"
"errors"
"io"
"strings"
"sync"
)
const (
minCompressionLevel = -2 // flate.HuffmanOnly not defined in Go < 1.6
maxCompressionLevel = flate.BestCompression
defaultCompressionLevel = 1
)
var (
flateWriterPools [maxCompressionLevel - minCompressionLevel + 1]sync.Pool
flateReaderPool = sync.Pool{New: func() interface{} {
return flate.NewReader(nil)
}}
)
func decompressNoContextTakeover(r io.Reader) io.ReadCloser {
const tail =
// Add four bytes as specified in RFC
"\x00\x00\xff\xff" +
// Add final block to squelch unexpected EOF error from flate reader.
"\x01\x00\x00\xff\xff"
fr, _ := flateReaderPool.Get().(io.ReadCloser)
fr.(flate.Resetter).Reset(io.MultiReader(r, strings.NewReader(tail)), nil)
return &flateReadWrapper{fr}
}
func isValidCompressionLevel(level int) bool {
return minCompressionLevel <= level && level <= maxCompressionLevel
}
func compressNoContextTakeover(w io.WriteCloser, level int) io.WriteCloser {
p := &flateWriterPools[level-minCompressionLevel]
tw := &truncWriter{w: w}
fw, _ := p.Get().(*flate.Writer)
if fw == nil {
fw, _ = flate.NewWriter(tw, level)
} else {
fw.Reset(tw)
}
return &flateWriteWrapper{fw: fw, tw: tw, p: p}
}
// truncWriter is an io.Writer that writes all but the last four bytes of the
// stream to another io.Writer.
type truncWriter struct {
w io.WriteCloser
n int
p [4]byte
}
func (w *truncWriter) Write(p []byte) (int, error) {
n := 0
// fill buffer first for simplicity.
if w.n < len(w.p) {
n = copy(w.p[w.n:], p)
p = p[n:]
w.n += n
if len(p) == 0 {
return n, nil
}
}
m := len(p)
if m > len(w.p) {
m = len(w.p)
}
if nn, err := w.w.Write(w.p[:m]); err != nil {
return n + nn, err
}
copy(w.p[:], w.p[m:])
copy(w.p[len(w.p)-m:], p[len(p)-m:])
nn, err := w.w.Write(p[:len(p)-m])
return n + nn, err
}
type flateWriteWrapper struct {
fw *flate.Writer
tw *truncWriter
p *sync.Pool
}
func (w *flateWriteWrapper) Write(p []byte) (int, error) {
if w.fw == nil {
return 0, errWriteClosed
}
return w.fw.Write(p)
}
func (w *flateWriteWrapper) Close() error {
if w.fw == nil {
return errWriteClosed
}
err1 := w.fw.Flush()
w.p.Put(w.fw)
w.fw = nil
if w.tw.p != [4]byte{0, 0, 0xff, 0xff} {
return errors.New("websocket: internal error, unexpected bytes at end of flate stream")
}
err2 := w.tw.w.Close()
if err1 != nil {
return err1
}
return err2
}
type flateReadWrapper struct {
fr io.ReadCloser
}
func (r *flateReadWrapper) Read(p []byte) (int, error) {
if r.fr == nil {
return 0, io.ErrClosedPipe
}
n, err := r.fr.Read(p)
if err == io.EOF {
// Preemptively place the reader back in the pool. This helps with
// scenarios where the application does not call NextReader() soon after
// this final read.
r.Close()
}
return n, err
}
func (r *flateReadWrapper) Close() error {
if r.fr == nil {
return io.ErrClosedPipe
}
err := r.fr.Close()
flateReaderPool.Put(r.fr)
r.fr = nil
return err
}

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +0,0 @@
// Copyright 2016 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 go1.5
package websocket
import "io"
func (c *Conn) read(n int) ([]byte, error) {
p, err := c.br.Peek(n)
if err == io.EOF {
err = errUnexpectedEOF
}
c.br.Discard(len(p))
return p, err
}

View File

@ -1,21 +0,0 @@
// Copyright 2016 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 !go1.5
package websocket
import "io"
func (c *Conn) read(n int) ([]byte, error) {
p, err := c.br.Peek(n)
if err == io.EOF {
err = errUnexpectedEOF
}
if len(p) > 0 {
// advance over the bytes just read
io.ReadFull(c.br, p)
}
return p, err
}

View File

@ -1,180 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package websocket implements the WebSocket protocol defined in RFC 6455.
//
// Overview
//
// The Conn type represents a WebSocket connection. A server application uses
// the Upgrade function from an Upgrader object with a HTTP request handler
// to get a pointer to a Conn:
//
// var upgrader = websocket.Upgrader{
// ReadBufferSize: 1024,
// WriteBufferSize: 1024,
// }
//
// func handler(w http.ResponseWriter, r *http.Request) {
// conn, err := upgrader.Upgrade(w, r, nil)
// if err != nil {
// log.Println(err)
// return
// }
// ... Use conn to send and receive messages.
// }
//
// Call the connection's WriteMessage and ReadMessage methods to send and
// receive messages as a slice of bytes. This snippet of code shows how to echo
// messages using these methods:
//
// for {
// messageType, p, err := conn.ReadMessage()
// if err != nil {
// return
// }
// if err = conn.WriteMessage(messageType, p); err != nil {
// return err
// }
// }
//
// In above snippet of code, p is a []byte and messageType is an int with value
// websocket.BinaryMessage or websocket.TextMessage.
//
// An application can also send and receive messages using the io.WriteCloser
// and io.Reader interfaces. To send a message, call the connection NextWriter
// method to get an io.WriteCloser, write the message to the writer and close
// the writer when done. To receive a message, call the connection NextReader
// method to get an io.Reader and read until io.EOF is returned. This snippet
// 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 close messages by sending a close message to the
// peer and returning a *CloseError from the the NextReader, ReadMessage or the
// message Read method.
//
// Connections handle received ping and pong messages by invoking callback
// functions set with SetPingHandler and SetPongHandler methods. The callback
// functions are called from the NextReader, ReadMessage and the message Read
// methods.
//
// The default ping handler sends a pong to the peer. The application's reading
// goroutine can block for a short time while the handler writes the pong data
// to the connection.
//
// The application must read the connection to process ping, pong 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, EnableWriteCompression, SetCompressionLevel) 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.
//
// Compression EXPERIMENTAL
//
// Per message compression extensions (RFC 7692) are experimentally supported
// by this package in a limited capacity. Setting the EnableCompression option
// to true in Dialer or Upgrader will attempt to negotiate per message deflate
// support.
//
// var upgrader = websocket.Upgrader{
// EnableCompression: true,
// }
//
// If compression was successfully negotiated with the connection's peer, any
// message received in compressed form will be automatically decompressed.
// All Read methods will return uncompressed bytes.
//
// Per message compression of messages written to a connection can be enabled
// or disabled by calling the corresponding Conn method:
//
// conn.EnableWriteCompression(false)
//
// Currently this package does not support compression with "context takeover".
// This means that messages must be compressed and decompressed in isolation,
// without retaining sliding window or dictionary state across messages. For
// more details refer to RFC 7692.
//
// Use of compression is experimental and may result in decreased performance.
package websocket

View File

@ -1,55 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"encoding/json"
"io"
)
// WriteJSON is deprecated, use c.WriteJSON instead.
func WriteJSON(c *Conn, v interface{}) error {
return c.WriteJSON(v)
}
// WriteJSON writes the JSON encoding of v to the connection.
//
// See the documentation for encoding/json Marshal for details about the
// conversion of Go values to JSON.
func (c *Conn) WriteJSON(v interface{}) error {
w, err := c.NextWriter(TextMessage)
if err != nil {
return err
}
err1 := json.NewEncoder(w).Encode(v)
err2 := w.Close()
if err1 != nil {
return err1
}
return err2
}
// ReadJSON is deprecated, use c.ReadJSON instead.
func ReadJSON(c *Conn, v interface{}) error {
return c.ReadJSON(v)
}
// ReadJSON reads the next JSON-encoded message from the connection and stores
// it in the value pointed to by v.
//
// See the documentation for the encoding/json Unmarshal function for details
// about the conversion of JSON to a Go value.
func (c *Conn) ReadJSON(v interface{}) error {
_, r, err := c.NextReader()
if err != nil {
return err
}
err = json.NewDecoder(r).Decode(v)
if err == io.EOF {
// One value is expected in the message.
err = io.ErrUnexpectedEOF
}
return err
}

View File

@ -1,55 +0,0 @@
// Copyright 2016 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 !appengine
package websocket
import "unsafe"
const wordSize = int(unsafe.Sizeof(uintptr(0)))
func maskBytes(key [4]byte, pos int, b []byte) int {
// Mask one byte at a time for small buffers.
if len(b) < 2*wordSize {
for i := range b {
b[i] ^= key[pos&3]
pos++
}
return pos & 3
}
// Mask one byte at a time to word boundary.
if n := int(uintptr(unsafe.Pointer(&b[0]))) % wordSize; n != 0 {
n = wordSize - n
for i := range b[:n] {
b[i] ^= key[pos&3]
pos++
}
b = b[n:]
}
// Create aligned word size key.
var k [wordSize]byte
for i := range k {
k[i] = key[(pos+i)&3]
}
kw := *(*uintptr)(unsafe.Pointer(&k))
// Mask one word at a time.
n := (len(b) / wordSize) * wordSize
for i := 0; i < n; i += wordSize {
*(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(i))) ^= kw
}
// Mask one byte at a time for remaining bytes.
b = b[n:]
for i := range b {
b[i] ^= key[pos&3]
pos++
}
return pos & 3
}

View File

@ -1,15 +0,0 @@
// Copyright 2016 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 appengine
package websocket
func maskBytes(key [4]byte, pos int, b []byte) int {
for i := range b {
b[i] ^= key[pos&3]
pos++
}
return pos & 3
}

Some files were not shown because too many files have changed in this diff Show More