├── .gitignore
├── examples
├── webrtc
│ ├── index.html
│ ├── README.md
│ ├── webrtc.html
│ └── js
│ │ └── namedwebsockets.js
├── chat
│ ├── README.md
│ └── chat.html
└── pubsub
│ ├── README.md
│ ├── hub.js
│ └── pubsub.html
├── _templates
├── README.md
└── console.html
├── LICENSE.txt
├── utils.go
├── client.go
├── channel.go
├── transport.go
├── proxy.go
├── peer.go
├── discovery.go
├── client_server_test.go
├── service.go
├── lib
└── namedwebsockets.js
├── README.md
└── templates.go
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 |
10 | # Architecture specific extensions/prefixes
11 | *.[568vq]
12 | [568vq].out
13 |
14 | *.cgo1.go
15 | *.cgo2.c
16 | _cgo_defun.c
17 | _cgo_gotypes.go
18 | _cgo_export.*
19 |
20 | _testmain.go
21 |
22 | *.exe
23 | *.test
--------------------------------------------------------------------------------
/examples/webrtc/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WebRTC signalling over Named WebSockets
5 |
6 |
7 |
8 | This web page is now available at webrtc.html .
9 | Redirecting you in 5 seconds...
10 |
11 |
--------------------------------------------------------------------------------
/_templates/README.md:
--------------------------------------------------------------------------------
1 | Network Web Socket Proxy Template Files
2 | ===
3 |
4 | This folder contains _source_ template files for Network Web Socket Proxy applications.
5 |
6 | In order to use these templates in package builds they must be compiled. To compile the template files we use the [go-bindata](https://github.com/jteeuwen/go-bindata) tool.
7 |
8 | `go-bindata` can be obtained using `go get`:
9 |
10 | go get github.com/jteeuwen/go-bindata/...
11 |
12 | If any files in this folder change they need re-packing with the following command before re-running the Network Web Socket Proxy:
13 |
14 | cd `go list -f '{{.Dir}}' github.com/namedwebsockets/networkwebsockets`
15 | go-bindata -ignore=README\\.md -o templates.go _templates/
16 |
17 | More information on `go-bindata` is available in the [go-bindata README](https://github.com/jteeuwen/go-bindata/blob/master/README.md).
--------------------------------------------------------------------------------
/examples/chat/README.md:
--------------------------------------------------------------------------------
1 | Network Web Sockets Chat Demo
2 | ===
3 |
4 | A simple chat client on top of [Network Web Sockets](https://github.com/namedwebsockets/networkwebsockets).
5 |
6 | #### Running the example
7 |
8 | 1. Ensure a Network Web Socket Proxy has been downloaded and is currently running on your local machine. You can download a pre-built Network Web Socket Proxy [here](https://github.com/namedwebsockets/networkwebsockets/releases).
9 |
10 | 2. Open [`chat.html`](http://namedwebsockets.github.io/networkwebsockets/examples/chat/chat.html) in a browser window.
11 |
12 | 3. Open [`chat.html`](http://namedwebsockets.github.io/networkwebsockets/examples/chat/chat.html) in another browser window on the local machine or on another device that is also running a Network Web Socket Proxy within the local network.
13 |
14 | 4. Send messages from one chat window and watch the chat message appear in the other chat window (and vice-versa).
15 |
--------------------------------------------------------------------------------
/examples/pubsub/README.md:
--------------------------------------------------------------------------------
1 | Network Web Sockets PubSub Demo
2 | ===
3 |
4 | A basic [publish/subscribe](https://en.wikipedia.org/wiki/Publish–subscribe_pattern) demo on top of [Network Web Sockets](https://github.com/namedwebsockets/networkkwebsockets).
5 |
6 | #### Running the example
7 |
8 | 1. Ensure a Network Web Socket Proxy has been downloaded and is currently running on your local machine. You can download a pre-built Network Web Socket Proxy [here](https://github.com/namedwebsockets/networkwebsockets/releases).
9 |
10 | 2. Open [`pubsub.html`](http://namedwebsockets.github.io/networkwebsockets/examples/pubsub/pubsub.html) on your local machine.
11 |
12 | 3. Open [`pubsub.html`](http://namedwebsockets.github.io/networkwebsockets/examples/pubsub/pubsub.html) in another browser window on the local machine or on another device that is also running a Network Web Socket Proxy within the local network.
13 |
14 | 4. Log in and out on one window using the interface provided and watch the authorization state get applied to the other window (and vice-versa).
15 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT) Copyright (c) 2014 Rich Tibbett
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
--------------------------------------------------------------------------------
/examples/webrtc/README.md:
--------------------------------------------------------------------------------
1 | Network Web Sockets WebRTC Conferencing Demo
2 | ===
3 |
4 | A pure P2P A/V conferencing demo via WebRTC on top of [Network Web Sockets](https://github.com/namedwebsockets/networkwebsockets).
5 |
6 | This demo establishes a Network Web Socket channel to initially transfer signalling data between WebRTC session peers. Using Network Web Sockets means no STUN or TURN servers are required for establishment of WebRTC sessions and no centralized servers are required to establish local WebRTC sessions.
7 |
8 | #### Running the example
9 |
10 | 1. Ensure a Network Web Socket Proxy has been downloaded and is currently running on your local machine. You can download a pre-built Network Web Socket Proxy [here](https://github.com/namedwebsockets/networkwebsockets/releases).
11 |
12 | 2. Open [webrtc.html](http://namedwebsockets.github.io/networkwebsockets/examples/webrtc/webrtc.html) on your local machine.
13 |
14 | 3. Open [webrtc.html](http://namedwebsockets.github.io/networkwebsockets/examples/webrtc/webrtc.html) in another browser window on your local machine (in the same browser or in a different browser) or on another device that is also running a Named WebSockets proxy in the local network.
15 |
16 | 4. Give each web page access to your web camera and microphone to participate in the conference.
17 |
18 | 5. Enjoy your zero-config WebRTC session. Other WebRTC peers can join and leave at any time by opening [the same webrtc.html](http://namedwebsockets.github.io/networkwebsockets/examples/webrtc/webrtc.html).
19 |
--------------------------------------------------------------------------------
/examples/pubsub/hub.js:
--------------------------------------------------------------------------------
1 | /***
2 | *
3 | * Basic Publish/Subscribe library for Named WebSockets
4 | * ------------------------------------------------------------------------
5 | * ------------------------------------------------------------------------
6 | * https://github.com/namedwebsockets/namedwebsockets/tree/master/examples/pubsub
7 | * ------------------------------------------------------------------------
8 | *
9 | * For an example of usage, please see `pubsub.html`.
10 | *
11 | */
12 | var NamedWS_PubSubHub = function(namedWebSocketObj) {
13 | this.ws = namedWebSocketObj;
14 | this.ws.addEventListener("open", function() {
15 | for(var msg in this.sendQueue) {
16 | this.ws.send(this.sendQueue[msg]);
17 | }
18 | this.sendQueue = [];
19 | }.bind(this));
20 |
21 | this.topicSubscriptions = [];
22 |
23 | this.ws.onmessage = function(messageEvent) {
24 | // Distribute received message to subscriber
25 | try {
26 | var msg = JSON.parse(messageEvent.data);
27 |
28 | if (msg.action && msg.action == "publish") {
29 | var subscriptions = this.topicSubscriptions[msg.topicURI];
30 | for (var callback in subscriptions) {
31 | (subscriptions[callback]).call(this, msg.payload);
32 | }
33 | }
34 | } catch (e) {
35 | console.error("Could not process publish message");
36 | }
37 | }.bind(this);
38 | };
39 |
40 | NamedWS_PubSubHub.prototype.constructor = NamedWS_PubSubHub;
41 |
42 | NamedWS_PubSubHub.prototype.subscribe = function(topicURI, successCallback) {
43 | if (!successCallback) return;
44 | var subscriptions = this.topicSubscriptions[topicURI] || [];
45 | subscriptions.push(successCallback);
46 | this.topicSubscriptions[topicURI] = subscriptions;
47 | };
48 |
49 | NamedWS_PubSubHub.prototype.unsubscribe = function(topicURI, successCallback) {
50 | if (!successCallback) return;
51 | var subscriptions = this.topicSubscriptions[topicURI] || [];
52 | for (var i in subscriptions) {
53 | if (successCallback == subscriptions[i]) {
54 | subscriptions.splice(i, 1);
55 | break;
56 | }
57 | }
58 | this.topicSubscriptions[topicURI] = subscriptions;
59 | }
60 |
61 | NamedWS_PubSubHub.prototype.publish = function(topicURI, payload, successCallback) {
62 | // Send over websocket
63 | var publishMsg = {
64 | action: "publish",
65 | topicURI: topicURI || "",
66 | payload: payload || {}
67 | };
68 |
69 | var msg = JSON.stringify(publishMsg)
70 |
71 | if (this.ws.readyState != 1) {
72 | this.sendQueue.push(msg);
73 | } else {
74 | this.ws.send(msg);
75 | }
76 |
77 | if (successCallback) successCallback.call(this);
78 | };
79 |
--------------------------------------------------------------------------------
/examples/chat/chat.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Named WebSockets Chat Demo
5 |
6 |
7 |
60 |
96 |
97 |
98 |
99 |
103 |
104 |
--------------------------------------------------------------------------------
/examples/pubsub/pubsub.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Named WebSockets Publish/Subscribe Demo
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
75 |
76 |
77 |
78 | Named WebSocket Publish/Subscribe Demo
79 |
80 | User name:
81 |
82 |
83 | Log:
84 |
85 |
86 |
87 |
88 |
89 | Open this page in another window to see federated login/logout working across Named WebSocket connected pages.
90 |
91 |
92 | Click here to open another window now
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/utils.go:
--------------------------------------------------------------------------------
1 | package networkwebsockets
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "log"
8 | "net/http"
9 | "net/url"
10 | "strings"
11 | "time"
12 |
13 | tls "github.com/richtr/go-tls-srp"
14 | "github.com/richtr/websocket"
15 | )
16 |
17 | func encodeWireMessage(action, source, target, payload string) ([]byte, error) {
18 | // Construct proxy wire message
19 | m := WireMessage{
20 | Action: action,
21 | Source: source,
22 | Target: target,
23 | Payload: payload,
24 | }
25 |
26 | return json.Marshal(m) // returns ([]byte, error)
27 | }
28 |
29 | func decodeWireMessage(msg []byte) (WireMessage, error) {
30 | var message WireMessage
31 | err := json.Unmarshal(msg, &message)
32 |
33 | return message, err
34 | }
35 |
36 | func upgradeHTTPToWebSocket(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
37 | // Chose a subprotocol from those offered in the client request
38 | selectedSubprotocol := ""
39 | if subprotocolsStr := strings.TrimSpace(r.Header.Get("Sec-Websocket-Protocol")); subprotocolsStr != "" {
40 | // Choose the first subprotocol requested in 'Sec-Websocket-Protocol' header
41 | selectedSubprotocol = strings.Split(subprotocolsStr, ",")[0]
42 | }
43 |
44 | upgrader := websocket.Upgrader{
45 | ReadBufferSize: 8192,
46 | WriteBufferSize: 8192,
47 | CheckOrigin: func(r *http.Request) bool {
48 | return true // allow all cross-origin access
49 | },
50 | }
51 |
52 | ws, err := upgrader.Upgrade(w, r, map[string][]string{
53 | "Access-Control-Allow-Origin": []string{"*"},
54 | "Access-Control-Allow-Credentials": []string{"true"},
55 | "Access-Control-Allow-Headers": []string{"content-type"},
56 | // Return requested subprotocol(s) as supported so peers can handle it
57 | "Sec-Websocket-Protocol": []string{selectedSubprotocol},
58 | })
59 | if err != nil {
60 | if _, ok := err.(websocket.HandshakeError); !ok {
61 | log.Println(err)
62 | }
63 | return nil, err
64 | }
65 |
66 | return ws, nil
67 | }
68 |
69 | func dialProxyFromDNSRecord(record *DNSRecord, channel *Channel) error {
70 |
71 | hosts := [...]string{record.AddrV4.String(), record.AddrV6.String()}
72 |
73 | for i := 0; i < len(hosts); i++ {
74 |
75 | if hosts[i] == "" {
76 | continue
77 | }
78 |
79 | addr := fmt.Sprintf("%s:%d", hosts[i], record.Port)
80 |
81 | // Build URL
82 | remoteWSUrl := url.URL{
83 | Scheme: "wss",
84 | Host: addr,
85 | Path: record.Path,
86 | }
87 |
88 | // Establish Proxy WebSocket connection over TLS-SRP
89 |
90 | tlsSrpDialer := &TLSSRPDialer{
91 | &websocket.Dialer{
92 | HandshakeTimeout: time.Duration(10) * time.Second,
93 | ReadBufferSize: 8192,
94 | WriteBufferSize: 8192,
95 | },
96 | &tls.Config{
97 | SRPUser: record.Hash_Base64,
98 | SRPPassword: channel.serviceName,
99 | },
100 | }
101 |
102 | ws, _, nErr := tlsSrpDialer.Dial(remoteWSUrl, map[string][]string{
103 | "Origin": []string{"localhost"},
104 | "Sec-WebSocket-Protocol": []string{"nws-proxy-draft-01"},
105 | })
106 | if nErr != nil {
107 | errStr := fmt.Sprintf("Proxy named web socket connection to wss://%s%s failed: %s", remoteWSUrl.Host, remoteWSUrl.Path, nErr)
108 | return errors.New(errStr)
109 | }
110 |
111 | log.Printf("Established proxy named web socket connection to wss://%s%s", remoteWSUrl.Host, remoteWSUrl.Path)
112 |
113 | // Create, bind and start a new proxy connection
114 | proxyConn := NewProxy(ws, false)
115 | proxyConn.setHash_Base64(record.Hash_Base64)
116 | proxyConn.Start(channel)
117 |
118 | return nil
119 |
120 | }
121 |
122 | return errors.New("Could not establish proxy named web socket connection")
123 |
124 | }
125 |
--------------------------------------------------------------------------------
/client.go:
--------------------------------------------------------------------------------
1 | package networkwebsockets
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 | "time"
7 |
8 | "github.com/richtr/websocket"
9 | )
10 |
11 | type ClientMessageHandler struct {
12 | client *Client
13 | }
14 |
15 | func (handler *ClientMessageHandler) Read(buf []byte) error {
16 | client := handler.client
17 | if client == nil {
18 | return errors.New("ClientMessageHandler requires an attached Client object")
19 | }
20 |
21 | message, err := decodeWireMessage(buf)
22 | if err != nil {
23 | return err
24 | }
25 |
26 | switch message.Action {
27 | case "connect":
28 | client.Connect <- message
29 | case "disconnect":
30 | client.Disconnect <- message
31 | case "status":
32 | client.Status <- message
33 | case "broadcast":
34 | client.Broadcast <- message
35 | case "message":
36 | client.Message <- message
37 | }
38 |
39 | return nil
40 | }
41 |
42 | func (handler *ClientMessageHandler) Write(buf []byte) error {
43 | client := handler.client
44 | if client == nil {
45 | return errors.New("ClientMessageHandler requires an attached Client object")
46 | }
47 |
48 | if !client.transport.open {
49 | return errors.New("Client is not active")
50 | }
51 |
52 | client.transport.conn.SetWriteDeadline(time.Now().Add(writeWait))
53 | client.transport.conn.WriteMessage(websocket.TextMessage, buf)
54 |
55 | return nil
56 | }
57 |
58 | func Dial(urlStr string, handler MessageHandler) (*Client, *http.Response, error) {
59 | d := websocket.Dialer{
60 | HandshakeTimeout: 10 * time.Second,
61 | ReadBufferSize: 8192,
62 | WriteBufferSize: 8192,
63 | }
64 |
65 | wsConn, httpResp, err := d.Dial(urlStr, nil)
66 | if err != nil {
67 | return nil, nil, err
68 | }
69 |
70 | transport := NewTransport(wsConn, handler)
71 |
72 | client := NewClient(transport)
73 |
74 | // Setup default client message handler if one has not been provided
75 | if client.transport.handler == nil {
76 | client.transport.handler = &ClientMessageHandler{client}
77 | }
78 |
79 | // Start read/write pumps
80 | client.Start()
81 |
82 | return client, httpResp, nil
83 | }
84 |
85 | // Client interface
86 |
87 | type Client struct {
88 | // Underlying transport object
89 | transport *Transport
90 |
91 | // incoming message channels
92 | Status chan WireMessage
93 | Connect chan WireMessage
94 | Disconnect chan WireMessage
95 | Message chan WireMessage
96 | Broadcast chan WireMessage
97 | }
98 |
99 | func NewClient(transport *Transport) *Client {
100 | client := &Client{
101 | transport: transport,
102 |
103 | Status: make(chan WireMessage, 255),
104 | Connect: make(chan WireMessage, 255),
105 | Disconnect: make(chan WireMessage, 255),
106 | Message: make(chan WireMessage, 255),
107 | Broadcast: make(chan WireMessage, 255),
108 | }
109 |
110 | return client
111 | }
112 |
113 | func (client *Client) Start() {
114 | // Start read/write pumps
115 | client.transport.Start()
116 | }
117 |
118 | func (client *Client) Stop() {
119 | // Stop read/write pumps
120 | client.transport.Stop()
121 | }
122 |
123 | // Default Client Message Handler Helper functions
124 |
125 | func (client *Client) SendBroadcastData(data string) {
126 | if wireData, err := encodeWireMessage("broadcast", "", "", data); err == nil {
127 | client.transport.Write(wireData)
128 | }
129 | }
130 |
131 | func (client *Client) SendMessageData(data string, targetId string) {
132 | if targetId == "" {
133 | return
134 | }
135 |
136 | if wireData, err := encodeWireMessage("message", "", targetId, data); err == nil {
137 | client.transport.Write(wireData)
138 | }
139 | }
140 |
141 | func (client *Client) SendStatusRequest() {
142 | if wireData, err := encodeWireMessage("status", "", "", ""); err == nil {
143 | client.transport.Write(wireData)
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/channel.go:
--------------------------------------------------------------------------------
1 | package networkwebsockets
2 |
3 | import (
4 | "encoding/base64"
5 | "fmt"
6 | "log"
7 |
8 | "github.com/richtr/bcrypt"
9 | )
10 |
11 | type Channel struct {
12 | serviceName string
13 |
14 | serviceHash string
15 |
16 | servicePath string
17 |
18 | proxyPath string
19 |
20 | // The current websocket connection instances to this named websocket
21 | peers []*Peer
22 |
23 | // The current websocket proxy connection instances to this named websocket
24 | proxies []*Proxy
25 |
26 | // Buffered channel of outbound service messages.
27 | broadcastBuffer chan *WireMessage
28 |
29 | // Attached DNS-SD discovery registration and browser for this Network Web Socket
30 | discoveryService *DiscoveryService
31 |
32 | done chan int // blocks until .Stop() is called
33 | }
34 |
35 | // Create a new Channel instance with a given service type
36 | func NewChannel(service *Service, serviceName string) *Channel {
37 | serviceHash_BCrypt, _ := bcrypt.HashBytes([]byte(serviceName))
38 | serviceHash_Base64 := base64.StdEncoding.EncodeToString(serviceHash_BCrypt)
39 |
40 | channel := &Channel{
41 | serviceName: serviceName,
42 | serviceHash: serviceHash_Base64,
43 |
44 | servicePath: fmt.Sprintf("/%s", serviceName),
45 |
46 | peers: make([]*Peer, 0),
47 | proxies: make([]*Proxy, 0),
48 | broadcastBuffer: make(chan *WireMessage, 512),
49 |
50 | done: make(chan int, 1),
51 | }
52 |
53 | channel.proxyPath = fmt.Sprintf("/%s", GenerateId())
54 |
55 | go channel.messageDispatcher()
56 |
57 | log.Printf("New '%s' channel peer created.", channel.serviceName)
58 |
59 | service.Channels[channel.servicePath] = channel
60 |
61 | // Terminate channel when it is closed
62 | go func() {
63 | <-channel.stopNotify()
64 | delete(service.Channels, channel.servicePath)
65 | }()
66 |
67 | // Add TLS-SRP credentials for access to this service to credentials store
68 | // TODO isolate this per socket
69 | serviceTab[channel.serviceHash] = channel.serviceName
70 |
71 | go channel.advertise(service.ProxyPort)
72 |
73 | if service.discoveryBrowser != nil {
74 |
75 | // Attempt to resolve discovered unknown service hashes with this service name
76 | recordsCache := make(map[string]*DNSRecord)
77 | for _, cachedRecord := range service.discoveryBrowser.cachedDNSRecords {
78 | if bcrypt.Match(channel.serviceName, cachedRecord.Hash_BCrypt) {
79 | if dErr := dialProxyFromDNSRecord(cachedRecord, channel); dErr != nil {
80 | log.Printf("err: %v", dErr)
81 | }
82 | } else {
83 | // Maintain as an unresolved entry in cache
84 | recordsCache[cachedRecord.Hash_Base64] = cachedRecord
85 | }
86 | }
87 |
88 | // Replace unresolved DNS-SD service entries cache
89 | service.discoveryBrowser.cachedDNSRecords = recordsCache
90 |
91 | }
92 |
93 | return channel
94 | }
95 |
96 | func (channel *Channel) advertise(port int) {
97 | if channel.discoveryService == nil {
98 | // Advertise new socket type on the network
99 | channel.discoveryService = NewDiscoveryService(channel.serviceName, channel.serviceHash, channel.proxyPath, port)
100 | channel.discoveryService.Register("local")
101 | }
102 | }
103 |
104 | // Send service broadcast messages on Channel connections
105 | func (channel *Channel) messageDispatcher() {
106 | for {
107 | select {
108 | case wsBroadcast, ok := <-channel.broadcastBuffer:
109 | if !ok {
110 | return
111 | }
112 | // Send message to local peers
113 | channel.localBroadcast(wsBroadcast)
114 | // Send message to remote proxies
115 | channel.remoteBroadcast(wsBroadcast)
116 | }
117 | }
118 | }
119 |
120 | // Broadcast a message to all peer connections for this Channel
121 | // instance (except to the src websocket connection)
122 | func (channel *Channel) localBroadcast(broadcast *WireMessage) {
123 | // Write to peer connections
124 | for _, peer := range channel.peers {
125 | // don't send back to self
126 | if peer.id == broadcast.Source {
127 | continue
128 | }
129 | if wireData, err := encodeWireMessage("broadcast", broadcast.Source, "", broadcast.Payload); err == nil {
130 | peer.transport.Write(wireData)
131 | }
132 | }
133 | }
134 |
135 | // Broadcast a message to all proxy connections for this Channel
136 | // instance (except to the src websocket connection)
137 | func (channel *Channel) remoteBroadcast(broadcast *WireMessage) {
138 | // Only send to remote proxies if this message was not received from a proxy itself
139 | if broadcast.fromProxy {
140 | return
141 | }
142 |
143 | // Write to proxy connections
144 | for _, proxy := range channel.proxies {
145 | // don't send back to self
146 | // only write to *writeable* proxy connections
147 | if !proxy.writeable || proxy.base.id == broadcast.Source {
148 | continue
149 | }
150 | if wireData, err := encodeWireMessage("broadcast", broadcast.Source, "", broadcast.Payload); err == nil {
151 | proxy.base.transport.Write(wireData)
152 | }
153 | }
154 | }
155 |
156 | // Destroy this Network Web Socket service instance, close all
157 | // peer and proxy connections.
158 | func (channel *Channel) Stop() {
159 | // Close discovery browser
160 | if channel.discoveryService != nil {
161 | channel.discoveryService.Shutdown()
162 | }
163 |
164 | for _, peer := range channel.peers {
165 | peer.Stop()
166 | }
167 |
168 | for _, proxy := range channel.proxies {
169 | proxy.Stop()
170 | }
171 |
172 | // Indicate object is closed
173 | channel.done <- 1
174 | }
175 |
176 | // StopNotify returns a channel that receives a empty integer
177 | // when the channel service is terminated.
178 | func (channel *Channel) stopNotify() <-chan int { return channel.done }
179 |
--------------------------------------------------------------------------------
/examples/webrtc/webrtc.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WebRTC signalling over Named WebSockets
5 |
6 |
7 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
200 |
201 |
202 |
--------------------------------------------------------------------------------
/transport.go:
--------------------------------------------------------------------------------
1 | package networkwebsockets
2 |
3 | import (
4 | "errors"
5 | "log"
6 | "net/http"
7 | "net/url"
8 | "strings"
9 | "sync"
10 | "time"
11 |
12 | tls "github.com/richtr/go-tls-srp"
13 | "github.com/richtr/websocket"
14 | )
15 |
16 | const (
17 | // Time allowed to write a message to any websocket.
18 | writeWait = 10 * time.Second
19 |
20 | // Time allowed to read the next pong message from any websocket.
21 | pongWait = 60 * time.Second
22 |
23 | // Send pings to peer with this period. Must be less than pongWait.
24 | pingPeriod = (pongWait * 9) / 10
25 |
26 | // Maximum message size allowed from any websocket.
27 | maxMessageSize = 8192
28 | )
29 |
30 | type MessageHandler interface {
31 | Read(buf []byte) error
32 | Write(buf []byte) error
33 | }
34 |
35 | // JSON structure to message sending
36 | type WireMessage struct {
37 | // Proxy message type: "connect", "disconnect", "message", "broadcast"
38 | Action string `json:"action"`
39 |
40 | Source string `json:"source,omitempty"`
41 |
42 | Target string `json:"target,omitempty"`
43 |
44 | // Message contents
45 | Payload string `json:"data,omitempty"`
46 |
47 | // Whether this message originated from a Proxy object
48 | fromProxy bool `json:"-"`
49 | }
50 |
51 | type Transport struct {
52 | conn *websocket.Conn
53 | handler MessageHandler
54 | open bool
55 | done chan int // blocks until .Stop() is called
56 | }
57 |
58 | func NewTransport(conn *websocket.Conn, handler MessageHandler) *Transport {
59 | transport := &Transport{
60 | conn: conn,
61 | handler: handler,
62 |
63 | done: make(chan int, 1),
64 | }
65 |
66 | return transport
67 | }
68 |
69 | func (t *Transport) Start() {
70 | var wg sync.WaitGroup
71 | wg.Add(2)
72 |
73 | go t.readPump(&wg)
74 | go t.writePump(&wg)
75 |
76 | t.open = true
77 |
78 | wg.Wait()
79 | }
80 |
81 | func (t *Transport) Stop() {
82 | t.open = false
83 |
84 | t.conn.Close()
85 | }
86 |
87 | // StopNotify returns a channel that receives a empty integer
88 | // when the transport is closed
89 | func (t *Transport) StopNotify() <-chan int { return t.done }
90 |
91 | func (t *Transport) Read(buf []byte) error {
92 | if !t.open {
93 | return errors.New("Transport is not currently active for reading")
94 | }
95 |
96 | if t.handler == nil {
97 | return errors.New("Cannot read message. Transport does not have a handler assigned")
98 | }
99 |
100 | return t.handler.Read(buf)
101 | }
102 |
103 | func (t *Transport) Write(buf []byte) error {
104 | if !t.open {
105 | return errors.New("Transport is not currently active for writing")
106 | }
107 |
108 | if t.handler == nil {
109 | return errors.New("Cannot write message. Transport does not have a handler assigned")
110 | }
111 |
112 | return t.handler.Write(buf)
113 | }
114 |
115 | // readPump pumps messages from an individual websocket connection to the dispatcher
116 | func (t *Transport) readPump(wg *sync.WaitGroup) {
117 | t.conn.SetReadLimit(maxMessageSize)
118 | t.conn.SetReadDeadline(time.Now().Add(pongWait))
119 | t.conn.SetPongHandler(func(string) error {
120 | t.conn.SetReadDeadline(time.Now().Add(pongWait))
121 | return nil
122 | })
123 |
124 | wg.Done()
125 |
126 | for {
127 | opCode, buf, err := t.conn.ReadMessage()
128 | if err != nil || opCode != websocket.TextMessage {
129 | break
130 | }
131 |
132 | // Pass incoming message to our assigned message handler
133 | if err := t.Read(buf); err != nil {
134 | log.Printf("err: %v", err)
135 | }
136 | }
137 |
138 | // Indicate object is closed
139 | t.done <- 1
140 | }
141 |
142 | // writePump keeps an individual websocket connection alive
143 | func (t *Transport) writePump(wg *sync.WaitGroup) {
144 | ticker := time.NewTicker(pingPeriod)
145 | defer func() {
146 | ticker.Stop()
147 | }()
148 |
149 | wg.Done()
150 |
151 | for {
152 | select {
153 | case <-ticker.C:
154 | t.conn.SetWriteDeadline(time.Now().Add(writeWait))
155 | if err := t.conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
156 | return
157 | }
158 | }
159 | }
160 | }
161 |
162 | /** TLS-SRP Dialer interface **/
163 |
164 | type TLSSRPDialer struct {
165 | *websocket.Dialer
166 |
167 | TLSClientConfig *tls.Config
168 | }
169 |
170 | // Dial creates a new TLS-SRP based client connection. Use requestHeader to specify the
171 | // origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie).
172 | // Use the response.Header to get the selected subprotocol
173 | // (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
174 | //
175 | // If the WebSocket handshake fails, ErrBadHandshake is returned along with a
176 | // non-nil *http.Response so that callers can handle redirects, authentication,
177 | // etc.
178 | func (d *TLSSRPDialer) Dial(url url.URL, requestHeader http.Header) (*websocket.Conn, *http.Response, error) {
179 | var deadline time.Time
180 |
181 | if d.HandshakeTimeout != 0 {
182 | deadline = time.Now().Add(d.HandshakeTimeout)
183 | }
184 |
185 | netConn, err := tls.Dial("tcp", url.Host, d.TLSClientConfig)
186 | if err != nil {
187 | return nil, nil, err
188 | }
189 |
190 | defer func() {
191 | if netConn != nil {
192 | netConn.Close()
193 | }
194 | }()
195 |
196 | if err := netConn.SetDeadline(deadline); err != nil {
197 | return nil, nil, err
198 | }
199 |
200 | if len(d.Subprotocols) > 0 {
201 | h := http.Header{}
202 | for k, v := range requestHeader {
203 | h[k] = v
204 | }
205 | h.Set("Sec-Websocket-Protocol", strings.Join(d.Subprotocols, ", "))
206 | requestHeader = h
207 | }
208 |
209 | conn, resp, err := websocket.NewClient(netConn, &url, requestHeader, d.ReadBufferSize, d.WriteBufferSize)
210 | if err != nil {
211 | return nil, resp, err
212 | }
213 |
214 | netConn.SetDeadline(time.Time{})
215 | netConn = nil // to avoid close in defer.
216 | return conn, resp, nil
217 | }
218 |
--------------------------------------------------------------------------------
/proxy.go:
--------------------------------------------------------------------------------
1 | package networkwebsockets
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "time"
7 |
8 | "github.com/richtr/websocket"
9 | )
10 |
11 | type Proxy struct {
12 | // Inherit attributes from Peer struct
13 | base Peer
14 |
15 | // Discovered proxy connection's base64 hash value
16 | // empty unless set via .setHash_Base64()
17 | Hash_Base64 string
18 |
19 | // List of connection ids that this proxy connection 'owns'
20 | peerIds map[string]bool
21 |
22 | // Whether this proxy connection is writeable
23 | writeable bool
24 | }
25 |
26 | type ProxyMessageHandler struct {
27 | proxy *Proxy
28 | }
29 |
30 | func (handler *ProxyMessageHandler) Read(buf []byte) error {
31 | proxy := handler.proxy
32 | if proxy == nil {
33 | return errors.New("ProxyMessageHandler requires an attached Proxy object")
34 | }
35 |
36 | message, err := decodeWireMessage(buf)
37 | if err != nil {
38 | return err
39 | }
40 |
41 | switch message.Action {
42 | case "connect":
43 |
44 | proxy.peerIds[message.Target] = true
45 |
46 | // Inform all local peer connections that this proxy owns this peer connection
47 | for _, peer := range proxy.base.channel.peers {
48 | if wireData, err := encodeWireMessage("connect", peer.id, message.Target, ""); err == nil {
49 | peer.transport.Write(wireData)
50 | }
51 | }
52 |
53 | return nil
54 |
55 | case "disconnect":
56 |
57 | delete(proxy.peerIds, message.Target)
58 |
59 | // Inform all local peer connections that this proxy no longer owns this peer connection
60 | for _, peer := range proxy.base.channel.peers {
61 | if wireData, err := encodeWireMessage("disconnect", peer.id, message.Target, ""); err == nil {
62 | peer.transport.Write(wireData)
63 | }
64 | }
65 |
66 | return nil
67 |
68 | case "broadcast":
69 |
70 | // broadcast message on to given target
71 | wsBroadcast := &WireMessage{
72 | Action: "broadcast",
73 | Source: message.Source,
74 | Target: "", // target all connections
75 | Payload: message.Payload,
76 | fromProxy: true,
77 | }
78 |
79 | proxy.base.channel.broadcastBuffer <- wsBroadcast
80 |
81 | return nil
82 |
83 | case "message":
84 |
85 | messageSent := false
86 |
87 | // Relay message to channel peer that matches target
88 | for _, peer := range proxy.base.channel.peers {
89 | if peer.id == message.Target {
90 | if wireData, err := encodeWireMessage("message", message.Source, message.Target, message.Payload); err == nil {
91 | peer.transport.Write(wireData)
92 | }
93 | messageSent = true
94 | break
95 | }
96 | }
97 |
98 | if !messageSent {
99 | fmt.Errorf("P2P message target could not be found. Not sent.")
100 | }
101 |
102 | return nil
103 | }
104 |
105 | return errors.New("Could not find target for message")
106 | }
107 |
108 | func (handler *ProxyMessageHandler) Write(buf []byte) error {
109 | proxy := handler.proxy
110 | if proxy == nil {
111 | return errors.New("ProxyMessageHandler requires an attached Proxy object")
112 | }
113 |
114 | if !proxy.base.active {
115 | return errors.New("Proxy is not active")
116 | }
117 |
118 | proxy.base.transport.conn.SetWriteDeadline(time.Now().Add(writeWait))
119 | proxy.base.transport.conn.WriteMessage(websocket.TextMessage, buf)
120 |
121 | return nil
122 | }
123 |
124 | func NewProxy(conn *websocket.Conn, isWriteable bool) *Proxy {
125 | proxyConn := &Proxy{
126 | base: Peer{
127 | id: GenerateId(),
128 | },
129 | Hash_Base64: "",
130 | writeable: isWriteable,
131 | peerIds: make(map[string]bool),
132 | }
133 |
134 | // Create a new peer socket message handler
135 | handler := &ProxyMessageHandler{proxyConn}
136 |
137 | // Create a new peer socket transporter
138 | transport := NewTransport(conn, handler)
139 |
140 | // Attach peer transport to peer object
141 | proxyConn.base.transport = transport
142 |
143 | return proxyConn
144 | }
145 |
146 | func (proxy *Proxy) Start(channel *Channel) error {
147 | if channel == nil {
148 | return errors.New("Proxy requires a channel to start")
149 | }
150 |
151 | if proxy.base.active {
152 | return errors.New("Proxy is already started")
153 | }
154 |
155 | proxy.base.channel = channel
156 |
157 | // Start connection read/write pumps
158 | proxy.base.transport.Start()
159 | go func() {
160 | <-proxy.base.transport.StopNotify()
161 | proxy.Stop()
162 | }()
163 |
164 | proxy.base.active = true
165 |
166 | // Add reference to this proxy connection to channel
167 | proxy.addConnection()
168 |
169 | return nil
170 | }
171 |
172 | func (proxy *Proxy) Stop() error {
173 | if !proxy.base.active {
174 | return errors.New("Proxy cannot be stopped because it is not currently active")
175 | }
176 |
177 | // Remove references to this proxy connection from channel
178 | proxy.removeConnection()
179 |
180 | // Close underlying websocket connection
181 | proxy.base.transport.Stop()
182 |
183 | // If no more local peers are connected then remove the current Network Web Socket service
184 | if len(proxy.base.channel.peers) == 0 {
185 | proxy.base.channel.Stop()
186 | }
187 |
188 | proxy.base.active = false
189 |
190 | return nil
191 | }
192 |
193 | func (proxy *Proxy) setHash_Base64(hash string) {
194 | proxy.Hash_Base64 = hash
195 | }
196 |
197 | // Set up a new Channel connection instance
198 | func (proxy *Proxy) addConnection() {
199 | proxy.base.channel.proxies = append(proxy.base.channel.proxies, proxy)
200 |
201 | if proxy.writeable {
202 | // Inform this proxy of all the peer connections we own
203 | for _, peer := range proxy.base.channel.peers {
204 | if wireData, err := encodeWireMessage("connect", proxy.base.id, peer.id, ""); err == nil {
205 | proxy.base.transport.Write(wireData)
206 | }
207 | }
208 | }
209 | }
210 |
211 | // Tear down an existing Channel connection instance
212 | func (proxy *Proxy) removeConnection() {
213 | for i, conn := range proxy.base.channel.proxies {
214 | if proxy.base.id == conn.base.id {
215 | proxy.base.channel.proxies[i] = nil // allow to be garbage-collected
216 | proxy.base.channel.proxies = append(proxy.base.channel.proxies[:i], proxy.base.channel.proxies[i+1:]...)
217 | break
218 | }
219 | }
220 |
221 | if proxy.writeable {
222 | // Inform this proxy of all the peer connections we no longer own
223 | for _, peer := range proxy.base.channel.peers {
224 | if wireData, err := encodeWireMessage("disconnect", proxy.base.id, peer.id, ""); err == nil {
225 | proxy.base.transport.Write(wireData)
226 | }
227 | }
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/peer.go:
--------------------------------------------------------------------------------
1 | package networkwebsockets
2 |
3 | import (
4 | "errors"
5 | "time"
6 |
7 | "github.com/richtr/websocket"
8 | )
9 |
10 | type Peer struct {
11 | // Unique identifier for this peer connection
12 | id string
13 |
14 | // The Network Web Socket channel to which this peer connection belongs
15 | channel *Channel
16 |
17 | // Transport object
18 | transport *Transport
19 |
20 | active bool
21 | }
22 |
23 | type PeerMessageHandler struct {
24 | peer *Peer
25 | }
26 |
27 | func (handler *PeerMessageHandler) Read(buf []byte) error {
28 | peer := handler.peer
29 | if peer == nil {
30 | return errors.New("PeerMessageHandler requires an attached Peer object")
31 | }
32 |
33 | message, err := decodeWireMessage(buf)
34 | if err != nil {
35 | return err
36 | }
37 |
38 | switch message.Action {
39 |
40 | case "connect":
41 | case "disconnect":
42 | // 'connect' and 'disconnect' events are write-only so will not be handled here
43 | return nil
44 |
45 | case "status":
46 |
47 | // Echo peer id back to callee
48 | wireData, err := encodeWireMessage("status", peer.id, peer.id, "")
49 |
50 | if err != nil {
51 | return err
52 | }
53 |
54 | peer.transport.Write(wireData)
55 |
56 | return nil
57 |
58 | case "broadcast":
59 |
60 | wsBroadcast := &WireMessage{
61 | Action: "broadcast",
62 | Source: peer.id,
63 | Target: "", // target all connections
64 | Payload: message.Payload,
65 | fromProxy: false,
66 | }
67 | peer.channel.broadcastBuffer <- wsBroadcast
68 |
69 | return nil
70 |
71 | case "message":
72 |
73 | if message.Target == "" {
74 | return errors.New("Message must have a target identifier")
75 | }
76 |
77 | wireData, err := encodeWireMessage("message", peer.id, message.Target, message.Payload)
78 |
79 | if err != nil {
80 | return err
81 | }
82 |
83 | // Relay message to peer channel that matches target
84 | for _, _peer := range peer.channel.peers {
85 | if _peer.id == message.Target {
86 | _peer.transport.Write(wireData)
87 | return nil
88 | }
89 | }
90 |
91 | // If we have not delivered the message yet then hunt for a
92 | // proxy that owns target peer id in known proxies
93 | for _, proxy := range peer.channel.proxies {
94 | if proxy.peerIds[message.Target] {
95 | proxy.base.transport.Write(wireData)
96 | return nil
97 | }
98 | }
99 |
100 | }
101 |
102 | return errors.New("Could not find target for message")
103 | }
104 |
105 | func (handler *PeerMessageHandler) Write(buf []byte) error {
106 | peer := handler.peer
107 | if peer == nil {
108 | return errors.New("PeerMessageHandler requires an attached Peer object")
109 | }
110 |
111 | if !peer.active {
112 | return errors.New("Peer is not active")
113 | }
114 |
115 | peer.transport.conn.SetWriteDeadline(time.Now().Add(writeWait))
116 | peer.transport.conn.WriteMessage(websocket.TextMessage, buf)
117 |
118 | return nil
119 | }
120 |
121 | func NewPeer(conn *websocket.Conn) *Peer {
122 | peerConn := &Peer{
123 | id: GenerateId(),
124 | }
125 |
126 | // Create a new peer socket message handler
127 | handler := &PeerMessageHandler{peerConn}
128 |
129 | // Create a new peer socket transporter
130 | transport := NewTransport(conn, handler)
131 |
132 | // Attach peer transport to peer object
133 | peerConn.transport = transport
134 |
135 | return peerConn
136 | }
137 |
138 | func (peer *Peer) Start(channel *Channel) error {
139 | if channel == nil {
140 | return errors.New("Peer requires a channel to start")
141 | }
142 |
143 | if peer.active {
144 | return errors.New("Peer is already started")
145 | }
146 |
147 | peer.channel = channel
148 |
149 | // Start connection read/write pumps
150 | peer.transport.Start()
151 | go func() {
152 | <-peer.transport.StopNotify()
153 | peer.Stop()
154 | }()
155 |
156 | peer.active = true
157 |
158 | // Add reference to this peer connection to channel
159 | peer.addConnection()
160 |
161 | return nil
162 | }
163 |
164 | func (peer *Peer) Stop() error {
165 | if !peer.active {
166 | return errors.New("Peer cannot be stopped because it is not currently active")
167 | }
168 |
169 | // Remove references to this peer connection from channel
170 | peer.removeConnection()
171 |
172 | // Close websocket connection
173 | peer.transport.Stop()
174 |
175 | // If no more local peers are connected then remove the current Network Web Socket service
176 | if len(peer.channel.peers) == 0 {
177 | peer.channel.Stop()
178 | }
179 |
180 | peer.active = false
181 |
182 | return nil
183 | }
184 |
185 | // Set up a new Channel connection instance
186 | func (peer *Peer) addConnection() {
187 | // Add this websocket instance to Network Web Socket broadcast list
188 | peer.channel.peers = append(peer.channel.peers, peer)
189 |
190 | for _, _peer := range peer.channel.peers {
191 | if _peer.id != peer.id {
192 | // Inform other local peer connections that we now own this peer
193 | if wireData, err := encodeWireMessage("connect", _peer.id, peer.id, ""); err == nil {
194 | _peer.transport.Write(wireData)
195 | }
196 |
197 | // Inform this peer of all the other peer connections we own
198 | if wireData, err := encodeWireMessage("connect", peer.id, _peer.id, ""); err == nil {
199 | peer.transport.Write(wireData)
200 | }
201 | }
202 | }
203 |
204 | for _, proxy := range peer.channel.proxies {
205 | // Inform all proxy connections that we now own this peer connection
206 | if proxy.writeable {
207 | if wireData, err := encodeWireMessage("connect", proxy.base.id, peer.id, ""); err == nil {
208 | proxy.base.transport.Write(wireData)
209 | }
210 | }
211 | // Inform current peer of all the peer connections other connected proxies own
212 | for peerId, _ := range proxy.peerIds {
213 | if wireData, err := encodeWireMessage("connect", proxy.base.id, peerId, ""); err == nil {
214 | peer.transport.Write(wireData)
215 | }
216 | }
217 | }
218 | }
219 |
220 | // Tear down an existing Channel connection instance
221 | func (peer *Peer) removeConnection() {
222 | for i, conn := range peer.channel.peers {
223 | if conn.id == peer.id {
224 | peer.channel.peers[i] = nil
225 | peer.channel.peers = append(peer.channel.peers[:i], peer.channel.peers[i+1:]...)
226 | break
227 | }
228 | }
229 |
230 | // Inform all local peer connections that we no longer own this peer connection
231 | for _, _peer := range peer.channel.peers {
232 | // don't notify peer if its id matches the peer's id
233 | if _peer.id != peer.id {
234 | if wireData, err := encodeWireMessage("disconnect", _peer.id, peer.id, ""); err == nil {
235 | _peer.transport.Write(wireData)
236 | }
237 | }
238 | }
239 |
240 | // Inform all proxy connections that we no longer own this peer connection
241 | for _, proxy := range peer.channel.proxies {
242 | if proxy.writeable {
243 | if wireData, err := encodeWireMessage("disconnect", proxy.base.id, peer.id, ""); err == nil {
244 | proxy.base.transport.Write(wireData)
245 | }
246 | }
247 | }
248 | }
249 |
--------------------------------------------------------------------------------
/discovery.go:
--------------------------------------------------------------------------------
1 | package networkwebsockets
2 |
3 | import (
4 | "encoding/base64"
5 | "errors"
6 | "fmt"
7 | "log"
8 | "net"
9 | "strings"
10 | "time"
11 |
12 | "github.com/richtr/bcrypt"
13 | "github.com/richtr/mdns"
14 | )
15 |
16 | const (
17 | ipv4mdns = "224.0.0.251"
18 | ipv6mdns = "ff02::fb"
19 | mdnsPort = 5406 // operate on our own multicast port (standard mDNS port is 5353)
20 | )
21 |
22 | var (
23 | network_ipv4Addr = &net.UDPAddr{
24 | IP: net.ParseIP(ipv4mdns),
25 | Port: mdnsPort,
26 | }
27 | network_ipv6Addr = &net.UDPAddr{
28 | IP: net.ParseIP(ipv6mdns),
29 | Port: mdnsPort,
30 | }
31 | )
32 |
33 | /** Network Web Socket DNS-SD Discovery Client interface **/
34 |
35 | type DiscoveryService struct {
36 | Name string
37 | Hash string
38 | Path string
39 | Port int
40 |
41 | server *mdns.Server
42 | }
43 |
44 | func NewDiscoveryService(name, hash, path string, port int) *DiscoveryService {
45 | discoveryService := &DiscoveryService{
46 | Name: name,
47 | Hash: hash,
48 | Path: path,
49 | Port: port,
50 | }
51 |
52 | return discoveryService
53 | }
54 |
55 | func (dc *DiscoveryService) Register(domain string) {
56 | dnssdServiceId := GenerateId()
57 |
58 | s := &mdns.MDNSService{
59 | Instance: dnssdServiceId,
60 | Service: "_nws._tcp",
61 | Domain: domain,
62 | Port: dc.Port,
63 | Info: fmt.Sprintf("hash=%s,path=%s", dc.Hash, dc.Path),
64 | }
65 |
66 | if err := s.Init(); err != nil {
67 | log.Printf("Could not register service on network. %v", err)
68 | return
69 | }
70 |
71 | var mdnsClientConfig *mdns.Config
72 |
73 | // Advertise service to the correct endpoint (local or network)
74 | mdnsClientConfig = &mdns.Config{
75 | IPv4Addr: network_ipv4Addr,
76 | IPv6Addr: network_ipv6Addr,
77 | }
78 |
79 | // Add the DNS zone record to advertise
80 | mdnsClientConfig.Zone = s
81 |
82 | serv, err := mdns.NewServer(mdnsClientConfig)
83 |
84 | if err != nil {
85 | log.Printf("Failed to create new mDNS server. %v", err)
86 | return
87 | }
88 |
89 | dc.server = serv
90 |
91 | log.Printf("New '%s' channel advertised as '%s' in %s network", dc.Name, fmt.Sprintf("%s._nws._tcp", dnssdServiceId), domain)
92 | }
93 |
94 | func (dc *DiscoveryService) Shutdown() {
95 | if dc.server != nil {
96 | dc.server.Shutdown()
97 | }
98 | }
99 |
100 | /** Network Web Socket DNS-SD Discovery Server interface **/
101 |
102 | type DiscoveryBrowser struct {
103 | // Network Web Socket DNS-SD records currently unresolved by this proxy instance
104 | cachedDNSRecords map[string]*DNSRecord
105 | closed bool
106 | }
107 |
108 | func NewDiscoveryBrowser() *DiscoveryBrowser {
109 | discoveryBrowser := &DiscoveryBrowser{
110 | cachedDNSRecords: make(map[string]*DNSRecord, 255),
111 | closed: false,
112 | }
113 |
114 | return discoveryBrowser
115 | }
116 |
117 | func (ds *DiscoveryBrowser) Browse(service *Service, timeoutSeconds int) {
118 |
119 | entries := make(chan *mdns.ServiceEntry, 255)
120 |
121 | recordsCache := make(map[string]*DNSRecord, 255)
122 |
123 | timeout := time.Duration(timeoutSeconds) * time.Second
124 |
125 | var targetIPv4 *net.UDPAddr
126 | var targetIPv6 *net.UDPAddr
127 |
128 | targetIPv4 = network_ipv4Addr
129 | targetIPv6 = network_ipv6Addr
130 |
131 | // Only look for Network Web Socket DNS-SD services
132 | params := &mdns.QueryParam{
133 | Service: "_nws._tcp",
134 | Domain: "local",
135 | Timeout: timeout,
136 | Entries: entries,
137 | IPv4mdns: targetIPv4,
138 | IPv6mdns: targetIPv6,
139 | }
140 |
141 | go func() {
142 | complete := false
143 | timeoutFinish := time.After(timeout)
144 |
145 | // Wait for responses until timeout
146 | for !complete {
147 | select {
148 | case discoveredService, ok := <-entries:
149 |
150 | if !ok {
151 | continue
152 | }
153 |
154 | serviceRecord, err := NewServiceRecordFromDNSRecord(discoveredService)
155 | if err != nil {
156 | log.Printf("err: %v", err)
157 | continue
158 | }
159 |
160 | // Ignore our own Channel services
161 | if service.isOwnProxyService(serviceRecord) {
162 | continue
163 | }
164 |
165 | // Ignore previously discovered Channel proxy services
166 | if service.isActiveProxyService(serviceRecord) {
167 | continue
168 | }
169 |
170 | // Resolve discovered service hash provided against available services
171 | var channel *Channel
172 | for _, knownService := range service.Channels {
173 | if bcrypt.Match(knownService.serviceName, serviceRecord.Hash_BCrypt) {
174 | channel = knownService
175 | break
176 | }
177 | }
178 |
179 | if channel != nil {
180 | // Create new web socket connection toward discovered proxy
181 | if dErr := dialProxyFromDNSRecord(serviceRecord, channel); dErr != nil {
182 | log.Printf("err: %v", dErr)
183 | continue
184 | }
185 | } else {
186 | // Store as an unresolved DNS-SD record
187 | recordsCache[serviceRecord.Hash_Base64] = serviceRecord
188 | continue
189 | }
190 |
191 | case <-timeoutFinish:
192 | // Replace unresolved DNS records cache
193 | ds.cachedDNSRecords = recordsCache
194 |
195 | complete = true
196 | }
197 | }
198 | }()
199 |
200 | // Run the mDNS/DNS-SD query
201 | err := mdns.Query(params)
202 |
203 | if err != nil {
204 | log.Printf("Could not perform mDNS/DNS-SD query. %v", err)
205 | return
206 | }
207 | }
208 |
209 | func (ds *DiscoveryBrowser) Shutdown() {
210 | ds.closed = true
211 | }
212 |
213 | /** Network Web Socket DNS Record interface **/
214 |
215 | type DNSRecord struct {
216 | *mdns.ServiceEntry
217 |
218 | Path string
219 | Hash_Base64 string
220 | Hash_BCrypt string
221 | }
222 |
223 | func NewServiceRecordFromDNSRecord(serviceEntry *mdns.ServiceEntry) (*DNSRecord, error) {
224 | servicePath := ""
225 | serviceHash_Base64 := ""
226 | serviceHash_BCrypt := ""
227 |
228 | if serviceEntry.Info == "" {
229 | return nil, errors.New("Could not find associated TXT record for advertised Network Web Socket service")
230 | }
231 |
232 | serviceParts := strings.FieldsFunc(serviceEntry.Info, func(r rune) bool {
233 | return r == '=' || r == ',' || r == ';' || r == ' '
234 | })
235 | if len(serviceParts) > 1 {
236 | for i := 0; i < len(serviceParts); i += 2 {
237 | if strings.ToLower(serviceParts[i]) == "path" {
238 | servicePath = serviceParts[i+1]
239 | }
240 | if strings.ToLower(serviceParts[i]) == "hash" {
241 | serviceHash_Base64 = serviceParts[i+1]
242 |
243 | if res, err := base64.StdEncoding.DecodeString(serviceHash_Base64); err == nil {
244 | serviceHash_BCrypt = string(res)
245 | }
246 | }
247 | }
248 | }
249 |
250 | if servicePath == "" || serviceHash_Base64 == "" || serviceHash_BCrypt == "" {
251 | return nil, errors.New("Could not resolve the provided Network Web Socket DNS Record")
252 | }
253 |
254 | // Create and return a new Network Web Socket DNS Record with the parsed information
255 | newServiceDNSRecord := &DNSRecord{serviceEntry, servicePath, serviceHash_Base64, serviceHash_BCrypt}
256 |
257 | return newServiceDNSRecord, nil
258 | }
259 |
--------------------------------------------------------------------------------
/client_server_test.go:
--------------------------------------------------------------------------------
1 | package networkwebsockets
2 |
3 | import (
4 | "log"
5 | "testing"
6 | )
7 |
8 | func createClient(t testing.TB, urlStr string) *Client {
9 | client, _, err := Dial(urlStr, nil) // use default ClientMessageHandler
10 | if err != nil {
11 | t.Fatalf("Dial: ", err)
12 | }
13 | return client
14 | }
15 |
16 | func getClientId(client *Client) string {
17 | // Request client's peer id
18 | client.SendStatusRequest()
19 | // Wait for response
20 | message := <-client.Status
21 | // Return client's peer id
22 | return message.Target
23 | }
24 |
25 | func checkConnect(t testing.TB, message WireMessage, expectedTarget string) {
26 | if message.Target != expectedTarget {
27 | t.Fatalf("connect=%s, want %s", message.Target, expectedTarget)
28 | }
29 | }
30 |
31 | func checkDisconnect(t testing.TB, message WireMessage, expectedTarget string) {
32 | if message.Target != expectedTarget {
33 | t.Fatalf("disconnect=%s, want %s", message.Target, expectedTarget)
34 | }
35 | }
36 |
37 | func checkBroadcast(t testing.TB, payload string, sender *Client, receivers []*Client) {
38 | // send broadcast message from sender
39 | sender.SendBroadcastData(payload)
40 |
41 | // check broadcast message arrived at all receivers
42 | for _, receiver := range receivers {
43 | message := <-receiver.Broadcast
44 | if message.Payload != payload {
45 | t.Fatalf("broadcast=%s, want %s", message.Payload, payload)
46 | }
47 | }
48 | }
49 |
50 | func checkMessage(t testing.TB, payload string, targetId string, sender *Client, receiver *Client) {
51 | if targetId == "" {
52 | t.Fatalf("No target identifier provided")
53 | }
54 |
55 | // send broadcast message from sender
56 | sender.SendMessageData(payload, targetId)
57 |
58 | // check broadcast message arrived at all receivers
59 | message := <-receiver.Message
60 | if message.Payload != payload {
61 | t.Fatalf("message=%s, want %s", message.Payload, payload)
62 | }
63 | }
64 |
65 | // TEST CASES
66 |
67 | func TestSameProxyClients(t *testing.T) {
68 |
69 | service := NewService("localhost", 21000)
70 | service.Start()
71 |
72 | // Create new Network Web Socket channel peers
73 | client1 := createClient(t, "ws://localhost:21000/testservice1")
74 | client2 := createClient(t, "ws://localhost:21000/testservice1")
75 | client3 := createClient(t, "ws://localhost:21000/testservice1")
76 |
77 | // Test status messaging (+ store client ids for future tests)
78 | client1Id := getClientId(client1)
79 | client2Id := getClientId(client2)
80 | client3Id := getClientId(client3)
81 |
82 | // Test connect messaging
83 | checkConnect(t, <-client1.Connect, client2Id)
84 | checkConnect(t, <-client1.Connect, client3Id)
85 | checkConnect(t, <-client2.Connect, client1Id)
86 | checkConnect(t, <-client2.Connect, client3Id)
87 | checkConnect(t, <-client3.Connect, client1Id)
88 | checkConnect(t, <-client3.Connect, client2Id)
89 |
90 | // Test broadcast messaging
91 | checkBroadcast(t, "hello world 1", client1, []*Client{client2, client3})
92 | checkBroadcast(t, "hello world 2", client2, []*Client{client1, client3})
93 | checkBroadcast(t, "hello world 3", client3, []*Client{client1, client2})
94 |
95 | // Test direct messaging
96 | checkMessage(t, "direct message 1", client2Id, client1, client2)
97 | checkMessage(t, "direct message 2", client3Id, client1, client3)
98 | checkMessage(t, "direct message 3", client1Id, client2, client1)
99 | checkMessage(t, "direct message 4", client3Id, client2, client3)
100 | checkMessage(t, "direct message 5", client1Id, client3, client1)
101 | checkMessage(t, "direct message 6", client2Id, client3, client2)
102 |
103 | // Test disconnect messaging
104 |
105 | client1.Stop()
106 | checkDisconnect(t, <-client2.Disconnect, client1Id)
107 | checkDisconnect(t, <-client3.Disconnect, client1Id)
108 |
109 | client2.Stop()
110 | checkDisconnect(t, <-client3.Disconnect, client2Id)
111 |
112 | client3.Stop()
113 |
114 | go service.Stop()
115 |
116 | <-service.StopNotify()
117 | }
118 |
119 | func TestMultipleProxyClients(t *testing.T) {
120 |
121 | service1 := NewService("localhost", 21000)
122 | service1.Start()
123 |
124 | service2 := NewService("localhost", 21001)
125 | service2.Start()
126 |
127 | // Create new Network Web Socket channel peers
128 | client1 := createClient(t, "ws://localhost:21000/testservice2")
129 | client2 := createClient(t, "ws://localhost:21001/testservice2")
130 | client3 := createClient(t, "ws://localhost:21001/testservice2")
131 |
132 | // Test status messaging (+ store client ids for future tests)
133 | client1Id := getClientId(client1)
134 | client2Id := getClientId(client2)
135 | client3Id := getClientId(client3)
136 |
137 | log.Println("Waiting for Network Web Socket proxies to discover and connect to each other...")
138 |
139 | // Test connect messaging
140 | checkConnect(t, <-client1.Connect, client2Id)
141 | checkConnect(t, <-client1.Connect, client3Id)
142 | checkConnect(t, <-client2.Connect, client3Id)
143 | checkConnect(t, <-client2.Connect, client1Id)
144 | checkConnect(t, <-client3.Connect, client2Id)
145 | checkConnect(t, <-client3.Connect, client1Id)
146 |
147 | // Test broadcast messaging
148 | checkBroadcast(t, "hello world 1", client1, []*Client{client2, client3})
149 | checkBroadcast(t, "hello world 2", client2, []*Client{client1, client3})
150 | checkBroadcast(t, "hello world 3", client3, []*Client{client1, client2})
151 |
152 | // Test direct messaging
153 | checkMessage(t, "direct message 1", client2Id, client1, client2)
154 | checkMessage(t, "direct message 2", client3Id, client1, client3)
155 | checkMessage(t, "direct message 3", client1Id, client2, client1)
156 | checkMessage(t, "direct message 4", client3Id, client2, client3)
157 | checkMessage(t, "direct message 5", client1Id, client3, client1)
158 | checkMessage(t, "direct message 6", client2Id, client3, client2)
159 |
160 | // Test disconnect messaging
161 | client1.Stop()
162 | checkDisconnect(t, <-client2.Disconnect, client1Id)
163 | checkDisconnect(t, <-client3.Disconnect, client1Id)
164 |
165 | client2.Stop()
166 | checkDisconnect(t, <-client3.Disconnect, client2Id)
167 |
168 | client3.Stop()
169 |
170 | go func() {
171 | service1.Stop()
172 | service2.Stop()
173 | }()
174 |
175 | <-service1.StopNotify()
176 | <-service2.StopNotify()
177 | }
178 |
179 | // BENCHMARKS
180 |
181 | func BenchmarkSameProxyClientSetup(b *testing.B) {
182 | service := NewService("localhost", 21000)
183 | service.Start()
184 |
185 | b.ResetTimer() // start benchmark timer
186 |
187 | // run the benchmark function b.N times
188 | for n := 0; n < b.N; n++ {
189 | // Create new Network Web Socket channel peers
190 | client := createClient(b, "ws://localhost:21000/benchmarkservice1")
191 | _ = getClientId(client) // wait for client connection to be established
192 | client.Stop()
193 | }
194 |
195 | b.StopTimer() // end benchmark timer
196 |
197 | go service.Stop()
198 |
199 | <-service.StopNotify()
200 | }
201 |
202 | func BenchmarkSameProxyClientMessaging(b *testing.B) {
203 | service := NewService("localhost", 21000)
204 | _ = service.Start()
205 |
206 | client1 := createClient(b, "ws://localhost:21000/benchmarkservice2")
207 | client2 := createClient(b, "ws://localhost:21000/benchmarkservice2")
208 |
209 | client2Id := getClientId(client2)
210 |
211 | b.ResetTimer() // start benchmark timer
212 |
213 | // run the benchmark function b.N times
214 | for n := 0; n < b.N; n++ {
215 | checkMessage(b, "direct benchmark message", client2Id, client1, client2)
216 | }
217 |
218 | b.StopTimer() // end benchmark timer
219 |
220 | go func() {
221 | client1.Stop()
222 | client2.Stop()
223 |
224 | service.Stop()
225 | }()
226 |
227 | <-service.StopNotify()
228 | }
229 |
230 | func BenchmarkSameProxyClientBroadcast(b *testing.B) {
231 | service := NewService("localhost", 21000)
232 | service.Start()
233 |
234 | client1 := createClient(b, "ws://localhost:21000/benchmarkservice3")
235 | client2 := createClient(b, "ws://localhost:21000/benchmarkservice3")
236 |
237 | b.ResetTimer() // start benchmark timer
238 |
239 | // run the benchmark function b.N times
240 | for n := 0; n < b.N; n++ {
241 | checkBroadcast(b, "benchmark test msg", client1, []*Client{client2})
242 | }
243 |
244 | b.StopTimer() // end benchmark timer
245 |
246 | go func() {
247 | client1.Stop()
248 | client2.Stop()
249 |
250 | service.Stop()
251 | }()
252 |
253 | <-service.StopNotify()
254 | }
255 |
256 | func BenchmarkDifferentProxyClientBroadcast(b *testing.B) {
257 | service1 := NewService("localhost", 21000)
258 | service1.Start()
259 |
260 | service2 := NewService("localhost", 21001)
261 | service2.Start()
262 |
263 | client1 := createClient(b, "ws://localhost:21000/benchmarkservice4")
264 | client2 := createClient(b, "ws://localhost:21001/benchmarkservice4")
265 |
266 | <-client1.Connect
267 | <-client2.Connect
268 |
269 | b.ResetTimer() // start benchmark timer
270 |
271 | // run the benchmark function b.N times
272 | for n := 0; n < b.N; n++ {
273 | checkBroadcast(b, "benchmark test msg", client1, []*Client{client2})
274 | }
275 |
276 | b.StopTimer() // end benchmark timer
277 |
278 | client1.Stop()
279 | client2.Stop()
280 |
281 | go service1.Stop()
282 | <-service1.StopNotify()
283 |
284 | go service2.Stop()
285 | <-service2.StopNotify()
286 | }
287 |
--------------------------------------------------------------------------------
/service.go:
--------------------------------------------------------------------------------
1 | package networkwebsockets
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "math/rand"
7 | "net"
8 | "net/http"
9 | "os"
10 | "regexp"
11 | "strconv"
12 | "strings"
13 | "text/template"
14 | "time"
15 |
16 | tls "github.com/richtr/go-tls-srp"
17 | )
18 |
19 | var (
20 | // Proxy path matchers
21 | serviceNameRegexStr = "[A-Za-z0-9\\+=\\*\\._-]{1,255}"
22 | isValidCreateRequest = regexp.MustCompile(fmt.Sprintf("^/%s$", serviceNameRegexStr))
23 | isValidProxyRequest = regexp.MustCompile(fmt.Sprintf("^/%s$", serviceNameRegexStr))
24 |
25 | // TLS-SRP configuration components
26 | Salt = []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}
27 | serviceTab = CredentialsStore(map[string]string{})
28 | )
29 |
30 | // Generate a new random identifier
31 | func GenerateId() string {
32 | rand.Seed(time.Now().UTC().UnixNano())
33 | return fmt.Sprintf("%d", rand.Int())
34 | }
35 |
36 | type HTTPHandler interface {
37 | ServeLocalRequest(w http.ResponseWriter, r *http.Request)
38 | ServeProxyRequest(w http.ResponseWriter, r *http.Request)
39 | }
40 |
41 | type DefaultServiceHandler struct {
42 | service *Service
43 | }
44 |
45 | func (sh *DefaultServiceHandler) ServeLocalRequest(w http.ResponseWriter, r *http.Request) {
46 | service := sh.service
47 |
48 | if service == nil {
49 | http.Error(w, fmt.Sprintln("This interface is not attached to a service"), 403)
50 | return
51 | }
52 |
53 | // Only allow access from localhost to all services
54 | if isRequestFromLocalHost := service.checkRequestIsFromLocalHost(r.Host); !isRequestFromLocalHost {
55 | http.Error(w, fmt.Sprintln("This interface is only accessible from the local machine"), 403)
56 | return
57 | }
58 |
59 | if r.Method != "GET" {
60 | http.Error(w, "Method Not Allowed", 405)
61 | return
62 | }
63 |
64 | serviceName := strings.TrimPrefix(r.URL.Path, "/")
65 |
66 | // Serve console page for use in web browser if no service name has been requested
67 | if serviceName == "" {
68 |
69 | consoleHTML, err := Asset("_templates/console.html")
70 | if err != nil {
71 | // Asset was not found.
72 | http.Error(w, "Not Found", 404)
73 | return
74 | }
75 |
76 | t := template.Must(template.New("console").Parse(string(consoleHTML)))
77 | if t == nil {
78 | http.Error(w, "Internal Server Error", 501)
79 | return
80 | }
81 |
82 | t.Execute(w, service.Port)
83 |
84 | return
85 | }
86 |
87 | if isValidRequest := isValidCreateRequest.MatchString(r.URL.Path); !isValidRequest {
88 | http.Error(w, "Not Found", 404)
89 | return
90 | }
91 |
92 | if isValidWSUpgradeRequest := strings.ToLower(r.Header.Get("Upgrade")); isValidWSUpgradeRequest != "websocket" {
93 | http.Error(w, "Bad Request", 400)
94 | return
95 | }
96 |
97 | // Resolve to network web socket channel
98 | channel := service.GetChannelByName(serviceName)
99 | if channel == nil {
100 | channel = NewChannel(service, serviceName)
101 | }
102 |
103 | // Serve network web socket channel peer
104 | ws, err := upgradeHTTPToWebSocket(w, r)
105 | if err != nil {
106 | http.Error(w, "Bad Request", 400)
107 | return
108 | }
109 |
110 | // Create, bind and start a new peer connection
111 | peer := NewPeer(ws)
112 | peer.Start(channel)
113 | }
114 |
115 | func (sh *DefaultServiceHandler) ServeProxyRequest(w http.ResponseWriter, r *http.Request) {
116 | service := sh.service
117 |
118 | if service == nil {
119 | http.Error(w, fmt.Sprintln("This interface is not attached to a service"), 403)
120 | return
121 | }
122 |
123 | if r.Method != "GET" {
124 | http.Error(w, "Method Not Allowed", 405)
125 | return
126 | }
127 |
128 | if isValidRequest := isValidProxyRequest.MatchString(r.URL.Path); !isValidRequest {
129 | http.Error(w, "Not Found", 404)
130 | return
131 | }
132 |
133 | if isValidWSUpgradeRequest := strings.ToLower(r.Header.Get("Upgrade")); isValidWSUpgradeRequest != "websocket" {
134 | http.Error(w, "Bad Request", 400)
135 | return
136 | }
137 |
138 | requestedWebSocketSubProtocols := r.Header.Get("Sec-Websocket-Protocol")
139 | if requestedWebSocketSubProtocols != "nws-proxy-draft-01" {
140 | http.Error(w, "Bad Request", 400)
141 | return
142 | }
143 |
144 | // Resolve servicePath to an active named websocket service
145 | for _, channel := range service.Channels {
146 | if channel.proxyPath == r.URL.Path {
147 | ws, err := upgradeHTTPToWebSocket(w, r)
148 | if err != nil {
149 | http.Error(w, "Bad Request", 400)
150 | return
151 | }
152 |
153 | // Create, bind and start a new proxy connection
154 | proxy := NewProxy(ws, true)
155 | proxy.Start(channel)
156 |
157 | return
158 | }
159 | }
160 |
161 | http.Error(w, "Not Found", 404)
162 | return
163 | }
164 |
165 | type Service struct {
166 | Host string
167 | Port int
168 |
169 | ProxyPort int
170 |
171 | Handler HTTPHandler
172 |
173 | // All Network Web Socket channels that this service manages
174 | Channels map[string]*Channel
175 |
176 | discoveryBrowser *DiscoveryBrowser
177 |
178 | done chan int // blocks until .Stop() is called on this service
179 |
180 | localListener net.Listener
181 | netListener net.Listener
182 | }
183 |
184 | func NewService(host string, port int) *Service {
185 | if host == "" {
186 | hostname, err := os.Hostname()
187 | if err != nil {
188 | log.Printf("Could not determine device hostname: %v\n", err)
189 | return nil
190 | }
191 | host = hostname
192 | }
193 |
194 | if port <= 1024 || port >= 65534 {
195 | port = 9009
196 | }
197 |
198 | service := &Service{
199 | Host: host,
200 | Port: port,
201 |
202 | ProxyPort: 0,
203 |
204 | Channels: make(map[string]*Channel),
205 |
206 | discoveryBrowser: NewDiscoveryBrowser(),
207 |
208 | done: make(chan int),
209 | }
210 |
211 | // Setup a new default http service handler
212 | service.Handler = &DefaultServiceHandler{service}
213 |
214 | return service
215 | }
216 |
217 | func (service *Service) Start() <-chan int {
218 | // Start HTTP/Network Web Socket creation server
219 | service.StartHTTPServer()
220 |
221 | // Start TLS-SRP Network Web Socket (wss) proxy server
222 | service.StartProxyServer()
223 |
224 | // Start mDNS/DNS-SD Network Web Socket discovery service
225 | service.StartDiscoveryBrowser(10)
226 |
227 | return service.StopNotify()
228 | }
229 |
230 | func (service *Service) StartHTTPServer() {
231 | // Create a new custom http server multiplexer
232 | serveMux := http.NewServeMux()
233 |
234 | // Serve network web socket creation endpoints for localhost clients
235 | serveMux.HandleFunc("/", service.Handler.ServeLocalRequest)
236 |
237 | // Listen and on loopback address + port
238 | listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", service.Port))
239 | if err != nil {
240 | log.Fatal("Could not serve web server. ", err)
241 | }
242 |
243 | service.localListener = listener
244 |
245 | log.Printf("Serving Network Web Socket Creator Proxy at address [ ws://localhost:%d/ ]", service.Port)
246 |
247 | go http.Serve(listener, serveMux)
248 | }
249 |
250 | func (service *Service) StartProxyServer() {
251 | // Create a new custom http server multiplexer
252 | serveMux := http.NewServeMux()
253 |
254 | // Serve secure network web socket proxy endpoints for network clients
255 | serveMux.HandleFunc("/", service.Handler.ServeProxyRequest)
256 |
257 | // Generate random server salt for use in TLS-SRP data storage
258 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
259 | b := make([]rune, 32)
260 | for i := range b {
261 | b[i] = letters[rand.Intn(len(letters))]
262 | }
263 | srpSaltKey := string(b)
264 |
265 | tlsServerConfig := &tls.Config{
266 | SRPLookup: serviceTab,
267 | SRPSaltKey: srpSaltKey,
268 | SRPSaltSize: len(Salt),
269 | }
270 |
271 | // Listen on all addresses + port
272 | tlsSrpListener, err := tls.Listen("tcp", ":0", tlsServerConfig)
273 | if err != nil {
274 | log.Fatal("Could not serve proxy server. ", err)
275 | }
276 |
277 | service.netListener = tlsSrpListener
278 |
279 | // Obtain and store the port of the proxy endpoint
280 | _, port, err := net.SplitHostPort(tlsSrpListener.Addr().String())
281 | if err != nil {
282 | log.Fatal("Could not determine bound port of proxy server. ", err)
283 | }
284 |
285 | service.ProxyPort, _ = strconv.Atoi(port)
286 |
287 | log.Printf("Serving Network Web Socket Network Proxy at address [ wss://%s:%d/ ]", service.Host, service.ProxyPort)
288 |
289 | go http.Serve(tlsSrpListener, serveMux)
290 | }
291 |
292 | func (service *Service) StartDiscoveryBrowser(timeoutSeconds int) {
293 | log.Printf("Listening for Network Web Socket services on the local network...")
294 |
295 | go func() {
296 | defer service.discoveryBrowser.Shutdown()
297 |
298 | for !service.discoveryBrowser.closed {
299 | service.discoveryBrowser.Browse(service, timeoutSeconds)
300 | }
301 | }()
302 | }
303 |
304 | // Check whether we know the given service name
305 | func (service *Service) GetChannelByName(serviceName string) *Channel {
306 | for _, channel := range service.Channels {
307 | if channel.serviceName == serviceName {
308 | return channel
309 | }
310 | }
311 | return nil
312 | }
313 |
314 | // Check whether a DNS-SD derived Network Web Socket hash is owned by the current proxy instance
315 | func (service *Service) isOwnProxyService(serviceRecord *DNSRecord) bool {
316 | for _, channel := range service.Channels {
317 | if channel.serviceHash == serviceRecord.Hash_Base64 {
318 | return true
319 | }
320 | }
321 | return false
322 | }
323 |
324 | // Check whether a DNS-SD derived Network Web Socket hash is currently connected as a service
325 | func (service *Service) isActiveProxyService(serviceRecord *DNSRecord) bool {
326 | for _, channel := range service.Channels {
327 | for _, proxy := range channel.proxies {
328 | if proxy.Hash_Base64 == serviceRecord.Hash_Base64 {
329 | return true
330 | }
331 | }
332 | }
333 | return false
334 | }
335 |
336 | // Stop stops the server gracefully, and shuts down the running goroutine.
337 | // Stop should be called after a Start(s), otherwise it will block forever.
338 | func (service *Service) Stop() {
339 | if service.discoveryBrowser != nil {
340 | service.discoveryBrowser.closed = true
341 | }
342 |
343 | if service.localListener != nil {
344 | service.localListener.Close()
345 | }
346 |
347 | if service.netListener != nil {
348 | service.netListener.Close()
349 | }
350 |
351 | service.done <- 1
352 | }
353 |
354 | // StopNotify returns a channel that receives a empty integer
355 | // when the server is stopped.
356 | func (service *Service) StopNotify() <-chan int { return service.done }
357 |
358 | //
359 | // HELPER FUNCTIONS
360 | //
361 |
362 | func (service *Service) checkRequestIsFromLocalHost(host string) bool {
363 | allowedLocalHosts := map[string]bool{
364 | fmt.Sprintf("localhost:%d", service.Port): true,
365 | fmt.Sprintf("127.0.0.1:%d", service.Port): true,
366 | fmt.Sprintf("::1:%d", service.Port): true,
367 | fmt.Sprintf("%s:%d", service.Host, service.Port): true,
368 | }
369 |
370 | if allowedLocalHosts[host] {
371 | return true
372 | }
373 |
374 | return false
375 | }
376 |
377 | /** Simple in-memory storage table for TLS-SRP usernames/passwords **/
378 |
379 | type CredentialsStore map[string]string
380 |
381 | func (cs CredentialsStore) Lookup(user string) (v, s []byte, grp tls.SRPGroup, err error) {
382 | grp = tls.SRPGroup4096
383 |
384 | p := cs[user]
385 | if p == "" {
386 | return nil, nil, grp, nil
387 | }
388 |
389 | v = tls.SRPVerifier(user, p, Salt, grp)
390 | return v, Salt, grp, nil
391 | }
392 |
--------------------------------------------------------------------------------
/lib/namedwebsockets.js:
--------------------------------------------------------------------------------
1 | /***
2 | * NetworkWebSocket shim library
3 | * ----------------------------------------------------------------
4 | *
5 | * API Usage:
6 | * ----------
7 | *
8 | * // Connect with other peers using the same service name in the current network
9 | * var ws = new NetworkWebSocket("myServiceName");
10 | *
11 | * ...then use the returned `ws` object just like a normal JavaScript WebSocket object.
12 | *
13 | **/
14 | (function(global) {
15 |
16 | // *Always* connect to our own localhost-based endpoint for _creating_ new Network Web Sockets
17 | var endpointUrlBase = "ws://localhost:9009/";
18 |
19 | function isValidServiceName(channelName) {
20 | return /^[A-Za-z0-9\=\+\._-]{1,255}$/.test(channelName);
21 | }
22 |
23 | function toJson(data) {
24 | try {
25 | return JSON.parse(data);
26 | } catch (e) {}
27 | return false;
28 | }
29 |
30 | var _NetworkWebSocket = function (channelName, subprotocols) {
31 | if (!isValidServiceName(channelName)) {
32 | throw "Invalid Service Name: " + channelName;
33 | }
34 |
35 | // *Actual* web socket connection to Network Web Socket proxy
36 | var webSocket = new WebSocket(endpointUrlBase + channelName, subprotocols);
37 |
38 | // Root NetworkWebSocket object
39 | var networkWebSocket = new P2PWebSocket(webSocket);
40 |
41 | function getPeerById(id) {
42 | for (var i = 0; i < networkWebSocket.peers.length; i++) {
43 | if (networkWebSocket.peers[i].id == id) {
44 | return networkWebSocket.peers[i];
45 | }
46 | }
47 | }
48 |
49 | // Peer NetworkWebSocket objects list
50 | networkWebSocket.peers = [];
51 |
52 | // override
53 | networkWebSocket.send = function(data) {
54 | if (this.readyState != P2PWebSocket.prototype.OPEN) {
55 | throw "message cannot be sent because the web socket is not open";
56 | }
57 |
58 | var message = {
59 | "action": "broadcast",
60 | "data": data
61 | };
62 | this.socket.send(JSON.stringify(message));
63 | };
64 |
65 | // override
66 | networkWebSocket.close = function(code, reason) {
67 | if (this.readyState != P2PWebSocket.prototype.OPEN) {
68 | throw "web socket cannot be closed because it is not open";
69 | }
70 |
71 | webSocket.close();
72 | };
73 |
74 | webSocket.onopen = function(event) {
75 |
76 | networkWebSocket.__handleEvent({
77 | type: "open",
78 | readyState: P2PWebSocket.prototype.OPEN
79 | });
80 |
81 | };
82 |
83 | // Incoming Network Web Socket message dispatcher
84 | webSocket.onmessage = function(event) {
85 | var json = toJson(event.data);
86 |
87 | if (!json) {
88 | return
89 | }
90 |
91 | switch(json.action) {
92 |
93 | case "connect":
94 | // fire connect event on root network web socket object
95 |
96 | // Create a new WebSocket shim object
97 | var peerWebSocket = new P2PWebSocket(webSocket, networkWebSocket, json.target);
98 |
99 | // Add to root web sockets p2p sockets enumeration
100 | networkWebSocket.peers.push(peerWebSocket);
101 |
102 | // Fire 'connect' event at root websocket object
103 | // **then** fire p2p websocket 'open' event (see above)
104 | var connectEvt = new CustomEvent('connect', {
105 | "bubbles": false,
106 | "cancelable": false,
107 | "detail": {
108 | "target": peerWebSocket
109 | }
110 | });
111 | networkWebSocket.dispatchEvent(connectEvt);
112 |
113 | window.setTimeout(function() {
114 | // Fire 'open' event at new websocket shim object
115 | peerWebSocket.__handleEvent({
116 | type: "open",
117 | readyState: P2PWebSocket.prototype.OPEN
118 | });
119 | }, 50);
120 |
121 | break;
122 |
123 | case "disconnect":
124 | // close peer network web socket object
125 |
126 | var peerWebSocket = getPeerById(json.target);
127 | if (!peerWebSocket) {
128 | return;
129 | }
130 |
131 | // Create and fire events:
132 | // - 'close' on p2p websocket object
133 | // - 'disconnect' on root websocket object
134 | peerWebSocket.__doClose(3000, "Closed", networkWebSocket);
135 |
136 | // Remove p2p websocket from root network web socket peers list
137 | for (var i = 0; i < networkWebSocket.peers.length; i++) {
138 | if (networkWebSocket.peers[i].id == json.target) {
139 | networkWebSocket.peers.splice(i,1);
140 | break;
141 | }
142 | }
143 |
144 | break;
145 |
146 | case "broadcast":
147 | // dispatch to root network web socket object
148 |
149 | // Re-encode data payload as string
150 | var payload = json.data;
151 | if (Object.prototype.toString.call(payload) != '[object String]') {
152 | payload = JSON.stringify(payload);
153 | }
154 |
155 | // TODO: Check shim websocket readyState and queue or fire immediately
156 | networkWebSocket.__handleEvent({
157 | type: "message",
158 | message: payload,
159 | senderId: json.source
160 | });
161 |
162 | break;
163 |
164 | case "message":
165 | // dispatch to peer network web socket object
166 |
167 | var peerWebSocket = getPeerById(json.source);
168 | if (!peerWebSocket) {
169 | return
170 | }
171 |
172 | // Re-encode data payload as string
173 | var payload = json.data;
174 | if (Object.prototype.toString.call(payload) != '[object String]') {
175 | payload = JSON.stringify(payload);
176 | }
177 |
178 | // TODO: Check shim websocket readyState and queue or fire immediately
179 | peerWebSocket.__handleEvent({
180 | type: "message",
181 | message: payload,
182 | senderId: json.source
183 | });
184 |
185 | break;
186 |
187 | }
188 | };
189 |
190 | webSocket.onclose = function(event) {
191 |
192 | // Close all peer connections
193 | for (var target in networkWebSocket.peers) {
194 | networkWebSocket.peers[target].__doClose(3000, "Closed", networkWebSocket);
195 | }
196 | networkWebSocket.peers = [];
197 |
198 | // Close root connection
199 | networkWebSocket.__doClose(3000, "Closed")
200 |
201 | };
202 |
203 | return networkWebSocket;
204 |
205 | };
206 |
207 | /**** START WEBSOCKET SHIM ****/
208 |
209 | var P2PWebSocket = function(rootWebSocket, parentWebSocket, targetId) {
210 | this.id = targetId || "";
211 | this.socket = rootWebSocket;
212 | this.parent = parentWebSocket;
213 |
214 | // Setup dynamic WebSocket interface attributes
215 | this.url = "#"; // no url (...that's kind of the whole point :)
216 | this.readyState = P2PWebSocket.prototype.CONNECTING; // initial state
217 | this.bufferedAmount = 0;
218 | this.extensions = this.socket.extensions; // inherit
219 | this.protocol = this.socket.protocol; // inherit
220 | this.binaryType = "blob"; // as per WebSockets spec
221 |
222 | this.__events = {};
223 | };
224 |
225 | P2PWebSocket.prototype.send = function(data) {
226 | if (this.readyState != P2PWebSocket.prototype.OPEN) {
227 | throw "message cannot be sent because the web socket is not open";
228 | }
229 |
230 | var message = {
231 | "action": "message",
232 | "target": this.id,
233 | "data": data
234 | };
235 | this.socket.send(JSON.stringify(message));
236 | };
237 |
238 | P2PWebSocket.prototype.close = function(code, reason) {
239 | if (this.readyState != P2PWebSocket.prototype.OPEN) {
240 | throw "web socket cannot be closed because it is not open";
241 | }
242 |
243 | this.__doClose(code || 3001, reason || "Closed", this.parent);
244 | };
245 |
246 | P2PWebSocket.prototype.addEventListener = function(type, listener, useCapture) {
247 | if (!(type in this.__events)) {
248 | this.__events[type] = [];
249 | }
250 | this.__events[type].push(listener);
251 | };
252 |
253 | P2PWebSocket.prototype.removeEventListener = function(type, listener, useCapture) {
254 | if (!(type in this.__events)) return;
255 | var events = this.__events[type];
256 | for (var i = events.length - 1; i >= 0; --i) {
257 | if (events[i] === listener) {
258 | events.splice(i, 1);
259 | break;
260 | }
261 | }
262 | };
263 |
264 | P2PWebSocket.prototype.dispatchEvent = function(event) {
265 |
266 | // Delay until next run loop (like real events)
267 | window.setTimeout(function() {
268 |
269 | var events = this.__events[event.type] || [];
270 | for (var i = 0; i < events.length; ++i) {
271 | events[i].call(this, event);
272 | }
273 | var handler = this["on" + event.type];
274 | if (handler) handler.call(this, event);
275 |
276 | }.bind(this), 2);
277 |
278 | };
279 |
280 | P2PWebSocket.prototype.__handleEvent = function(flashEvent) {
281 |
282 | // Delay until next run loop (like real events)
283 | window.setTimeout(function() {
284 |
285 | if ("readyState" in flashEvent) {
286 | this.readyState = flashEvent.readyState;
287 | }
288 |
289 | var jsEvent;
290 | if (flashEvent.type == "open" || flashEvent.type == "closing" || flashEvent.type == "error") {
291 | jsEvent = this.__createSimpleEvent(flashEvent.type);
292 | } else if (flashEvent.type == "close") {
293 | jsEvent = this.__createSimpleEvent("close");
294 | jsEvent.code = flashEvent.code;
295 | jsEvent.reason = flashEvent.reason;
296 | } else if (flashEvent.type == "message") {
297 | jsEvent = this.__createMessageEvent(flashEvent.senderId, flashEvent.message);
298 | } else {
299 | throw "unknown event type: " + flashEvent.type;
300 | }
301 |
302 | this.dispatchEvent(jsEvent);
303 |
304 | // Fire callback (if any provided)
305 | if (flashEvent.callback) flashEvent.callback.call(this);
306 |
307 | }.bind(this), 1);
308 | };
309 |
310 | P2PWebSocket.prototype.__createSimpleEvent = function(type) {
311 | if (document.createEvent && window.Event) {
312 | var event = document.createEvent("Event");
313 | event.initEvent(type, false, false);
314 | return event;
315 | } else {
316 | return {type: type, bubbles: false, cancelable: false};
317 | }
318 | };
319 |
320 | P2PWebSocket.prototype.__createMessageEvent = function(senderId, data) {
321 | if (window.MessageEvent && typeof(MessageEvent) == "function") {
322 | return new MessageEvent("message", {
323 | "view": window,
324 | "bubbles": false,
325 | "cancelable": false,
326 | "senderId": senderId,
327 | "data": data
328 | });
329 | } else if (document.createEvent && window.MessageEvent) {
330 | var event = document.createEvent("MessageEvent");
331 | event.initMessageEvent("message", false, false, data, null, null, window, null);
332 | return event;
333 | } else {
334 | return {type: "message", data: data, bubbles: false, cancelable: false};
335 | }
336 | };
337 |
338 | P2PWebSocket.prototype.__doClose = function(code, reason, parentWebSocket) {
339 | // Fire 'open' event at new websocket shim object
340 | this.__handleEvent({
341 | type: "closing",
342 | readyState: P2PWebSocket.prototype.CLOSING,
343 | callback: function() {
344 | // Fire 'open' event at new websocket shim object
345 | this.__handleEvent({
346 | type: "close",
347 | readyState: P2PWebSocket.prototype.CLOSED,
348 | code: code,
349 | reason: reason,
350 | callback: function() {
351 | if (parentWebSocket) {
352 | // Fire 'disconnect' event at root websocket object
353 | var disconnectEvt = new CustomEvent('disconnect', {
354 | "bubbles": false,
355 | "cancelable": false,
356 | "detail": {
357 | "target": this
358 | }
359 | });
360 | parentWebSocket.dispatchEvent(disconnectEvt);
361 | }
362 | }
363 | });
364 | }
365 | });
366 | }
367 |
368 | /**
369 | * Define the WebSocket readyState enumeration.
370 | */
371 | P2PWebSocket.prototype.CONNECTING = 0;
372 | P2PWebSocket.prototype.OPEN = 1;
373 | P2PWebSocket.prototype.CLOSING = 2;
374 | P2PWebSocket.prototype.CLOSED = 3;
375 |
376 | /**** END WEBSOCKET SHIM ****/
377 |
378 | var NetworkWebSocket = function (channelName, subprotocols) {
379 | return new _NetworkWebSocket(channelName, subprotocols);
380 | };
381 |
382 | // Expose global functions
383 |
384 | if (!global.NetworkWebSocket) {
385 | global.NetworkWebSocket = (global.module || {}).exports = NetworkWebSocket;
386 | }
387 |
388 | })(this);
--------------------------------------------------------------------------------
/examples/webrtc/js/namedwebsockets.js:
--------------------------------------------------------------------------------
1 | /***
2 | * NetworkWebSocket shim library
3 | * ----------------------------------------------------------------
4 | *
5 | * API Usage:
6 | * ----------
7 | *
8 | * // Connect with other peers using the same service name in the current network
9 | * var ws = new NetworkWebSocket("myServiceName");
10 | *
11 | * ...then use the returned `ws` object just like a normal JavaScript WebSocket object.
12 | *
13 | **/
14 | (function(global) {
15 |
16 | // *Always* connect to our own localhost-based endpoint for _creating_ new Network Web Sockets
17 | var endpointUrlBase = "ws://localhost:9009/";
18 |
19 | function isValidServiceName(channelName) {
20 | return /^[A-Za-z0-9\=\+\._-]{1,255}$/.test(channelName);
21 | }
22 |
23 | function toJson(data) {
24 | try {
25 | return JSON.parse(data);
26 | } catch (e) {}
27 | return false;
28 | }
29 |
30 | var _NetworkWebSocket = function (channelName, subprotocols) {
31 | if (!isValidServiceName(channelName)) {
32 | throw "Invalid Service Name: " + channelName;
33 | }
34 |
35 | // *Actual* web socket connection to Network Web Socket proxy
36 | var webSocket = new WebSocket(endpointUrlBase + channelName, subprotocols);
37 |
38 | // Root NetworkWebSocket object
39 | var networkWebSocket = new P2PWebSocket(webSocket);
40 |
41 | function getPeerById(id) {
42 | for (var i = 0; i < networkWebSocket.peers.length; i++) {
43 | if (networkWebSocket.peers[i].id == id) {
44 | return networkWebSocket.peers[i];
45 | }
46 | }
47 | }
48 |
49 | // Peer NetworkWebSocket objects list
50 | networkWebSocket.peers = [];
51 |
52 | // override
53 | networkWebSocket.send = function(data) {
54 | if (this.readyState != P2PWebSocket.prototype.OPEN) {
55 | throw "message cannot be sent because the web socket is not open";
56 | }
57 |
58 | var message = {
59 | "action": "broadcast",
60 | "data": data
61 | };
62 | this.socket.send(JSON.stringify(message));
63 | };
64 |
65 | // override
66 | networkWebSocket.close = function(code, reason) {
67 | if (this.readyState != P2PWebSocket.prototype.OPEN) {
68 | throw "web socket cannot be closed because it is not open";
69 | }
70 |
71 | webSocket.close();
72 | };
73 |
74 | webSocket.onopen = function(event) {
75 |
76 | networkWebSocket.__handleEvent({
77 | type: "open",
78 | readyState: P2PWebSocket.prototype.OPEN
79 | });
80 |
81 | };
82 |
83 | // Incoming Network Web Socket message dispatcher
84 | webSocket.onmessage = function(event) {
85 | var json = toJson(event.data);
86 |
87 | if (!json) {
88 | return
89 | }
90 |
91 | switch(json.action) {
92 |
93 | case "connect":
94 | // fire connect event on root network web socket object
95 |
96 | // Create a new WebSocket shim object
97 | var peerWebSocket = new P2PWebSocket(webSocket, networkWebSocket, json.target);
98 |
99 | // Add to root web sockets p2p sockets enumeration
100 | networkWebSocket.peers.push(peerWebSocket);
101 |
102 | // Fire 'connect' event at root websocket object
103 | // **then** fire p2p websocket 'open' event (see above)
104 | var connectEvt = new CustomEvent('connect', {
105 | "bubbles": false,
106 | "cancelable": false,
107 | "detail": {
108 | "target": peerWebSocket
109 | }
110 | });
111 | networkWebSocket.dispatchEvent(connectEvt);
112 |
113 | window.setTimeout(function() {
114 | // Fire 'open' event at new websocket shim object
115 | peerWebSocket.__handleEvent({
116 | type: "open",
117 | readyState: P2PWebSocket.prototype.OPEN
118 | });
119 | }, 50);
120 |
121 | break;
122 |
123 | case "disconnect":
124 | // close peer network web socket object
125 |
126 | var peerWebSocket = getPeerById(json.target);
127 | if (!peerWebSocket) {
128 | return;
129 | }
130 |
131 | // Create and fire events:
132 | // - 'close' on p2p websocket object
133 | // - 'disconnect' on root websocket object
134 | peerWebSocket.__doClose(3000, "Closed", networkWebSocket);
135 |
136 | // Remove p2p websocket from root network web socket peers list
137 | for (var i = 0; i < networkWebSocket.peers.length; i++) {
138 | if (networkWebSocket.peers[i].id == json.target) {
139 | networkWebSocket.peers.splice(i,1);
140 | break;
141 | }
142 | }
143 |
144 | break;
145 |
146 | case "broadcast":
147 | // dispatch to root network web socket object
148 |
149 | // Re-encode data payload as string
150 | var payload = json.data;
151 | if (Object.prototype.toString.call(payload) != '[object String]') {
152 | payload = JSON.stringify(payload);
153 | }
154 |
155 | // TODO: Check shim websocket readyState and queue or fire immediately
156 | networkWebSocket.__handleEvent({
157 | type: "message",
158 | message: payload,
159 | senderId: json.source
160 | });
161 |
162 | break;
163 |
164 | case "message":
165 | // dispatch to peer network web socket object
166 |
167 | var peerWebSocket = getPeerById(json.source);
168 | if (!peerWebSocket) {
169 | return
170 | }
171 |
172 | // Re-encode data payload as string
173 | var payload = json.data;
174 | if (Object.prototype.toString.call(payload) != '[object String]') {
175 | payload = JSON.stringify(payload);
176 | }
177 |
178 | // TODO: Check shim websocket readyState and queue or fire immediately
179 | peerWebSocket.__handleEvent({
180 | type: "message",
181 | message: payload,
182 | senderId: json.source
183 | });
184 |
185 | break;
186 |
187 | }
188 | };
189 |
190 | webSocket.onclose = function(event) {
191 |
192 | // Close all peer connections
193 | for (var target in networkWebSocket.peers) {
194 | networkWebSocket.peers[target].__doClose(3000, "Closed", networkWebSocket);
195 | }
196 | networkWebSocket.peers = [];
197 |
198 | // Close root connection
199 | networkWebSocket.__doClose(3000, "Closed")
200 |
201 | };
202 |
203 | return networkWebSocket;
204 |
205 | };
206 |
207 | /**** START WEBSOCKET SHIM ****/
208 |
209 | var P2PWebSocket = function(rootWebSocket, parentWebSocket, targetId) {
210 | this.id = targetId || "";
211 | this.socket = rootWebSocket;
212 | this.parent = parentWebSocket;
213 |
214 | // Setup dynamic WebSocket interface attributes
215 | this.url = "#"; // no url (...that's kind of the whole point :)
216 | this.readyState = P2PWebSocket.prototype.CONNECTING; // initial state
217 | this.bufferedAmount = 0;
218 | this.extensions = this.socket.extensions; // inherit
219 | this.protocol = this.socket.protocol; // inherit
220 | this.binaryType = "blob"; // as per WebSockets spec
221 |
222 | this.__events = {};
223 | };
224 |
225 | P2PWebSocket.prototype.send = function(data) {
226 | if (this.readyState != P2PWebSocket.prototype.OPEN) {
227 | throw "message cannot be sent because the web socket is not open";
228 | }
229 |
230 | var message = {
231 | "action": "message",
232 | "target": this.id,
233 | "data": data
234 | };
235 | this.socket.send(JSON.stringify(message));
236 | };
237 |
238 | P2PWebSocket.prototype.close = function(code, reason) {
239 | if (this.readyState != P2PWebSocket.prototype.OPEN) {
240 | throw "web socket cannot be closed because it is not open";
241 | }
242 |
243 | this.__doClose(code || 3001, reason || "Closed", this.parent);
244 | };
245 |
246 | P2PWebSocket.prototype.addEventListener = function(type, listener, useCapture) {
247 | if (!(type in this.__events)) {
248 | this.__events[type] = [];
249 | }
250 | this.__events[type].push(listener);
251 | };
252 |
253 | P2PWebSocket.prototype.removeEventListener = function(type, listener, useCapture) {
254 | if (!(type in this.__events)) return;
255 | var events = this.__events[type];
256 | for (var i = events.length - 1; i >= 0; --i) {
257 | if (events[i] === listener) {
258 | events.splice(i, 1);
259 | break;
260 | }
261 | }
262 | };
263 |
264 | P2PWebSocket.prototype.dispatchEvent = function(event) {
265 |
266 | // Delay until next run loop (like real events)
267 | window.setTimeout(function() {
268 |
269 | var events = this.__events[event.type] || [];
270 | for (var i = 0; i < events.length; ++i) {
271 | events[i].call(this, event);
272 | }
273 | var handler = this["on" + event.type];
274 | if (handler) handler.call(this, event);
275 |
276 | }.bind(this), 2);
277 |
278 | };
279 |
280 | P2PWebSocket.prototype.__handleEvent = function(flashEvent) {
281 |
282 | // Delay until next run loop (like real events)
283 | window.setTimeout(function() {
284 |
285 | if ("readyState" in flashEvent) {
286 | this.readyState = flashEvent.readyState;
287 | }
288 |
289 | var jsEvent;
290 | if (flashEvent.type == "open" || flashEvent.type == "closing" || flashEvent.type == "error") {
291 | jsEvent = this.__createSimpleEvent(flashEvent.type);
292 | } else if (flashEvent.type == "close") {
293 | jsEvent = this.__createSimpleEvent("close");
294 | jsEvent.code = flashEvent.code;
295 | jsEvent.reason = flashEvent.reason;
296 | } else if (flashEvent.type == "message") {
297 | jsEvent = this.__createMessageEvent(flashEvent.senderId, flashEvent.message);
298 | } else {
299 | throw "unknown event type: " + flashEvent.type;
300 | }
301 |
302 | this.dispatchEvent(jsEvent);
303 |
304 | // Fire callback (if any provided)
305 | if (flashEvent.callback) flashEvent.callback.call(this);
306 |
307 | }.bind(this), 1);
308 | };
309 |
310 | P2PWebSocket.prototype.__createSimpleEvent = function(type) {
311 | if (document.createEvent && window.Event) {
312 | var event = document.createEvent("Event");
313 | event.initEvent(type, false, false);
314 | return event;
315 | } else {
316 | return {type: type, bubbles: false, cancelable: false};
317 | }
318 | };
319 |
320 | P2PWebSocket.prototype.__createMessageEvent = function(senderId, data) {
321 | if (window.MessageEvent && typeof(MessageEvent) == "function") {
322 | return new MessageEvent("message", {
323 | "view": window,
324 | "bubbles": false,
325 | "cancelable": false,
326 | "senderId": senderId,
327 | "data": data
328 | });
329 | } else if (document.createEvent && window.MessageEvent) {
330 | var event = document.createEvent("MessageEvent");
331 | event.initMessageEvent("message", false, false, data, null, null, window, null);
332 | return event;
333 | } else {
334 | return {type: "message", data: data, bubbles: false, cancelable: false};
335 | }
336 | };
337 |
338 | P2PWebSocket.prototype.__doClose = function(code, reason, parentWebSocket) {
339 | // Fire 'open' event at new websocket shim object
340 | this.__handleEvent({
341 | type: "closing",
342 | readyState: P2PWebSocket.prototype.CLOSING,
343 | callback: function() {
344 | // Fire 'open' event at new websocket shim object
345 | this.__handleEvent({
346 | type: "close",
347 | readyState: P2PWebSocket.prototype.CLOSED,
348 | code: code,
349 | reason: reason,
350 | callback: function() {
351 | if (parentWebSocket) {
352 | // Fire 'disconnect' event at root websocket object
353 | var disconnectEvt = new CustomEvent('disconnect', {
354 | "bubbles": false,
355 | "cancelable": false,
356 | "detail": {
357 | "target": this
358 | }
359 | });
360 | parentWebSocket.dispatchEvent(disconnectEvt);
361 | }
362 | }
363 | });
364 | }
365 | });
366 | }
367 |
368 | /**
369 | * Define the WebSocket readyState enumeration.
370 | */
371 | P2PWebSocket.prototype.CONNECTING = 0;
372 | P2PWebSocket.prototype.OPEN = 1;
373 | P2PWebSocket.prototype.CLOSING = 2;
374 | P2PWebSocket.prototype.CLOSED = 3;
375 |
376 | /**** END WEBSOCKET SHIM ****/
377 |
378 | var NetworkWebSocket = function (channelName, subprotocols) {
379 | return new _NetworkWebSocket(channelName, subprotocols);
380 | };
381 |
382 | // Expose global functions
383 |
384 | if (!global.NetworkWebSocket) {
385 | global.NetworkWebSocket = (global.module || {}).exports = NetworkWebSocket;
386 | }
387 |
388 | })(this);
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Network Web Sockets
2 | ===
3 |
4 | #### Local Network broadcast channels with secure service discovery and encrypted proxy communication
5 |
6 | Network Web Sockets allow web pages, native applications and devices to create [*encrypted*](https://github.com/namedwebsockets/networkwebsockets/wiki/Introduction-to-Secure-DNS-based-Service-Discovery-\(DNS-SSD\)) Web Socket networks by discovering, binding and connecting peers that share the same *channel name* in the local network.
7 |
8 | _Channel names_ can either be:
9 |
10 | * Common, memorable strings such as e.g. "webchat" to allow any service to connect to a _public_ channel, or:
11 | * Pseudo-secure strings such as randomly-generated hashes e.g. "cJWHi8q7SvNWAiSerpfxW3inYjXiKNqR" that are known and shared out-of-band between two or more actors to connect to a _private_ channel.
12 |
13 | Network Web Sockets prevents channel name discovery and channel message injection by snooping on the traffic in the local network. This is achieved using the mechanisms described in our [DNS-based Secure Service Discovery (DNS-SSD) draft](https://github.com/namedwebsockets/networkwebsockets/wiki/Introduction-to-Secure-DNS-based-Service-Discovery-(DNS-SSD)) and encrypting all communications between participating nodes within different channels.
14 |
15 | Any channel within the Network Web Sockets ecosystem is considered as secure as its out-of-band key sharing mechanisms (whether that is on a public forum online or via other more secure sharing mechanisms). If a channel key is available to a user, then that user will be able to join that channel (although nothing stops channels performing _additional_ authentication over the channel whenever a new peer connects). Similarly, if a user is unaware of a channel name, they will have no way of discovering that channel name via any traffic flowing in the local network.
16 |
17 | Web pages, native applications and devices can create ad-hoc inter-applicaton communication bridges between and among themselves for a variety of purposes:
18 |
19 | * For discovering matching peer services on the local device and/or the local network.
20 | * To create full-duplex, encrypted communications channels between network devices, native applications and web applications.
21 | * To create full-duplex, encrypted communication channels between web pages on different domains.
22 | * To import and export data between network devices, native applications and web applications.
23 | * To create initial local session signalling channels for establishing P2P sessions (for e.g. [WebRTC](#examples) signalling channel bootstrapping).
24 | * To establish low latency local network multiplayer signalling channels for games.
25 | * To enable collaborative editing, sharing and other forms of communication between different web pages and applications on a local device or a local network.
26 |
27 | A web page or application can create a new Network Web Socket by choosing a *channel name* (any alphanumeric name) via any of the available [Network Web Socket interfaces](#network-web-socket-interfaces). When other peers join the same *channel name* then they will join all other peers in the same Network Web Socket broadcast network.
28 |
29 | You can read more about the secure discovery process and proxy-to-proxy encryption used by Network Web Sockets on [this wiki page](https://github.com/namedwebsockets/networkwebsockets/wiki/Introduction-to-Secure-DNS-based-Service-Discovery-\(DNS-SSD\)).
30 |
31 | ### Getting started
32 |
33 | This repository contains an implementation of a _Network Web Socket Proxy_, written in Go, required to use Network Web Sockets.
34 |
35 | You can either [download a pre-built Network Web Sockets binary](https://github.com/namedwebsockets/networkwebsockets/releases) or [build a Network Web Socket Proxy from source](https://github.com/namedwebsockets/networkwebsockets/wiki/Building-a-Network-Web-Socket-Proxy-from-Source) to get up and running.
36 |
37 | Once you have a Network Web Socket Proxy up and running on your local machine then you are ready to [create and share your own Network Web Sockets](#network-web-socket-interfaces). A number of [Network Web Socket client examples](#examples) are also provided to help get you started.
38 |
39 | ##### [Download a pre-built platform binary](https://github.com/namedwebsockets/networkwebsockets/releases)
40 |
41 | ##### [How to build from source](https://github.com/namedwebsockets/networkwebsockets/wiki/Building-a-Network-Web-Socket-Proxy-from-Source)
42 |
43 | ### Network Web Socket Interfaces
44 |
45 | #### Local HTTP Test Console
46 |
47 | Once a Network Web Socket Proxy is up and running, you can access a test console in your web browser and play around with _Network Web Sockets_ at `http://localhost:9009`.
48 |
49 | #### JavaScript Interfaces
50 |
51 | The [Network Web Sockets JavaScript polyfill library](https://github.com/namedwebsockets/networkwebsockets/blob/master/lib/namedwebsockets.js) exposes a new JavaScript interface on the root global object for your convenience as follows:
52 |
53 | * `NetworkWebSocket` for creating/binding named websockets to share on the local network.
54 |
55 | You must include the polyfill file in your own projects to create these JavaScript interfaces. Assuming we have added the [Network Web Sockets JavaScript polyfill](https://github.com/namedwebsockets/networkwebsockets/blob/master/lib/namedwebsockets.js) to our page then we can create a new `NetworkWebSocket` connection object via the JavaScript polyfill as follows:
56 |
57 | ```javascript
58 | // Create a new Network Web Socket peer in the network
59 | var ws = new NetworkWebSocket("myChannelName");
60 | ```
61 |
62 | We then wait for our peer to be successfully added to the network:
63 |
64 | ```javascript
65 | ws.onopen = function() {
66 | console.log('Our channel peer is now connected to the `myChannelName` web socket network');
67 | };
68 | ```
69 |
70 | We can listen for incoming _broadcast_ messages from channel peers in the network as follows:
71 |
72 | ```javascript
73 | ws.onmessage = function(event) {
74 | console.log("Broadcast message received: " + event.data);
75 | };
76 | ```
77 |
78 | We can send _broadcast_ messages to all the other _currently known_ channel peers in the network as follows:
79 |
80 | ```javascript
81 | ws.send('This is a broadcast message to *all* other channel peers');
82 | ```
83 |
84 | When we create a Network Web Socket connection object then the Network Web Socket Proxy will start to discover and connect to all other `myChannelName` channel peers that are being advertised in the local network.
85 |
86 | Each time a new channel peer is discovered in the network a Web Socket proxy connection to that peer is established and a new `connect` event is queued and fired against our root Network Web Socket object:
87 |
88 | ```javascript
89 | ws.onconnect = function(event) {
90 | console.log('Another peer has been discovered and connected to our `myChannelName` web socket network!');
91 | };
92 | ```
93 |
94 | In this `connect` event, we are provided with a direct, _peer-to-peer_ Web Socket connection object that can be used to communicate directly with this newly discovered and connected peer.
95 |
96 | We can send a _direct message_ to a channel peer and listen for _direct messages_ from this channel peer as follows:
97 |
98 | ```javascript
99 | // Wait for a new channel peer to connect to our `myChannelName` web socket network
100 | ws.onconnect = function(event) {
101 |
102 | // Retrieve the new direct P2P Web Socket connection object with the newly connected channel peer
103 | var peerWS = evt.detail.target;
104 |
105 | // Wait for this new direct p2p channel connection to be opened
106 | peerWS.onopen = function() {
107 |
108 | // Listen for direct messages from this peer
109 | peerWS.onmessage = function(event) {
110 | console.log("Direct message received from [" + peerWS.id + "]: " + event.data);
111 | }
112 |
113 | // Send a direct message to this peer
114 | peerWS.send('This is a direct message to the new channel peer *only*'):
115 |
116 | };
117 | };
118 | ```
119 |
120 | With both broadcast and direct messaging capabilities it is possible to build advanced services on top of Network Web Sockets. We are excited to see what you come up with!
121 |
122 | #### Web Socket Interfaces
123 |
124 | Devices and services running on the local machine can register Network Web Sockets without needing to use the JavaScript API. Thus, we can connect up other applications and devices sitting in the local network such as TVs, Set-Top Boxes, Fridges, Home Automation Systems (assuming they run their own Network Web Socket Proxy client also).
125 |
126 | To create a new Network Web Socket connection to a channel from anywhere _on the local machine_ (i.e. to become a 'channel peer') you can establish a Web Socket connection to a running Network Web Socket Proxy at the following URL:
127 |
128 | ```
129 | ws://localhost:/
130 | ```
131 |
132 | where:
133 |
134 | * `port` is the port on which your Network Web Socket Proxy is running (by default, `9009`),
135 | * `channelName` is the name of the channel you want to create, and;
136 |
137 | Messages sent and received on this Web Socket connection have a well-defined data format.
138 |
139 | This Web Socket connection will notify you when channel peers connect and disconnect from `` and when broadcast or direct messages are sent to you from other connected channel peers. This Web Socket connection can also be used to send broadcast or direct messages toward all other connected channel peers.
140 |
141 | When a new channel peer connects to `` on the network a new message is sent to your connection as follows:
142 |
143 | ```javascript
144 | {
145 | action: "connect", // a new channel peer has connected to
146 | source: "", // your channel peer's id
147 | target: "" // the unique id of the new channel peer connection
148 | }
149 | ```
150 |
151 | Similarly when a channel peer disconnects from `` on the network a new message is sent to your connection as follows:
152 |
153 | ```javascript
154 | {
155 | action: "disconnect", // an existing channel peer has disconnected from
156 | source: "", // your channel peer's id
157 | target: "" // the unique id of the existing channel peer connection
158 | }
159 | ```
160 |
161 | To send a _broadcast message_ to all other connected channel peers you can send it over your connection as follows:
162 |
163 | ```javascript
164 | {
165 | action: "broadcast", // this is a sent broadcast message
166 | data: "" // the data you want to send to all other channel peers
167 | }
168 | ```
169 |
170 | When receiving a _broadcast message_ from another connected channel peer it is sent to you over your connection as follows:
171 |
172 | ```javascript
173 | {
174 | action: "broadcast", // this is a received broadcast message
175 | source: "", // the sending channel peer's id
176 | data: "" // the data you want to send to all other channel peers
177 | }
178 | ```
179 |
180 | To send a _direct message_ to another channel peer, bypassing the broadcast channel, you can send it over your connection as follows:
181 |
182 | ```javascript
183 | {
184 | action: "message", // this is a sent direct message
185 | target: "", // the id of an existing channel peer you want to send a direct message to
186 | data: "" // the data you want to send to
187 | }
188 | ```
189 |
190 | When receiving a _direct message_ from another channel peer, that has bypassed the broadcast channel, it is sent to you over your connection as follows:
191 |
192 | ```javascript
193 | {
194 | action: "message", // this is a received direct message
195 | source: "", // the id of the channel peer that sent you this direct message
196 | target: "", // your channel peer's id
197 | data: "" // the data sent to you by
198 | }
199 | ```
200 |
201 | ### Examples
202 |
203 | Some example services built with Network Web Sockets:
204 |
205 | * [Chat example](https://github.com/namedwebsockets/networkwebsockets/tree/master/examples/chat)
206 | * [PubSub example](https://github.com/namedwebsockets/networkwebsockets/tree/master/examples/pubsub)
207 | * [WebRTC example](https://github.com/namedwebsockets/networkwebsockets/tree/master/examples/webrtc)
208 |
209 | ### Feedback
210 |
211 | If you find any bugs or issues please report them on the [Network Web Sockets Issue Tracker](https://github.com/namedwebsockets/networkwebsockets/issues).
212 |
213 | If you would like to contribute to this project please consider [forking this repo](https://github.com/namedwebsockets/networkwebsockets/fork), making your changes and then creating a new [Pull Request](https://github.com/namedwebsockets/networkwebsockets/pulls) back to the main code repository.
214 |
215 | ### License
216 |
217 | The MIT License (MIT) Copyright (c) 2014 Rich Tibbett.
218 |
219 | See the [LICENSE](https://github.com/namedwebsockets/networkwebsockets/tree/master/LICENSE.txt) file for more information.
220 |
--------------------------------------------------------------------------------
/_templates/console.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Network Web Sockets Test Console
5 |
6 |
396 |
397 |
417 |
543 |
544 |
551 |
552 |
553 | Network Web Sockets Test Console
554 |
555 | Connection Status: - Idle
556 |
557 | Service Name:
558 |
559 |
560 |
561 |
562 |
563 | Peer Count: -
564 |
565 |
566 |
567 | Message Contents:
568 |
569 |
570 |
571 |
572 | Broadcast Messaging
573 |
574 |
575 | Broadcast to all connected peers
576 |
577 | Direct Messaging
578 |
579 | Not available. No peers connected
580 |
581 |
582 |
583 |
584 |
585 |
586 | Log:
587 |
588 |
589 |
590 |
--------------------------------------------------------------------------------
/templates.go:
--------------------------------------------------------------------------------
1 | package networkwebsockets
2 |
3 | import (
4 | "bytes"
5 | "compress/gzip"
6 | "fmt"
7 | "io"
8 | "strings"
9 | )
10 |
11 | func bindata_read(data []byte, name string) ([]byte, error) {
12 | gz, err := gzip.NewReader(bytes.NewBuffer(data))
13 | if err != nil {
14 | return nil, fmt.Errorf("Read %q: %v", name, err)
15 | }
16 |
17 | var buf bytes.Buffer
18 | _, err = io.Copy(&buf, gz)
19 | gz.Close()
20 |
21 | if err != nil {
22 | return nil, fmt.Errorf("Read %q: %v", name, err)
23 | }
24 |
25 | return buf.Bytes(), nil
26 | }
27 |
28 | func templates_console_html() ([]byte, error) {
29 | return bindata_read([]byte{
30 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x00, 0xff, 0xec, 0x5c,
31 | 0x7b, 0x73, 0x1b, 0x47, 0x72, 0xff, 0x1b, 0xfa, 0x14, 0xa3, 0xbd, 0xab,
32 | 0xc3, 0x83, 0x78, 0x50, 0xf2, 0xb9, 0x2a, 0xc1, 0x83, 0x55, 0x14, 0x88,
33 | 0xcb, 0xd1, 0x91, 0x49, 0x95, 0x08, 0xdb, 0x95, 0xc8, 0x8c, 0x6e, 0x81,
34 | 0x1d, 0x10, 0x2b, 0x2e, 0x76, 0x91, 0xdd, 0x05, 0x61, 0x9c, 0x8c, 0xef,
35 | 0x9e, 0xee, 0xe9, 0x79, 0xed, 0x0b, 0x80, 0x28, 0x39, 0xf9, 0x27, 0xac,
36 | 0xb2, 0x40, 0xce, 0xa3, 0xa7, 0xfb, 0xd7, 0x8f, 0xe9, 0xe9, 0x19, 0x78,
37 | 0xf8, 0xf2, 0xea, 0x76, 0x3c, 0xfd, 0x8f, 0x77, 0x13, 0xb6, 0x4c, 0x57,
38 | 0x01, 0x7b, 0xf7, 0xd3, 0x9b, 0xb7, 0xd7, 0x63, 0xe6, 0x74, 0x7a, 0xbd,
39 | 0x5f, 0xbe, 0x1b, 0xf7, 0x7a, 0x57, 0xd3, 0x2b, 0xf6, 0xf7, 0xe9, 0x8f,
40 | 0x6f, 0xd9, 0x5f, 0xbb, 0xe7, 0xaf, 0x7a, 0xbd, 0xc9, 0x8d, 0x73, 0xf1,
41 | 0x62, 0x88, 0x23, 0xf1, 0x83, 0xbb, 0xde, 0xc5, 0x8b, 0xda, 0x30, 0xf5,
42 | 0xd3, 0x80, 0x5f, 0xdc, 0xf0, 0x74, 0x1b, 0xc5, 0x8f, 0xec, 0x17, 0x3e,
43 | 0x63, 0x77, 0xd1, 0xfc, 0x91, 0xa7, 0x09, 0x9b, 0xf2, 0x24, 0x65, 0xe3,
44 | 0x28, 0x4c, 0xa2, 0x80, 0x0f, 0x7b, 0x34, 0xee, 0x05, 0xcc, 0x48, 0xe6,
45 | 0xb1, 0xbf, 0x4e, 0x61, 0x6e, 0xaf, 0xd5, 0x6a, 0xbd, 0xa8, 0xb5, 0x98,
46 | 0x9c, 0x0c, 0x73, 0x69, 0x2a, 0x4b, 0x96, 0xfe, 0x8a, 0x05, 0xfe, 0x2c,
47 | 0x76, 0xe3, 0x1d, 0x0e, 0xe8, 0x7c, 0xe5, 0x0f, 0xd0, 0x40, 0x32, 0x97,
48 | 0xef, 0xae, 0xd9, 0x4f, 0x89, 0xfb, 0xc0, 0xfb, 0x59, 0xa2, 0xb2, 0x1b,
49 | 0x7f, 0x7a, 0x3d, 0xe4, 0x38, 0xe4, 0xf3, 0x94, 0x6d, 0xfd, 0x74, 0xc9,
50 | 0xa2, 0x74, 0xc9, 0x63, 0xb6, 0xe6, 0x3c, 0x4e, 0xd8, 0x26, 0xf1, 0xc3,
51 | 0x07, 0x06, 0x0d, 0x2c, 0x71, 0x57, 0xf0, 0x0f, 0x8f, 0x9f, 0xfc, 0x39,
52 | 0x67, 0x21, 0xfe, 0xe1, 0x87, 0xa2, 0x63, 0xbe, 0x89, 0x63, 0x1e, 0xa6,
53 | 0x2c, 0x24, 0x89, 0x14, 0xd5, 0x27, 0x37, 0x66, 0xdb, 0x84, 0x8d, 0xa0,
54 | 0x7d, 0x5b, 0x90, 0xb6, 0xe1, 0xac, 0x76, 0x77, 0x44, 0xeb, 0x06, 0x48,
55 | 0x39, 0xcd, 0x81, 0xe4, 0xa7, 0xdb, 0xed, 0x02, 0xcd, 0x10, 0xd6, 0xe5,
56 | 0x82, 0x78, 0xcc, 0xd3, 0x4d, 0x1c, 0x72, 0x8f, 0xfd, 0x63, 0x9b, 0xfc,
57 | 0x83, 0x45, 0xb3, 0x4f, 0xc8, 0xe5, 0xa7, 0x0d, 0x80, 0x1c, 0xf8, 0x8f,
58 | 0x9c, 0xb9, 0x2c, 0x8c, 0xe2, 0x95, 0x1b, 0xb0, 0x1f, 0xdc, 0x27, 0xf7,
59 | 0x4e, 0x60, 0xcc, 0x0c, 0xa4, 0x34, 0xbc, 0x4b, 0xa4, 0x5b, 0xbd, 0x17,
60 | 0xb5, 0xc6, 0x62, 0x13, 0xce, 0x53, 0x3f, 0x0a, 0x1b, 0x0f, 0x41, 0x34,
61 | 0x73, 0x83, 0x26, 0xfb, 0x0c, 0xca, 0x01, 0xf9, 0x5b, 0x97, 0xc1, 0xd6,
62 | 0xdd, 0x25, 0x2d, 0x36, 0x97, 0x40, 0xa4, 0x11, 0x8b, 0x36, 0x31, 0x8b,
63 | 0xb6, 0x21, 0x0b, 0xa2, 0xb9, 0x1b, 0x2c, 0xa3, 0x24, 0xed, 0xcc, 0xdc,
64 | 0x04, 0x38, 0xe1, 0xa1, 0xb7, 0x8e, 0x7c, 0x10, 0x78, 0x11, 0xc5, 0xec,
65 | 0xe3, 0x3c, 0xe6, 0x6e, 0x0a, 0x18, 0x7d, 0xb4, 0xe5, 0xb4, 0x4d, 0xe2,
66 | 0x45, 0x0d, 0x91, 0x50, 0x93, 0x7e, 0x8a, 0x83, 0x37, 0x40, 0x05, 0x60,
67 | 0x71, 0xb6, 0x49, 0xbf, 0xd7, 0xd3, 0xc4, 0xfb, 0x9f, 0x3f, 0xff, 0x79,
68 | 0xbf, 0xef, 0x39, 0x03, 0x60, 0x48, 0x71, 0xc9, 0xfc, 0xe4, 0x67, 0x37,
69 | 0xf0, 0x3d, 0x0b, 0xa9, 0xc6, 0x7c, 0xe9, 0x02, 0x87, 0x01, 0xfe, 0x8e,
70 | 0xdc, 0xd7, 0x6a, 0x84, 0x10, 0xeb, 0xfd, 0xd7, 0x87, 0xcb, 0xce, 0x7f,
71 | 0xba, 0x9d, 0x7f, 0x9e, 0x77, 0xfe, 0xf5, 0xd7, 0xd1, 0xaf, 0x67, 0xbf,
72 | 0x76, 0x3f, 0x76, 0xee, 0x3f, 0xbf, 0x6a, 0xbf, 0xfe, 0xfe, 0xfb, 0xfd,
73 | 0x9f, 0x7b, 0xdd, 0x14, 0xec, 0x32, 0x33, 0x17, 0x00, 0xdf, 0xdb, 0x4b,
74 | 0xa5, 0xd1, 0x0f, 0x09, 0xe0, 0xe2, 0xb9, 0xa9, 0x2b, 0xe8, 0xa2, 0x0a,
75 | 0xd3, 0x78, 0xa7, 0x7e, 0xc5, 0x1f, 0xb9, 0xd2, 0x0f, 0x77, 0xb7, 0x37,
76 | 0xdd, 0xb5, 0x1b, 0x27, 0x9c, 0x46, 0x0f, 0x68, 0xc4, 0x9e, 0xcd, 0xdd,
77 | 0x74, 0xbe, 0x64, 0x0d, 0xe4, 0x6b, 0x0f, 0x8c, 0x29, 0xce, 0x16, 0x6e,
78 | 0x90, 0x70, 0xb9, 0x1c, 0x42, 0xf1, 0xb1, 0x60, 0xfa, 0x23, 0xa6, 0xd9,
79 | 0xb0, 0x79, 0x6c, 0xb3, 0x64, 0x33, 0x5b, 0xc7, 0x51, 0x1a, 0xcd, 0xa3,
80 | 0x20, 0x21, 0x69, 0xfd, 0x05, 0x6b, 0xbc, 0x3c, 0x02, 0x0b, 0x8d, 0xac,
81 | 0xa5, 0xcb, 0x38, 0xda, 0x32, 0xe7, 0x3a, 0x7c, 0xc2, 0xc1, 0x4c, 0x8e,
82 | 0x66, 0x38, 0xa4, 0xcf, 0x1c, 0x76, 0xc6, 0xac, 0x39, 0xc0, 0x9e, 0xe0,
83 | 0x8f, 0x6c, 0x61, 0x9e, 0x6e, 0xdc, 0xa0, 0xc5, 0xb6, 0xa0, 0xc4, 0x84,
84 | 0x38, 0x94, 0x66, 0x41, 0x40, 0x95, 0x68, 0x99, 0x01, 0x9b, 0xbf, 0x81,
85 | 0xdb, 0x0a, 0x01, 0xb7, 0x96, 0x60, 0x68, 0x14, 0xc6, 0xea, 0xf3, 0x56,
86 | 0x90, 0xe1, 0x21, 0x27, 0xee, 0x40, 0xb2, 0xf3, 0x3e, 0x8a, 0xd2, 0x62,
87 | 0xb4, 0x20, 0xd3, 0x96, 0x0b, 0x86, 0x45, 0x40, 0x71, 0xdd, 0x77, 0xaf,
88 | 0xdf, 0x99, 0xa5, 0x35, 0x53, 0x44, 0x58, 0x03, 0xfe, 0xc0, 0xd3, 0x77,
89 | 0xe0, 0xe9, 0x6f, 0x76, 0xd7, 0x5e, 0xc3, 0xf7, 0x24, 0x74, 0x68, 0xd9,
90 | 0x0d, 0xa4, 0xec, 0x03, 0xa9, 0xf3, 0x01, 0x7c, 0x0c, 0x0b, 0x8b, 0x74,
91 | 0x45, 0x80, 0xe8, 0x06, 0x3c, 0x7c, 0x48, 0x97, 0x30, 0xe4, 0xec, 0x4c,
92 | 0x4e, 0x16, 0x3a, 0x2a, 0x1f, 0xfd, 0xc1, 0xbf, 0xef, 0x82, 0x2a, 0x46,
93 | 0x23, 0xa6, 0x97, 0xd2, 0x66, 0x52, 0x39, 0x63, 0x20, 0x86, 0x09, 0x8b,
94 | 0xda, 0x5b, 0x6a, 0x42, 0xae, 0xab, 0x70, 0x49, 0x20, 0x3a, 0x24, 0x88,
95 | 0x4e, 0x39, 0x51, 0x10, 0xea, 0xc3, 0xbd, 0xc2, 0x37, 0x7a, 0xe2, 0x71,
96 | 0xec, 0x7b, 0xbc, 0x6c, 0x74, 0x02, 0x0a, 0xb3, 0xac, 0xd3, 0x78, 0x07,
97 | 0x09, 0x99, 0x2e, 0xfd, 0xa4, 0x0b, 0xfe, 0xef, 0xed, 0xee, 0x52, 0x37,
98 | 0xe5, 0xec, 0xe5, 0x28, 0x83, 0x79, 0x57, 0x28, 0x33, 0xdd, 0xad, 0x79,
99 | 0xf7, 0xf6, 0xdd, 0xe4, 0x46, 0x09, 0x2c, 0xed, 0x72, 0xc5, 0x13, 0x0c,
100 | 0xcb, 0xe0, 0x35, 0x61, 0x08, 0x1a, 0x9e, 0x61, 0x70, 0x0d, 0xf1, 0x73,
101 | 0xee, 0xaa, 0xe0, 0x67, 0x19, 0xa0, 0x9f, 0x30, 0x1c, 0x15, 0xad, 0x79,
102 | 0xe8, 0x0c, 0x08, 0x0a, 0xfc, 0x17, 0x75, 0xa4, 0x08, 0x8d, 0x24, 0x7d,
103 | 0xc7, 0x15, 0xcc, 0x3a, 0x7d, 0xc6, 0x9c, 0x59, 0x1c, 0xb9, 0xde, 0xdc,
104 | 0x4d, 0x52, 0xa7, 0x4d, 0x7d, 0x28, 0x01, 0xf4, 0xe0, 0x87, 0xa0, 0x32,
105 | 0x20, 0x4f, 0x01, 0x39, 0x12, 0x23, 0x72, 0x43, 0xb8, 0x78, 0x92, 0xc6,
106 | 0x10, 0xd7, 0xfc, 0xc5, 0xae, 0x21, 0x57, 0x68, 0x36, 0x85, 0x9b, 0x14,
107 | 0x81, 0x63, 0x45, 0xe4, 0xe6, 0x41, 0x24, 0xa2, 0x9c, 0x86, 0x6e, 0x1e,
108 | 0x79, 0x60, 0xe1, 0x00, 0x16, 0x44, 0x19, 0x0a, 0x30, 0xcf, 0xc6, 0x90,
109 | 0x19, 0x10, 0x6d, 0x17, 0xd5, 0x38, 0x8a, 0xb5, 0x3d, 0x8d, 0xa4, 0x5f,
110 | 0x40, 0x8f, 0x29, 0xf8, 0xb6, 0x59, 0x7e, 0x1b, 0x22, 0x96, 0x49, 0x09,
111 | 0x4d, 0x5f, 0x14, 0xe2, 0x44, 0x5b, 0x18, 0xfe, 0x04, 0xaa, 0xa2, 0xcd,
112 | 0xa3, 0xc4, 0x6a, 0x3e, 0x7e, 0x04, 0xa7, 0xf6, 0x02, 0x3e, 0xc1, 0x51,
113 | 0x0d, 0xa9, 0x74, 0x90, 0x00, 0xa2, 0x8e, 0xe0, 0x80, 0x54, 0x61, 0x64,
114 | 0xee, 0x1f, 0x92, 0x58, 0x68, 0x89, 0x7c, 0x56, 0x23, 0x7f, 0x1d, 0xce,
115 | 0xa3, 0x15, 0xee, 0xcb, 0x25, 0x81, 0x48, 0x59, 0x83, 0xe7, 0x27, 0x6b,
116 | 0x8c, 0xc7, 0x3c, 0xce, 0xc9, 0x62, 0xcc, 0xa5, 0x28, 0x8e, 0xb4, 0xa8,
117 | 0x4f, 0xa0, 0x23, 0xe8, 0x97, 0x5b, 0x82, 0xe8, 0xed, 0xca, 0x50, 0xaf,
118 | 0x4c, 0xff, 0xe5, 0x27, 0xa5, 0x47, 0xed, 0xc1, 0xc6, 0x2a, 0x13, 0x48,
119 | 0x23, 0xe6, 0xcb, 0x06, 0x0e, 0xe9, 0x92, 0x31, 0x2a, 0xac, 0x6a, 0x73,
120 | 0x0c, 0x7b, 0x8e, 0x0c, 0xa7, 0x4e, 0x9f, 0x22, 0x00, 0x88, 0xb4, 0xf0,
121 | 0x63, 0xae, 0x37, 0x5f, 0xb1, 0x22, 0x03, 0x1e, 0x62, 0x8c, 0x7d, 0x12,
122 | 0x5f, 0xdb, 0x1b, 0x64, 0xf4, 0xd3, 0xb3, 0xc7, 0xb8, 0x09, 0x8b, 0x6c,
123 | 0xc0, 0x0e, 0xb7, 0x94, 0x52, 0xe9, 0x48, 0x29, 0x85, 0xc3, 0x08, 0x70,
124 | 0x5a, 0xa4, 0x6c, 0x17, 0x62, 0x52, 0x5b, 0x40, 0xd3, 0x4d, 0xdd, 0xf8,
125 | 0x41, 0x05, 0x52, 0x62, 0xe0, 0xd2, 0xf3, 0x70, 0x63, 0x10, 0xfc, 0x1a,
126 | 0x3e, 0x13, 0xb6, 0x7e, 0xbd, 0xd6, 0xbf, 0xf3, 0x70, 0xb3, 0xe2, 0xb1,
127 | 0x8b, 0x70, 0xd0, 0xbc, 0x8a, 0x88, 0xba, 0xde, 0x24, 0xcb, 0x46, 0x86,
128 | 0x4b, 0x7b, 0xa5, 0xbf, 0x21, 0x50, 0x75, 0x89, 0x54, 0x5d, 0x42, 0xe5,
129 | 0xa6, 0x7a, 0xe9, 0x24, 0xb7, 0x3f, 0xd0, 0xac, 0x56, 0x0b, 0xb3, 0xaa,
130 | 0x56, 0x8b, 0x70, 0x46, 0xae, 0xcc, 0xd0, 0x3a, 0x1a, 0xa5, 0xa2, 0xd4,
131 | 0x48, 0x38, 0xe0, 0x38, 0x03, 0xdf, 0x6e, 0x1a, 0xc8, 0xe4, 0x6a, 0x93,
132 | 0x27, 0x85, 0xd7, 0x18, 0x12, 0xb0, 0x68, 0x45, 0xf6, 0xad, 0x79, 0x69,
133 | 0xab, 0x80, 0x5e, 0x73, 0x66, 0x9b, 0xd9, 0x2c, 0xe0, 0x09, 0x04, 0x1a,
134 | 0xb1, 0xf9, 0xb7, 0x55, 0x3b, 0xf8, 0xe8, 0x9c, 0x07, 0x2e, 0xf4, 0x15,
135 | 0xba, 0x3c, 0x9e, 0xba, 0x7e, 0x00, 0xcd, 0x8a, 0x08, 0xb4, 0x11, 0xce,
136 | 0xd0, 0x96, 0x41, 0x43, 0xf6, 0xef, 0xe9, 0x73, 0xdf, 0x1c, 0x54, 0xa0,
137 | 0xa9, 0x3c, 0x80, 0xd8, 0x34, 0x32, 0x68, 0x34, 0xb7, 0x7e, 0xe8, 0x45,
138 | 0x5b, 0x88, 0x78, 0xe9, 0xd4, 0x5f, 0xf1, 0x68, 0x93, 0x9a, 0xf4, 0x50,
139 | 0x6f, 0x4e, 0x06, 0x72, 0x1b, 0x25, 0x37, 0x15, 0x30, 0x18, 0x0c, 0x0b,
140 | 0x96, 0x56, 0xcb, 0xb0, 0x5c, 0x1a, 0x12, 0x4a, 0xa3, 0xc2, 0x97, 0x05,
141 | 0x06, 0x5b, 0xfe, 0x7d, 0x9b, 0x7d, 0x7f, 0xae, 0x65, 0x9b, 0x01, 0x95,
142 | 0xc7, 0x81, 0xed, 0x73, 0x00, 0x47, 0xd1, 0xed, 0x28, 0x4c, 0x23, 0xaf,
143 | 0x47, 0xdd, 0xac, 0xcc, 0x77, 0xec, 0xe4, 0x21, 0xeb, 0x19, 0x35, 0x95,
144 | 0x08, 0xbc, 0xcc, 0x5a, 0xb2, 0xc6, 0x95, 0x62, 0x86, 0xe2, 0xbd, 0xe8,
145 | 0xca, 0xb0, 0xf1, 0x0a, 0x5b, 0x15, 0x80, 0x27, 0x86, 0x63, 0xc6, 0x3a,
146 | 0x60, 0xfe, 0xc8, 0x76, 0x1d, 0x23, 0x44, 0xd6, 0x94, 0x73, 0x56, 0x2f,
147 | 0xc6, 0x1a, 0xb9, 0xeb, 0x3a, 0xa4, 0x94, 0xcf, 0xc8, 0xab, 0xcc, 0x8b,
148 | 0xc6, 0x62, 0x57, 0xf8, 0xee, 0xfc, 0xfc, 0xbc, 0xcd, 0x1c, 0xf1, 0x87,
149 | 0xe7, 0x14, 0xa3, 0x82, 0xed, 0x9e, 0xef, 0xf9, 0x0a, 0x7c, 0x27, 0xc7,
150 | 0xd6, 0x22, 0x8e, 0x56, 0x95, 0xb1, 0x8c, 0x32, 0x12, 0x99, 0xaf, 0xd4,
151 | 0xbe, 0x36, 0xf7, 0x3a, 0x2d, 0xfb, 0xb2, 0x75, 0x65, 0x1c, 0xae, 0x62,
152 | 0x95, 0x64, 0x1d, 0x40, 0xde, 0xdc, 0xf0, 0xdb, 0xaf, 0x94, 0x5e, 0xb5,
153 | 0x79, 0x65, 0x3d, 0xb1, 0xd2, 0xf4, 0x4c, 0x16, 0x62, 0xf4, 0xa8, 0xbc,
154 | 0x53, 0x87, 0xcd, 0x13, 0xc2, 0xfc, 0x7b, 0xde, 0xe1, 0x21, 0x26, 0x12,
155 | 0x22, 0x7f, 0x61, 0x6b, 0x77, 0x17, 0x00, 0x61, 0xe6, 0x26, 0x8c, 0x72,
156 | 0x15, 0xcb, 0x50, 0x65, 0x97, 0x14, 0x15, 0x87, 0x5b, 0x36, 0x79, 0x4b,
157 | 0xe7, 0x42, 0xe3, 0x4f, 0x69, 0x74, 0x27, 0x08, 0x74, 0xe1, 0x30, 0x16,
158 | 0x34, 0xe4, 0xe4, 0x26, 0xe6, 0x22, 0xf5, 0x0f, 0xf2, 0xcc, 0x49, 0x03,
159 | 0xee, 0xeb, 0x06, 0x2f, 0xb3, 0x46, 0x2e, 0x5d, 0x52, 0xf3, 0x8b, 0xc6,
160 | 0x3d, 0xbd, 0xbd, 0xba, 0xed, 0xb3, 0xf1, 0x92, 0xcf, 0x1f, 0x29, 0x64,
161 | 0x18, 0x1b, 0xb1, 0x52, 0x20, 0xb4, 0xfd, 0xff, 0xde, 0xf0, 0x0d, 0x67,
162 | 0x60, 0x0a, 0xc2, 0x09, 0xfc, 0xd5, 0x8a, 0x7b, 0x3e, 0xf4, 0x05, 0xbb,
163 | 0x8a, 0x60, 0x57, 0x1e, 0x60, 0x64, 0x7c, 0x91, 0x5b, 0xbe, 0x0e, 0x31,
164 | 0xf2, 0xef, 0xbe, 0x82, 0x49, 0xb5, 0x63, 0xee, 0xc7, 0xe3, 0x6b, 0xaf,
165 | 0x4f, 0xa8, 0x25, 0x70, 0x02, 0x9e, 0x73, 0x13, 0x66, 0xab, 0xd4, 0xab,
166 | 0xc8, 0x97, 0x2b, 0xf7, 0xdb, 0x05, 0x17, 0xe2, 0xe7, 0xf4, 0xe0, 0x52,
167 | 0x80, 0xff, 0xff, 0xed, 0x47, 0xd9, 0xcf, 0x29, 0xbb, 0xd3, 0x1f, 0x6c,
168 | 0x3c, 0x7b, 0x9d, 0xd1, 0xda, 0xd9, 0x69, 0xe1, 0xdc, 0x90, 0x49, 0xb5,
169 | 0x71, 0x7f, 0x10, 0x03, 0x00, 0x66, 0x32, 0x2c, 0x73, 0x30, 0x4f, 0x32,
170 | 0x07, 0x57, 0x0a, 0x6e, 0x58, 0xa2, 0x2a, 0x8f, 0x6a, 0x4a, 0x0d, 0x15,
171 | 0x91, 0x92, 0xa6, 0xdf, 0x7f, 0xd9, 0x26, 0xa0, 0x42, 0xe1, 0xb1, 0x43,
172 | 0xa7, 0x11, 0x43, 0x04, 0x3e, 0x23, 0x42, 0xc5, 0x69, 0xa2, 0x9c, 0x85,
173 | 0xa6, 0x39, 0x12, 0x54, 0x9c, 0xa0, 0xb1, 0x4f, 0x0c, 0xc0, 0x9a, 0x63,
174 | 0x8b, 0xdd, 0x4d, 0x2f, 0xdf, 0x4f, 0xd9, 0x2f, 0x93, 0x37, 0x77, 0xb7,
175 | 0xe3, 0x7f, 0x9f, 0x4c, 0xd9, 0xdd, 0xdf, 0xaf, 0x7f, 0x64, 0xd8, 0xd3,
176 | 0x93, 0x85, 0x19, 0x3b, 0xd9, 0xb0, 0x55, 0x80, 0x5c, 0x5a, 0x29, 0xf0,
177 | 0xda, 0xc5, 0x72, 0x9f, 0xd5, 0x40, 0x60, 0x5d, 0xcb, 0x13, 0xbd, 0x38,
178 | 0xd0, 0xe1, 0x36, 0xa3, 0xdb, 0xd9, 0xef, 0xbf, 0x33, 0x47, 0x9c, 0x5a,
179 | 0xad, 0x83, 0x26, 0xf4, 0x67, 0xe8, 0xea, 0x6e, 0x22, 0x0f, 0xdd, 0xb9,
180 | 0x75, 0xd4, 0xe1, 0xe7, 0x0e, 0x84, 0x5d, 0x33, 0x6f, 0x17, 0xba, 0x2b,
181 | 0x7f, 0x6e, 0x25, 0xfb, 0x7e, 0x98, 0xf2, 0x78, 0xe1, 0xce, 0xc1, 0x38,
182 | 0x52, 0x70, 0xa5, 0xd9, 0x26, 0xe5, 0x89, 0xa2, 0xb9, 0x89, 0x03, 0x66,
183 | 0xfd, 0x8c, 0x98, 0xf3, 0x27, 0x67, 0x80, 0x75, 0xcf, 0x30, 0x62, 0xd8,
184 | 0xd7, 0x10, 0x65, 0x47, 0x37, 0xad, 0x27, 0xec, 0x11, 0x92, 0x42, 0x16,
185 | 0x2d, 0xe8, 0xfc, 0xbd, 0x8c, 0x02, 0xd8, 0xcb, 0x45, 0xb1, 0xaf, 0xdf,
186 | 0x54, 0xc4, 0x2c, 0x4f, 0x23, 0x62, 0x15, 0x49, 0xda, 0xf8, 0xf6, 0xe6,
187 | 0x66, 0x32, 0x9e, 0x5e, 0xdf, 0xfc, 0x9b, 0x58, 0xca, 0x0f, 0xfd, 0xd4,
188 | 0x77, 0x03, 0x08, 0x35, 0x30, 0x53, 0xd1, 0x9a, 0x6d, 0x16, 0x0b, 0x1e,
189 | 0x73, 0xef, 0x72, 0x15, 0x6d, 0x84, 0xd0, 0xe7, 0x1a, 0x07, 0xfe, 0x5b,
190 | 0xca, 0xc3, 0x04, 0x0d, 0x5b, 0x2e, 0x63, 0x9f, 0xd2, 0x4d, 0xa7, 0xa4,
191 | 0x0d, 0xe7, 0x3c, 0x3f, 0xd5, 0x10, 0xca, 0x22, 0x92, 0x16, 0xd7, 0x9e,
192 | 0xaa, 0x3a, 0x07, 0x54, 0xf9, 0xcd, 0x4d, 0x9d, 0xf9, 0xa1, 0x1b, 0xef,
193 | 0xa6, 0xc0, 0xbf, 0x42, 0x6a, 0x16, 0x44, 0x33, 0x02, 0x0b, 0xe2, 0xe4,
194 | 0x1a, 0x5c, 0x4e, 0xcb, 0x0a, 0x61, 0x73, 0xcd, 0xe7, 0x2f, 0xd4, 0xd4,
195 | 0x8f, 0x1f, 0x29, 0x57, 0xc3, 0x42, 0x04, 0xd6, 0x15, 0x84, 0xe9, 0x55,
196 | 0x60, 0x53, 0x5d, 0x58, 0x79, 0x76, 0x5d, 0xe5, 0x9b, 0x94, 0x55, 0x44,
197 | 0xac, 0x2d, 0x29, 0xaa, 0xd8, 0x35, 0x95, 0x4c, 0x40, 0x34, 0x67, 0x14,
198 | 0x26, 0x0d, 0x9f, 0x9a, 0xb3, 0x75, 0x96, 0x7d, 0xce, 0xf8, 0x8f, 0x56,
199 | 0x59, 0x0e, 0x61, 0x77, 0xbc, 0xb4, 0xf2, 0xd5, 0x20, 0x3e, 0xaf, 0xac,
200 | 0x42, 0xe8, 0x49, 0x5b, 0x50, 0x41, 0x4b, 0x6c, 0xb7, 0x10, 0x00, 0x20,
201 | 0x78, 0xbd, 0x52, 0x4c, 0x8a, 0x80, 0xa0, 0x23, 0xa9, 0xe5, 0xf5, 0xc7,
202 | 0x44, 0x77, 0x3d, 0x4f, 0xec, 0x51, 0x6f, 0x21, 0x5b, 0xe6, 0x21, 0xd8,
203 | 0xa2, 0x85, 0x02, 0x0e, 0x68, 0x8b, 0x3c, 0x1a, 0x7b, 0xda, 0x78, 0x83,
204 | 0x30, 0x76, 0xd7, 0x10, 0x18, 0xb9, 0x55, 0x39, 0x16, 0xa3, 0xe8, 0xe2,
205 | 0xc2, 0xb2, 0x58, 0x53, 0x31, 0xb6, 0x1a, 0x3f, 0xe0, 0xd0, 0x7b, 0x19,
206 | 0xb5, 0x29, 0xb2, 0x97, 0xf4, 0xd3, 0x69, 0x5d, 0xad, 0x7a, 0x8c, 0xff,
207 | 0x58, 0x9c, 0x0e, 0xfe, 0x20, 0x11, 0xcc, 0x89, 0x4a, 0x5c, 0x38, 0x28,
208 | 0x5f, 0x2c, 0x61, 0x1a, 0xc7, 0x64, 0x8e, 0x19, 0xd4, 0x29, 0xcf, 0x13,
209 | 0x70, 0x68, 0x7a, 0x85, 0xc7, 0x8e, 0x0b, 0x71, 0xfc, 0xe8, 0x74, 0x7c,
210 | 0xab, 0xe2, 0x29, 0xa9, 0xf8, 0x80, 0x0b, 0x1c, 0x23, 0xb4, 0xd8, 0x72,
211 | 0x33, 0x95, 0x64, 0xf4, 0x81, 0x81, 0xa9, 0x13, 0x83, 0x39, 0x2e, 0x50,
212 | 0x05, 0xf7, 0x20, 0x48, 0x99, 0x73, 0x7b, 0x55, 0x2a, 0x00, 0xe1, 0xe8,
213 | 0x8a, 0x07, 0xee, 0x8e, 0x41, 0xd8, 0xf4, 0x03, 0xd8, 0xfa, 0x7e, 0x83,
214 | 0x14, 0x68, 0x83, 0x97, 0x35, 0xd1, 0x9a, 0x35, 0xc4, 0xdd, 0x10, 0xac,
215 | 0x19, 0x48, 0xc9, 0x30, 0x76, 0x1f, 0x39, 0xed, 0x8b, 0xaa, 0xe0, 0x01,
216 | 0xe0, 0xa8, 0x0a, 0x46, 0x36, 0x01, 0xe6, 0x2b, 0x8c, 0x82, 0x95, 0x9f,
217 | 0xd6, 0x32, 0x68, 0x0e, 0xd8, 0xd9, 0x99, 0xaf, 0xcb, 0x96, 0x1a, 0x3f,
218 | 0xca, 0x14, 0x71, 0x89, 0x36, 0x8d, 0x6f, 0xaa, 0xba, 0xa4, 0xe2, 0x83,
219 | 0x52, 0xb2, 0x58, 0x32, 0xf2, 0xc1, 0x81, 0xe8, 0xc3, 0xce, 0x98, 0xc5,
220 | 0xc6, 0x40, 0xd7, 0x50, 0xe5, 0xd0, 0xa6, 0x9a, 0x53, 0x46, 0x1c, 0x71,
221 | 0xc7, 0xe8, 0xee, 0x89, 0xf6, 0x66, 0x9b, 0xbd, 0x6e, 0xea, 0xfc, 0xa0,
222 | 0x42, 0x11, 0x99, 0xb4, 0xd0, 0x56, 0xc4, 0x22, 0x70, 0x13, 0x52, 0xcf,
223 | 0x1f, 0xa7, 0x0d, 0x92, 0xcc, 0x31, 0x01, 0xcc, 0x41, 0x9b, 0xcf, 0xae,
224 | 0x5c, 0xab, 0x15, 0xb7, 0xe4, 0x91, 0x35, 0xc6, 0x6a, 0xcf, 0xd5, 0xcc,
225 | 0x3f, 0x25, 0x62, 0xc0, 0x40, 0xad, 0x63, 0xcd, 0x11, 0xee, 0x05, 0xc6,
226 | 0x4d, 0x55, 0x19, 0xd4, 0x75, 0x59, 0x27, 0x06, 0x43, 0x88, 0xdb, 0x95,
227 | 0xfd, 0x3c, 0x8e, 0xa3, 0xd8, 0x51, 0x4c, 0xca, 0xe5, 0x8c, 0x51, 0x89,
228 | 0x5b, 0x43, 0x7e, 0xe7, 0xaf, 0xd6, 0x2a, 0xeb, 0xce, 0x11, 0x91, 0xe9,
229 | 0x24, 0xe3, 0x01, 0x86, 0xda, 0x0a, 0x0e, 0x45, 0x44, 0xfe, 0x92, 0x45,
230 | 0xd4, 0x8c, 0x81, 0x3d, 0xa1, 0x2b, 0x42, 0x74, 0x06, 0x37, 0x6c, 0xc9,
231 | 0x8e, 0x91, 0x81, 0x3b, 0x8f, 0x2e, 0xb4, 0x9d, 0xc2, 0xa8, 0xda, 0x32,
232 | 0x8f, 0xb0, 0xfa, 0x23, 0x0d, 0x2b, 0x00, 0xa2, 0x8e, 0x16, 0x6d, 0x7b,
233 | 0x75, 0xb5, 0x5d, 0xda, 0xeb, 0x67, 0x2e, 0x58, 0x36, 0xe1, 0x63, 0x88,
234 | 0xf7, 0xb7, 0x54, 0xb2, 0x93, 0xa7, 0x19, 0x70, 0xa1, 0x1c, 0x83, 0x96,
235 | 0x65, 0x08, 0x66, 0xb2, 0x65, 0x43, 0xc9, 0x69, 0x53, 0x27, 0xee, 0xa2,
236 | 0x18, 0x88, 0xfe, 0x35, 0x73, 0xe1, 0x10, 0xd6, 0x00, 0x89, 0xdd, 0x70,
237 | 0x87, 0xd7, 0x7f, 0x4f, 0xbe, 0xc7, 0xbd, 0x66, 0x89, 0x41, 0xa9, 0xc1,
238 | 0x4d, 0x56, 0xd2, 0x68, 0x5c, 0xb5, 0xcc, 0x47, 0x5f, 0x1d, 0xdb, 0x50,
239 | 0x4a, 0x94, 0x9c, 0xdf, 0x50, 0xcc, 0xd6, 0xe1, 0x45, 0xf3, 0xcd, 0x4a,
240 | 0x2c, 0x2e, 0xe6, 0xd0, 0xe8, 0xbf, 0xfc, 0x85, 0x49, 0x5f, 0x9c, 0xe4,
241 | 0x6e, 0x01, 0xb8, 0xa4, 0x56, 0x36, 0xad, 0xe1, 0x88, 0x0f, 0x69, 0x49,
242 | 0x14, 0x94, 0x30, 0xc1, 0xa5, 0x4e, 0xda, 0xc7, 0xa8, 0xbe, 0x4b, 0x1f,
243 | 0x34, 0x4e, 0x9e, 0x53, 0xb8, 0x72, 0x3d, 0x5b, 0x6b, 0xb2, 0xef, 0x33,
244 | 0xe9, 0x89, 0x28, 0xc8, 0x32, 0xb2, 0x2a, 0x15, 0x33, 0x53, 0x3e, 0x96,
245 | 0x4d, 0xfb, 0xc1, 0xf1, 0xed, 0xa4, 0xcc, 0xb8, 0x6c, 0x8c, 0x8c, 0x71,
246 | 0x65, 0x93, 0x50, 0x89, 0x4a, 0x66, 0x1a, 0x80, 0x85, 0x34, 0xa3, 0x45,
247 | 0xc3, 0x6e, 0x6e, 0x0a, 0x1b, 0x57, 0x04, 0x95, 0x91, 0xeb, 0x43, 0xd9,
248 | 0x96, 0x65, 0xec, 0xda, 0xe4, 0x8f, 0xea, 0xbe, 0xee, 0xc9, 0xe7, 0x5b,
249 | 0xc8, 0x15, 0x69, 0x41, 0x79, 0x4f, 0x57, 0x5a, 0x41, 0xaf, 0xac, 0x9f,
250 | 0x3b, 0x4a, 0x08, 0x68, 0xd6, 0xf2, 0x50, 0x4f, 0xfe, 0xc2, 0xaf, 0x69,
251 | 0x01, 0x7f, 0x82, 0x51, 0x64, 0xe5, 0x3c, 0xd1, 0x36, 0xec, 0x49, 0x05,
252 | 0x13, 0xa9, 0x02, 0xc3, 0x36, 0x17, 0xd2, 0x05, 0x1c, 0xb2, 0x37, 0x41,
253 | 0xa0, 0xfe, 0x95, 0xf0, 0x88, 0xbf, 0xbe, 0xd4, 0x9c, 0xac, 0x65, 0x90,
254 | 0x70, 0x5f, 0x92, 0xff, 0x46, 0xf6, 0x25, 0x53, 0xdd, 0xaa, 0x94, 0xbc,
255 | 0x70, 0x62, 0x26, 0x1c, 0x9f, 0x73, 0xb1, 0x20, 0xa3, 0x65, 0xa1, 0x5a,
256 | 0x23, 0xa5, 0x54, 0x9b, 0x52, 0x9b, 0x10, 0x38, 0x7a, 0x8d, 0x30, 0x7e,
257 | 0x7b, 0x7b, 0x07, 0xc7, 0x53, 0x31, 0x5c, 0x05, 0xa4, 0x3e, 0x2b, 0x5c,
258 | 0x82, 0x3c, 0xef, 0x0a, 0xa4, 0x8a, 0xd7, 0x0c, 0xb7, 0xba, 0xac, 0x74,
259 | 0x22, 0xb7, 0x93, 0x2b, 0x39, 0x1e, 0x01, 0xee, 0x33, 0x01, 0xb3, 0x26,
260 | 0x00, 0x58, 0xf7, 0x15, 0xe6, 0x72, 0x54, 0xb5, 0x50, 0xe4, 0xe4, 0xa5,
261 | 0x9a, 0xa9, 0x65, 0x84, 0xb6, 0xef, 0x0f, 0x4e, 0xb9, 0x6d, 0x23, 0x07,
262 | 0x31, 0xb3, 0x2a, 0x6e, 0xcd, 0x2c, 0xb2, 0x6d, 0xfb, 0xce, 0xab, 0xe2,
263 | 0xea, 0xec, 0xe0, 0xe5, 0x59, 0xf9, 0xf5, 0x99, 0x7d, 0x38, 0x45, 0x65,
264 | 0xe8, 0x8e, 0xbd, 0xfa, 0x6d, 0x6f, 0x0a, 0xfa, 0x39, 0x20, 0x72, 0xbb,
265 | 0x60, 0x46, 0x9a, 0x66, 0xa1, 0xee, 0x6f, 0xd1, 0x12, 0xf9, 0xbd, 0x7a,
266 | 0x5c, 0xd4, 0xa3, 0xa7, 0x6e, 0x57, 0x7c, 0xe1, 0x87, 0x74, 0xf4, 0x36,
267 | 0xe5, 0x1a, 0x2b, 0x65, 0xb3, 0x2e, 0x46, 0xf1, 0xad, 0x56, 0xaf, 0xd2,
268 | 0xd5, 0x4c, 0x45, 0x45, 0x56, 0x4a, 0x0e, 0x1c, 0x68, 0x61, 0xc4, 0xab,
269 | 0xea, 0x11, 0xd2, 0xf2, 0x61, 0xd0, 0xeb, 0xc3, 0x83, 0x26, 0x57, 0x30,
270 | 0xe6, 0x3b, 0x53, 0x44, 0x9b, 0xdc, 0x5c, 0x1d, 0x2a, 0xa1, 0x7d, 0xcd,
271 | 0xd3, 0x26, 0x6b, 0xc3, 0x28, 0x3c, 0x91, 0x3a, 0x30, 0x57, 0x65, 0x08,
272 | 0x60, 0xb0, 0x93, 0xdf, 0xd6, 0x18, 0x87, 0xe8, 0x6d, 0x9b, 0x5e, 0x38,
273 | 0x81, 0x4e, 0x71, 0x72, 0xa4, 0xf6, 0x6e, 0x9e, 0x36, 0xad, 0x5e, 0xd1,
274 | 0x09, 0x02, 0xc8, 0xb7, 0x72, 0xdd, 0x55, 0xe4, 0x6d, 0x02, 0x71, 0x96,
275 | 0xff, 0xbc, 0x6f, 0x76, 0x39, 0xac, 0x15, 0x8b, 0x73, 0x52, 0x7e, 0x8a,
276 | 0x54, 0xfe, 0xbe, 0xa9, 0xd2, 0x9a, 0xda, 0xb0, 0xa7, 0x9e, 0x3f, 0xda,
277 | 0x2f, 0x21, 0xcd, 0x1b, 0xa4, 0x20, 0x7a, 0x68, 0xac, 0x92, 0x07, 0xe9,
278 | 0x7d, 0x7a, 0x4f, 0x01, 0xd3, 0x9d, 0x04, 0x1c, 0x7f, 0x15, 0x77, 0x00,
279 | 0x75, 0x18, 0x56, 0x6f, 0x76, 0xdd, 0x35, 0x44, 0x21, 0x6f, 0xbc, 0xf4,
280 | 0x03, 0x2f, 0xbf, 0x79, 0x4d, 0xe1, 0xdc, 0x71, 0x03, 0x21, 0xa1, 0x81,
281 | 0x18, 0x5e, 0x41, 0x03, 0x78, 0xfb, 0x19, 0xab, 0x33, 0x06, 0xff, 0x9d,
282 | 0x31, 0x58, 0x01, 0xff, 0xfa, 0x35, 0xac, 0xcb, 0x87, 0x2c, 0x36, 0x0b,
283 | 0x58, 0x95, 0xdb, 0x24, 0x16, 0x17, 0x8a, 0xa7, 0xc1, 0x41, 0x96, 0x68,
284 | 0x1a, 0x70, 0x95, 0xc2, 0xd2, 0xe3, 0x28, 0x4c, 0x69, 0x5b, 0x84, 0x89,
285 | 0xba, 0x1e, 0x62, 0xbf, 0xb4, 0x12, 0x95, 0x7b, 0xee, 0xbd, 0x77, 0x3d,
286 | 0x3f, 0xfa, 0xd9, 0x0d, 0x36, 0xbc, 0xf1, 0x10, 0x47, 0x9b, 0xb5, 0x79,
287 | 0xcc, 0x47, 0x9b, 0x6b, 0x90, 0xd8, 0x5b, 0xab, 0x59, 0x33, 0x79, 0xb3,
288 | 0x13, 0x8f, 0xdd, 0xcc, 0x24, 0x4a, 0x4b, 0x4b, 0x0f, 0xa1, 0x41, 0xe5,
289 | 0xdb, 0x2c, 0xe8, 0x12, 0x27, 0x50, 0x62, 0x26, 0xff, 0x0e, 0x4b, 0xf6,
290 | 0x3e, 0x21, 0x7b, 0xb9, 0xa7, 0x57, 0x6a, 0x48, 0xbd, 0x55, 0x17, 0x05,
291 | 0x41, 0x7d, 0xaf, 0xc7, 0xc4, 0x68, 0xc2, 0xd4, 0x28, 0xdb, 0xd6, 0xb5,
292 | 0x78, 0xa6, 0x06, 0x1c, 0x7b, 0x89, 0xae, 0xf6, 0x62, 0xd3, 0x22, 0xf6,
293 | 0x41, 0x99, 0x81, 0x08, 0x04, 0x1b, 0x51, 0x22, 0x77, 0x8c, 0x93, 0xc3,
294 | 0x6e, 0xed, 0xa0, 0x2b, 0xe3, 0xa7, 0x74, 0x58, 0xf5, 0xeb, 0xe4, 0xca,
295 | 0xb9, 0xcf, 0xbe, 0x63, 0x93, 0xf1, 0xa9, 0x61, 0x15, 0x2b, 0xac, 0xf5,
296 | 0x30, 0xa3, 0xb1, 0xfe, 0xb4, 0x0f, 0x8c, 0x43, 0x08, 0x14, 0x0a, 0x03,
297 | 0x54, 0x7a, 0xfd, 0x32, 0x10, 0xbd, 0x8a, 0x22, 0xf7, 0xba, 0x75, 0x19,
298 | 0xf2, 0xac, 0x3b, 0xeb, 0xbd, 0x52, 0x56, 0x62, 0x1e, 0x21, 0x96, 0x2b,
299 | 0x8d, 0x0c, 0x05, 0x17, 0x07, 0x33, 0x91, 0xa8, 0xe2, 0x64, 0x9b, 0xbb,
300 | 0x8a, 0x47, 0xb2, 0x16, 0x6d, 0xa9, 0x69, 0x69, 0xa6, 0x75, 0xf9, 0x5e,
301 | 0x57, 0xbc, 0xcd, 0x8d, 0x98, 0x83, 0xe6, 0x6d, 0x33, 0x02, 0x66, 0xee,
302 | 0x74, 0xbb, 0x8a, 0x6f, 0x5b, 0xf0, 0xc2, 0x5b, 0x26, 0x96, 0x79, 0xfd,
303 | 0x53, 0x6d, 0xe9, 0x06, 0xb1, 0x82, 0xb5, 0x67, 0x95, 0xf8, 0xa1, 0x0a,
304 | 0x68, 0xf9, 0x90, 0xaf, 0x72, 0x05, 0xbc, 0x25, 0x19, 0x63, 0x19, 0xbc,
305 | 0xb0, 0x80, 0x4d, 0xd0, 0xbe, 0xf9, 0x06, 0x39, 0x1d, 0x47, 0x5e, 0x2d,
306 | 0xe5, 0x90, 0xe1, 0x5e, 0x25, 0x30, 0x54, 0xc7, 0xe6, 0xb1, 0x80, 0xe7,
307 | 0x30, 0x4b, 0x52, 0x6b, 0x09, 0x4f, 0x2f, 0xd5, 0x9d, 0x42, 0x03, 0x1f,
308 | 0x52, 0xe0, 0x26, 0x8c, 0xa5, 0x4b, 0xf3, 0x7b, 0xf3, 0x88, 0x70, 0xd2,
309 | 0x98, 0xde, 0xa4, 0xe1, 0xc9, 0xf4, 0x0e, 0x13, 0x44, 0x84, 0x80, 0x14,
310 | 0x55, 0x14, 0xcb, 0xa8, 0x1d, 0xe3, 0x48, 0xa6, 0xc4, 0xc4, 0xd1, 0xf3,
311 | 0xc9, 0x98, 0xd4, 0xe0, 0x34, 0x4a, 0xfb, 0x12, 0x93, 0x2c, 0x3e, 0x49,
312 | 0xcb, 0x59, 0xa5, 0xf0, 0xcb, 0xf7, 0xe3, 0x9f, 0xaf, 0xfa, 0x22, 0x92,
313 | 0x67, 0x5e, 0xa4, 0x55, 0xd0, 0xcc, 0x57, 0xc9, 0x73, 0x14, 0x63, 0xbe,
314 | 0x88, 0x79, 0xb2, 0xc4, 0x0b, 0x66, 0xac, 0xc4, 0x36, 0x8e, 0xe2, 0xfd,
315 | 0x7f, 0x69, 0xff, 0xf5, 0x4e, 0x3d, 0x67, 0xe5, 0x57, 0x1a, 0x75, 0x19,
316 | 0x9e, 0x4e, 0xb2, 0xe3, 0xe7, 0x6b, 0xf9, 0x74, 0x15, 0x9f, 0x64, 0xb5,
317 | 0xdf, 0xc0, 0xa1, 0x32, 0xe6, 0xfb, 0x0d, 0xe8, 0xe5, 0xed, 0xf8, 0x64,
318 | 0x92, 0xa5, 0xc6, 0x27, 0x1f, 0x32, 0x56, 0x9a, 0x5f, 0xd9, 0x7b, 0x07,
319 | 0x69, 0xd5, 0x22, 0x91, 0x97, 0x8f, 0x73, 0x24, 0x9c, 0xc2, 0xfc, 0xc5,
320 | 0x7b, 0xe7, 0x0f, 0x68, 0xfe, 0xd9, 0xcb, 0x7b, 0xdf, 0xc3, 0xa8, 0x76,
321 | 0xcf, 0x4a, 0xec, 0x21, 0x3b, 0xf0, 0x04, 0x3f, 0x93, 0x2b, 0xbd, 0xbf,
322 | 0xfe, 0xf9, 0x72, 0x3a, 0x61, 0xe8, 0x70, 0xec, 0x6f, 0xef, 0x6f, 0x7f,
323 | 0x3c, 0xb0, 0x6c, 0xb9, 0x43, 0xca, 0x7b, 0xe9, 0x52, 0x37, 0x53, 0x9b,
324 | 0x67, 0x16, 0x31, 0x03, 0xff, 0xff, 0x2a, 0x68, 0x9e, 0xe5, 0x47, 0x1a,
325 | 0xb6, 0x6a, 0xa6, 0x33, 0x29, 0xa3, 0x99, 0xfb, 0x75, 0x49, 0x47, 0xd1,
326 | 0xa7, 0xf1, 0x25, 0x49, 0xd7, 0xa8, 0xb1, 0xb0, 0xfd, 0x59, 0x4f, 0x09,
327 | 0x32, 0x9d, 0xe6, 0x11, 0x73, 0xae, 0x22, 0x2a, 0xb0, 0xb8, 0x31, 0x6f,
328 | 0x0d, 0x4c, 0x4e, 0x53, 0x90, 0x4a, 0xdc, 0x4c, 0x96, 0xcb, 0x63, 0xeb,
329 | 0xc1, 0x98, 0xd2, 0x31, 0x5f, 0xb7, 0x12, 0x49, 0xca, 0x97, 0x02, 0xc1,
330 | 0xc0, 0x54, 0x3d, 0x0e, 0x18, 0x95, 0x27, 0xc6, 0xaa, 0x64, 0x43, 0xe3,
331 | 0x74, 0x64, 0x41, 0x96, 0x8a, 0x24, 0x46, 0xcc, 0x69, 0xe9, 0xe2, 0x72,
332 | 0x06, 0x12, 0x21, 0x4e, 0xa6, 0x54, 0x2c, 0xd1, 0xb8, 0x9b, 0xdc, 0x4c,
333 | 0xc9, 0x78, 0xb3, 0xbd, 0x19, 0xdc, 0xca, 0x1f, 0xe3, 0x55, 0x64, 0x23,
334 | 0x65, 0xef, 0xf0, 0xf2, 0x43, 0xcd, 0x13, 0xbc, 0xbc, 0x10, 0xf6, 0x3b,
335 | 0xbc, 0xb2, 0x59, 0x65, 0x82, 0xe4, 0xfc, 0x15, 0x45, 0x62, 0xd3, 0x5b,
336 | 0xb2, 0xf7, 0xaa, 0xa5, 0x8d, 0xd3, 0x16, 0x88, 0x95, 0x3f, 0xea, 0xd3,
337 | 0xff, 0x3e, 0xd7, 0xa2, 0x0a, 0xee, 0x64, 0x8c, 0xeb, 0x65, 0x89, 0x75,
338 | 0x65, 0x92, 0xec, 0x83, 0x87, 0xaf, 0xaf, 0x4a, 0x18, 0x55, 0x30, 0x41,
339 | 0x96, 0x26, 0xc1, 0x21, 0x3b, 0x56, 0xa3, 0xa4, 0x7c, 0x66, 0x52, 0x6e,
340 | 0x51, 0x45, 0xb9, 0x54, 0xf1, 0x8a, 0x01, 0xd0, 0xfc, 0xb9, 0x92, 0xd4,
341 | 0xa2, 0xe4, 0x03, 0x88, 0xb1, 0xf8, 0xd6, 0x21, 0xd0, 0x19, 0xf2, 0xd5,
342 | 0x05, 0x42, 0xeb, 0x3e, 0x41, 0x50, 0xc3, 0xad, 0xa6, 0xcb, 0x6e, 0x22,
343 | 0xf9, 0x4e, 0x54, 0xc3, 0x3d, 0xec, 0xc1, 0x28, 0xa7, 0xe8, 0xea, 0xd5,
344 | 0xec, 0xdd, 0xe1, 0x9b, 0x0b, 0xcf, 0x8f, 0xe9, 0xeb, 0x6d, 0x7d, 0x46,
345 | 0x93, 0xbf, 0xc6, 0xc4, 0x15, 0x86, 0xd7, 0xe1, 0x7a, 0x93, 0x03, 0x51,
346 | 0xd6, 0x83, 0x09, 0xc7, 0x46, 0xdd, 0xc7, 0x11, 0x75, 0x6d, 0x6a, 0xd6,
347 | 0x24, 0xba, 0x24, 0x42, 0xfe, 0x62, 0x74, 0x7e, 0xa7, 0x6c, 0x88, 0x08,
348 | 0x21, 0x65, 0x6a, 0x25, 0x9b, 0x2e, 0x9b, 0x22, 0xbe, 0xa0, 0xc8, 0xcc,
349 | 0xd5, 0x93, 0x0c, 0x23, 0xea, 0x1d, 0x9a, 0x51, 0xbf, 0x3b, 0xe3, 0xc1,
350 | 0x24, 0xb0, 0x9e, 0x06, 0x55, 0x89, 0x10, 0xe0, 0xc8, 0xac, 0x08, 0x72,
351 | 0x72, 0x17, 0x21, 0x34, 0x04, 0xaa, 0x3d, 0xcf, 0x71, 0xca, 0x66, 0xe7,
352 | 0xd4, 0x04, 0x47, 0xe0, 0x83, 0xee, 0xeb, 0xdc, 0x33, 0x23, 0x86, 0xa5,
353 | 0x6d, 0xbb, 0x2e, 0x62, 0x21, 0x91, 0x65, 0xb8, 0x7c, 0xa8, 0xe4, 0xa4,
354 | 0x99, 0x3b, 0xee, 0xef, 0xf3, 0x95, 0x9b, 0x74, 0x87, 0xdf, 0x66, 0x05,
355 | 0x0b, 0x47, 0xd2, 0xe2, 0x29, 0xc1, 0xc8, 0x41, 0xee, 0x9d, 0xfb, 0xbe,
356 | 0x4a, 0x8a, 0xb0, 0x24, 0x89, 0x4d, 0x2e, 0x80, 0xa7, 0x1b, 0xa1, 0x0d,
357 | 0x4d, 0x06, 0x2b, 0xb1, 0x58, 0xba, 0x08, 0xbd, 0xce, 0x3c, 0x0a, 0xa2,
358 | 0xb8, 0xff, 0x27, 0x4f, 0xfc, 0x0c, 0xf4, 0x5a, 0xb4, 0xc0, 0xb0, 0x47,
359 | 0xdf, 0xb3, 0x1d, 0xce, 0x22, 0x6f, 0x87, 0xeb, 0x0d, 0x97, 0xaf, 0x4f,
360 | 0xf8, 0xb2, 0x2d, 0x0c, 0x42, 0x58, 0x86, 0xeb, 0x0b, 0xe0, 0x34, 0x8e,
361 | 0xc2, 0x87, 0x8b, 0xb1, 0xf9, 0xd6, 0x1e, 0xe5, 0xe6, 0x7d, 0x5c, 0x43,
362 | 0x74, 0xb1, 0x61, 0xb2, 0x76, 0x43, 0xe6, 0x7b, 0x23, 0xfb, 0x8a, 0xf9,
363 | 0xa2, 0x03, 0x03, 0xa0, 0xdd, 0xee, 0xa6, 0xed, 0xd9, 0xb9, 0xb8, 0xf6,
364 | 0x70, 0x11, 0xd1, 0x3b, 0xec, 0xad, 0x2f, 0x68, 0x25, 0x84, 0x6a, 0x28,
365 | 0x2c, 0x03, 0xbf, 0x0d, 0x3a, 0x72, 0x50, 0x6d, 0x8e, 0x5e, 0x3f, 0xf3,
366 | 0x95, 0x43, 0xbd, 0xf4, 0xb0, 0x27, 0x26, 0xd0, 0x5c, 0x01, 0xa5, 0x58,
367 | 0x47, 0x4c, 0x65, 0x16, 0xa8, 0x2c, 0xf1, 0xff, 0x09, 0xbf, 0xff, 0xcb,
368 | 0xb9, 0x43, 0xe5, 0x95, 0x91, 0xe3, 0x7a, 0xcb, 0x68, 0x2e, 0x8f, 0xb3,
369 | 0x8e, 0x3d, 0x5f, 0xcc, 0x82, 0x4c, 0x35, 0x8d, 0x88, 0x67, 0x93, 0xc9,
370 | 0xea, 0xb9, 0x12, 0x0b, 0x87, 0xe1, 0xd1, 0xc8, 0x9f, 0x3f, 0xea, 0x41,
371 | 0x8d, 0xe6, 0x41, 0x52, 0x99, 0xbc, 0x58, 0x53, 0x33, 0xb9, 0x8a, 0xc3,
372 | 0x94, 0x96, 0x47, 0x26, 0x33, 0x36, 0x8b, 0xd8, 0x49, 0x92, 0x58, 0xa7,
373 | 0x0c, 0x3b, 0x0d, 0x98, 0x48, 0xd7, 0x44, 0x48, 0x2f, 0xd5, 0x94, 0x0e,
374 | 0xf8, 0x46, 0x51, 0x16, 0xc5, 0x65, 0x5c, 0xa1, 0x14, 0x81, 0xa6, 0x5e,
375 | 0x43, 0xde, 0x3f, 0x31, 0xe9, 0x72, 0x49, 0x85, 0x62, 0x66, 0x31, 0x7d,
376 | 0x2a, 0x63, 0x16, 0x0c, 0x90, 0x5e, 0x6c, 0x1d, 0xc5, 0xd1, 0x36, 0x19,
377 | 0x39, 0x7f, 0x75, 0x18, 0xd6, 0x62, 0x49, 0x59, 0x25, 0x78, 0x00, 0x69,
378 | 0x45, 0xc7, 0xa6, 0x6e, 0xb0, 0x50, 0xcc, 0xbd, 0xd1, 0x15, 0x35, 0x62,
379 | 0x13, 0x32, 0x41, 0x8b, 0xbd, 0x0c, 0x72, 0x96, 0xba, 0x64, 0xe8, 0x14,
380 | 0x41, 0x63, 0x94, 0x8b, 0x78, 0x4a, 0x63, 0x2d, 0x60, 0x91, 0x92, 0xac,
381 | 0x22, 0x3e, 0x62, 0xe4, 0x65, 0x10, 0x00, 0x9f, 0xb0, 0xa9, 0x18, 0x1e,
382 | 0xd2, 0x48, 0x3c, 0xb4, 0xd5, 0x7b, 0x0e, 0xed, 0x41, 0x62, 0xe7, 0xb1,
383 | 0xb0, 0x2a, 0x4a, 0x71, 0x45, 0x9b, 0xcc, 0x01, 0x11, 0xb4, 0x3a, 0x31,
384 | 0x22, 0x49, 0xfb, 0x3b, 0x79, 0xd7, 0x13, 0xa3, 0x5f, 0x76, 0x3a, 0xec,
385 | 0x17, 0x1f, 0xb8, 0x9b, 0xe1, 0x0b, 0xcd, 0xf5, 0x26, 0x70, 0x91, 0x41,
386 | 0xf1, 0xfd, 0x75, 0x4d, 0x41, 0xce, 0xde, 0xe2, 0x17, 0xca, 0xd3, 0x25,
387 | 0xdf, 0xd9, 0xdf, 0xee, 0x2e, 0x7c, 0xa5, 0xbd, 0x21, 0xe6, 0xea, 0x66,
388 | 0xba, 0x3a, 0xef, 0x74, 0x4a, 0x6c, 0xb6, 0xdc, 0x51, 0xcc, 0x81, 0x54,
389 | 0x63, 0x2e, 0x76, 0x5c, 0x69, 0x6f, 0x47, 0xfc, 0x84, 0xd2, 0x6e, 0xa7,
390 | 0xcc, 0x9e, 0xf1, 0xfd, 0x36, 0x4c, 0x82, 0x8c, 0xcb, 0xb9, 0x78, 0x1b,
391 | 0x3d, 0xf4, 0x21, 0x3e, 0x42, 0x1b, 0x86, 0x49, 0x8a, 0x8f, 0x10, 0xf9,
392 | 0xc4, 0xff, 0x9d, 0xe0, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0xa7, 0x89,
393 | 0x85, 0x89, 0xd8, 0x40, 0x00, 0x00,
394 | },
395 | "_templates/console.html",
396 | )
397 | }
398 |
399 | // Asset loads and returns the asset for the given name.
400 | // It returns an error if the asset could not be found or
401 | // could not be loaded.
402 | func Asset(name string) ([]byte, error) {
403 | cannonicalName := strings.Replace(name, "\\", "/", -1)
404 | if f, ok := _bindata[cannonicalName]; ok {
405 | return f()
406 | }
407 | return nil, fmt.Errorf("Asset %s not found", name)
408 | }
409 |
410 | // AssetNames returns the names of the assets.
411 | func AssetNames() []string {
412 | names := make([]string, 0, len(_bindata))
413 | for name := range _bindata {
414 | names = append(names, name)
415 | }
416 | return names
417 | }
418 |
419 | // _bindata is a table, holding each asset generator, mapped to its name.
420 | var _bindata = map[string]func() ([]byte, error){
421 | "_templates/console.html": templates_console_html,
422 | }
423 |
--------------------------------------------------------------------------------