├── .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 |
100 | 101 | 102 |
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 | 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 | 558 | 559 | 560 | 561 |

562 |

563 |

565 |
566 |

567 | 568 |
569 | 570 |
571 |

572 |

Broadcast Messaging

573 |

574 | 575 | 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 | --------------------------------------------------------------------------------