Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Init

  • Loading branch information...
commit 2f23c822413af31f71958428155ae6f904840fbd 1 parent f1ce1d8
Petr Pilař authored
2  LICENSE
... ... @@ -1,4 +1,4 @@
1   -Copyright (c) 2011, Palmik, yihuang
  1 +Copyright (c) 2011, Palmik
2 2
3 3 All rights reserved.
4 4
18 src/Network/Wai/Sock/Application.hs
... ... @@ -1,11 +1,19 @@
1 1 module Network.Wai.Sock.Application
2   -( Application
  2 +( Application(..)
  3 +, ApplicationSettings(..)
3 4 ) where
4 5
5 6 ------------------------------------------------------------------------------
6   -import qualified Data.ByteString.Lazy as LB (ByteString)
7   -import qualified Data.ByteString as SB (ByteString)
8   -import qualified Data.Conduit as C (Source, Sink)
  7 +import qualified Data.ByteString.Lazy as BL (ByteString)
  8 +import qualified Data.Conduit as C (Source, Sink)
  9 +import qualified Data.Text as TS (Text)
9 10 ------------------------------------------------------------------------------
10 11
11   -type Application m = C.Source m LB.ByteString -> C.Sink LB.ByteString m () -> m ()
  12 +data Application m = Application
  13 + { applicationDefinition :: C.Source m BL.ByteString -> C.Sink BL.ByteString m () -> m ()
  14 + , applicationSettings :: ApplicationSettings
  15 + }
  16 +
  17 +data ApplicationSettings = ApplicationSettings
  18 + { appSettingsPrefix :: [TS.Text]
  19 + }
60 src/Network/Wai/Sock/Environment.hs
... ... @@ -0,0 +1,60 @@
  1 +{-# LANGUAGE FlexibleContexts #-}
  2 +{-# LANGUAGE RecordWildCards #-}
  3 +
  4 +module Network.Wai.Sock.Environment
  5 +( Environment
  6 +) where
  7 +
  8 +------------------------------------------------------------------------------
  9 +import Control.Applicative
  10 +import Control.Concurrent.MVar.Lifted
  11 +import Control.Monad.Base
  12 +import Control.Monad.Trans.Control
  13 +------------------------------------------------------------------------------
  14 +import qualified Data.ByteString.Lazy as LB (ByteString)
  15 +import qualified Data.Conduit as C (Source, Sink)
  16 +import qualified Data.HashMap.Strict as HM (HashMap, insert, lookup)
  17 +------------------------------------------------------------------------------
  18 +import Network.Wai.Sock.Session
  19 +------------------------------------------------------------------------------
  20 +
  21 +newtype Environment = Environment
  22 + { envSessions :: MVar (HM.HashMap SessionID (MVar Session))
  23 + }
  24 +
  25 +addSession :: MonadBaseControl IO m
  26 + => SessionID
  27 + -> Session
  28 + -> Environment
  29 + -> m ()
  30 +addSession sid s Environment{..} = do
  31 + ms <- newMVar s
  32 + modifyMVar_ envSessions (return . HM.insert sid ms)
  33 +
  34 +-- | Applies the given function on session with given ID and saves the new value.
  35 +-- If session with the supplied ID does not exist, it's virtually no-op.
  36 +modifySession :: MonadBaseControl IO m
  37 + => (Session -> m Session)
  38 + -> SessionID
  39 + -> Environment
  40 + -> m ()
  41 +modifySession f sid Environment{..} = withMVar envSessions go
  42 + where go smap = case HM.lookup sid smap of
  43 + Just ms -> modifyMVar_ ms f
  44 + Nothing -> return ()
  45 +
  46 +
  47 +-- | Retrieves session with the given ID, if there is no such session, it's created first.
  48 +getSession :: MonadBaseControl IO m
  49 + => SessionID
  50 + -> Environment
  51 + -> m (MVar Session)
  52 +getSession sid Environment{..} = modifyMVar envSessions go
  53 + where go smap = case HM.lookup sid smap of
  54 + Just ms -> return (smap, ms)
  55 + Nothing -> do
  56 + ms <- newSession sid >>= newMVar
  57 + return (HM.insert sid ms smap, ms)
  58 +
  59 +
  60 +
13 src/Network/Wai/Sock/Frame.hs
... ... @@ -0,0 +1,13 @@
  1 +module Network.Wai.Sock.Frame
  2 +( Frame(..)
  3 +) where
  4 +
  5 +------------------------------------------------------------------------------
  6 +import qualified Data.Text as TS (Text, unpack)
  7 +------------------------------------------------------------------------------
  8 +
  9 +data Frame
  10 + = FrameOpen
  11 + | FrameHeartbeat
  12 + | FrameMessage TS.Text
  13 + | FrameClose Int TS.Text
109 src/Network/Wai/Sock/Handler.hs
... ... @@ -1,3 +1,6 @@
  1 +{-# LANGUAGE OverloadedStrings #-}
  2 +{-# LANGUAGE RecordWildCards #-}
  3 +
1 4 module Network.Wai.Sock.Handler
2 5 (
3 6 ) where
@@ -8,25 +11,117 @@ import System.Random (randomRIO)
8 11 import Control.Applicative
9 12 import Control.Monad.IO.Class
10 13 ------------------------------------------------------------------------------
11   -import qualified Data.Aeson as AE (encode)
  14 +import qualified Data.Aeson as AE (encode, object)
  15 +import Data.Aeson ((.=))
12 16 import qualified Data.Binary as BI (encode)
13   -import qualified Data.ByteString.Lazy as LB (ByteString, toChunks)
14   -import qualified Data.ByteString as SB (ByteString, empty)
  17 +import qualified Data.ByteString.Lazy as BL (ByteString, toChunks, fromChunks)
  18 +import qualified Data.ByteString as BS (ByteString, empty, concat)
15 19 import Data.Digest.Pure.MD5 (md5)
16 20 import Data.Int (Int64)
17 21 import Data.Maybe
18 22 import Data.Monoid
19   -import qualified Data.Text as ST (Text, isPrefixOf, isSuffixOf)
  23 +import qualified Data.Text as TS (Text, isPrefixOf, isSuffixOf)
  24 +import qualified Data.Text.Encoding as TS (encodeUtf8)
20 25 ------------------------------------------------------------------------------
21 26 import Blaze.ByteString.Builder (Builder)
22 27 import qualified Blaze.ByteString.Builder.ByteString as B (fromLazyByteString)
23 28 import qualified Blaze.ByteString.Builder.Char.Utf8 as B (fromString, fromLazyText)
24 29 ------------------------------------------------------------------------------
25 30 import qualified Network.HTTP.Types as H
26   -import qualified Network.Wai as W (Application)
  31 +import qualified Network.Wai as W (Application, Request(..), Response(..), responseLBS)
27 32 ------------------------------------------------------------------------------
28 33 import Network.Wai.Sock.Application
  34 +import Network.Wai.Sock.Environment
  35 +import Network.Wai.Sock.Frame
  36 +import Network.Wai.Sock.Server
  37 +import Network.Wai.Sock.Session
  38 +------------------------------------------------------------------------------
  39 +
  40 +sock :: Environment -> ([TS.Text] -> Maybe (Application m, [TS.Text], [TS.Text])) -> W.Application
  41 +sock mvsm r req = undefined
  42 +
  43 +------------------------------------------------------------------------------
  44 +-- | Standard responses (greeting, info, iframe)
  45 +
  46 +responseGreeting :: W.Response
  47 +responseGreeting = response200 headerPlain "Welcome to SockJS!\n"
  48 +
  49 +responseIframe :: ServerSettings -- ^ Server Settings
  50 + -> W.Request
  51 + -> W.Response
  52 +responseIframe ServerSettings{..} req =
  53 + case lookup "If-None-Match" (W.requestHeaders req) of
  54 + (Just s) | s == hashed -> response304
  55 + _ -> response200 headers content
  56 + where
  57 + content =
  58 + "<!DOCTYPE html>\n\
  59 + \<html>\n\
  60 + \<head>\n\
  61 + \ <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n\
  62 + \ <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n\
  63 + \ <script>\n\
  64 + \ document.domain = document.domain;\n\
  65 + \ _sockjs_onload = function(){SockJS.bootstrap_iframe();};\n\
  66 + \ </script>\n\
  67 + \ <script src=\"" <> convertTS2BL serverSettingsSockURL <> "\"></script>\n\
  68 + \</head>\n\
  69 + \<body>\n\
  70 + \ <h2>Don't panic!</h2>\n\
  71 + \ <p>This is a SockJS hidden iframe. It's used for cross domain magic.</p>\n\
  72 + \</body>\n\
  73 + \</html>"
  74 + hashed = convertBL2BS . BI.encode $ md5 content
  75 + headers = headerHTML ++ headerCache ++ headerETag hashed
  76 +
  77 +responseInfo :: ServerSettings -- ^ Server Settings
  78 + -> Int64 -- ^ Entropy
  79 + -> W.Response
  80 +responseInfo ServerSettings{..} ent = response200 headerJSON . AE.encode $ AE.object
  81 + [ "websocket" .= serverSettingsWebsocketsEnabled
  82 + , "cookie_needed" .= serverSettingsCookiesNeeded
  83 + , "origins" .= serverSettingsAllowedOrigins
  84 + , "entropy" .= ent
  85 + ]
  86 +
29 87 ------------------------------------------------------------------------------
  88 +-- | Response utility functions.
  89 +
  90 +response404 :: W.Response
  91 +response404 = W.responseLBS H.status404 headerPlain mempty
  92 +
  93 +response200 :: H.ResponseHeaders -> BL.ByteString -> W.Response
  94 +response200 = W.responseLBS H.status200
  95 +
  96 +response304 :: W.Response
  97 +response304 = W.responseLBS H.status304 [] mempty
  98 +
  99 +------------------------------------------------------------------------------
  100 +-- | Header utility functions.
  101 +
  102 +headerPlain :: H.ResponseHeaders
  103 +headerPlain = [("Content-Type", "text/plain; charset=UTF-8")]
  104 +
  105 +headerHTML :: H.ResponseHeaders
  106 +headerHTML = [("Content-Type", "text/html; charset=UTF-8")]
  107 +
  108 +headerJSON :: H.ResponseHeaders
  109 +headerJSON = [("Content-Type", "application/json; charset=UTF-8")]
  110 +
  111 +headerCache :: H.ResponseHeaders
  112 +headerCache = [("Cache-Control", "public; max-age=31536000;"),("Expires", "31536000")]
  113 +
  114 +headerETag :: H.Ascii -> H.ResponseHeaders
  115 +headerETag etag = [("ETag", etag)]
  116 +
  117 +------------------------------------------------------------------------------
  118 +-- | Other utility functions.
  119 +
  120 +convertBS2BL :: BS.ByteString -> BL.ByteString
  121 +convertBS2BL = BL.fromChunks . (:[])
  122 +
  123 +convertBL2BS :: BL.ByteString -> BS.ByteString
  124 +convertBL2BS = BS.concat . BL.toChunks
30 125
31   -handler :: Monad m => (ST.Text -> Application m) -> W.Application
32   -handler = undefined
  126 +convertTS2BL :: TS.Text -> BL.ByteString
  127 +convertTS2BL = convertBS2BL . TS.encodeUtf8
27 src/Network/Wai/Sock/Server.hs
... ... @@ -0,0 +1,27 @@
  1 +{-# LANGUAGE OverloadedStrings #-}
  2 +
  3 +module Network.Wai.Sock.Server
  4 +( ServerSettings(..)
  5 +) where
  6 +
  7 +------------------------------------------------------------------------------
  8 +import qualified Data.Text as TS (Text)
  9 +import Data.Default
  10 +------------------------------------------------------------------------------
  11 +
  12 +data ServerSettings = ServerSettings
  13 + { serverSettingsWebsocketsEnabled :: Bool
  14 + , serverSettingsCookiesNeeded :: Bool
  15 + , serverSettingsAllowedOrigins :: TS.Text
  16 + , serverSettingsSockURL :: TS.Text
  17 + , serverSettingsSockVersion :: TS.Text
  18 + }
  19 +
  20 +instance Default ServerSettings where
  21 + def = ServerSettings
  22 + { serverSettingsWebsocketsEnabled = True
  23 + , serverSettingsCookiesNeeded = True
  24 + , serverSettingsAllowedOrigins = "*:*"
  25 + , serverSettingsSockURL = "http://cdn.sockjs.org/sockjs-0.3.min.js"
  26 + , serverSettingsSockVersion = "0.3"
  27 + }
40 src/Network/Wai/Sock/Session.hs
... ... @@ -0,0 +1,40 @@
  1 +{-# LANGUAGE FlexibleContexts #-}
  2 +
  3 +module Network.Wai.Sock.Session
  4 +( Session(..)
  5 +, SessionStatus(..)
  6 +, SessionID
  7 +
  8 +, newSession
  9 +) where
  10 +
  11 +------------------------------------------------------------------------------
  12 +import Control.Applicative
  13 +import Control.Concurrent.Chan.Lifted
  14 +import Control.Monad.Base
  15 +------------------------------------------------------------------------------
  16 +import qualified Data.ByteString.Lazy as BL (ByteString)
  17 +import qualified Data.ByteString as BS (ByteString)
  18 +import qualified Data.Conduit as C (Source, Sink)
  19 +import qualified Data.Text as TS (Text)
  20 +------------------------------------------------------------------------------
  21 +
  22 +newSession :: MonadBase IO m
  23 + => SessionID
  24 + -> m Session
  25 +newSession sid = Session sid SessionFresh <$> newChan
  26 +
  27 +data Session = Session
  28 + { sessionID :: SessionID
  29 + , sessionStatus :: SessionStatus
  30 + , sessionIncomingBuffer :: Chan BS.ByteString
  31 + }
  32 +
  33 +-- | SessionID
  34 +type SessionID = TS.Text
  35 +
  36 +-- | SessionStatus
  37 +data SessionStatus
  38 + = SessionFresh -- ^ Right after creation, Session is "Fresh"
  39 + | SessionOpened -- ^ Right after we send opening frame, Session is "Opened". We also start the timeout & heartbeat timer at this point.
  40 + | SessionClosed -- ^ Right after we send closing frame, Session if "Closed".
44 static/client.html
... ... @@ -1,44 +0,0 @@
1   -<html>
2   - <head>
3   - <title>Haskell Sockjs example</title>
4   - <script type="text/JavaScript"
5   - src="http://code.jquery.com/jquery-1.6.3.min.js"></script>
6   - <script type="text/JavaScript"
7   - src="sockjs.js"></script>
8   - <script type="text/JavaScript" src="client.js"></script>
9   - <link rel="stylesheet" type="text/css" href="screen.css"></script>
10   - </head>
11   - <body>
12   - <h1>Haskell Sockjs example</h1>
13   - <div id="main">
14   - <div id="warnings">
15   - </div>
16   - <div id="join-section">
17   - <h2>Join</h2>
18   - <form id="join-form" action="">
19   - <label for="user">Username: </label>
20   - <input id="user" type="text" size="12" />
21   - <input id="send" type="submit" value="Join" />
22   - </form>
23   - </div>
24   - <div id="users-section" style="display: none">
25   - <h2>Users</h2>
26   - <ul id="users">
27   - </ul>
28   - </div>
29   - <div id="chat-section" style="display: none">
30   - <h2>Chat</h2>
31   - <div id="messages">
32   - </div>
33   - <br />
34   - <form id="message-form" action="">
35   - <input id="text" type="text" size="40" />
36   - <input id="send" type="submit" value="Send" />
37   - </form>
38   - </div>
39   - </div>
40   - <div id="footer">
41   - Source code available <a href="https://github.com/yihuang/wai-sockjs/blob/master/Apps.hs">here</a>, origin websockets version available <a href="http://github.com/jaspervdj/websockets/tree/master/example">here</a>
42   - </div>
43   - </body>
44   -</html>
91 static/client.js
... ... @@ -1,91 +0,0 @@
1   -function createSockjs(path) {
2   - var host = window.location.host;
3   - if(host == '') host = 'localhost';
4   - var uri = 'http://' + host + path;
5   - return new SockJS(uri);
6   -}
7   -
8   -function log() {
9   - if(window.console && console.log && console.log.apply) {
10   - console.log.apply(console, arguments);
11   - }
12   -}
13   -
14   -var users = [];
15   -
16   -function refreshUsers() {
17   - $('#users').html('');
18   - for(i in users) {
19   - $('#users').append($(document.createElement('li')).text(users[i]));
20   - }
21   -}
22   -
23   -function onMessage(event) {
24   - log(event);
25   - var p = $(document.createElement('p')).text(event.data);
26   -
27   - $('#messages').append(p);
28   - $('#messages').animate({scrollTop: $('#messages')[0].scrollHeight});
29   -
30   - if(event.data.match(/^[^:]* joined/)) {
31   - var user = event.data.replace(/ .*/, '');
32   - users.push(user);
33   - refreshUsers();
34   - }
35   -
36   - if(event.data.match(/^[^:]* disconnected/)) {
37   - var user = event.data.replace(/ .*/, '');
38   - var idx = users.indexOf(user);
39   - users = users.slice(0, idx).concat(users.slice(idx + 1));
40   - refreshUsers();
41   - }
42   -}
43   -
44   -$(document).ready(function () {
45   - $('#join-form').submit(function () {
46   - $('#warnings').html('');
47   - var user = $('#user').val();
48   - var ws = createSockjs('/chat');
49   -
50   - ws.onopen = function() {
51   - log('open');
52   - ws.send('join ' + user);
53   - };
54   - ws.onclose = function() {
55   - log('closed');
56   - }
57   -
58   - ws.onmessage = function(event) {
59   - log(event);
60   - if(event.data.match('^Welcome! Users: ')) {
61   - /* Calculate the list of initial users */
62   - var str = event.data.replace(/^Welcome! Users: /, '');
63   - if(str != "") {
64   - users = str.split(", ");
65   - refreshUsers();
66   - }
67   -
68   - $('#join-section').hide();
69   - $('#chat-section').show();
70   - $('#users-section').show();
71   -
72   - ws.onmessage = onMessage;
73   - ws.onheartbeat = function(ev) {log(ev);}
74   -
75   - $('#message-form').submit(function () {
76   - var text = $('#text').val();
77   - ws.send(text);
78   - $('#text').val('');
79   - return false;
80   - });
81   - } else {
82   - $('#warnings').append(event.data);
83   - ws.close();
84   - }
85   - };
86   -
87   - $('#join').append('Connecting...');
88   -
89   - return false;
90   - });
91   -});
86 static/screen.css
... ... @@ -1,86 +0,0 @@
1   -html {
2   - font-family: sans-serif;
3   - background-color: #335;
4   - font-size: 16px;
5   -}
6   -
7   -body {
8   -}
9   -
10   -h1 {
11   - text-align: center;
12   - font-size: 20px;
13   - color: #fff;
14   - padding: 10px 10px 20px 10px;
15   -}
16   -
17   -h2 {
18   - border-bottom: 1px solid black;
19   - display: block;
20   - font-size: 18px;
21   -}
22   -
23   -div#main {
24   - width: 600px;
25   - margin: 0px auto 0px auto;
26   - padding: 0px;
27   - background-color: #fff;
28   - height: 460px;
29   -}
30   -
31   -div#warnings {
32   - color: red;
33   - font-weight: bold;
34   - margin: 10px;
35   -}
36   -
37   -div#join-section {
38   - float: left;
39   - margin: 10px;
40   -}
41   -
42   -div#users-section {
43   - width: 170px;
44   - float: right;
45   - padding: 0px;
46   - margin: 10px;
47   -}
48   -
49   -ul#users {
50   - list-style-type: none;
51   - padding-left: 0px;
52   - height: 300px;
53   - overflow: auto;
54   -}
55   -
56   -div#chat-section {
57   - width: 390px;
58   - float: left;
59   - margin: 10px;
60   -}
61   -
62   -div#messages {
63   - margin: 0px;
64   - height: 300px;
65   - overflow: auto;
66   -}
67   -
68   -div#messages p {
69   - margin: 0px;
70   - padding: 0px;
71   -}
72   -
73   -div#footer {
74   - text-align: center;
75   - font-size: 12px;
76   - color: #fff;
77   - margin: 10px 0px 30px 0px;
78   -}
79   -
80   -div#footer a {
81   - color: #fff;
82   -}
83   -
84   -div.clear {
85   - clear: both;
86   -}
1,665 static/sockjs.js
... ... @@ -1,1665 +0,0 @@
1   -/* SockJS client, version 0.1.1, http://sockjs.org, MIT License
2   -
3   -Copyright (C) 2011 VMware, Inc.
4   -
5   -Permission is hereby granted, free of charge, to any person obtaining a copy
6   -of this software and associated documentation files (the "Software"), to deal
7   -in the Software without restriction, including without limitation the rights
8   -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9   -copies of the Software, and to permit persons to whom the Software is
10   -furnished to do so, subject to the following conditions:
11   -
12   -The above copyright notice and this permission notice shall be included in
13   -all copies or substantial portions of the Software.
14   -
15   -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16   -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17   -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18   -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19   -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20   -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21   -THE SOFTWARE.
22   -*/
23   -
24   -// JSON2 by Douglas Crockford (minified).
25   -var JSON;JSON||(JSON={}),function(){function str(a,b){var c,d,e,f,g=gap,h,i=b[a];i&&typeof i=="object"&&typeof i.toJSON=="function"&&(i=i.toJSON(a)),typeof rep=="function"&&(i=rep.call(b,a,i));switch(typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";gap+=indent,h=[];if(Object.prototype.toString.apply(i)==="[object Array]"){f=i.length;for(c=0;c<f;c+=1)h[c]=str(c,i)||"null";e=h.length===0?"[]":gap?"[\n"+gap+h.join(",\n"+gap)+"\n"+g+"]":"["+h.join(",")+"]",gap=g;return e}if(rep&&typeof rep=="object"){f=rep.length;for(c=0;c<f;c+=1)typeof rep[c]=="string"&&(d=rep[c],e=str(d,i),e&&h.push(quote(d)+(gap?": ":":")+e))}else for(d in i)Object.prototype.hasOwnProperty.call(i,d)&&(e=str(d,i),e&&h.push(quote(d)+(gap?": ":":")+e));e=h.length===0?"{}":gap?"{\n"+gap+h.join(",\n"+gap)+"\n"+g+"}":"{"+h.join(",")+"}",gap=g;return e}}function quote(a){escapable.lastIndex=0;return escapable.test(a)?'"'+a.replace(escapable,function(a){var b=meta[a];return typeof b=="string"?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function f(a){return a<10?"0"+a:a}"use strict",typeof Date.prototype.toJSON!="function"&&(Date.prototype.toJSON=function(a){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(a){return this.valueOf()});var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;typeof JSON.stringify!="function"&&(JSON.stringify=function(a,b,c){var d;gap="",indent="";if(typeof c=="number")for(d=0;d<c;d+=1)indent+=" ";else typeof c=="string"&&(indent=c);rep=b;if(!b||typeof b=="function"||typeof b=="object"&&typeof b.length=="number")return str("",{"":a});throw new Error("JSON.stringify")}),typeof JSON.parse!="function"&&(JSON.parse=function(text,reviver){function walk(a,b){var c,d,e=a[b];if(e&&typeof e=="object")for(c in e)Object.prototype.hasOwnProperty.call(e,c)&&(d=walk(e,c),d!==undefined?e[c]=d:delete e[c]);return reviver.call(a,b,e)}var j;text=String(text),cx.lastIndex=0,cx.test(text)&&(text=text.replace(cx,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)}));if(/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,""))){j=eval("("+text+")");return typeof reviver=="function"?walk({"":j},""):j}throw new SyntaxError("JSON.parse")})}()
26   -
27   -
28   -// [*] Including lib/index.js
29   -// Public object
30   -SockJS = (function(){
31   - var _document = document;
32   - var _window = window;
33   -
34   -// [*] Including lib/reventtarget.js
35   -/* Simplified implementation of DOM2 EventTarget.
36   - * http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget
37   - */
38   -var REventTarget = function() {};
39   -REventTarget.prototype.addEventListener = function (eventType, listener) {
40   - if(!this._listeners) {
41   - this._listeners = {};
42   - }
43   - if(!(eventType in this._listeners)) {
44   - this._listeners[eventType] = [];
45   - }
46   - var arr = this._listeners[eventType];
47   - if(utils.arrIndexOf(arr, listener) === -1) {
48   - arr.push(listener);
49   - }
50   - return;
51   -};
52   -
53   -REventTarget.prototype.removeEventListener = function (eventType, listener) {
54   - if(!(this._listeners && (eventType in this._listeners))) {
55   - return;
56   - }
57   - var arr = this._listeners[eventType];
58   - var idx = utils.arrIndexOf(arr, listener);
59   - if (idx !== -1) {
60   - if(arr.length > 1) {
61   - this._listeners[eventType] = arr.slice(0, idx).concat( arr.slice(idx+1) );
62   - } else {
63   - delete this._listeners[eventType];
64   - }
65   - return;
66   - }
67   - return;
68   -};
69   -
70   -REventTarget.prototype.dispatchEvent = function (event) {
71   - var t = event.type;
72   - var args = Array.prototype.slice.call(arguments, 0);
73   - if (this['on'+t]) {
74   - this['on'+t].apply(this, args);
75   - }
76   - if (this._listeners && t in this._listeners) {
77   - for(var i=0; i < this._listeners[t].length; i++) {
78   - this._listeners[t][i].apply(this, args);
79   - }
80   - }
81   -};
82   -// [*] End of lib/reventtarget.js
83   -
84   -
85   -// [*] Including lib/simpleevent.js
86   -var SimpleEvent = function(type, obj) {
87   - this.type = type;
88   - if (typeof obj !== 'undefined') {
89   - for(var k in obj) {
90   - if (!obj.hasOwnProperty(k)) continue;
91   - this[k] = obj[k];
92   - }
93   - }
94   -};
95   -
96   -SimpleEvent.prototype.toString = function() {
97   - var r = [];
98   - for(var k in this) {
99   - if (!this.hasOwnProperty(k)) continue;
100   - var v = this[k];
101   - if (typeof v === 'function') v = '[function]';
102   - r.push(k + '=' + v);
103   - }
104   - return 'SimpleEvent(' + r.join(', ') + ')';
105   -};
106   -// [*] End of lib/simpleevent.js
107   -
108   -
109   -// [*] Including lib/utils.js
110   -var utils = {};
111   -var random_string_chars = ['a','b','c','d','e','f','g','h','i','j',
112   - 'k','l','m','n','o','p','q','r','s','t',
113   - 'u','v','w','x','y','z',
114   - '0','1','2','3','4','5','6','7','8','9','_'];
115   -utils.random_string = function(letters, max) {
116   - max = max || random_string_chars.length;
117   - var i, ret = [];
118   - for(i=0; i < letters; i++) {
119   - ret.push( random_string_chars[Math.floor(Math.random() * max)] );
120   - }
121   - return ret.join('');
122   -};
123   -utils.random_number = function(max) {
124   - return Math.floor(Math.random() * max);
125   -};
126   -utils.random_number_string = function(max) {
127   - var s = ''+utils.random_number(max);
128   - var t = (''+(max - 1)).length;
129   - while (s.length < t) {s = '0' + s;}
130   - return s;
131   -};
132   -
133   -utils.attachMessage = function(listener) {
134   - utils.attachEvent('message', listener);
135   -};
136   -utils.attachEvent = function(event, listener) {
137   - if (typeof _window.addEventListener !== 'undefined') {
138   - _window.addEventListener(event, listener, false);
139   - } else {
140   - // IE quirks.
141   - // According to: http://stevesouders.com/misc/test-postmessage.php
142   - // the message gets delivered only to 'document', not 'window'.
143   - _document.attachEvent("on" + event, listener);
144   - // I get 'window' for ie8.
145   - _window.attachEvent("on" + event, listener);
146   - }
147   -};
148   -
149   -utils.detachMessage = function(listener) {
150   - utils.detachEvent('message', listener);
151   -};
152   -utils.detachEvent = function(event, listener) {
153   - if (typeof _window.addEventListener !== 'undefined') {
154   - _window.removeEventListener(event, listener, false);
155   - } else {
156   - _document.detachEvent("on" + event, listener);
157   - _window.detachEvent("on" + event, listener);
158   - }
159   -};
160   -
161   -
162   -// Assuming that url looks like: http://asdasd:111/asd
163   -utils.getOrigin = function(url) {
164   - url += '/';
165   - var parts = url.split('/').slice(0, 3);
166   - return parts.join('/');
167   -};
168   -
169   -utils.objectExtend = function(dst, src) {
170   - for(var k in src) {
171   - if (src.hasOwnProperty(k)) {
172   - dst[k] = src[k];
173   - }
174   - }
175   - return dst;
176   -};
177   -
178   -// Try to clear some headers, in order to save bandwidth. For
179   -// reference see:
180   -// http://blog.mibbit.com/?p=143
181   -// http://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest
182   -var xhrDefaultHeaders = {
183   - "User-Agent": '',
184   - "Accept": '',
185   - "Accept-Language": '',
186   - "Content-Type": "text/plain;charset=UTF-8"
187   -};
188   -
189   -if (navigator &&
190   - (navigator.userAgent.indexOf('Chrome')!= -1 ||
191   - navigator.userAgent.indexOf('Safari') != -1)) {
192   - delete xhrDefaultHeaders['User-Agent'];
193   -}
194   -
195   -// References:
196   -// http://ajaxian.com/archives/100-line-ajax-wrapper
197   -// http://msdn.microsoft.com/en-us/library/cc288060(v=VS.85).aspx
198   -utils.createXDR = function(method, url, payload, callback) {
199   - var mock_xhr = {status: null, responseText:'', readyState:1};
200   - var xdr = new XDomainRequest();
201   - // IE caches even POSTs
202   - url += ((url.indexOf('?') === -1) ? '?' : '&') + 't='+utils.random_string(8);
203   - var cleanup = function() {
204   - if (xdr) {
205   - onerror = xdr.onerror = xdr.ontimeout = xdr.onprogress =
206   - xdr.onload = null;
207   - try {
208   - xdr.abort();
209   - } catch (x) {}
210   - xdr = callback = null;
211   - }
212   - };
213   - var onerror = xdr.ontimeout = xdr.onerror = function() {
214   - mock_xhr.status = 500;
215   - mock_xhr.readyState = 4;
216   - callback(mock_xhr);
217   - cleanup();
218   - };
219   - xdr.onload = function() {
220   - mock_xhr.status = 200;
221   - mock_xhr.readyState = 4;
222   - mock_xhr.responseText = xdr.responseText;
223   - callback(mock_xhr);
224   - cleanup();
225   - };
226   - xdr.onprogress = function() {
227   - mock_xhr.status = 200;
228   - mock_xhr.readyState = 3;
229   - mock_xhr.responseText = xdr.responseText;
230   - callback(mock_xhr);
231   - };
232   - try {
233   - // Fails with AccessDenied if port number is bogus
234   - xdr.open(method, url);
235   - xdr.send(payload);
236   - } catch (x) {
237   - utils.delay(onerror);
238   - }
239   - return function (abort_reason) {
240   - if (callback) {
241   - callback(mock_xhr, null, abort_reason);
242   - cleanup();
243   - }
244   - };
245   -};
246   -
247   -utils.createXHR = function(method, url, payload, callback) {
248   - var xhr;
249   - if (_window.ActiveXObject) {
250   - // IE caches POSTs
251   - url += ((url.indexOf('?') === -1) ? '?' : '&') + 't='+(+new Date);
252   - try {
253   - xhr = new ActiveXObject('Microsoft.XMLHTTP');
254   - } catch(x) {}
255   - }
256   - if (!xhr) {
257   - xhr = new XMLHttpRequest();
258   - }
259   - xhr.open(method, url, true);
260   -
261   - for (var k in xhrDefaultHeaders) {
262   - try {
263   - xhr.setRequestHeader(k, xhrDefaultHeaders[k]);
264   - } catch(x) {
265   - delete xhrDefaultHeaders[k];
266   - }
267   - }
268   - if ('withCredentials' in xhr) {
269   - // Set cookies on CORS, please.
270   - xhr.withCredentials = "true";
271   - }
272   -
273   - var cleanup = function() {
274   - if (xhr) {
275   - // IE needs this field to be a function
276   - try {
277   - xhr.onreadystatechange = null;
278   - } catch (x) {
279   - xhr.onreadystatechange = function(){};
280   - }
281   - // Explorer tends to keep connection open, even after the
282   - // tab gets closed: http://bugs.jquery.com/ticket/5280
283   - try {
284   - xhr.abort();
285   - } catch(e) {};
286   - utils.detachEvent('unload', cleanup);
287   - }
288   - callback = xhr = null;
289   - };
290   -
291   - xhr.onreadystatechange = function (e) {
292   - if (xhr && callback) {
293   - callback(xhr, e);
294   - if (xhr && xhr.readyState === 4) {
295   - cleanup();
296   - }
297   - }
298   - };
299   - xhr.send(payload);
300   - utils.attachEvent('unload', cleanup);
301   - return function (abort_reason) {
302   - if (callback) {
303   - callback(xhr, null, abort_reason);
304   - cleanup();
305   - }
306   - };
307   -};
308   -
309   -var WPrefix = '_jp';
310   -
311   -utils.polluteGlobalNamespace = function() {
312   - if (!(WPrefix in _window)) {
313   - _window[WPrefix] = {};
314   - }
315   -};
316   -
317   -utils.createIframe = function (iframe_url, error_callback) {
318   - var iframe = _document.createElement('iframe');
319   - var tref;
320   - var unattach = function() {
321   - clearTimeout(tref);
322   - // Explorer had problems with that.
323   - try {iframe.onload = null;} catch (x) {}
324   - iframe.onerror = null;
325   - };
326   - var cleanup = function() {
327   - if (iframe) {
328   - unattach();
329   - iframe.parentNode.removeChild(iframe);
330   - iframe.src = "about:blank";
331   - iframe = null;
332   - utils.detachEvent('unload', cleanup);
333   - }
334   - };
335   - var onerror = function(r) {
336   - if (iframe) {
337   - cleanup();
338   - error_callback(r);
339   - }
340   - };
341   - iframe.src = iframe_url;
342   - iframe.style.display = 'none';
343   - iframe.style.position = 'absolute';
344   - iframe.onerror = function(){onerror('onerror');};
345   - iframe.onload = function() {
346   - // `onload` is triggered before scripts on the iframe are
347   - // executed. Give it few seconds to actually load stuff.
348   - clearTimeout(tref);
349   - tref = setTimeout(function(){onerror('onload timeout');}, 2000);
350   - };
351   - _document.body.appendChild(iframe);
352   - tref = setTimeout(function(){onerror('timeout');}, 5000);
353   - utils.attachEvent('unload', cleanup);
354   - return {
355   - iframe: iframe,
356   - cleanup: cleanup,
357   - loaded: unattach
358   - };
359   -};
360   -
361   -utils.createHtmlfile = function (iframe_url, error_callback) {
362   - var doc = new ActiveXObject('htmlfile');
363   - var tref;
364   - var iframe;
365   - var unattach = function() {
366   - clearTimeout(tref);
367   - };
368   - var cleanup = function() {
369   - if (doc) {
370   - unattach();
371   - utils.detachEvent('unload', cleanup);
372   - try {
373   - iframe.src = "about:blank";
374   - } catch (x) {}
375   - iframe.parentNode.removeChild(iframe);
376   - iframe = doc = null;
377   - CollectGarbage();
378   - }
379   - };
380   - var onerror = function(r) {
381   - if (doc) {
382   - cleanup();
383   - error_callback(r);
384   - }
385   - };
386   -
387   - doc.open();
388   - doc.write('<html><script>' +
389   - 'document.domain="' + document.domain + '";' +
390   - '</script></html>');
391   - doc.close();
392   - doc.parentWindow[WPrefix] = _window[WPrefix];
393   - var c = doc.createElement('div');
394   - doc.body.appendChild(c);
395   - iframe = doc.createElement('iframe');
396   - c.appendChild(iframe);
397   - iframe.src = iframe_url;
398   - tref = setTimeout(function(){onerror('timeout');}, 5000);
399   - utils.attachEvent('unload', cleanup);
400   - return {
401   - iframe: iframe,
402   - cleanup: cleanup,
403   - loaded: unattach
404   - };
405   -};
406   -
407   -utils.closeFrame = function (code, reason) {
408   - return 'c'+JSON.stringify([code, reason]);
409   -};
410   -
411   -utils.userSetCode = function (code) {
412   - return code === 1000 || (code >= 3000 && code <= 4999);
413   -};
414   -
415   -utils.log = function() {
416   - if (_window.console && console.log && console.log.apply) {
417   - console.log.apply(console, arguments);
418   - }
419   -};
420   -
421   -utils.bind = function(fun, that) {
422   - if (fun.bind) {
423   - return fun.bind(that);
424   - } else {
425   - return function() {
426   - return fun.apply(that, arguments);
427   - };
428   - }
429   -};
430   -
431   -utils.amendUrl = function(url) {
432   - var dl = _document.location;
433   - if (!url) {
434   - throw new Error('Wrong url for SockJS');
435   - }
436   - // '//abc' --> 'http://abc'
437   - if (url.indexOf('//') === 0) {
438   - url = dl.protocol + url;
439   - }
440   - // '/abc' --> 'http://localhost:80/abc'
441   - if (url.indexOf('/') === 0) {
442   - url = dl.protocol + '//' + dl.host + url;
443   - }
444   - // strip trailing slashes
445   - url = url.replace(/[/]+$/,'');
446   - return url;
447   -};
448   -
449   -// IE doesn't support [].indexOf.
450   -utils.arrIndexOf = function(arr, obj){
451   - for(var i=0; i < arr.length; i++){
452   - if(arr[i] === obj){
453   - return i;
454   - }
455   - }
456   - return -1;
457   -};
458   -
459   -utils.delay = function(t, fun) {
460   - if(typeof t === 'function') {
461   - fun = t;
462   - t = 0;
463   - }
464   - return setTimeout(fun, t);
465   -};
466   -// [*] End of lib/utils.js
467   -
468   -
469   -// [*] Including lib/sockjs.js
470   -var SockJS = function(url, protocols, options) {
471   - var that = this;
472   - that._options = {devel: false, debug: false, chunking: undefined};
473   - if (options) {
474   - utils.objectExtend(that._options, options);
475   - }
476   - that._base_url = utils.amendUrl(url);
477   - that._server = that._options.server || utils.random_number_string(1000);
478   - that._connid = utils.random_string(8);
479   - that._trans_url = that._base_url + '/' + that._server + '/' + that._connid;
480   - that._protocols = ['websocket',
481   - 'xhr-streaming',
482   - 'iframe-eventsource',
483   - 'iframe-htmlfile',
484   - 'xhr-polling',
485   - 'iframe-xhr-polling',
486   - 'jsonp-polling'];
487   - switch(typeof protocols) {
488   - case 'undefined': break;
489   - case 'string': that._protocols = [protocols]; break;
490   - default: that._protocols = protocols; break;
491   - }
492   - that.protocol = null;
493   - that.readyState = SockJS.CONNECTING;
494   - that._didClose();
495   -};
496   -// Inheritance
497   -SockJS.prototype = new REventTarget();
498   -
499   -SockJS.version = "0.1.1.dirty";
500   -
501   -SockJS.CONNECTING = 0;
502   -SockJS.OPEN = 1;
503   -SockJS.CLOSING = 2;
504   -SockJS.CLOSED = 3;
505   -
506   -SockJS.prototype._debug = function() {
507   - if (this._options.debug)
508   - utils.log.apply(utils, arguments);
509   -};
510   -
511   -SockJS.prototype._dispatchOpen = function() {
512   - var that = this;
513   - if (that.readyState === SockJS.CONNECTING) {
514   - if (that._transport_tref) {
515   - clearTimeout(that._transport_tref);
516   - that._transport_tref = null;
517   - }
518   - that.readyState = SockJS.OPEN;
519   - that.dispatchEvent(new SimpleEvent("open"));
520   - } else {
521   - // The server might have been restarted, and lost track of our
522   - // connection.
523   - that._didClose(1006, "Server lost session");
524   - }
525   -};
526   -
527   -SockJS.prototype._dispatchMessage = function(data) {
528   - var that = this;
529   - if (that.readyState !== SockJS.OPEN)
530   - return;
531   - that.dispatchEvent(new SimpleEvent("message", {data: data}));
532   -};
533   -
534   -SockJS.prototype._dispatchHeartbeat = function(data) {
535   - var that = this;
536   - if (that.readyState !== SockJS.OPEN)
537   - return;
538   - that.dispatchEvent(new SimpleEvent('heartbeat', {}));
539   -};
540   -
541   -SockJS.prototype._didClose = function(code, reason) {
542   - var that = this;
543   - if (that.readyState !== SockJS.CONNECTING &&
544   - that.readyState !== SockJS.OPEN &&
545   - that.readyState !== SockJS.CLOSING)
546   - throw new Error('INVALID_STATE_ERR');
547   - if (that._transport)
548   - that._transport.doCleanup();
549   - that._transport = null;
550   - if (that._transport_tref) {
551   - clearTimeout(that._transport_tref);
552   - that._transport_tref = null;
553   - }
554   - var close_event = new SimpleEvent("close", {code: code,
555   - reason: reason,
556   - wasClean: utils.userSetCode(code)});
557   -
558   - if (!utils.userSetCode(code) && that.readyState === SockJS.CONNECTING) {
559   - if (that._try_next_protocol(close_event)) {
560   - that._transport_tref = setTimeout(
561   - function() {
562   - if (that.readyState === SockJS.CONNECTING) {
563   - // I can't understand how it is possible to run
564   - // this timer, when the state is CLOSED, but
565   - // apparently in IE everythin is possible.
566   - that._didClose(2007,
567   - "Transport timeouted");
568   - }
569   - }, 5001);
570   - return;
571   - }
572   - close_event = new SimpleEvent("close", {code: 2000,
573   - reason: "All transports failed",
574   - wasClean: false,
575   - last_event: close_event});
576   - }
577   - that.readyState = SockJS.CLOSED;
578   -
579   - utils.delay(function() {
580   - that.dispatchEvent(close_event);
581   - });
582   -};
583   -
584   -SockJS.prototype._didMessage = function(data) {
585   - var that = this;
586   - var type = data.slice(0, 1);
587   - switch(type) {
588   - case 'o':
589   - that._dispatchOpen();
590   - break;
591   - case 'a':
592   - var payload = JSON.parse(data.slice(1) || '[]');
593   - for(var i=0; i < payload.length; i++){
594   - that._dispatchMessage(payload[i]);
595   - }
596   - break;
597   - case 'm':
598   - var payload = JSON.parse(data.slice(1) || 'null');
599   - that._dispatchMessage(payload);
600   - break;
601   - case 'c':
602   - var payload = JSON.parse(data.slice(1) || '[]');
603   - that._didClose(payload[0], payload[1]);
604   - break;
605   - case 'h':
606   - that._dispatchHeartbeat();
607   - break;
608   - }
609   -};
610   -
611   -SockJS.prototype._try_next_protocol = function(close_event) {
612   - var that = this;
613   - if (that.protocol) {
614   - that._debug('Closed transport:', that.protocol, ''+close_event);
615   - that.protocol = null;
616   - }
617   -
618   - while(1) {
619   - var protocol = that.protocol = that._protocols.shift();
620   - if (!protocol) {
621   - return false;
622   - }
623   - // Some protocols require chunking, we may need to run the
624   - // test beforehand.
625   - if (SockJS[protocol] &&
626   - SockJS[protocol].need_chunking === true &&
627   - that._options.chunking === undefined) {
628   - that._protocols.unshift(protocol);
629   - that.protocol = 'chunking-test';
630   - // Assert false, in case test timeouts.
631   - that._options.chunking = false;
632   - chunkingTest(that._base_url, function(chunking) {
633   - that._options.chunking = chunking;
634   - that._try_next_protocol();
635   - }, that._options);
636   - return true;
637   - }
638   -
639   - if (!SockJS[protocol] ||
640   - (SockJS[protocol].need_chunking === true &&
641   - that._options.chunking !== true) ||
642   - !SockJS[protocol].enabled(that._options)) {
643   - that._debug('Skipping transport:', protocol);
644   - } else {
645   - that._debug('Opening transport:', protocol);
646   - that._transport = new SockJS[protocol](that, that._trans_url,
647   - that._base_url);
648   - return true;
649   - }
650   - }
651   -};
652   -
653   -SockJS.prototype.close = function(code, reason) {
654   - var that = this;
655   - if (code && !utils.userSetCode(code))
656   - throw new Error("INVALID_ACCESS_ERR");
657   - if(that.readyState !== SockJS.CONNECTING &&
658   - that.readyState !== SockJS.OPEN) {
659   - return false;
660   - }
661   - that.readyState = SockJS.CLOSING;
662   - that._didClose(code || 1000, reason || "Normal closure");
663   - return true;
664   -};
665   -
666   -SockJS.prototype.send = function(data) {
667   - var that = this;
668   - if (that.readyState === SockJS.CONNECTING)
669   - throw new Error('INVALID_STATE_ERR');