diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06ed895..8f1e231 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,24 +12,34 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: purescript-contrib/setup-purescript@main + - name: Set up a PureScript toolchain + uses: purescript-contrib/setup-purescript@main with: purescript: "unstable" + purs-tidy: "0.9.0" - - uses: actions/setup-node@v2 + - name: Cache PureScript dependencies + uses: actions/cache@v2 with: - node-version: "14" - + key: ${{ runner.os }}-spago-${{ hashFiles('**/*.dhall') }} + path: | + .spago + output - name: Install dependencies - run: | - npm install -g bower - npm install - bower install --production + run: spago install - name: Build source - run: npm run-script build + run: spago build --no-install --purs-args '--censor-lib --strict' + + - name: Install test dependencies + run: spago -x spago.dev.dhall install + + - name: Build tests + run: spago -x spago.dev.dhall build --no-install --purs-args '--censor-lib --strict' - name: Run tests - run: | - bower install - npm run-script test --if-present + run: spago -x spago.dev.dhall test --no-install + + - name: Check formatting + run: purs-tidy check src test + diff --git a/.gitignore b/.gitignore index b846b63..7b09146 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /node_modules/ /output/ package-lock.json +generated-docs/ diff --git a/CHANGELOG.md b/CHANGELOG.md index ce00c0d..32b8643 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Breaking changes: New features: +- Use __spec__ for tests. Upgraded `ci.yml`. Solves #35. (#47 by @jamesdbrock) +- New function `HTTP.onRequest`. Solves #46. (#47 by @jamesdbrock) + Bugfixes: Other improvements: @@ -32,7 +35,7 @@ New features: Other improvements: - Migrated CI to GitHub Actions, updated installation instructions to use Spago, and migrated from `jshint` to `eslint` (#30) - Added a changelog and pull request template (#34) - + ## [v5.0.2](https://github.com/purescript-node/purescript-node-http/releases/tag/v5.0.2) - 2019-07-24 - Relaxed upper bounds on `node-buffer` diff --git a/README.md b/README.md index b22d1fc..13f50a7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [](https://github.com/purescript-node/purescript-node-http/actions?query=workflow%3ACI+branch%3Amaster) [](https://pursuit.purescript.org/packages/purescript-node-http) -A wrapper for Node's HTTP APIs. +A wrapper for Node’s [HTTP](https://nodejs.org/docs/latest/api/http.html) API. ## Installation @@ -12,6 +12,14 @@ A wrapper for Node's HTTP APIs. spago install node-http ``` + +## Test + +``` +spago -x spago.dev.dhall test +``` + ## Documentation Module documentation is [published on Pursuit](http://pursuit.purescript.org/packages/purescript-node-http). + diff --git a/bower.json b/bower.json index 9de0635..6d545c9 100644 --- a/bower.json +++ b/bower.json @@ -1,33 +1,32 @@ { - "name": "purescript-node-http", - "license": "MIT", - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "output" - ], - "repository": { - "type": "git", - "url": "https://github.com/purescript-node/purescript-node-http.git" - }, - "devDependencies": { - "purescript-console": "^6.0.0" - }, - "dependencies": { - "purescript-arraybuffer-types": "^3.0.2", - "purescript-contravariant": "^6.0.0", - "purescript-effect": "^4.0.0", - "purescript-foreign": "^7.0.0", - "purescript-foreign-object": "^4.0.0", - "purescript-maybe": "^6.0.0", - "purescript-node-buffer": "^8.0.0", - "purescript-node-net": "^4.0.0", - "purescript-node-streams": "^7.0.0", - "purescript-node-url": "^6.0.0", - "purescript-nullable": "^6.0.0", - "purescript-options": "^7.0.0", - "purescript-prelude": "^6.0.0", - "purescript-unsafe-coerce": "^6.0.0" - } + "name": "purescript-node-http", + "license": [ + "MIT" + ], + "repository": { + "type": "git", + "url": "https://github.com/purescript-node/purescript-node-http" + }, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "output" + ], + "dependencies": { + "purescript-arraybuffer-types": "^v3.0.2", + "purescript-contravariant": "^v6.0.0", + "purescript-effect": "^v4.0.0", + "purescript-foreign": "^v7.0.0", + "purescript-foreign-object": "^v4.1.0", + "purescript-maybe": "^v6.0.0", + "purescript-node-buffer": "^v8.0.0", + "purescript-node-net": "^v4.0.0", + "purescript-node-streams": "^v7.0.0", + "purescript-node-url": "^v6.0.0", + "purescript-nullable": "^v6.0.0", + "purescript-options": "^v7.0.0", + "purescript-prelude": "^v6.0.1", + "purescript-unsafe-coerce": "^v6.0.0" + } } diff --git a/packages.dhall b/packages.dhall new file mode 100644 index 0000000..2ffa9a7 --- /dev/null +++ b/packages.dhall @@ -0,0 +1,5 @@ +let upstream = + https://github.com/purescript/package-sets/releases/download/psc-0.15.4-20221102/packages.dhall + sha256:8628e413718876ce26983db1d0ce9d9e1588129117fa3bb8ed9f618db6914127 + +in upstream diff --git a/spago.dev.dhall b/spago.dev.dhall new file mode 100644 index 0000000..ef45499 --- /dev/null +++ b/spago.dev.dhall @@ -0,0 +1,19 @@ +-- Spago configuration for testing. + +let conf = ./spago.dhall + +in conf // +{ sources = [ "src/**/*.purs", "test/**/*.purs" ] +, dependencies = conf.dependencies # + [ "aff" + , "console" + , "either" + , "exceptions" + , "foldable-traversable" + , "node-process" + , "parallel" + , "partial" + , "spec" + , "tuples" + ] +} diff --git a/spago.dhall b/spago.dhall new file mode 100644 index 0000000..8c55a42 --- /dev/null +++ b/spago.dhall @@ -0,0 +1,22 @@ +{ name = "node-http" +, dependencies = + [ "arraybuffer-types" + , "contravariant" + , "effect" + , "foreign" + , "foreign-object" + , "maybe" + , "node-buffer" + , "node-net" + , "node-streams" + , "node-url" + , "nullable" + , "options" + , "prelude" + , "unsafe-coerce" + ] +, packages = ./packages.dhall +, sources = [ "src/**/*.purs" ] +, license = "MIT" +, repository = "https://github.com/purescript-node/purescript-node-http" +} diff --git a/src/Node/HTTP.js b/src/Node/HTTP.js index 85881a4..357f03f 100644 --- a/src/Node/HTTP.js +++ b/src/Node/HTTP.js @@ -64,6 +64,16 @@ export function onUpgrade(server) { }; } +export function onRequest(server) { + return function (cb) { + return function () { + server.on("request", function (req, res) { + return cb(req)(res)(); + }); + }; + }; +} + export function setHeader(res) { return function (key) { return function (value) { diff --git a/src/Node/HTTP.purs b/src/Node/HTTP.purs index 589281b..c5b8fa5 100644 --- a/src/Node/HTTP.purs +++ b/src/Node/HTTP.purs @@ -12,6 +12,7 @@ module Node.HTTP , listenSocket , onConnect , onUpgrade + , onRequest , httpVersion , requestHeaders @@ -77,6 +78,9 @@ foreign import onConnect :: Server -> (Request -> Socket -> Buffer -> Effect Uni -- | Listen to `upgrade` events on the server foreign import onUpgrade :: Server -> (Request -> Socket -> Buffer -> Effect Unit) -> Effect Unit +-- | Listen to `request` events on the server +foreign import onRequest :: Server -> (Request -> Response -> Effect Unit) -> Effect Unit + -- | Get the request HTTP version httpVersion :: Request -> String httpVersion = _.httpVersion <<< unsafeCoerce diff --git a/src/Node/HTTP/Secure.purs b/src/Node/HTTP/Secure.purs index 79c3ec7..28bde54 100644 --- a/src/Node/HTTP/Secure.purs +++ b/src/Node/HTTP/Secure.purs @@ -90,16 +90,17 @@ import Unsafe.Coerce (unsafeCoerce) -- | Create an HTTPS server, given the SSL options and a function to be executed -- | when a request is received. -foreign import createServerImpl :: - Foreign -> - (Request -> Response -> Effect Unit) -> - Effect Server +foreign import createServerImpl + :: Foreign + -> (Request -> Response -> Effect Unit) + -> Effect Server -- | Create an HTTPS server, given the SSL options and a function to be executed -- | when a request is received. -createServer :: Options SSLOptions -> - (Request -> Response -> Effect Unit) -> - Effect Server +createServer + :: Options SSLOptions + -> (Request -> Response -> Effect Unit) + -> Effect Server createServer = createServerImpl <<< options -- | The type of HTTPS server options @@ -120,16 +121,22 @@ rejectUnauthorized = opt "rejectUnauthorized" -- | The npnProtocols option can be a String, a Buffer, a Uint8Array, or an -- | array of any of those types. data NPNProtocols + npnProtocolsString :: String -> NPNProtocols npnProtocolsString = unsafeCoerce + npnProtocolsBuffer :: Buffer -> NPNProtocols npnProtocolsBuffer = unsafeCoerce + npnProtocolsUint8Array :: Uint8Array -> NPNProtocols npnProtocolsUint8Array = unsafeCoerce + npnProtocolsStringArray :: Array String -> NPNProtocols npnProtocolsStringArray = unsafeCoerce + npnProtocolsBufferArray :: Array Buffer -> NPNProtocols npnProtocolsBufferArray = unsafeCoerce + npnProtocolsUint8ArrayArray :: Array Uint8Array -> NPNProtocols npnProtocolsUint8ArrayArray = unsafeCoerce @@ -140,16 +147,22 @@ npnProtocols = opt "NPNProtocols" -- | The alpnProtocols option can be a String, a Buffer, a Uint8Array, or an -- | array of any of those types. data ALPNProtocols + alpnProtocolsString :: String -> ALPNProtocols alpnProtocolsString = unsafeCoerce + alpnProtocolsBuffer :: Buffer -> ALPNProtocols alpnProtocolsBuffer = unsafeCoerce + alpnProtocolsUint8Array :: Uint8Array -> ALPNProtocols alpnProtocolsUint8Array = unsafeCoerce + alpnProtocolsStringArray :: Array String -> ALPNProtocols alpnProtocolsStringArray = unsafeCoerce + alpnProtocolsBufferArray :: Array Buffer -> ALPNProtocols alpnProtocolsBufferArray = unsafeCoerce + alpnProtocolsUint8ArrayArray :: Array Uint8Array -> ALPNProtocols alpnProtocolsUint8ArrayArray = unsafeCoerce @@ -167,8 +180,10 @@ ticketKeys = opt "ticketKeys" -- | The PFX option can take either a String or a Buffer data PFX + pfxString :: String -> PFX pfxString = unsafeCoerce + pfxBuffer :: Buffer -> PFX pfxBuffer = unsafeCoerce @@ -179,12 +194,16 @@ pfx = opt "pfx" -- | The key option can be a String, a Buffer, an array of strings, or an array -- | of buffers. data Key + keyString :: String -> Key keyString = unsafeCoerce + keyBuffer :: Buffer -> Key keyBuffer = unsafeCoerce + keyStringArray :: Array String -> Key keyStringArray = unsafeCoerce + keyBufferArray :: Array Buffer -> Key keyBufferArray = unsafeCoerce @@ -199,12 +218,16 @@ passphrase = opt "passphrase" -- | The cert option can be a String, a Buffer, an array of strings, or an array -- | of buffers. data Cert + certString :: String -> Cert certString = unsafeCoerce + certBuffer :: Buffer -> Cert certBuffer = unsafeCoerce + certStringArray :: Array String -> Cert certStringArray = unsafeCoerce + certBufferArray :: Array Buffer -> Cert certBufferArray = unsafeCoerce @@ -215,12 +238,16 @@ cert = opt "cert" -- | The CA option can be a String, a Buffer, an array of strings, or an array -- | of buffers. data CA + caString :: String -> CA caString = unsafeCoerce + caBuffer :: Buffer -> CA caBuffer = unsafeCoerce + caStringArray :: Array String -> CA caStringArray = unsafeCoerce + caBufferArray :: Array Buffer -> CA caBufferArray = unsafeCoerce @@ -231,12 +258,16 @@ ca = opt "ca" -- | The CRL option can be a String, a Buffer, an array of strings, or an array -- | of buffers. data CRL + crlString :: String -> CRL crlString = unsafeCoerce + crlBuffer :: Buffer -> CRL crlBuffer = unsafeCoerce + crlStringArray :: Array String -> CRL crlStringArray = unsafeCoerce + crlBufferArray :: Array Buffer -> CRL crlBufferArray = unsafeCoerce @@ -258,8 +289,10 @@ ecdhCurve = opt "ecdhCurve" -- | The DHParam option can take either a String or a Buffer data DHParam + dhparamString :: String -> DHParam dhparamString = unsafeCoerce + dhparamBuffer :: Buffer -> DHParam dhparamBuffer = unsafeCoerce diff --git a/test/Main.js b/test/Main.js deleted file mode 100644 index 8c3e2b4..0000000 --- a/test/Main.js +++ /dev/null @@ -1 +0,0 @@ -export const stdout = process.stdout; diff --git a/test/Main.purs b/test/Main.purs index 8370e59..620f1dd 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -2,136 +2,104 @@ module Test.Main where import Prelude +import Data.Either (Either(..)) import Data.Foldable (foldMap) import Data.Maybe (Maybe(..), fromMaybe) import Data.Options (Options, options, (:=)) import Data.Tuple (Tuple(..)) import Effect (Effect) +import Effect.Aff (Milliseconds(..), launchAff_, makeAff, nonCanceler) import Effect.Console (log, logShow) +import Effect.Exception (Error) import Foreign.Object (fromFoldable, lookup) import Node.Encoding (Encoding(..)) -import Node.HTTP (Request, Response, listen, createServer, setHeader, requestHeaders, requestMethod, requestURL, responseAsStream, requestAsStream, setStatusCode, onUpgrade) +import Node.HTTP (Request, Response, close, createServer, listen, onRequest, onUpgrade, requestAsStream, requestHeaders, requestMethod, requestURL, responseAsStream, setHeader, setStatusCode) import Node.HTTP.Client as Client import Node.HTTP.Secure as HTTPS import Node.Net.Socket as Socket -import Node.Stream (Writable, end, pipe, writeString) -import Partial.Unsafe (unsafeCrashWith) +import Node.Process as Node.Process +import Node.Stream (end, pipe, writeString) +import Partial.Unsafe (unsafeCrashWith, unsafePartial) +import Test.MockCert (cert, key) +import Test.Spec (describe, it) +import Test.Spec.Assertions (shouldReturn) +import Test.Spec.Reporter (consoleReporter) +import Test.Spec.Runner (defaultConfig, runSpec') import Unsafe.Coerce (unsafeCoerce) -foreign import stdout :: forall r. Writable r - main :: Effect Unit -main = do - testBasic - testUpgrade - testHttpsServer - testHttps - testCookies +main = unsafePartial $ launchAff_ do + runSpec' (defaultConfig { timeout = Just (Milliseconds 2000.0) }) [ consoleReporter ] do + describe "HTTP" do + it "test basic" do + flip shouldReturn unit $ makeAff \complete -> do + testBasic complete + pure nonCanceler + it "test upgrade" do + flip shouldReturn unit $ makeAff \complete -> do + testUpgrade complete + pure nonCanceler + it "test HttpsServer" do + flip shouldReturn unit $ makeAff \complete -> do + testHttpsServer complete + pure nonCanceler respond :: Request -> Response -> Effect Unit respond req res = do setStatusCode res 200 - let inputStream = requestAsStream req - outputStream = responseAsStream res + let + inputStream = requestAsStream req + outputStream = responseAsStream res log (requestMethod req <> " " <> requestURL req) case requestMethod req of "GET" -> do - let html = foldMap (_ <> "\n") - [ "
" - ] + let + html = foldMap (_ <> "\n") + [ "" + ] setHeader res "Content-Type" "text/html" _ <- writeString outputStream UTF8 html mempty end outputStream (const $ pure unit) "POST" -> void $ pipe inputStream outputStream _ -> unsafeCrashWith "Unexpected HTTP method" -testBasic :: Effect Unit -testBasic = do - server <- createServer respond +testBasic :: (Either Error Unit -> Effect Unit) -> Effect Unit +testBasic complete = do + server <- createServer \_ _ -> pure unit + onRequest server \req res -> do + respond req res + close server $ complete (Right unit) listen server { hostname: "localhost", port: 8080, backlog: Nothing } $ void do log "Listening on port 8080." - simpleReq "http://localhost:8080" - -mockCert :: String -mockCert = - """-----BEGIN CERTIFICATE----- -MIIDWDCCAkCgAwIBAgIJAKm4yWuzx7UpMA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV -BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMR0wGwYDVQQKDBRwdXJlc2NyaXB0 -LW5vZGUtaHR0cDAeFw0xNzA3MjMwMTM4MThaFw0xNzA4MjIwMTM4MThaMEExCzAJ -BgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMR0wGwYDVQQKDBRwdXJlc2Ny -aXB0LW5vZGUtaHR0cDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMrI -7YGwOVZJGemgeGm8e6MTydSQozxlHYwshHDb83pB2LUhkguSRHoUe9CO+uDGemKP -BHMHFCS1Nuhgal3mnCPNbY/57mA8LDIpjJ/j9UD85Aw5c89yEd8MuLoM1T0q/APa -LOmKMgzvfpA0S1/6Hr5Ef/tGdE1gFluVirhgUqvbIBJzqTraQq89jwf+4YmzjCO7 -/6FIY0pn4xgcSGyd3i2r/DGbL42QlNmq2MarxxdFJo1llK6YIBhS/fAJCp6hsAnX -+m4hClvJ17Rt+46q4C7KCP6J1U5jFIMtDF7jw6uBr/macenF/ApAHUW0dAiBP9qG -fI2l64syxNSUS3of9p0CAwEAAaNTMFEwHQYDVR0OBBYEFPlsFrLCVM6zgXzKMkDN -lzkLLoCfMB8GA1UdIwQYMBaAFPlsFrLCVM6zgXzKMkDNlzkLLoCfMA8GA1UdEwEB -/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKvNsmnuO65CUnU1U85UlXYSpyA2 -f1SVCwKsRB9omFCbtJv8nZOrFSfooxdNJ0LiS7t4cs6v1+441+Sg4aLA14qy4ezv -Fmjt/0qfS3GNjJRr9KU9ZdZ3oxu7qf2ILUneSJOuU/OjP42rZUV6ruyauZB79PvB -25ENUhpA9z90REYjHuZzUeI60/aRwqQgCCwu5XYeIIxkD+WBPh2lxCfASwQ6/1Iq -fEkZtgzKvcprF8csbb2RNu2AVF2jdxChtl/FCUlSSX13VCROf6dOYJPid9s/wKpE -nN+b2NNE8OJeuskvEckzDe/hbkVptUNi4q2G8tBoKjPPTjdiLjtxuNz7OT0= ------END CERTIFICATE-----""" - -mockKey :: String -mockKey = - """-----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDKyO2BsDlWSRnp -oHhpvHujE8nUkKM8ZR2MLIRw2/N6Qdi1IZILkkR6FHvQjvrgxnpijwRzBxQktTbo -YGpd5pwjzW2P+e5gPCwyKYyf4/VA/OQMOXPPchHfDLi6DNU9KvwD2izpijIM736Q -NEtf+h6+RH/7RnRNYBZblYq4YFKr2yASc6k62kKvPY8H/uGJs4wju/+hSGNKZ+MY -HEhsnd4tq/wxmy+NkJTZqtjGq8cXRSaNZZSumCAYUv3wCQqeobAJ1/puIQpbyde0 -bfuOquAuygj+idVOYxSDLQxe48Orga/5mnHpxfwKQB1FtHQIgT/ahnyNpeuLMsTU -lEt6H/adAgMBAAECggEBALSe/54SXx/SAPitbFOSBPYefBmPszXqQsVGKbl00IvG -9sVvX2xbHg83C4masS9g2kXLaYUjevevSXb12ghFjjH9mmcxkPe64QrVI2KPYzY9 -isqwqczOp8hqxmdBYvYWwV6VCIgEBcyrzamYSsL0QEntLamc+Z6pxYBR1LuhYEGd -Vq0A+YL/4CZi320+pt05u/635Daon33JqhvDa0QK5xvFYKEcB+IY5eqByOx7nJl8 -A55oVagBVjpi//rwoge5aCfbcdyHUmBFYkuCI6SJhvwDmfSHWDkyWWsZAJY5sosN -a824N7XX5ZiBYir+E4ldC6ZlFOnQK5f6Fr0MJeM8uikCgYEA+HAgYgKBpezCrJ0B -I/inIfynaW8k3SCSQhYvqPK591cBKXwghCG2vpUwqIVO/ROP070L9/EtNrFs5fPv -xHQA8P3Weeail6gl9UR5oKNU3bcbIFunUtWi1ua86g/aaofub/hBq2xR+HSnV91W -Ycwewyfc/0j94kDOAFgSGOz0BscCgYEA0PUQXtuu05YTmz2TDtknCcQOVm/UnEg6 -1FsKPzmoxWsAMtHXf3FbD3vHql1JfPTJPNcxEEL6fhA1l7ntailHltx8dt9bXmYJ -ANM0n8uSKde5MoFbMhmyYTcRxJW9EC2ivqLotd5iL1mbfvdF02cWmr/5KNxUO1Hk -7TkJturwo3sCgYBc/gNxDEUhKX05BU/O+hz9QMgdVAf1aWK1r/5I/AoWBhAeSiMV -slToA4oCGlwVqMPWWtXnCfSFm2YKsQNXgqBzlGA6otTLdZo3s1jfgyOaFhbmRshb -3jGkxRuDdUmpRJZAfSl/k/0exfN5lRTnaHM/U2WKfPTjQqSZRl4HzHIPMwKBgFVE -W0zKClou+Is1oifB9wsmJM+izLiFRPRYviK0raj5k9gpBu3rXMRBt2VOsek6nk+k -ZFIFcuA0Txo99aKHe74U9PkxBcDMlEnw5Z17XYaTj/ALFyKnl8HRzf9RNxg99xYh -tiJYv+ogf7JcxvKQM4osYkkJN5oJPgiLaOpqjo23AoGBAN3g5kvsYj3OKGh89pGk -osLeL+NNUBDvFsrvFzPMwPGDup6AB1qX1pc4RfyQGzDJqUSTpioWI5v1O6Pmoiak -FO0u08Tb/091Bir5kgglUSi7VnFD3v8ffeKpkkJvtYUj7S9yoH9NQPVhKVCq6mna -TbGfXbnVfNmqgQh71+k02p6S ------END PRIVATE KEY-----""" - -testHttpsServer :: Effect Unit -testHttpsServer = do - server <- HTTPS.createServer sslOpts respond + simpleReq "http://localhost:8080" + +testHttpsServer :: (Either Error Unit -> Effect Unit) -> Effect Unit +testHttpsServer complete = do + server <- HTTPS.createServer sslOpts \_ _ -> pure unit + onRequest server \req res -> do + respond req res + close server $ complete (Right unit) listen server { hostname: "localhost", port: 8081, backlog: Nothing } $ void do log "Listening on port 8081." complexReq $ - Client.protocol := "https:" <> - Client.method := "GET" <> - Client.hostname := "localhost" <> - Client.port := 8081 <> - Client.path := "/" <> - Client.rejectUnauthorized := false + Client.protocol := "https:" + <> Client.method := "GET" + <> Client.hostname := "localhost" + <> Client.port := 8081 + <> Client.path := "/" + <> + Client.rejectUnauthorized := false where - sslOpts = - HTTPS.key := HTTPS.keyString mockKey <> - HTTPS.cert := HTTPS.certString mockCert + sslOpts = + HTTPS.key := HTTPS.keyString key <> + HTTPS.cert := HTTPS.certString cert -testHttps :: Effect Unit -testHttps = - simpleReq "https://pursuit.purescript.org/packages/purescript-node-http/badge" - -testCookies :: Effect Unit -testCookies = +testCookies :: (Either Error Unit -> Effect Unit) -> Effect Unit +testCookies _ = + -- TODO I don't see how this tests cookies simpleReq "https://httpbin.org/cookies/set?cookie1=firstcookie&cookie2=secondcookie" @@ -147,7 +115,7 @@ complexReq opts = do req <- Client.request opts logResponse end (Client.requestAsStream req) (const $ pure unit) where - optsR = unsafeCoerce $ options opts + optsR = unsafeCoerce $ options opts logResponse :: Client.Response -> Effect Unit logResponse response = void do @@ -157,68 +125,73 @@ logResponse response = void do logShow $ Client.responseCookies response log "Response:" let responseStream = Client.responseAsStream response - pipe responseStream stdout + pipe responseStream Node.Process.stdout -testUpgrade :: Effect Unit -testUpgrade = do - server <- createServer respond +testUpgrade :: (Either Error Unit -> Effect Unit) -> Effect Unit +testUpgrade complete = do + server <- createServer \_ _ -> pure unit + onRequest server \req res -> do + respond req res onUpgrade server handleUpgrade - listen server { hostname: "localhost", port: 3000, backlog: Nothing } - $ void do - log "Listening on port 3000." - sendRequests + listen server { hostname: "localhost", port: 3000, backlog: Nothing } do + log "Listening on port 3000." + sendRequests (close server $ complete (Right unit)) where handleUpgrade req socket _ = do let upgradeHeader = fromMaybe "" $ lookup "upgrade" $ requestHeaders req if upgradeHeader == "websocket" then - void $ Socket.writeString - socket - "HTTP/1.1 101 Switching Protocols\r\nContent-Length: 0\r\n\r\n" - UTF8 + void + $ Socket.writeString + socket + "HTTP/1.1 101 Switching Protocols\r\nContent-Length: 0\r\n\r\n" + UTF8 $ pure unit else - void $ Socket.writeString - socket - "HTTP/1.1 426 Upgrade Required\r\nContent-Length: 0\r\n\r\n" - UTF8 + void + $ Socket.writeString + socket + "HTTP/1.1 426 Upgrade Required\r\nContent-Length: 0\r\n\r\n" + UTF8 $ pure unit - sendRequests = do + sendRequests complete' = do -- This tests that the upgrade callback is not called when the request is not an HTTP upgrade reqSimple <- Client.request (Client.port := 3000) \response -> do if (Client.statusCode response /= 200) then unsafeCrashWith "Unexpected response to simple request on `testUpgrade`" else - pure unit + pure unit end (Client.requestAsStream reqSimple) (const $ pure unit) {- These two requests test that the upgrade callback is called and that it has access to the original request and can write to the underlying TCP socket -} - let headers = Client.RequestHeaders $ fromFoldable - [ Tuple "Connection" "upgrade" - , Tuple "Upgrade" "something" - ] + let + headers = Client.RequestHeaders $ fromFoldable + [ Tuple "Connection" "upgrade" + , Tuple "Upgrade" "something" + ] reqUpgrade <- Client.request - (Client.port := 3000 <> Client.headers := headers) - \response -> do - if (Client.statusCode response /= 426) then - unsafeCrashWith "Unexpected response to upgrade request on `testUpgrade`" - else + (Client.port := 3000 <> Client.headers := headers) + \response -> do + if (Client.statusCode response /= 426) then + unsafeCrashWith "Unexpected response to upgrade request on `testUpgrade`" + else pure unit end (Client.requestAsStream reqUpgrade) (const $ pure unit) - let wsHeaders = Client.RequestHeaders $ fromFoldable - [ Tuple "Connection" "upgrade" - , Tuple "Upgrade" "websocket" - ] + let + wsHeaders = Client.RequestHeaders $ fromFoldable + [ Tuple "Connection" "upgrade" + , Tuple "Upgrade" "websocket" + ] reqWSUpgrade <- Client.request - (Client.port := 3000 <> Client.headers := wsHeaders) - \response -> do - if (Client.statusCode response /= 101) then - unsafeCrashWith "Unexpected response to websocket upgrade request on `testUpgrade`" - else - pure unit + (Client.port := 3000 <> Client.headers := wsHeaders) + \response -> do + if (Client.statusCode response /= 101) then + unsafeCrashWith "Unexpected response to websocket upgrade request on `testUpgrade`" + else + pure unit end (Client.requestAsStream reqWSUpgrade) (const $ pure unit) - pure unit + complete' diff --git a/test/MockCert.purs b/test/MockCert.purs new file mode 100644 index 0000000..17d00e8 --- /dev/null +++ b/test/MockCert.purs @@ -0,0 +1,61 @@ +module Test.MockCert where + +-- https://letsencrypt.org/docs/certificates-for-localhost/#making-and-trusting-your-own-certificates +-- +-- Generate localhost.crt and localhost.key with 10 year expiration: +-- +-- openssl req -x509 -out localhost.crt -keyout localhost.key -newkey rsa:2048 -nodes -sha256 -days 3650 -subj '/CN=localhost' -extensions EXT -config <( printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") +-- + +cert :: String +cert = + """-----BEGIN CERTIFICATE----- +MIIDDzCCAfegAwIBAgIUUyn89RHpZC9irOiqJpcBqFRw2HgwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMTExODAyMTkyN1oXDTMyMTEx +NTAyMTkyN1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA0REkgizCB39n47Z3JXcW+GPPym4MXBb9HAHBJbH1+m/R +0EkdunDyXr8cKveABgq3/kazWjXlGwNXUklKYCydcnmtNVBub4s1wXAsegRaPMmo +RzisW7FWaqcLcBMAuwrub2NTVsX0HtO5qZiEKNx6AAbWFizFmMQ9K/9VprT1OLWy +vtIOlR/YK+PKruNWeNpvhx91zmwb69lgrqUcwMHguLWgoz0JJgzh7cerexbT+eKC +CuA9Ub8ctQD8SIl3eF7OzsvmQHSr+yABo3TJj7UZLh0B3j1uB8RLQvenVilc4YPz +MK/R6Jf8RjRssGommbUqVaXRjJfYQ2As2tkzRS90cwIDAQABo1kwVzAUBgNVHREE +DTALgglsb2NhbGhvc3QwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMB +MB0GA1UdDgQWBBS5+ngK++/FbHQ4Uf8qMZDK6tSNlDANBgkqhkiG9w0BAQsFAAOC +AQEAj5nTUka4P/hWkV+Wa9Rp/ijqv2ah2ukU1u73QyprG2/gHmFpYvNFJ7lG9O9r +Wuvsz4g4moX9kgt/9GnpUbZBUE7zPau74P06lFcXhKAhiZcpsS+CZbMIsbfilWS0 +SBbs8OTLvexOqPP4pTvlc67zPkuB3tjOnHhPar8VSAiBp2s0l6UF2vWZ69Xj3ice +DadE6thrH41GN/OSROKWL6dEueNTuQaU1Rx9Nxh8hvKiDJZ7l8oiHGYERoGwJJro +tWBqRvX/C4TpnS+ckhOyqrHUXN66lVaact9GaBd7n6oCKzDY/GtENCLJnNKte5VI +SATt1Hpnw3S/zwX9imqABqneAA== +-----END CERTIFICATE-----""" + +key :: String +key = + """-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRESSCLMIHf2fj +tncldxb4Y8/KbgxcFv0cAcElsfX6b9HQSR26cPJevxwq94AGCrf+RrNaNeUbA1dS +SUpgLJ1yea01UG5vizXBcCx6BFo8yahHOKxbsVZqpwtwEwC7Cu5vY1NWxfQe07mp +mIQo3HoABtYWLMWYxD0r/1WmtPU4tbK+0g6VH9gr48qu41Z42m+HH3XObBvr2WCu +pRzAweC4taCjPQkmDOHtx6t7FtP54oIK4D1Rvxy1APxIiXd4Xs7Oy+ZAdKv7IAGj +dMmPtRkuHQHePW4HxEtC96dWKVzhg/Mwr9Hol/xGNGywaiaZtSpVpdGMl9hDYCza +2TNFL3RzAgMBAAECggEAJggqTgv6WAbTTVdaIVSitxjhKgAO+4mrDbc7/bF7/8zr +rCpA4DO/w4CcjSxs+6xjgDw4UEbRoLJg5jUy9H/pPHPqEHLLRDtc0g2n6aJ1D+3X +UO18XUnLYKd2qzKpxVzdtyGofXaRTDJT6gg2soA5KVwVAf+vCnVYc3KFkEgG/AOt +jhvbxK+xA4CGjGPYxASO5K3IVJxb419hi8dizgtdJaotysvfspth5WOOoiBtVhuB +6ORZt9DbN1AK9U3nV76NsjHeQWcMsDqt8w/KRkok4X9rkQ86pylZcDUyoqkf+aYB +09FgDiw2iSj9k6kkR0y1o/sRsCN7PoRmJgEhrRWPcQKBgQDlflzyoaVUCIAZQAMo +O3vJE/AEOnvB+eHmqGSi6nGHUxJavxm8dxJRqY2fzA/VeVvp19Nxs1Eh8pskHav4 +n+syRtGzkKIE0x9/KThhgzbqZl+NT5afHMhUHvmepMf8J+71giMC2v2yQC0aFVi7 +3frv3YNuBbC69FWkeYOjq/MJZwKBgQDpNs6nYmtR3bLBWKoLSOTLGV8Bhhhzt+tu +nm6LVA464ib039m5BoWne890InxgaDNHfuL++n473JFXuwQMBBY3YLD3OPa5uW4a +gt+oYUJKh+qGio395GnZ0W/Sf5GBpdPJ+pTMqGqlo/NWSPuwCdMd5T6RfvnEJzzv +0/jZCAJ5FQKBgCE2yaMADBp+ZHPDFPHksgSnEwy5niGz1aL5ah8+CRJJzpU9pS7m +mMsi2/Ftqjj+KHROnTaOekaMgzGV7ca89mA/aagwXZKPL7bKs3NBd1gzWs7r3uPG +WaP7G6t/M8ZlzSrRG9oU8bSznxNwVXhTJzdB+vyYbDySkjaMs6WjhDgvAoGBAJj0 +mE8R7r9Pv1it9UDXey91oWkXcNwciW4QvQHmjDq0bsZ2No7ypyA0xNgvchGs5c0D +fI+s7LQIMs8uWjYjTArgAND0bGVdJ8h9g4Ek4NyPDhNVtlEJyR7SDRwrDNzSTPiQ +v50G7INc51D1JxXLK8rUutekRt4Ouhm1leWKKk0NAoGBALvc9wF7XcgGHZa1RRk9 +jH0vOkrn632Epzml1mXg0//2mw+7iQP3q5KtRruaIk6ifLSHznzqAkowhKFH+iCH +wnecLhsl5FnL0JAipIxBHdX0iTttJf4UR/2wTo3RalGjEcMjMCUrSdkhBjRH4Gdc +fBuXFtwhIuiggNR7UlHxbYpq +-----END PRIVATE KEY-----"""