packages feed

wai-cors 0.2.2 → 0.2.3

raw patch · 11 files changed

+628/−92 lines, 11 filesdep +directorydep +filepathdep +networkdep ~base-unicode-symbolsdep ~http-typesdep ~transformers

Dependencies added: directory, filepath, network, process, text, wai-cors, wai-websockets, warp, websockets

Dependency ranges changed: base-unicode-symbols, http-types, transformers, wai

Files

CHANGELOG.md view
@@ -1,3 +1,16 @@+0.2.3+=====++*   Added a test-suite to the package that uses PhantomJS to simulate a+    browser client.++*   Pass on websocket requests unchanged to the application. Add documentation+    that reminds the application author to check the `Origin` header for+    websocket requests.++*   Move development source repository from https://github.com/alephcloud/wai-cors+    to https://github.com/larskuhtz/wai-cors.+ 0.2.2 ===== 
README.md view
@@ -1,21 +1,53 @@-[![Build Status](https://travis-ci.org/alephcloud/wai-cors.svg?branch=master)](https://travis-ci.org/alephcloud/wai-cors)+[![Build Status](https://travis-ci.org/larskuhtz/wai-cors.svg?branch=master)](https://travis-ci.org/larskuhtz/wai-cors)  Cross-Origin Resource Sharing (CORS) For Wai ============================================ -This package provides a Haskell implemenation of CORS for+This package provides a Haskell implementation of CORS for [WAI](http://hackage.haskell.org/package/wai) that aims to be compliant with [http://www.w3.org/TR/cors](http://www.w3.org/TR/cors). +Note On Security+----------------++This implementation doesn't include any server side enforcement. By complying+with the CORS standard it enables the client (i.e. the web browser) to enforce+the CORS policy. For application authors it is strongly recommended to take+into account the security considerations in section 6.3 of the+[CORS standard](http://wwww.w3.org/TR/cors). In particular the application should+check that the value of the `Origin` header matches the expectations.++Websocket connections don't support CORS and are ignored by the CORS implementation+in this package. However Websocket requests usually (at least for some+browsers) include the @Origin@ header. Applications are expected to check the+value of this header and respond with an error in case that its content doesn't+match the expectations.++Installation+------------++Assuming the availability of recent versions of+[GHC](https://www.haskell.org/ghc/) and [cabal](https://www.haskell.org/cabal/)+this package is installed via++```.bash+cabal update+cabal install wai-cors+```+ Usage ----- -The file `test/server.hs` shows how to support simple cross-origin requests (as+The function 'simpleCors' enables support of simple cross-origin requests. More+advanced CORS policies can be enabled by passing a 'CorsResourcePolicy' to the+'cors' middleware.++The file `examples/Scotty.hs` shows how to support simple cross-origin requests (as defined in [http://www.w3.org/TR/cors](http://www.w3.org/TR/cors)) in a [scotty](http://hackage.haskell.org/package/scotty) application. -~~~{.haskell}+```.haskell {-# LANGUAGE UnicodeSyntax #-} {-# LANGUAGE OverloadedStrings #-} @@ -30,30 +62,40 @@ main = scotty 8080 $ do     middleware simpleCors     matchAny  "/" $ text "Success"-~~~+``` -The result of following curl command will include the HTTP response +The result of following curl command will include the HTTP response header `Access-Control-Allow-Origin: *`. -~~~{.bash}+```.bash curl -i http://127.0.0.1:8888 -H 'Origin: 127.0.0.1' -v-~~~+```  Documentation for more general usage can be found in the module [Network.Wai.Middleware.Cors](http://hackage.haskell.org/package/wai-cors/docs/Network-Wai-Middleware-Cors.html). -TEST+Test ---- -Currently there is only basic support to test simple cross-origin-request from a browser.+In order to run the automated test suite [PhantomJS](http://phantomjs.org/) (at+least version 2.0) must be installed in the system. -Start server:+```.bash+cabal install --only-dependencies --enable-tests+cabal test --show-details=streaming+``` -~~~{.bash}+If [PhantomJS](http://phantomjs.org/) is not available the tests can be+exectued manually in a modern web-browser as follows.++Start the server application:++```.bash cd test-runHaskell server.hs-~~~+ghc -main-is Server Server.hs+./Server+``` -Open the file `test/index.html` in a modern web-browser in order to run some-simple tests.+Open the file `test/index.html` in a modern web-browser. On page load a Javascript+script is exectued that runs the test suite and prints the result on the page.+
constraints view
@@ -1,11 +1,12 @@ constraints: array ==0.5.1.0,-             attoparsec ==0.12.1.5,+             attoparsec ==0.13.0.0,              base ==4.8.0.0,              base-unicode-symbols ==0.2.2.4,+             binary ==0.7.3.0,              blaze-builder ==0.4.0.1,              bytestring ==0.10.6.0,              case-insensitive ==1.2.0.4,-             charset ==0.3.7,+             charset ==0.3.7.1,              containers ==0.5.6.2,              deepseq ==1.4.1.1,              ghc-prim ==0.4.0.0,@@ -14,13 +15,13 @@              integer-gmp ==1.0.0.0,              mtl ==2.2.1,              nats ==1,-             network ==2.6.0.2,+             network ==2.6.1.0,              parsec ==3.1.9,-             parsers ==0.12.1.1,+             parsers ==0.12.2.1,              rts ==1.0,              scientific ==0.3.3.8,              semigroups ==0.16.2.2,-             text ==1.2.0.4,+             text ==1.2.1.0,              time ==1.5.0.1,              transformers ==0.4.2.0,              unix ==2.7.1.0,
+ examples/Scotty.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE UnicodeSyntax #-}+{-# LANGUAGE OverloadedStrings #-}++-- |+-- Module: Main+-- Description: Exampe for using wai-cors with scotty+-- Copyright: © 2015 Lars Kuhtz <lakuhtz@gmail.com>+-- License: MIT+-- Maintainer: Lars Kuhtz <lakuhtz@gmail.com>+-- Stability: experimental+--+module Main+( main+) where++import Network.Wai.Middleware.Cors+import Web.Scotty++main ∷ IO ()+main = scotty 8080 $ do+    middleware simpleCors+    matchAny  "/" $ text "Success"+
src/Network/Wai/Middleware/Cors.hs view
@@ -1,4 +1,5 @@ -- ------------------------------------------------------ --+-- Copyright © 2015 Lars Kuhtz <lakuhtz@gmail.com> -- Copyright © 2014 AlephCloud Systems, Inc. -- ------------------------------------------------------ -- @@ -14,8 +15,29 @@ -- | An implemenation of Cross-Origin resource sharing (CORS) for WAI that -- aims to be compliant with <http://www.w3.org/TR/cors>. ----- The function 'simpleCors' enables support of simple cross-origin requests.+-- The function 'simpleCors' enables support of simple cross-origin requests. More+-- advanced CORS policies can be enabled by passing a 'CorsResourcePolicy' to the+-- 'cors' middleware. --+-- = Note On Security+--+-- This implementation doens't include any server side enforcement. By+-- complying with the CORS standard it enables the client (i.e. the web+-- browser) to enforce the CORS policy. For application authors it is strongly+-- recommended to take into account the security considerations in section 6.3+-- of <http://wwww.w3.org/TR/cors>. In particular the application should check+-- that the value of the @Origin@ header matches it's expectations.+--+-- = Websockets+--+-- Websocket connections don't support CORS and are ignored by this CORS+-- implementation. However Websocket requests usually (at least for some+-- browsers) include the @Origin@ header. Applications are expected to check+-- the value of this header and respond with an error in case that its content+-- doesn't match the expectations.+--+-- = Example+-- -- The following is an example how to enable support for simple cross-origin requests -- for a <http://hackage.haskell.org/package/scotty scotty> application. --@@ -58,6 +80,10 @@ #define MIN_VESION_base(x,y,z) 1 #endif +#ifndef MIN_VESION_wai+#define MIN_VESION_wai(x,y,z) 1+#endif+ #if ! MIN_VERSION_base(4,8,0) import Control.Applicative #endif@@ -147,7 +173,7 @@     -- If this is 'False' and the request does not include an @Origin@ header     -- the request is passed on unchanged to the application.     ---    -- /since version 0.2/+    -- @since 0.2     , corsRequireOrigin ∷ !Bool      -- | In the case that@@ -165,7 +191,7 @@     -- * an response with HTTP status 400 (bad request) and short     --   error message is returned if this field is 'False'.     ---    -- /since version 0.2/+    -- @since 0.2     --     , corsIgnoreFailures ∷ !Bool     }@@ -273,6 +299,9 @@ #else cors policyPattern app r #endif+    -- We don't handle websockets, even if they include an @Origin@ header+    | isWebSocketsReq r = runApp+     | Just policy ← policyPattern r = case hdrOrigin of          -- No origin header: requect request@@ -519,4 +548,12 @@     ∷ B8.ByteString -- ^ body     → WAI.Response corsFailure msg = WAI.responseLBS HTTP.status400 [("Content-Type", "text/html; charset-utf-8")] (LB8.fromStrict msg)++-- Copied from the [wai-websocket package](https://github.com/yesodweb/wai/blob/master/wai-websockets/Network/Wai/Handler/WebSockets.hs#L21)+--+isWebSocketsReq+    ∷ WAI.Request+    → Bool+isWebSocketsReq req =+    fmap CI.mk (lookup "upgrade" $ WAI.requestHeaders req) == Just "websocket" 
+ test/PhantomJS.hs view
@@ -0,0 +1,70 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UnicodeSyntax #-}++-- |+-- Module: Main+-- Description: Test suite that uses PhantomJS to simulate a browser+-- Copyright: © 2015 Lars Kuhtz <lakuhtz@gmail.com>+-- License: MIT+-- Maintainer: Lars Kuhtz <lakuhtz@gmail.com>+-- Stability: experimental+--+module Main+( main+) where++import Control.Concurrent++import Data.Monoid.Unicode+import Data.String++import System.Directory+import System.FilePath+import System.Exit+import System.IO+import System.Process++-- internal modules++import qualified Server as Server++phantomJsBinaryPath ∷ FilePath+phantomJsBinaryPath = "phantomjs"++phantomJsArgs ∷ IsString a ⇒ [a]+phantomJsArgs = ["--ignore-ssl-errors=true"]+--phantomJsArgs = ["--ignore-ssl-errors=true", "--debug=true"]++phantomJsScriptPath ∷ FilePath+phantomJsScriptPath = "test/phantomjs.js"++indexFilePath ∷ FilePath+indexFilePath = "test/index.html"++runPhantomJs ∷ IO ()+runPhantomJs = do++    -- check that phantomJS binary is available+    -- FIXME check the version+    findExecutable phantomJsBinaryPath >>= \case+        Nothing → do+            hPutStrLn stderr $ "Missing PhantomJS exectuable: in order to run this test-suite PhantomJS must be availabe on the system"+            exitFailure+        Just _ → return ()++    pwd ← getCurrentDirectory+    -- FIXME I consider it an API bug of the directory package that in order+    -- to get the exit code we have also capture stdout and stderr.+    (code, out, err) ← readProcessWithExitCode phantomJsBinaryPath (args pwd) ""+    hPutStrLn stdout out+    hPutStrLn stderr err+    exitWith code+  where+    args pwd = phantomJsArgs ⊕ [phantomJsScriptPath] ⊕ [pwd </> indexFilePath]++main ∷ IO ()+main = do+    _ ← forkIO $ Server.main+    runPhantomJs
+ test/Server.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UnicodeSyntax #-}++-- |+-- Module: Server+-- Description: Test HTTP server for wai-cors+-- Copyright: © 2015 Lars Kuhtz <lakuhtz@gmail.com>.+-- License: MIT+-- Maintainer: Lars Kuhtz <lakuhtz@gmail.com>+-- Stability: experimental+--+module Server+( main+) where++#ifndef MIN_VERSION_wai+#define MIN_VERSION_wai(a,b,c) 1+#endif++import Control.Concurrent+import Control.Exception+import Control.Monad++import Network.Socket (withSocketsDo)+import Network.Wai.Middleware.Cors+import qualified Network.HTTP.Types as HTTP+import qualified Network.Wai as WAI+import qualified Network.Wai.Handler.Warp as WARP+import qualified Network.Wai.Handler.WebSockets as WS+import qualified Network.WebSockets as WS++import qualified Data.Text as T++main ∷ IO ()+main = withSocketsDo $+    WARP.run 8080 $ simpleCors waiapp++waiapp ∷ WAI.Application+waiapp = WS.websocketsOr WS.defaultConnectionOptions wsserver $ \_ → app+  where+#if MIN_VERSION_wai(2,0,0)+    app respond = respond $ WAI.responseLBS HTTP.status200 [] "Success"+#else+    app = WAI.responseLBS HTTP.status200 [] "Success"+#endif++-- -------------------------------------------------------------------------- --+-- Websockets Server++wsserver ∷ WS.ServerApp+wsserver pc = do+    c ← WS.acceptRequest pc+    forever (go c) `catch` \case+        WS.CloseRequest _code _msg → WS.sendClose c ("closed" ∷ T.Text)+        e → throwIO e+  where+    go c = do+        msg ← WS.receiveDataMessage c+        forkIO $ WS.sendDataMessage c msg+
test/index.html view
@@ -1,64 +1,283 @@ <!DOCTYPE html>+<!-- Copyright (c) 2015 Lars Kuhtz <lakuhtz@gmail.coml> --> <html>     <head>      <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>     <script type="text/javascript">      var url = 'http://localhost:8080'-    var simpleTest = function (label, result, method, creds, contentType, headers) {+    var wsurl = 'ws://localhost:8080' -        var xhr = new XMLHttpRequest();-        xhr.open(method, url, true);+/* ************************************************************************** */+/* Utils */ -        var succ = function() {-            $('#results').append('<p style="color:green">' + label + ': success</p>')+    /* asynchronous array map function+     *+     * The function f is called on each element in the value and+     * is expected to return a promise object.+     *+     * asyncMap itself returns a promise object that is completed+     * when the promises for all elements completed. When all element+     * promises resolve the array promises is resolved. If at least+     * one element promise is reject the array promises is rejected.+     *+     * The done and fail callbacks are called with an object of the form+     *+     * { 'successes':successes, 'failures': failures, 'results': results }+     *+     * The results that array has one value for each element of the original+     * array. The values in the result array are objects of the form+     *+     * { success: trueOrFalse, result: elementPromiseResult }+     *+     * The progress callback is given an object of the form+     *+     * { successes: sumS, failures: numF, missing: numberOfMissingElementPromises }+     */+    var asyncMap = function (a,f) {+        var def = $.Deferred();+        var successes = 0;+        var failures = 0;+        var results = new Array(a.length);+        var c = a.length;++        a.map(function (val, idx, arr) {+            t = f(val);+            t.done(function(arg) {+                successes++;+                results[idx] = { 'success':true, 'result':arg };+            });+            t.fail(function(arg) {+                failures++;+                results[idx] = { 'success':false, 'result':arg };+            });+            t.always(function() {+                c--;+                if (c > 0) {+                    def.notify({ 'successes':successes, 'failures': failures, 'missing': c });+                } else {+                    var result = { 'successes':successes, 'failures': failures, 'results': results };+                    if (failures === 0) {+                        def.resolve(result);+                    } else {+                        def.reject(result);+                    };+                };+            });+        });+        return def.promise();+    };++    // monoid concatenation for asyncMap results+    var concatResults = function (r1, r2) {+        return {+            'successes': r1.successes + r2.successes,+            'failures': r1.failures + r2.failures,+            'results': r1.results.concat(r2.results)         };+    } -        var fail = function() {-            $('#results').append('<p style="color:red">' + label + ': failure</p>')+    // monoid identity for asyncMap results+    var emptyResult = { 'successes': 0, 'failures': 0, 'results':[] };++    // monad join for asyncMap results+    var flattenResults = function (r) {+        return r.results.reduce(function (p,c,i,a) { return concatResults(p,c.result); }, emptyResult);+    };++/* ************************************************************************** */+/* PhantomJS Integration */++    // Integration with PhantomJS+    //+    // call with: phantomjs --ignore-ssl-errors=true phantomjs.js $PWD/index.html+    //+    if (typeof window.callPhantom === 'function') {+        window.setTimeout(function () { window.callPhantom("timeout"); },3000);+    };++    var phantomJsCb = function (result) { +        if (typeof window.callPhantom === 'function') {+            window.callPhantom(result);         };+    }; -        xhr.onerror = result ? fail : succ;-        xhr.onload = result ? succ : fail;+/* ************************************************************************** */+/* Run Tests */ -        xhr.setRequestHeader('content-type', contentType);+    var runTest = function (params) { -        for (var i = 0; i < headers.length; ++i) {-            var e = headers[i];-            xhr.setRequestHeader(e[1], e[2]);+        var test = $.Deferred();+        var req = $.ajax(+            { 'method': params.method+            , 'url': url+            , 'contentType': params.contentType+            , 'crossDomain': true+            , 'headers': params.headers+            });++        var succ = function () { test.resolve(params); }+        var fail = function () { test.reject(params); }+        req.done(params.expectedResult ? succ : fail);+        req.fail(params.expectedResult ? fail : succ);++        return test.promise();+    }++    var pageTest = function (params) {+        test = runTest(params);++        test.done(function() {+            $('#results').append('<p style="color:green">' + params.label + ': success</p>');+        });++        test.fail(function() {+            $('#results').append('<p style="color:red">' + params.label + ': failure</p>');+        });+        return test;+    }++    var runAllPageTests = function (tests) {+        return asyncMap(tests, pageTest);+    }++/* ************************************************************************** */+/* WebSockets */++    /* a super simple ws service client+     *+     * TODO add proper callbacks (e.g. close, error);+     * TODO in case we use this for anything more complex we should add proper+     *      streaming by supporting chunking.+     *+     */+    var WsService = function (url, timeout) {+        var timeout = timeout ? timeout : 1000;+        var curMsgId = 0;+        var ready = $.Deferred();+        var websocket = new WebSocket(url);+        var newMsgId = function () { return ++curMsgId; };++        // maps message ids data to promise objects+        var msgMap = {}++        var ws = function (msg) {+            var p = $.Deferred();+            var req = { id: newMsgId(), msg: msg};+            ready.then(function () { websocket.send(JSON.stringify(req)); });+            msgMap[req.id] = p;+            setTimeout(function () { p.reject("timeout: no response from websocket after " + timeout + " milliseconds"); }, timeout);+            return p.promise();+        };++        websocket.onmessage = function (evt) {+            var resp = JSON.parse(evt.data);+            var prom = msgMap[resp.id];+            if (prom) {+                prom.resolve(resp.msg);+            } else {+                console.log("unexpected message from websocket: " + JSON.stringify(resp));+            };+        };++        var wserror = function (evt) {+            for (i in msgMap) {+                if (msgMap.hasOwnProperty(i)) {+                    msgMap[i].fail();+                };+            };+            msgMap = {};+        };++        websocket.onopen = function(evt) {+            ready.resolve();+        };++        websocket.onerror = function(evt) {+            console.log("websocket error: " + evt.data);+            wserror(evt);+        };++        websocket.onclose = function(evt) {+            console.log("websocket closed unexpectedly: " + evt.data);+            wserror(evt);+        };++        return ws;+    };++    var websocketTest = function () {++        var ws = WsService(wsurl);++        var wstest = function (msg) {+            var p = ws(msg);++            p.done(function() {+                $('#wsresults').append('<p style="color:green">' + msg + ': success</p>');+            });++            p.fail(function() {+                $('#wsresults').append('<p style="color:red">' + msg + ': failure</p>');+            });+            return p;         }-        xhr.send();++        return asyncMap(["msg 0", "msg 1", "msg 2"], wstest);+    };++/* ************************************************************************** */+/* Test Vectors */++    var mkTest = function (label, expectedResult, method, credentials, contentType, headers) {+        return {+            label: label,+            expectedResult: expectedResult,+            method: method,+            credentials: credentials,+            contentType: contentType,+            headers: headers+        };     } -    $(document).ready(function() {-        simpleTest("simple GET 1", true, "GET", false, 'text/plain', []);-        simpleTest("simple GET 2", true, "GET", false, 'application/x-www-form-urlencoded', []);-        simpleTest("simple GET 3", true, "GET", false, 'multipart/form-data', []);-        simpleTest("simple GET 4", false, "GET", false, 'application/json', []);-        simpleTest("simple GET 5", false, "GET", false, 'text/plain', [['abc','abc']]);+    var tests = [+        mkTest("simple GET 1", true, "GET", false, 'text/plain', []),+        mkTest("simple GET 2", true, "GET", false, 'application/x-www-form-urlencoded', []),+        mkTest("simple GET 3", true, "GET", false, 'multipart/form-data', []),+        mkTest("simple GET 4", false, "GET", false, 'application/json', []),+        mkTest("simple GET 5", false, "GET", false, 'text/plain', {'abc': 'abc'}), -        simpleTest("simple HEAD 1", true, "HEAD", false, 'text/plain', []);-        simpleTest("simple HEAD 2", true, "HEAD", false, 'application/x-www-form-urlencoded', []);-        simpleTest("simple HEAD 3", true, "HEAD", false, 'multipart/form-data', []);-        simpleTest("simple HEAD 4", false, "HEAD", false, 'application/json', []);-        simpleTest("simple HEAD 5", false, "HEAD", false, 'text/plain', [['abc','abc']]);+        mkTest("simple HEAD 1", true, "HEAD", false, 'text/plain', []),+        mkTest("simple HEAD 2", true, "HEAD", false, 'application/x-www-form-urlencoded', []),+        mkTest("simple HEAD 3", true, "HEAD", false, 'multipart/form-data', []),+        mkTest("simple HEAD 4", false, "HEAD", false, 'application/json', []),+        mkTest("simple HEAD 5", false, "HEAD", false, 'text/plain', {'abc': 'abc'}), -        simpleTest("simple POST 1", true, "POST", false, 'text/plain', []);-        simpleTest("simple POST 2", true, "POST", false, 'application/x-www-form-urlencoded', []);-        simpleTest("simple POST 3", true, "POST", false, 'multipart/form-data', []);-        simpleTest("simple POST 4", false, "POST", false, 'application/json', []);-        simpleTest("simple POST 5", false, "POST", false, 'test/plain', [['abc','abc']]);+        mkTest("simple POST 1", true, "POST", false, 'text/plain', []),+        mkTest("simple POST 2", true, "POST", false, 'application/x-www-form-urlencoded', []),+        mkTest("simple POST 3", true, "POST", false, 'multipart/form-data', []),+        mkTest("simple POST 4", false, "POST", false, 'application/json', []),+        mkTest("simple POST 5", false, "POST", false, 'test/plain', {'abc':'abc'}), -        simpleTest("simple PUT 1", false, "PUT", false, 'text/plain', []);-        simpleTest("simple DELETE 1", false, "DELETE", false, 'text/plain', []);-        simpleTest("simple PATCH 1", false, "PATCH", false, 'text/plain', []);+        mkTest("simple PUT 1", false, "PUT", false, 'text/plain', []),+        mkTest("simple DELETE 1", false, "DELETE", false, 'text/plain', []),+        mkTest("simple PATCH 1", false, "PATCH", false, 'text/plain', []),+    ];++    $(document).ready(function() {+        var p = runAllPageTests(tests);+        var q = websocketTest();+        var r = asyncMap([p,q], function (x) { return x; });+        r.always(function (x) { phantomJsCb(flattenResults(x)); });     });      </script>     </head>     <body>         <h1>Results</h1>-        <div id="results"/>+        <div id="results"></div>+        <h1>Websocket Test Results</h1>+        <div id="wsresults"></div>     </body> </html> 
+ test/phantomjs.js view
@@ -0,0 +1,39 @@+/*+ * Copyright (c) 2015 Lars Kuhtz <lakuhtz@gmail.com>+ */++var page = require('webpage').create();+var system = require('system');+var index = null;++if (system.args.length === 1) {+    index = 'https://rawgit.com/alephcloud/wai-cors/master/test/index.html'+} else if (system.args.length === 2) {+    index = 'file://' + system.args[1];+} else {+    console.log('Usage:');+    console.log('   phantomjs phanomjs.js <absolute-index-file-path>');+    console.log('   phantomjs phanomjs.js');+    phantom.exit(3);+}++page.open(index, function(status) {+  console.log("Status: " + status);+  if(status === "success") {+        console.log("test page loaded successfully");+  } else {+      console.log("failed to load test page");+      phantom.exit(2);+  }+});++page.onCallback = function(data) {+    if (data.failures > 0) {+        console.log("Some tests failed");+        console.log( page.plainText );+        phantom.exit(1);+    } else {+        console.log( page.plainText );+        phantom.exit(0);+    }+}
− test/server.hs
@@ -1,19 +0,0 @@--- ------------------------------------------------------ ----- Copyright © 2014 AlephCloud Systems, Inc.--- ------------------------------------------------------ ----{-# LANGUAGE UnicodeSyntax #-}-{-# LANGUAGE OverloadedStrings #-}--module Main-( main-) where--import Network.Wai.Middleware.Cors-import Web.Scotty--main ∷ IO ()-main = scotty 8080 $ do-    middleware simpleCors-    matchAny  "/" $ text "Success"-
wai-cors.cabal view
@@ -1,9 +1,10 @@ -- ------------------------------------------------------ --+-- Copyright © 2015 Lars Kuhtz <lakuhtz@gmail.com> -- Copyright © 2014 AlephCloud Systems, Inc. -- ------------------------------------------------------ --  Name: wai-cors-Version: 0.2.2+Version: 0.2.3 Synopsis: CORS for WAI  Description:@@ -12,13 +13,13 @@     <http://hackage.haskell.org/package/wai Wai>     that aims to be compliant with <http://www.w3.org/TR/cors>. -Homepage: https://github.com/alephcloud/wai-cors-Bug-reports: https://github.com/alephcloud/wai-cors/issues+Homepage: https://github.com/larskuhtz/wai-cors+Bug-reports: https://github.com/larskuhtz/wai-cors/issues License: MIT License-file: LICENSE-Author: Lars Kuhtz <lars@alephcloud.com>-Maintainer: Lars Kuhtz <lars@alephcloud.com>-Copyright: Copyright (c) 2014 AlephCloud Systems, Inc.+Author: Lars Kuhtz <lakuhtz@gmail.com>+Maintainer: Lars Kuhtz <lakuhtz@gmail.com>+Copyright: (c) 2015 Lars Kuhtz <lakuhtz@gmail.com>, (c) 2014 AlephCloud Systems, Inc. Category: Web Build-type: Simple Cabal-version: >= 1.14.0@@ -28,16 +29,18 @@     CHANGELOG.md     constraints     test/index.html-    test/server.hs+    test/phantomjs.js+    examples/Scotty.hs  source-repository head     type: git-    location: https://github.com/alephcloud/wai-cors+    location: https://github.com/larskuhtz/wai-cors+    branch: master  source-repository this     type: git-    location: https://github.com/alephcloud/wai-cors-    tag: 0.2.2+    location: https://github.com/larskuhtz/wai-cors+    tag: 0.2.3  flag transformers-3     description: use transformers<0.3 (this is set automatically)@@ -49,6 +52,11 @@     default: False     manual: False +flag wai-2+    description: use version wai<3 (this is set automatically)+    default: False+    manual: False+ Library     default-language: Haskell2010     hs-source-dirs: src@@ -69,10 +77,14 @@     if flag (wai-1)         build-depends:             wai < 2.0,-            resourcet >= 0.4-    else+            resourcet >= 0.4,+            transformers < 0.4+    if flag (wai-2)         build-depends:-            wai >= 2.0+            wai < 3.0 && >= 2.0+    if ! flag (wai-1) && ! flag (wai-2)+        build-depends:+            wai >= 3.0      if flag(transformers-3)         build-depends:@@ -82,5 +94,41 @@     else         build-depends:             mtl >= 2.2,-            transformers >= 0.4+            transformers >= 0.4,+            wai >= 2++    ghc-options: -Wall++test-suite phantomjs+    type: exitcode-stdio-1.0+    default-language: Haskell2010+    main-is: PhantomJS.hs+    hs-source-dirs: test++    other-modules:+        Server++    build-depends:+        base == 4.*,+        base-unicode-symbols >= 0.2,+        directory >= 1.2,+        filepath >= 1.4,+        http-types >= 0.8,+        network >= 2.6,+        process >= 1.2,+        text >= 1.2,+        wai-cors++    -- we only build the tests suite with the most recent wai version+    -- cabal fails to resolves the old dependencies otherwise.+    if flag (wai-1) || flag (wai-2)+        buildable: False+    else+        build-depends:+            wai-websockets >= 3.0,+            warp >= 3.0,+            wai >= 3.0,+            websockets >= 0.9++    ghc-options: -threaded -Wall -with-rtsopts=-N