From e3fd62fd686c763754f6c87d04f73764aec5a82f Mon Sep 17 00:00:00 2001 From: James Brock Date: Thu, 23 Jun 2022 13:29:09 +0900 Subject: [PATCH 1/3] New module HTTP2 HTTP2 module with low-level `Effect` bindings and high-level `Aff` bindings. Add spago.dhall and spago.dev.dhall. Tests terminate. Use purescript-spec for tests. Upgrade ci.yml. New function HTTP.onRequest. --- .github/workflows/ci.yml | 34 ++- .gitignore | 1 + CHANGELOG.md | 6 +- README.md | 2 +- bower.json | 72 +++--- packages.dhall | 5 + spago.dev.dhall | 15 ++ spago.dhall | 32 +++ src/Node/HTTP.js | 10 + src/Node/HTTP.purs | 4 + src/Node/HTTP/Secure.purs | 47 +++- src/Node/HTTP2.js | 10 + src/Node/HTTP2.purs | 313 ++++++++++++++++++++++++++ src/Node/HTTP2/Client.js | 51 +++++ src/Node/HTTP2/Client.purs | 205 +++++++++++++++++ src/Node/HTTP2/Client/Aff.purs | 277 +++++++++++++++++++++++ src/Node/HTTP2/Constants.purs | 66 ++++++ src/Node/HTTP2/Internal.js | 114 ++++++++++ src/Node/HTTP2/Internal.purs | 112 ++++++++++ src/Node/HTTP2/Server.js | 40 ++++ src/Node/HTTP2/Server.purs | 289 ++++++++++++++++++++++++ src/Node/HTTP2/Server/Aff.purs | 389 +++++++++++++++++++++++++++++++++ test/HTTP2.purs | 94 ++++++++ test/HTTP2Aff.purs | 302 +++++++++++++++++++++++++ test/Main.js | 1 - test/Main.purs | 285 ++++++++++++------------ test/MockCert.purs | 61 ++++++ 27 files changed, 2649 insertions(+), 188 deletions(-) create mode 100644 packages.dhall create mode 100644 spago.dev.dhall create mode 100644 spago.dhall create mode 100644 src/Node/HTTP2.js create mode 100644 src/Node/HTTP2.purs create mode 100644 src/Node/HTTP2/Client.js create mode 100644 src/Node/HTTP2/Client.purs create mode 100644 src/Node/HTTP2/Client/Aff.purs create mode 100644 src/Node/HTTP2/Constants.purs create mode 100644 src/Node/HTTP2/Internal.js create mode 100644 src/Node/HTTP2/Internal.purs create mode 100644 src/Node/HTTP2/Server.js create mode 100644 src/Node/HTTP2/Server.purs create mode 100644 src/Node/HTTP2/Server/Aff.purs create mode 100644 test/HTTP2.purs create mode 100644 test/HTTP2Aff.purs delete mode 100644 test/Main.js create mode 100644 test/MockCert.purs 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..6f18e8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ Breaking changes: New features: +- New module `HTTP2`. Solves #44. (#45 by @jamesdbrock) +- Use __spec__ for tests. Upgraded `ci.yml`. Solves #35. (#45 by @jamesdbrock) +- New function `HTTP.onRequest`. Solves #46. (#45 by @jamesdbrock) + Bugfixes: Other improvements: @@ -32,7 +36,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..2dbb6ff 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Build status](https://github.com/purescript-node/purescript-node-http/workflows/CI/badge.svg?branch=master)](https://github.com/purescript-node/purescript-node-http/actions?query=workflow%3ACI+branch%3Amaster) [![Pursuit](https://pursuit.purescript.org/packages/purescript-node-http/badge)](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) and [HTTP/2](https://nodejs.org/docs/latest/api/http2.html) APIs. ## Installation diff --git a/bower.json b/bower.json index 9de0635..f7553aa 100644 --- a/bower.json +++ b/bower.json @@ -1,33 +1,43 @@ { - "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-aff": "^v7.1.0", + "purescript-arraybuffer-types": "^v3.0.2", + "purescript-console": "^v6.0.0", + "purescript-contravariant": "^v6.0.0", + "purescript-control": "^v6.0.0", + "purescript-effect": "^v4.0.0", + "purescript-either": "^v6.1.0", + "purescript-exceptions": "^v6.0.0", + "purescript-foldable-traversable": "^v6.0.0", + "purescript-foreign": "^v7.0.0", + "purescript-foreign-object": "^v4.1.0", + "purescript-maybe": "^v6.0.0", + "purescript-newtype": "^v5.0.0", + "purescript-node-buffer": "^v8.0.0", + "purescript-node-net": "^v4.0.0", + "purescript-node-streams": "^v7.0.0", + "purescript-node-streams-aff": "https://github.com/purescript-node/purescript-node-streams-aff.git#v4.0.0", + "purescript-node-url": "^v6.0.0", + "purescript-nullable": "^v6.0.0", + "purescript-options": "^v7.0.0", + "purescript-parallel": "^v6.0.0", + "purescript-partial": "^v4.0.0", + "purescript-prelude": "^v6.0.1", + "purescript-transformers": "^v6.0.0", + "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..a9c8762 --- /dev/null +++ b/spago.dev.dhall @@ -0,0 +1,15 @@ +-- Spago configuration for testing. + +let conf = ./spago.dhall + +in conf // +{ sources = [ "src/**/*.purs", "test/**/*.purs" ] +, dependencies = conf.dependencies # + [ "console" + , "node-process" + , "st" + , "spec" + , "strings" + , "tuples" + ] +} diff --git a/spago.dhall b/spago.dhall new file mode 100644 index 0000000..bdd65b4 --- /dev/null +++ b/spago.dhall @@ -0,0 +1,32 @@ +{ name = "node-http" +, dependencies = + [ "aff" + , "arraybuffer-types" + , "contravariant" + , "control" + , "effect" + , "either" + , "exceptions" + , "foldable-traversable" + , "foreign" + , "foreign-object" + , "maybe" + , "newtype" + , "node-buffer" + , "node-net" + , "node-streams" + , "node-streams-aff" + , "node-url" + , "nullable" + , "options" + , "parallel" + , "partial" + , "prelude" + , "transformers" + , "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/src/Node/HTTP2.js b/src/Node/HTTP2.js new file mode 100644 index 0000000..db97b37 --- /dev/null +++ b/src/Node/HTTP2.js @@ -0,0 +1,10 @@ +import http2 from "http2"; + +export const sensitiveHeaders = h => { + return {...h, ...{[http2.sensitiveHeaders]: Object.keys(h)}}; +}; + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax +export const unionHeadersImpl = l => r => { + return {...l, ...r} +}; diff --git a/src/Node/HTTP2.purs b/src/Node/HTTP2.purs new file mode 100644 index 0000000..5c1b9bf --- /dev/null +++ b/src/Node/HTTP2.purs @@ -0,0 +1,313 @@ +-- | Bindings to the [*Node.js* HTTP/2](https://nodejs.org/docs/latest/api/http2.html) API. +-- | +-- | ## `Effect` event-callback asynchronous API +-- | +-- | The __Node.HTTP2.Client__ and __Node.HTTP2.Server__ modules provide a +-- | low-level `Effect` event-callback API which is a thin FFI wrapper +-- | around the __*Node.js* HTTP/2__ types and functions. +-- | +-- | ## `Aff` asynchronous API +-- | +-- | The __Node.HTTP2.Client.Aff__ and __Node.HTTP2.Server.Aff__ modules provide +-- | a high-level asynchronous `Aff` API so that +-- | [“you don’t even have to think about callbacks.”](https://github.com/purescript-contrib/purescript-aff/tree/main/docs#escaping-callback-hell) +-- | That’s nice because when we attach callbacks to every possible event +-- | and then handle the events, it’s hard to keep track of the order +-- | in which events are occuring, and what context we’re in when an event +-- | handler is called. +-- | +-- | With the `Aff` API we can write HTTP/2 clients and services in plain flat +-- | one-thing-after-another +-- | effect style. But network peers are perverse and they may not +-- | send things to us in the order in which we expect. So we will want to +-- | reintroduce some of the indeterminacy that we get from an event-callback +-- | API. That’s what the +-- | [`Parallel ParAff Aff`](https://pursuit.purescript.org/packages/purescript-aff/docs/Effect.Aff#t:ParAff) +-- | instance is for. +-- | We can use the functions in +-- | [`Control.Parallel`](https://pursuit.purescript.org/packages/purescript-parallel/docs/Control.Parallel) +-- | to run `Aff` effects concurrently. +-- | +-- | #### Example: Asynchronous client and server +-- | +-- | Consider a function `push1_secureServer :: Aff Unit` which runs an +-- | HTTP/2 server that +-- | [pushes an extra HTTP/2 stream](https://en.wikipedia.org/wiki/HTTP/2_Server_Push) +-- | to its clients. +-- | This function does the following steps. +-- | +-- | 1. Wait for a connection. +-- | 2. Wait to receive a request. +-- | 3. Send a response stream. +-- | 4. Push a new stream, send the stream. +-- | 5. Close the connection. +-- | +-- | Also consider a function `push1_client :: Aff Unit` which opens an +-- | HTTP/2 client connection that can receive pushed streams. +-- | This function does the following steps. +-- | +-- | 1. Open a connection. +-- | 2. Wait for the connection, then send a request. +-- | 3. We have to do the next +-- | two steps concurrently because we don't know whether the server +-- | will send the main response stream or the pushed stream first. +-- | And we don’t know which stream will complete first, either. +-- | +-- | - 3a. Wait for the response stream. +-- | - 3b. Wait for a pushed stream. +-- | +-- | 4. Wait for the connection to close. +-- | +-- | With `Aff`, we can __run both of these functions at the same time in +-- | the same thread and connect them to each other__. +-- | There’s a lot of asynchronous back-and-forth going on here, +-- | with each function waiting for the other one at multiple points. +-- | So how do we run +-- | these two functions concurrently without deadlocking? +-- | With [`Control.Parallel.parSequence_`](https://pursuit.purescript.org/packages/purescript-parallel/6.0.0/docs/Control.Parallel#v:parSequence): +-- | +-- | ``` +-- | parSequence_ +-- | [ push1_secureServer +-- | , push1_client +-- | ] +-- | ``` +-- | +-- | If you haven’t programmed with an asynchronous effect monad like `Aff` +-- | before, I hope this gives you an idea of how wieldy asynchronous +-- | effect monads are. +-- | +-- | This pair of functions is the `push1` test in our `HTTP2Aff` +-- | test suite. +-- | You can see the definitions +-- | for `push1_secureServer` and `push1_client` in +-- | [`test/HTTP2Aff.purs`](https://github.com/purescript-node/purescript-node-http/tree/master/test/HTTP2Aff.purs). +-- | +-- | #### Aff Idiom: Concurrent effects with homogeneous return values. +-- | +-- | We have `operation1`, `operation2`, and `operation3`, which are +-- | all side-effecting `Aff Unit` operations. We want to start them +-- | all at the same time. We don’t know in what order they’ll complete, +-- | but we want to wait until they’ve all completed before we continue. +-- | +-- | ``` +-- | parSequence_ +-- | [ operation1 +-- | , operation2 +-- | , operation3 +-- | ] +-- | ``` +-- | +-- | We can also use do-blocks in this idiom. And collect the results +-- | if the results are all some interesting type instead of `Unit`. +-- | +-- | ``` +-- | [result1, result2, result3] <- parSequence +-- | [ do +-- | operation1 +-- | , do +-- | operation2 +-- | , do +-- | operation3 +-- | ] +-- | ``` +-- | +-- | #### Aff Idiom: Concurrent effects with heterogeneous return values +-- | +-- | We have `operation1`, `operation2`, and `operation3`, which are +-- | all `Aff` operations with return values of different types. +-- | We want to start them +-- | all at the same time. We don’t know in what order they’ll complete, +-- | but we need all of their results before we can continue. +-- | We can collect their results in a +-- | [`Tuple3`](https://pursuit.purescript.org/packages/purescript-tuples/docs/Data.Tuple.Nested). +-- | +-- | ``` +-- | result1 /\ result2 /\ result3 <- sequential $ tuple3 <$> +-- | do parallel +-- | operation1 +-- | <*> +-- | do parallel +-- | operation2 +-- | <*> +-- | do parallel +-- | operation3 +-- | ``` +-- | +-- | #### Aff Idiom: Surprising events +-- | +-- | We have `event1`, `event2`, and `event3`, which are +-- | all side-effecting `Aff Unit` operations. We know one of them +-- | will complete next, but we don’t know which one. We want to wait for +-- | all of them at the same time. +-- | +-- | ``` +-- | parOneOf +-- | [ event1 +-- | , event2 +-- | , event3 +-- | ] +-- | ``` +module Node.HTTP2 + ( HeadersObject(..) + , toHeaders + , sensitiveHeaders + , headerKeys + , headerString + , headerArray + , headerStatus + , OptionsObject(..) + , toOptions + , Flags + , SettingsObject(..) + ) where + +import Prelude + +import Control.Monad.Except (runExcept) +import Data.Either (hush) +import Data.Maybe (Maybe, fromJust) +import Data.Newtype (class Newtype) +import Data.Traversable (traverse) +import Foreign (Foreign, readArray, readInt, readString, unsafeToForeign) +import Foreign.Index (readProp) +import Foreign.Keys (keys) +import Foreign.Object (union) +import Partial.Unsafe (unsafePartial) +import Unsafe.Coerce (unsafeCoerce) + +-- | An HTTP/2 “headers object.” +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#headers-object +-- | +-- | Construct with the `toHeaders` function. +-- | The “no headers” literal is `toHeaders {}`. +-- | +-- | The `Monoid` instance allows us to merge `HeadersObject` with `<>`. +newtype HeadersObject = HeadersObject Foreign + +derive instance Newtype HeadersObject _ + +instance Semigroup HeadersObject where + append l r = unionHeadersImpl l r + +instance Monoid HeadersObject where + mempty = unsafeCoerce {} + +foreign import unionHeadersImpl :: HeadersObject -> HeadersObject -> HeadersObject + +-- | Use this function to construct a `HeadersObject` object. +-- | +-- | Rules for Headers: +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#headers-object +-- | +-- | > Headers are represented as own-properties on JavaScript objects. +-- | > The property keys will be serialized to lower-case. +-- | > Property values should be strings (if they are not they will +-- | > be coerced to strings) or an Array of strings (in order to send +-- | > more than one value per header field). +-- | +-- | This function provides no type-level enforcement of these rules. +-- | +-- | Example: +-- | +-- | ``` +-- | toHeaders +-- | { ":status": "200" +-- | , "content-type": "text-plain" +-- | , "ABC": ["has", "more", "than", "one", "value"] +-- | } +-- | ``` +toHeaders :: forall r. Record r -> HeadersObject +toHeaders = HeadersObject <<< unsafeToForeign + +-- | Use this function to construct a “sensitive” `HeadersObject`. +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#sensitive-headers +-- | +-- | Example: +-- | +-- | ``` +-- | toHeaders +-- | { "content-type": "text-plain" +-- | } +-- | <> +-- | sensitiveHeaders +-- | { "cookie": "some-cookie" +-- | , "other-sensitive-header": "very secret data" +-- | } +-- | ``` +foreign import sensitiveHeaders :: forall r. Record r -> HeadersObject + +-- | Get all of the keys from a `HeadersObject`. +-- | +-- | The value pointed to by each key may be either a `String` +-- | or an `Array String`. +headerKeys :: HeadersObject -> Array String +headerKeys (HeadersObject h) = unsafePartial $ fromJust $ hush $ runExcept $ keys h + +-- | Try to read a `String` value from the `HeadersObject` at the given key. +headerString :: HeadersObject -> String -> Maybe String +headerString (HeadersObject h) n = hush $ runExcept do + readString =<< readProp n h + +-- | Try to read an `Array String` value from the `HeadersObject` at the given key. +headerArray :: HeadersObject -> String -> Maybe (Array String) +headerArray (HeadersObject h) n = hush $ runExcept do + traverse readString =<< readArray =<< readProp n h + +-- | https://nodejs.org/docs/latest/api/http2.html#headers-object +-- | +-- | > For incoming headers: +-- | > +-- | > * The `:status` header is converted to `number`. +headerStatus :: HeadersObject -> Maybe Int +headerStatus (HeadersObject h) = hush $ runExcept do + readInt =<< readProp ":status" h + +-- | https://httpwg.org/specs/rfc7540.html#FrameHeader +type Flags = Int + +-- | A *Node.js* “options object.” +-- | +-- | Construct with the `toOptions` function, or for more type-safety +-- | use +-- | [`Data.Options.options`](https://pursuit.purescript.org/packages/purescript-options/docs/Data.Options#v:options). +-- | The “no options” literal is `toOptions {}`. +-- | +-- | The `Monoid` instance allows us to merge `OptionsObject`s with `<>`. +-- | The options +-- | in the first, left-side `OptionsObject` will override the +-- | second `OptionsObject`. +newtype OptionsObject = OptionsObject Foreign + +derive instance Newtype OptionsObject _ + +instance Semigroup OptionsObject where + append l r = unsafeCoerce $ union (unsafeCoerce l) (unsafeCoerce r) + +instance Monoid OptionsObject where + mempty = toOptions {} + +-- | Use this function to construct an `Options`. +-- | +-- | Example: +-- | +-- | ``` +-- | toOptions +-- | { unknownProtocolTimeout: 2.0 +-- | , settings: +-- | { maxConcurrentStreams: 100 +-- | } +-- | , noDelay: true +-- | , keepAliveInitialDelay: 0.5 +-- | } +-- | ``` +toOptions :: forall r. Record r -> OptionsObject +toOptions = OptionsObject <<< unsafeToForeign + +-- | An HTTP/2 “Settings object.” +-- | +-- | https://nodejs.org/api/http2.html#settings-object +newtype SettingsObject = SettingsObject Foreign diff --git a/src/Node/HTTP2/Client.js b/src/Node/HTTP2/Client.js new file mode 100644 index 0000000..f59b8b2 --- /dev/null +++ b/src/Node/HTTP2/Client.js @@ -0,0 +1,51 @@ +import http2 from "http2"; + +// https://nodejs.org/docs/latest/api/http2.html#http2connectauthority-options-listener +// https://nodejs.org/docs/latest/api/http2.html#event-connect +export const connect = authority => options => listener => () => { + return http2.connect(authority, options, + (session,socket) => listener(session)(socket)() + ); +}; + +// https://stackoverflow.com/questions/67790720/node-js-net-connect-error-in-spite-of-try-catch +export const connectWithError = authority => options => listener => cberror => () => { + return http2.connect(authority, options, + (session,socket) => listener(session)(socket)() + ).once("error", err => cberror(err)()); +}; + +export const onceReady = socket => callback => () => { + socket.once("ready", callback); + return () => socket.removeEventListener("ready", callback); +}; + +// https://nodejs.org/docs/latest/api/http2.html#clienthttp2sessionrequestheaders-options +export const request = clienthttp2session => headers => options => () => { + return clienthttp2session.request(headers, options); +}; + +export const destroy = clienthttp2stream => () => { + clienthttp2stream.destroy(); +}; + +// https://nodejs.org/docs/latest/api/http2.html#event-response +export const onceResponse = clienthttp2stream => callback => () => { + const cb = (headers,flags) => callback(headers)(flags)(); + clienthttp2stream.once("response", cb); + return () => clienthttp2stream.removeEventListener("response", cb); +}; + +// https://nodejs.org/docs/latest/api/http2.html#event-headers +export const onceHeaders = clienthttp2stream => callback => () => { + const cb = (headers,flags) => callback(headers)(flags)(); + clienthttp2stream.once("headers", cb); + return () => clienthttp2stream.removeEventListener("headers", cb); +}; + +// https://nodejs.org/docs/latest/api/http2.html#event-push +export const oncePush = clienthttp2stream => callback => () => { + const cb = (headers,flags) => callback(headers)(flags)(); + clienthttp2stream.once("push", cb); + return () => clienthttp2stream.removeEventListener("push", cb); +}; diff --git a/src/Node/HTTP2/Client.purs b/src/Node/HTTP2/Client.purs new file mode 100644 index 0000000..b3787c1 --- /dev/null +++ b/src/Node/HTTP2/Client.purs @@ -0,0 +1,205 @@ +-- | Low-level bindings to the *Node.js* HTTP/2 Client Core API. +-- | +-- | ## Client-side example +-- | +-- | Equivalent to +-- | https://nodejs.org/docs/latest/api/http2.html#client-side-example +-- | +-- | ``` +-- | ca <- Node.FS.Sync.readFile "localhost-cert.pem" +-- | +-- | client <- connect +-- | (URL.parse "https://localhost:8443") +-- | (toOptions {ca}) +-- | (\_ _ -> pure unit) +-- | _ <- onceErrorSession client Console.errorShow +-- | +-- | req <- request client +-- | (toHeaders {":path": "/"}) +-- | (toOptions {}) +-- | +-- | _ <- onceResponse req +-- | \headers flags -> +-- | for_ (headerKeys headers) \name -> +-- | Console.log $ +-- | name <> ": " <> fromMaybe "" (headerValueString headers name) +-- | +-- | dataRef <- liftST $ Control.Monad.ST.Ref.new "" +-- | Node.Stream.onDataString (toDuplex req) Node.Encoding.UTF8 +-- | \chunk -> void $ liftST $ +-- | Control.Monad.ST.Ref.modify (_ <> chunk) dataRef +-- | Node.Stream.onEnd (toDuplex req) do +-- | dataString <- liftST $ Control.Monad.ST.Ref.read dataRef +-- | Console.log $ "\n" <> dataString +-- | close client +-- | ``` +module Node.HTTP2.Client + ( ClientHttp2Session + , connect + , connectWithError + , onceReady + , request + , onceErrorSession + , onceResponse + , onStream + , onceStream + , onceHeaders + , closeSession + , ClientHttp2Stream + , oncePush + , onceErrorStream + , toDuplex + , onceTrailers + , onceWantTrailers + , sendTrailers + , onData + , onceEnd + , destroy + , closeStream + ) where + +import Prelude + +import Effect (Effect) +import Effect.Exception (Error) +import Node.Buffer (Buffer) +import Node.HTTP2 (Flags, HeadersObject, OptionsObject) +import Node.HTTP2.Internal as Internal +import Node.Net.Socket (Socket) +import Node.Stream (Duplex) +import Node.URL (URL) +import Unsafe.Coerce (unsafeCoerce) + +-- | > Every `Http2Session` instance is associated with exactly one `net.Socket` or `tls.TLSSocket` when it is created. When either the `Socket` or the `Http2Session` are destroyed, both will be destroyed. +-- | +-- | See [__Class: ClientHttp2Session__](https://nodejs.org/docs/latest/api/http2.html#class-clienthttp2session) +foreign import data ClientHttp2Session :: Type + +-- | https://nodejs.org/docs/latest/api/http2.html#http2connectauthority-options-listener +foreign import connect :: URL -> OptionsObject -> (ClientHttp2Session -> Socket -> Effect Unit) -> Effect ClientHttp2Session + +-- | https://stackoverflow.com/questions/67790720/node-js-net-connect-error-in-spite-of-try-catch +foreign import connectWithError :: URL -> OptionsObject -> (ClientHttp2Session -> Socket -> Effect Unit) -> (Error -> Effect Unit) -> Effect ClientHttp2Session + +-- | https://nodejs.org/api/net.html#event-ready +foreign import onceReady :: Socket -> (Effect Unit) -> Effect (Effect Unit) + +-- | A client-side `Http2Stream`. +-- | +-- | See [__Class: ClientHttp2Stream__](https://nodejs.org/docs/latest/api/http2.html#class-clienthttp2stream) +foreign import data ClientHttp2Stream :: Type + +-- |https://nodejs.org/docs/latest/api/http2.html#clienthttp2sessionrequestheaders-options +foreign import request :: ClientHttp2Session -> HeadersObject -> OptionsObject -> Effect ClientHttp2Stream + +-- | https://nodejs.org/docs/latest/api/http2.html#destruction +foreign import destroy :: ClientHttp2Stream -> Effect Unit + +-- | https://nodejs.org/docs/latest/api/http2.html#http2sessionclosecallback +closeSession :: ClientHttp2Session -> Effect Unit -> Effect Unit +closeSession = unsafeCoerce Internal.closeSession + +-- | https://nodejs.org/docs/latest/api/http2.html#event-response +-- | +-- | Listen for one event, then remove the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +foreign import onceResponse :: ClientHttp2Stream -> (HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) + +-- | https://nodejs.org/docs/latest/api/http2.html#event-headers +-- | +-- | Listen for one event, then remove the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +foreign import onceHeaders :: ClientHttp2Stream -> (HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) + +-- | https://nodejs.org/docs/latest/api/http2.html#event-stream +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#push-streams-on-the-client +-- | +-- | Listen for one event, then remove the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +onceStream :: ClientHttp2Session -> (ClientHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) +onceStream = unsafeCoerce Internal.onceStream + +-- | https://nodejs.org/docs/latest/api/http2.html#event-stream +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#push-streams-on-the-client +-- | +-- | Returns an effect for removing the event listener. +onStream :: ClientHttp2Session -> (ClientHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) +onStream = unsafeCoerce Internal.onStream + +-- | https://nodejs.org/docs/latest/api/http2.html#event-error +-- | +-- | Listen for one event, then remove the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +onceErrorSession :: ClientHttp2Session -> (Error -> Effect Unit) -> Effect (Effect Unit) +onceErrorSession = unsafeCoerce Internal.onceEmitterError + +-- | https://nodejs.org/docs/latest/api/http2.html#event-error_1 +-- | +-- | Listen for one event, then remove the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +onceErrorStream :: ClientHttp2Stream -> (Error -> Effect Unit) -> Effect (Effect Unit) +onceErrorStream = unsafeCoerce Internal.onceEmitterError + +-- | https://nodejs.org/docs/latest/api/http2.html#event-push +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#push-streams-on-the-client +foreign import oncePush :: ClientHttp2Stream -> (HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) + +-- | https://nodejs.org/docs/latest/api/http2.html#event-trailers +-- | +-- | Listen for one event, then remove the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +onceTrailers :: ClientHttp2Stream -> (HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) +onceTrailers = unsafeCoerce Internal.onceTrailers + +-- | https://nodejs.org/docs/latest/api/http2.html#event-wanttrailers +-- | +-- | Listen for one event, then remove the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +onceWantTrailers :: ClientHttp2Stream -> Effect Unit -> Effect (Effect Unit) +onceWantTrailers = unsafeCoerce Internal.onceWantTrailers + +-- | https://nodejs.org/docs/latest/api/http2.html#http2streamsendtrailersheaders +-- | +-- | > When sending a request or sending a response, the `options.waitForTrailers` option must be set in order to keep the `Http2Stream` open after the final `DATA` frame so that trailers can be sent. +sendTrailers :: ClientHttp2Stream -> HeadersObject -> Effect Unit +sendTrailers = unsafeCoerce Internal.sendTrailers + +-- | https://nodejs.org/docs/latest/api/stream.html#event-data +-- | +-- | Returns an effect for removing the event listener. +onData :: ClientHttp2Stream -> (Buffer -> Effect Unit) -> Effect (Effect Unit) +onData = unsafeCoerce Internal.onData + +-- | https://nodejs.org/docs/latest/api/net.html#event-end +-- | +-- | Listen for one event, then remove the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +onceEnd :: ClientHttp2Stream -> Effect Unit -> Effect (Effect Unit) +onceEnd = unsafeCoerce Internal.onceEnd + +-- | https://nodejs.org/docs/latest/api/http2.html#http2streamclosecode-callback +closeStream :: ClientHttp2Stream -> Int -> Effect Unit -> Effect Unit +closeStream stream code = Internal.closeStream (unsafeCoerce stream) code + +-- | Coerce to a duplex stream. +toDuplex :: ClientHttp2Stream -> Duplex +toDuplex = unsafeCoerce diff --git a/src/Node/HTTP2/Client/Aff.purs b/src/Node/HTTP2/Client/Aff.purs new file mode 100644 index 0000000..da7070a --- /dev/null +++ b/src/Node/HTTP2/Client/Aff.purs @@ -0,0 +1,277 @@ +-- | Bindings to the *Node.js* HTTP/2 Client Core API. +-- | +-- | ## Client-side example +-- | +-- | Equivalent to +-- | [*Node.js* HTTP/2 Core API __Client-side example__](https://nodejs.org/docs/latest/api/http2.html#client-side-example) +-- | +-- | ``` +-- | import Node.Stream.Aff (readAll) +-- | +-- | ca <- liftEffect $ Node.FS.Sync.readFile "localhost-cert.pem" +-- | +-- | either (liftEffect <<< Console.errorShow) pure =<< attempt do +-- | client <- connect +-- | (toOptions {ca}) +-- | (URL.parse "https://localhost:8443") +-- | +-- | stream <- request client +-- | (toOptions {}) +-- | (toHeaders {":path": "/"}) +-- | +-- | headers <- waitResponse stream +-- | liftEffect $ for_ (headerKeys headers) \name -> +-- | Console.log $ +-- | name <> ": " <> fromMaybe "" (headerString headers name) +-- | +-- | body <- toStringUTF8 =<< (fst <$> readAll (toDuplex stream)) +-- | liftEffect $ Console.log $ "\n" <> body +-- | +-- | closeSession client +-- | ``` +module Node.HTTP2.Client.Aff + ( connect + , request + , waitResponse + , waitPush + , waitHeadersAdditional + , waitEnd + , waitWantTrailers + , sendTrailers + , closeStream + , closeSession + , module ReClient + ) where + +import Prelude + +import Control.Alt (alt) +import Control.Parallel (parallel, sequential) +import Data.Either (Either(..)) +import Data.Maybe (Maybe(..)) +import Effect.Aff (Aff, effectCanceler, makeAff, nonCanceler) +import Effect.Exception (catchException) +import Node.HTTP2 (HeadersObject, OptionsObject) +import Node.HTTP2.Client (ClientHttp2Session, ClientHttp2Stream, toDuplex) +import Node.HTTP2.Client (ClientHttp2Session, ClientHttp2Stream, toDuplex) as ReClient +import Node.HTTP2.Client as Client +import Node.Stream.Aff.Internal as Node.Stream.Aff.Internal +import Node.URL (URL) + +-- | Connect a client `Http2Session`. +-- | +-- | See [`http2.connect(authority[, options][, listener])`](https://nodejs.org/docs/latest/api/http2.html#http2connectauthority-options-listener) +connect :: OptionsObject -> URL -> Aff ClientHttp2Session +connect options url = makeAff \complete -> do + _ <- Client.connectWithError url options + (\session _ -> complete (Right session)) + (\err -> complete (Left err)) + pure nonCanceler + +-- | Gracefully closes the `Http2Session`, allowing any existing streams +-- | to complete on their own and preventing new `Http2Stream` instances +-- | from being created. +-- | +-- | See [`http2session.close([callback])`](https://nodejs.org/docs/latest/api/http2.html#http2sessionclosecallback) +closeSession :: ClientHttp2Session -> Aff Unit +closeSession session = makeAff \complete -> do + catchException (complete <<< Left) do + Client.closeSession session do + (complete (Right unit)) + pure nonCanceler + +-- | Begin a client request to the connected server. +-- | +-- | See [`clienthttp2session.request(headers[, options])`](https://nodejs.org/docs/latest/api/http2.html#clienthttp2sessionrequestheaders-options) +-- | +-- | With `toOptions {endStream: false}`, this can be followed by calls +-- | to +-- | [`Node.Stream.Aff.write`](https://pursuit.purescript.org/packages/purescript-node-streams-aff/docs/Node.Stream.Aff#v:write) +-- | and +-- | [`Node.Stream.Aff.end`](https://pursuit.purescript.org/packages/purescript-node-streams-aff/docs/Node.Stream.Aff#v:end). +-- | The default is `{endStream: true}`. +-- | +-- | Follow with calls to +-- | `waitResponse` +-- | and +-- | [`Node.Stream.Aff.readAll`](https://pursuit.purescript.org/packages/purescript-node-streams-aff/docs/Node.Stream.Aff#v:readAll). +request :: ClientHttp2Session -> OptionsObject -> HeadersObject -> Aff ClientHttp2Stream +request session options headers = makeAff \complete -> do + -- One of the request() options is `abortsignal` which takes a + -- Web.Fetch.AbortController.signal. + -- We want the request() to be cancellable. + -- Unfortunately request() is completely synchronous so there is no way + -- to asynchronously wait on it or cancel the request(). + stream <- catchException (pure <<< Left) do + Right <$> Client.request session headers options + complete stream + pure nonCanceler + +-- | Wait to receive response headers from the server. +-- | +-- | Follow with +-- | `waitPush` or +-- | [`Node.Stream.Aff.readAll`](https://pursuit.purescript.org/packages/purescript-node-streams-aff/docs/Node.Stream.Aff#v:readAll). +waitResponse :: ClientHttp2Stream -> Aff HeadersObject +waitResponse stream = makeAff \complete -> do + onceErrorStreamCancel <- Client.onceErrorStream stream (complete <<< Left) + onceResponseCancel <- Client.onceResponse stream \headers _ -> do + onceErrorStreamCancel + complete (Right headers) + pure $ effectCanceler do + onceErrorStreamCancel + onceResponseCancel + +-- | Wait to receive a pushed stream from the server. +-- | +-- | Returns the client request headers for a request which the client +-- | did not send, and also the response headers and the pushed stream. +-- | +-- | See [Push streams on the client](https://nodejs.org/docs/latest/api/http2.html#push-streams-on-the-client) +-- | +-- | Follow with +-- | [`Node.Stream.Aff.readAll`](https://pursuit.purescript.org/packages/purescript-node-streams-aff/docs/Node.Stream.Aff#v:readAll). +waitPush + :: ClientHttp2Session + -> Aff { headersRequest :: HeadersObject, headersResponse :: HeadersObject, streamPushed :: ClientHttp2Stream } +waitPush session = do + { streamPushed, headersRequest } <- waitStream + { headersResponse } <- waitPush' streamPushed + pure { headersRequest, headersResponse, streamPushed } + where + waitStream = makeAff \complete -> do + -- What if there is some other session error event while we're waiting + -- for 'stream' and concurrently (parSequence_) waiting for some other event? + -- Maybe it's right that all waiters get errored. + -- Do we need to remove the once 'stream' event too in error event handler? + onceErrorSessionCancel <- Client.onceErrorSession session (complete <<< Left) + onceStreamCancel <- Client.onceStream session \streamPushed headersRequest _ -> do + onceErrorSessionCancel + complete (Right { streamPushed, headersRequest }) + pure $ effectCanceler do + onceErrorSessionCancel + onceStreamCancel + waitPush' streamPushed = makeAff \complete -> do + onceErrorStreamCancel <- Client.onceErrorStream streamPushed (complete <<< Left) + oncePushCancel <- Client.oncePush streamPushed \headersResponse _ -> do + onceErrorStreamCancel + complete (Right { headersResponse }) + pure $ effectCanceler do + onceErrorStreamCancel + oncePushCancel + +-- | Wait for an additional block of headers to be received from a stream, +-- | such as when a block of `1xx` informational headers is received. +-- | +-- | See +-- | [Event: `'headers'`](https://nodejs.org/docs/latest/api/http2.html#event-headers) +waitHeadersAdditional :: ClientHttp2Stream -> Aff HeadersObject +waitHeadersAdditional stream = makeAff \complete -> do + onceErrorStreamCancel <- Client.onceErrorStream stream (complete <<< Left) + onceHeadersCancel <- Client.onceHeaders stream \headers _ -> do + onceErrorStreamCancel + complete (Right headers) + pure $ effectCanceler do + onceErrorStreamCancel + onceHeadersCancel + +-- | Close the client stream. +-- | +-- | See [`http2stream.close(code[, callback])`](https://nodejs.org/docs/latest/api/http2.html#http2streamclosecode-callback) +-- | +-- | Note that `http2stream.close()` __cannot__ be called instead of +-- | `http2stream.sendTrailers()` as suggested by this passage. +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#clienthttp2sessionrequestheaders-options +-- | +-- | > When `options.waitForTrailers` is set, the `Http2Stream` will +-- | > not automatically close when the final `DATA` frame is +-- | > transmitted. User code must call either +-- | > `http2stream.sendTrailers()` or `http2stream.close()` to +-- | > close the `Http2Stream`. +-- | +closeStream :: ClientHttp2Stream -> Int -> Aff Unit +closeStream stream code = makeAff \complete -> do + catchException (complete <<< Left) do + Client.closeStream stream code $ complete (Right unit) + pure nonCanceler + +-- | Wait for the +-- | [`wantTrailers`](https://nodejs.org/docs/latest/api/http2.html#event-wanttrailers) +-- | event. +-- | +-- | > When initiating a `request` or `response`, the `waitForTrailers` option must +-- | > be set for this event to be emitted. +-- | +-- | Follow this with a call to `sendTrailers`. +waitWantTrailers :: ClientHttp2Stream -> Aff Unit +waitWantTrailers stream = makeAff \complete -> do + onceWantTrailersCancel <- Client.onceWantTrailers stream $ complete (Right unit) + pure $ effectCanceler do + onceWantTrailersCancel + +-- | Send a trailing `HEADERS` frame to the connected HTTP/2 peer. +-- | This will cause the `Http2Stream` to immediately close and must +-- | only be called after the final `DATA` frame is signalled with +-- | [`Node.Stream.Aff.end`](https://pursuit.purescript.org/packages/purescript-node-streams-aff/docs/Node.Stream.Aff#v:end). +-- | +-- | See [`http2stream.sendTrailers(headers)`](https://nodejs.org/docs/latest/api/http2.html#http2streamsendtrailersheaders) +-- | +-- | > When sending a request or sending a response, the +-- | > `options.waitForTrailers` option must be set in order to keep +-- | > the `Http2Stream` open after the final `DATA` frame so that +-- | > trailers can be sent. +sendTrailers :: ClientHttp2Stream -> HeadersObject -> Aff Unit +sendTrailers stream headers = makeAff \complete -> do + catchException (complete <<< Left) do + Client.sendTrailers stream headers + complete (Right unit) + pure nonCanceler + +-- | Wait for the end of the `Readable` response stream from the server. +-- | Maybe return +-- | trailing header fields (“trailers”) if found at the end of the stream. +waitEnd :: ClientHttp2Stream -> Aff (Maybe HeadersObject) +waitEnd stream = do + result :: Either HeadersObject Unit <- sequential $ + alt + do + parallel do + Left <$> waitTrailers stream + do + parallel do + Right <$> waitEnd' stream + case result of + Left trailers -> do + waitEnd' stream + pure (Just trailers) + Right _ -> pure Nothing + where + + -- | Wait to receive a block of headers associated with trailing header fields. + -- | + -- | See + -- | [Event: `'trailers'`](https://nodejs.org/docs/latest/api/http2.html#event-trailers) + waitTrailers :: ClientHttp2Stream -> Aff HeadersObject + waitTrailers stream' = makeAff \complete -> do + onceErrorStreamCancel <- Client.onceErrorStream stream' (complete <<< Left) + onceTrailersCancel <- Client.onceTrailers stream' \headers _flags -> do + onceErrorStreamCancel + complete (Right headers) + pure $ effectCanceler do + onceErrorStreamCancel + onceTrailersCancel + + -- | Wait for the end of the `Readable` stream from the server. + waitEnd' :: ClientHttp2Stream -> Aff Unit + waitEnd' stream' = makeAff \complete -> do + readable <- Node.Stream.Aff.Internal.readable (toDuplex stream') + if readable then do + onceErrorStreamCancel <- Client.onceErrorStream stream' (complete <<< Left) + onceEndCancel <- Client.onceEnd stream' $ complete (Right unit) + pure $ effectCanceler do + onceErrorStreamCancel + onceEndCancel + else do + complete (Right unit) + pure nonCanceler diff --git a/src/Node/HTTP2/Constants.purs b/src/Node/HTTP2/Constants.purs new file mode 100644 index 0000000..5c669d6 --- /dev/null +++ b/src/Node/HTTP2/Constants.purs @@ -0,0 +1,66 @@ +-- | # `http2.constants` +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#http2constants +module Node.HTTP2.Constants where + +import Data.Maybe (Maybe(..)) + +-- | Error codes for `RST_STREAM` and `GOAWAY`. +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#http2constants +type NGHTTP2 = Int + +-- | Get the Constant string for an NGHTTP2 error code. +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#error-codes-for-rst_stream-and-goaway +ngHTTP2_String :: NGHTTP2 -> Maybe String +ngHTTP2_String 0 = Just "NGHTTP2_NO_ERROR" +ngHTTP2_String 1 = Just "NGHTTP2_PROTOCOL_ERROR" +ngHTTP2_String 2 = Just "NGHTTP2_INTERNAL_ERROR" +ngHTTP2_String 3 = Just "NGHTTP2_FLOW_CONTROL_ERROR" +ngHTTP2_String 4 = Just "NGHTTP2_SETTINGS_TIMEOUT" +ngHTTP2_String 5 = Just "NGHTTP2_STREAM_CLOSED" +ngHTTP2_String 6 = Just "NGHTTP2_FRAME_SIZE_ERROR" +ngHTTP2_String 7 = Just "NGHTTP2_REFUSED_STREAM" +ngHTTP2_String 8 = Just "NGHTTP2_CANCEL" +ngHTTP2_String 9 = Just "NGHTTP2_COMPRESSION_ERROR" +ngHTTP2_String 10 = Just "NGHTTP2_CONNECT_ERROR" +ngHTTP2_String 11 = Just "NGHTTP2_ENHANCE_YOUR_CALM" +ngHTTP2_String 12 = Just "NGHTTP2_INADEQUATE_SECURITY" +ngHTTP2_String 13 = Just "NGHTTP2_HTTP_1_1_REQUIRED" +ngHTTP2_String _ = Nothing + +-- | Get the Name for an NGHTTP2 error code. +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#error-codes-for-rst_stream-and-goaway +ngHTTP2_Name :: NGHTTP2 -> Maybe String +ngHTTP2_Name 0 = Just "No Error" +ngHTTP2_Name 1 = Just "Protocol Error" +ngHTTP2_Name 2 = Just "Internal Error" +ngHTTP2_Name 3 = Just "Flow Control Error" +ngHTTP2_Name 4 = Just "Settings Timeout" +ngHTTP2_Name 5 = Just "Stream Closed" +ngHTTP2_Name 6 = Just "Frame Size Error" +ngHTTP2_Name 7 = Just "Refused Stream" +ngHTTP2_Name 8 = Just "Cancel" +ngHTTP2_Name 9 = Just "Compression Error" +ngHTTP2_Name 10 = Just "Connect Error" +ngHTTP2_Name 11 = Just "Enhance Your Calm" +ngHTTP2_Name 12 = Just "Inadequate Security" +ngHTTP2_Name 13 = Just "HTTP/1.1 Required" +ngHTTP2_Name _ = Nothing + +ngHTTP2_NO_ERROR = 0 :: NGHTTP2 +ngHTTP2_PROTOCOL_ERROR = 1 :: NGHTTP2 +ngHTTP2_INTERNAL_ERROR = 2 :: NGHTTP2 +ngHTTP2_FLOW_CONTROL_ERROR = 3 :: NGHTTP2 +ngHTTP2_SETTINGS_TIMEOUT = 4 :: NGHTTP2 +ngHTTP2_STREAM_CLOSED = 5 :: NGHTTP2 +ngHTTP2_FRAME_SIZE_ERROR = 6 :: NGHTTP2 +ngHTTP2_REFUSED_STREAM = 7 :: NGHTTP2 +ngHTTP2_CANCEL = 8 :: NGHTTP2 +ngHTTP2_COMPRESSION_ERROR = 9 :: NGHTTP2 +ngHTTP2_CONNECT_ERROR = 10 :: NGHTTP2 +ngHTTP2_ENHANCE_YOUR_CALM = 11 :: NGHTTP2 +ngHTTP2_INADEQUATE_SECURITY = 12 :: NGHTTP2 +ngHTTP2_HTTP_1_1_REQUIRED = 13 :: NGHTTP2 diff --git a/src/Node/HTTP2/Internal.js b/src/Node/HTTP2/Internal.js new file mode 100644 index 0000000..815d3c4 --- /dev/null +++ b/src/Node/HTTP2/Internal.js @@ -0,0 +1,114 @@ + +export const localSettings = http2session => () => { + return http2session.localSettings; +} + +// https://nodejs.org/docs/latest/api/http2.html#http2streamrespondheaders-options +export const respond = http2stream => headers => options => () => { + http2stream.respond(headers,options); +}; + +// https://nodejs.org/docs/latest/api/http2.html#http2sessionclosecallback +export const closeSession = http2session => callback => () => { + if (http2session.closed) { + callback(); + } + else { + http2session.close(() => callback()); + } +}; + +// https://nodejs.org/docs/latest/api/http2.html#serverclosecallback +export const closeServer = http2server => callback => () => { + http2server.close(() => callback()); +}; + +// https://nodejs.org/docs/latest/api/http2.html#event-close_1 +export const onceClose = http2stream => callback => () => { + const cb = () => callback(http2stream.rstCode)(); + http2stream.once("close", cb); + return () => {http2stream.removeEventListener("close", cb);}; +}; + +// https://nodejs.org/docs/latest/api/http2.html#event-stream +export const onceStream = foreign => callback => () => { + const cb = (stream, headers, flags) => callback(stream)(headers)(flags)(); + foreign.once("stream", cb); + return () => {foreign.removeListener("stream", cb);}; +}; + +// https://nodejs.org/docs/latest/api/http2.html#event-stream +export const onStream = foreign => callback => () => { + const cb = (stream, headers, flags) => callback(stream)(headers)(flags)(); + foreign.on("stream", cb); + return () => {foreign.removeListener("stream", cb);}; +}; + +// https://nodejs.org/docs/latest/api/events.html#nodeeventtargetoncetype-listener-options +export const onceError = eventtarget => callback => () => { + const cb = error => callback(error)(); + eventtarget.once("error", cb); + return () => {eventtarget.removeEventListener("error", cb);}; +}; + +// https://nodejs.org/docs/latest/api/net.html#event-close +export const onceServerClose = server => callback => () => { + const cb = () => callback(); + server.once("close", cb); + return () => {server.removeEventListener("close", cb);}; +}; + +// https://nodejs.org/docs/latest/api/events.html#emitteronceeventname-listener +export const onceEmitterError = eventemitter => callback => () => { + const cb = error => callback(error)(); + eventemitter.once("error", cb); + return () => {eventemitter.removeListener("error", cb);}; +}; + +export const onEmitterError = eventemitter => callback => () => { + const cb = error => callback(error)(); + eventemitter.on("error", cb); + return () => {eventemitter.removeListener("error", cb);}; +}; + +export const throwAllErrors = eventtarget => () => { + eventtarget.addEventListener("error", error => {throw error;}); +}; + +export const onceWantTrailers = http2stream => callback => () => { + const cb = () => callback(); + http2stream.once("wantTrailers", cb); + return () => {http2stream.removeEventListener("wantTrailers", cb);}; +}; + +// https://nodejs.org/docs/latest/api/http2.html#http2streamsendtrailersheaders +export const sendTrailers = http2stream => headers => () => { + http2stream.sendTrailers(headers); +}; + +export const onceTrailers = http2stream => callback => () => { + const cb = (headers,flags) => callback(headers)(flags)(); + http2stream.once("trailers", cb); + return () => {http2stream.removeEventListener("trailers", cb);}; +}; + +export const onData = http2stream => callback => () => { + const cb = chunk => callback(chunk)(); + http2stream.on("data", cb); + return () => {http2stream.removeEventListener("data", cb);}; +}; + +export const onceEnd = netsocket => callback => () => { + const cb = () => callback(); + netsocket.once("end", cb); + return () => {netsocket.removeListener("end", cb);}; +}; + +export const session = http2stream => { + return http2stream.session; +}; + +// https://nodejs.org/docs/latest/api/http2.html#http2streamclosecode-callback +export const closeStream = http2stream => code => callback => () => { + http2stream.close(code, () => callback()); +}; diff --git a/src/Node/HTTP2/Internal.purs b/src/Node/HTTP2/Internal.purs new file mode 100644 index 0000000..68d43b6 --- /dev/null +++ b/src/Node/HTTP2/Internal.purs @@ -0,0 +1,112 @@ +-- | Internals. You should not need to import anything from this module. +-- | If you need to import something from this module then please open an +-- | issue about that. +module Node.HTTP2.Internal where + +import Prelude + +import Effect (Effect) +import Effect.Exception (Error) +import Foreign (Foreign) +import Node.Buffer (Buffer) +import Node.HTTP2 (Flags, HeadersObject, OptionsObject, SettingsObject) +import Node.HTTP2.Constants (NGHTTP2) + +-- | Private type which can be coerced into `ClientHttp2Session` +-- | or `ServerHttp2Session`. +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#class-http2session +-- | +-- | > Every `Http2Session` instance is associated with exactly one +-- | > `net.Socket` or `tls.TLSSocket` when it is created. When either +-- | > the `Socket` or the `Http2Session` are destroyed, both will be destroyed. +foreign import data Http2Session :: Type + +-- | https://nodejs.org/api/http2.html#http2sessionlocalsettings +foreign import localSettings :: Http2Session -> Effect SettingsObject + +-- | Listen for one event, call the callback, then remove +-- | the event listener. +-- | Returns an effect for removing the event listener before the event +-- | is raised. +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#event-stream +foreign import onceStream :: Foreign -> (Http2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) + +-- | https://nodejs.org/docs/latest/api/http2.html#event-stream +foreign import onStream :: Foreign -> (Http2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) + +-- | Listen for one NodeEventTarget `'error'`, call the callback, then remove +-- | the event listener. +-- | Returns an effect for removing the event listener before the event +-- | is raised. +foreign import onceError :: Foreign -> (Error -> Effect Unit) -> Effect (Effect Unit) + +-- | Listen for one EventEmitter `'error'`, call the callback, then remove +-- | the event listener. +-- | Returns an effect for removing the event listener before the event +-- | is raised. +foreign import onceEmitterError :: Foreign -> (Error -> Effect Unit) -> Effect (Effect Unit) + +-- | EventEmitter `on 'error'` +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +foreign import onEmitterError :: Foreign -> (Error -> Effect Unit) -> Effect (Effect Unit) + +-- | https://nodejs.org/docs/latest/api/http2.html#http2sessionclosecallback +foreign import closeSession :: Http2Session -> Effect Unit -> Effect Unit + +-- | https://nodejs.org/docs/latest/api/http2.html#serverclosecallback +foreign import closeServer :: Foreign -> Effect Unit -> Effect Unit + +-- | https://nodejs.org/docs/latest/api/net.html#event-close +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +foreign import onceServerClose :: Foreign -> Effect Unit -> Effect (Effect Unit) + +-- | To an `EventTarget` attach an `'error'` listener which will always throw +-- | a synchronous `Error`. +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#error-handling +-- | +-- | > (Errors) will be reported using either a synchronous throw or via +-- | > an 'error' event on the `Http2Stream`, `Http2Session` or +-- | > `Http2Server` objects, depending on where and when the error occurs. +-- | +-- | https://nodejs.org/api/events.html#eventtargetaddeventlistenertype-listener-options +foreign import throwAllErrors :: Foreign -> Effect Unit + +-- | Private type which can be coerced into ClientHttp2Stream +-- | or ServerHttp2Stream or Duplex. +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#class-http2stream +foreign import data Http2Stream :: Type + +-- | https://nodejs.org/docs/latest/api/http2.html#event-close_1 +foreign import onceClose :: Http2Stream -> (NGHTTP2 -> Effect Unit) -> Effect (Effect Unit) + +-- | https://nodejs.org/docs/latest/api/http2.html#http2streamrespondheaders-options +foreign import respond :: Http2Stream -> HeadersObject -> OptionsObject -> Effect Unit + +-- | https://nodejs.org/docs/latest/api/http2.html#event-wanttrailers +foreign import onceWantTrailers :: Http2Stream -> Effect Unit -> Effect (Effect Unit) + +-- | https://nodejs.org/docs/latest/api/http2.html#http2streamsendtrailersheaders +foreign import sendTrailers :: Http2Stream -> HeadersObject -> Effect Unit + +-- | https://nodejs.org/docs/latest/api/http2.html#event-trailers +foreign import onceTrailers :: Http2Stream -> (HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) + +-- | https://nodejs.org/docs/latest/api/stream.html#event-data +foreign import onData :: Http2Stream -> (Buffer -> Effect Unit) -> Effect (Effect Unit) + +-- | https://nodejs.org/docs/latest/api/net.html#event-end +foreign import onceEnd :: Http2Stream -> Effect Unit -> Effect (Effect Unit) + +-- | https://nodejs.org/api/http2.html#http2streamsession +foreign import session :: Http2Stream -> Http2Session + +-- | https://nodejs.org/docs/latest/api/http2.html#http2streamclosecode-callback +foreign import closeStream :: Http2Stream -> Int -> Effect Unit -> Effect Unit diff --git a/src/Node/HTTP2/Server.js b/src/Node/HTTP2/Server.js new file mode 100644 index 0000000..b9c12f3 --- /dev/null +++ b/src/Node/HTTP2/Server.js @@ -0,0 +1,40 @@ +import http2 from "http2"; + +export const createServer = options => () => { + const server = http2.createServer(options); + return server; +}; + +export const createSecureServer = options => () => { + const server = http2.createSecureServer(options); + return server; +}; + +// https://nodejs.org/docs/latest/api/net.html#serverlistenoptions-callback +export const listen = server => options => callback => () => { + // TODO the completion callback should be Maybe Error -> Effect Unit + server.listen(options, () => callback()); +}; + +export const onceSession = http2server => callback => () => { + const cb = session => callback(session)(); + http2server.once("session", cb); + return () => http2server.removeEventListener("session", cb); +}; + +// https://nodejs.org/docs/latest/api/http2.html#http2streampushallowed +export const pushAllowed = http2stream => () => { + return http2stream.pushAllowed; +}; + +// https://nodejs.org/docs/latest/api/http2.html#http2streampushstreamheaders-options-callback +export const pushStream = http2stream => headers => options => callback => () => { + http2stream.pushStream(headers, options, + (err,pushStream2,headers2) => callback(err)(pushStream2)(headers2)() + ); +}; + +// https://nodejs.org/docs/latest/api/http2.html#http2streamadditionalheadersheaders +export const additionalHeaders = http2stream => headers => () => { + http2stream.additionalHeaders(headers); +}; diff --git a/src/Node/HTTP2/Server.purs b/src/Node/HTTP2/Server.purs new file mode 100644 index 0000000..3f18b62 --- /dev/null +++ b/src/Node/HTTP2/Server.purs @@ -0,0 +1,289 @@ +-- | Low-level bindings to the *Node.js* HTTP/2 Server Core API. +-- | +-- | ## Server-side example +-- | +-- | Equivalent to +-- | https://nodejs.org/docs/latest/api/http2.html#server-side-example +-- | +-- | ``` +-- | key <- Node.FS.Sync.readFile "localhost-privkey.pem" +-- | cert <- Node.FS.Sync.readFile "localhost-cert.pem" +-- | +-- | server <- createSecureServer (toOptions {key, cert}) +-- | _ <- onErrorServerSecure server Console.errorShow +-- | +-- | _ <- onceStreamSecure server \stream headers flags -> do +-- | respond stream +-- | (toHeaders +-- | { "content-type": "text/html; charset=utf-8" +-- | , ":status": 200 +-- | } +-- | ) +-- | (toOptions {}) +-- | void $ Node.Stream.writeString (toDuplex stream) +-- | Node.Encoding.UTF8 +-- | "

Hello World

" +-- | (\_ -> pure unit) +-- | Node.Stream.end (toDuplex stream) (\_ -> pure unit) +-- | +-- | listenSecure server +-- | (toOptions { port: 8443 }) +-- | (pure unit) +-- | ``` +module Node.HTTP2.Server + ( Http2Server + , createServer + , listen + , onceSession + , onStream + , onceStream + , onErrorServer + , closeServer + , onceCloseServer + , Http2SecureServer + , createSecureServer + , listenSecure + , onceSessionSecure + , onStreamSecure + , onceStreamSecure + , onErrorServerSecure + , closeServerSecure + , onceCloseServerSecure + , ServerHttp2Session + , respond + , localSettings + , closeSession + , ServerHttp2Stream + , session + , onceErrorStream + , pushAllowed + , pushStream + , onceTrailers + , additionalHeaders + , onceWantTrailers + , sendTrailers + , onceEnd + , toDuplex + , closeStream + ) where + +import Prelude + +import Data.Nullable (Nullable) +import Effect (Effect) +import Effect.Exception (Error) +import Node.HTTP2 (Flags, HeadersObject, OptionsObject, SettingsObject) +import Node.HTTP2.Internal as Internal +import Node.Stream (Duplex) +import Unsafe.Coerce (unsafeCoerce) + +-- | An HTTP/2 server with one listening socket for unencrypted connections. +-- | +-- | See [__Class: Http2Server__](https://nodejs.org/docs/latest/api/http2.html#class-http2server) +foreign import data Http2Server :: Type + +-- | https://nodejs.org/docs/latest/api/http2.html#http2createserveroptions-onrequesthandler +foreign import createServer :: OptionsObject -> Effect Http2Server + +-- | https://nodejs.org/docs/latest/api/net.html#serverlistenoptions-callback +foreign import listen :: Http2Server -> OptionsObject -> Effect Unit -> Effect Unit + +-- | https://nodejs.org/docs/latest/api/http2.html#serverclosecallback +closeServer :: Http2Server -> Effect Unit -> Effect Unit +closeServer = unsafeCoerce Internal.closeServer + +-- | https://nodejs.org/docs/latest/api/net.html#event-close +-- | +-- | Listen for one event, then remove the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +onceCloseServer :: Http2Server -> Effect Unit -> Effect (Effect Unit) +onceCloseServer = unsafeCoerce Internal.onceServerClose + +-- | An HTTP/2 server with one listening socket for encrypted connections. +-- | +-- | See [__Class: Http2SecureServer__](https://nodejs.org/docs/latest/api/http2.html#class-http2secureserver) +foreign import data Http2SecureServer :: Type + +-- | https://nodejs.org/docs/latest/api/http2.html#http2createsecureserveroptions-onrequesthandler +-- | +-- | Required options: `key :: String`, `cert :: String`. +foreign import createSecureServer :: OptionsObject -> Effect Http2SecureServer + +-- | https://nodejs.org/docs/latest/api/net.html#serverlistenoptions-callback +listenSecure :: Http2SecureServer -> OptionsObject -> Effect Unit -> Effect Unit +listenSecure = unsafeCoerce listen + +-- | https://nodejs.org/docs/latest/api/http2.html#serverclosecallback +closeServerSecure :: Http2SecureServer -> Effect Unit -> Effect Unit +closeServerSecure = unsafeCoerce Internal.closeServer + +-- | https://nodejs.org/docs/latest/api/net.html#event-close +-- | +-- | Listen for one event, then remove the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +onceCloseServerSecure :: Http2SecureServer -> Effect Unit -> Effect (Effect Unit) +onceCloseServerSecure = unsafeCoerce Internal.onceServerClose + +-- | https://nodejs.org/docs/latest/api/net.html#event-error +-- | +-- | the 'close' event will not be emitted directly following this event unless server.close() is manually called. +-- | +-- | Returns an effect for removing the event listener. +onErrorServer :: Http2Server -> (Error -> Effect Unit) -> Effect (Effect Unit) +onErrorServer = unsafeCoerce Internal.onEmitterError + +-- | https://nodejs.org/docs/latest/api/net.html#event-error +-- | +-- | the 'close' event will not be emitted directly following this event unless server.close() is manually called. +-- | +-- | Returns an effect for removing the event listener. +onErrorServerSecure :: Http2SecureServer -> (Error -> Effect Unit) -> Effect (Effect Unit) +onErrorServerSecure = unsafeCoerce Internal.onEmitterError + +-- | https://nodejs.org/docs/latest/api/http2.html#class-serverhttp2session +-- | +-- | > Every `Http2Session` instance is associated with exactly one +-- | > `net.Socket` or `tls.TLSSocket` when it is created. When either +-- | > the `Socket` or the `Http2Session` are destroyed, both will be destroyed. +-- | +-- | > On the server side, user code should rarely have occasion to work +-- | > with the `Http2Session` object directly, with most actions typically +-- | > taken through interactions with either the `Http2Server` or `Http2Stream` objects. +foreign import data ServerHttp2Session :: Type + +session :: ServerHttp2Stream -> ServerHttp2Session +session = unsafeCoerce Internal.session + +-- | https://nodejs.org/docs/latest/api/http2.html#event-session +-- | +-- | Listen for one event, call the callback, then remove +-- | the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +foreign import onceSession :: Http2Server -> (ServerHttp2Session -> Effect Unit) -> Effect (Effect Unit) + +-- | https://nodejs.org/api/http2.html#http2sessionlocalsettings +localSettings :: ServerHttp2Session -> Effect SettingsObject +localSettings = unsafeCoerce Internal.localSettings + +-- | https://nodejs.org/docs/latest/api/http2.html#http2sessionclosecallback +closeSession :: ServerHttp2Session -> Effect Unit -> Effect Unit +closeSession = unsafeCoerce Internal.closeSession + +-- | Listen for one event, call the callback, then remove +-- | the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#event-stream +onceStream :: Http2Server -> (ServerHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) +onceStream = unsafeCoerce Internal.onceStream + +-- | https://nodejs.org/docs/latest/api/http2.html#event-stream +-- | +-- | Returns an effect for removing the event listener. +onStream :: Http2Server -> (ServerHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) +onStream = unsafeCoerce Internal.onStream + +-- | Listen for one event, call the callback, then remove +-- | the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#event-stream +onceStreamSecure :: Http2SecureServer -> (ServerHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) +onceStreamSecure = unsafeCoerce Internal.onceStream + +-- | https://nodejs.org/docs/latest/api/http2.html#event-stream +-- | +-- | Returns an effect for removing the event listener. +onStreamSecure :: Http2SecureServer -> (ServerHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) +onStreamSecure = unsafeCoerce Internal.onStream + +-- | https://nodejs.org/docs/latest/api/http2.html#http2streamrespondheaders-options +respond :: ServerHttp2Stream -> HeadersObject -> OptionsObject -> Effect Unit +respond = unsafeCoerce Internal.respond + +-- | https://nodejs.org/docs/latest/api/http2.html#event-session_1 +-- | +-- | Listen for one event, then remove the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +onceSessionSecure :: Http2SecureServer -> (ServerHttp2Session -> Effect Unit) -> Effect Unit +onceSessionSecure = unsafeCoerce onceSession + +-- | An HTTP/2 server `Http2Stream` connected to a client. +-- | +-- | See [__Class: ServerHttp2Stream__](https://nodejs.org/docs/latest/api/http2.html#class-serverhttp2stream) +foreign import data ServerHttp2Stream :: Type + +-- | https://nodejs.org/docs/latest/api/http2.html#http2streampushallowed +foreign import pushAllowed :: ServerHttp2Stream -> Effect Boolean + +-- | https://nodejs.org/docs/latest/api/http2.html#http2streampushstreamheaders-options-callback +-- | +-- | > Calling `http2stream.pushStream()` from within a pushed stream is not permitted and will throw an error. +-- | +-- | https://www.rfc-editor.org/rfc/rfc7540#section-8.2.1 +foreign import pushStream :: ServerHttp2Stream -> HeadersObject -> OptionsObject -> (Nullable Error -> ServerHttp2Stream -> HeadersObject -> Effect Unit) -> Effect Unit + +-- | https://nodejs.org/docs/latest/api/http2.html#event-trailers +-- | +-- | Listen for one event, then remove the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +onceTrailers :: ServerHttp2Stream -> (HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) +onceTrailers = unsafeCoerce Internal.onceTrailers + +-- | https://nodejs.org/docs/latest/api/http2.html#http2streamadditionalheadersheaders +foreign import additionalHeaders :: ServerHttp2Stream -> HeadersObject -> Effect Unit + +-- | https://nodejs.org/docs/latest/api/http2.html#event-error_1 +-- | +-- | Listen for one event, then remove the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +onceErrorStream :: ServerHttp2Stream -> (Error -> Effect Unit) -> Effect (Effect Unit) +onceErrorStream = unsafeCoerce Internal.onceEmitterError + +-- | https://nodejs.org/docs/latest/api/http2.html#event-wanttrailers +-- | +-- | Listen for one event, then remove the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +onceWantTrailers :: ServerHttp2Stream -> Effect Unit -> Effect (Effect Unit) +onceWantTrailers = unsafeCoerce Internal.onceWantTrailers + +-- | https://nodejs.org/docs/latest/api/http2.html#http2streamsendtrailersheaders +-- | +-- | > When sending a request or sending a response, the `options.waitForTrailers` option must be set in order to keep the `Http2Stream` open after the final `DATA` frame so that trailers can be sent. +sendTrailers :: ServerHttp2Stream -> HeadersObject -> Effect Unit +sendTrailers = unsafeCoerce Internal.sendTrailers + +-- | https://nodejs.org/docs/latest/api/net.html#event-end +-- | +-- | Listen for one event, then remove the event listener. +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +onceEnd :: ServerHttp2Stream -> Effect Unit -> Effect (Effect Unit) +onceEnd = unsafeCoerce Internal.onceEnd + +-- | https://nodejs.org/docs/latest/api/http2.html#http2streamclosecode-callback +closeStream :: ServerHttp2Stream -> Int -> Effect Unit -> Effect Unit +closeStream stream code = Internal.closeStream (unsafeCoerce stream) code + +-- | Coerce to a duplex stream. +toDuplex :: ServerHttp2Stream -> Duplex +toDuplex = unsafeCoerce diff --git a/src/Node/HTTP2/Server/Aff.purs b/src/Node/HTTP2/Server/Aff.purs new file mode 100644 index 0000000..cab371d --- /dev/null +++ b/src/Node/HTTP2/Server/Aff.purs @@ -0,0 +1,389 @@ +-- | Bindings to the *Node.js* HTTP/2 Server Core API. +-- | +-- | ## Server-side example +-- | +-- | Equivalent to +-- | [*Node.js* HTTP/2 Core API __Server-side example__](https://nodejs.org/docs/latest/api/http2.html#server-side-example) +-- | +-- | ``` +-- | import Node.Stream.Aff (write, end) +-- | +-- | key <- Node.FS.Sync.readFile "localhost-privkey.pem" +-- | cert <- Node.FS.Sync.readFile "localhost-cert.pem" +-- | +-- | either (liftEffect <<< Console.errorShow) pure =<< attempt do +-- | server <- createSecureServer (toOptions {key, cert}) +-- | listenSecure server (toOptions {port:8443}) +-- | \session headers stream -> do +-- | respond stream +-- | (toOptions {}) +-- | (toHeaders +-- | { "content-type": "text/html; charset=utf-8" +-- | , ":status": 200 +-- | } +-- | ) +-- | write (toDuplex stream) =<< fromStringUTF8 ("

Hello World") +-- | end (toDuplex stream) +-- | ``` +module Node.HTTP2.Server.Aff + ( createServer + , listen + , createSecureServer + , listenSecure + , respond + , pushAllowed + , pushStream + , sendHeadersAdditional + , waitEnd + , waitWantTrailers + , sendTrailers + , closeStream + , closeSession + , closeServer + , closeSecureServer + , module ReServer + ) where + +import Prelude + +import Control.Alt (alt) +import Control.Parallel (parallel, sequential) +import Data.Either (Either(..)) +import Data.Maybe (Maybe(..)) +import Data.Nullable (toMaybe) +import Effect.Aff (Aff, effectCanceler, launchAff_, makeAff, nonCanceler) +import Effect.Class (liftEffect) +import Effect.Exception (catchException) +import Node.HTTP2 (HeadersObject, OptionsObject) +import Node.HTTP2.Server (Http2SecureServer, Http2Server, ServerHttp2Session, ServerHttp2Stream, toDuplex) +import Node.HTTP2.Server (Http2Server, Http2SecureServer, ServerHttp2Session, ServerHttp2Stream, toDuplex) as ReServer +import Node.HTTP2.Server as Server +import Node.Stream.Aff.Internal as Node.Stream.Aff.Internal + +-- | Create an insecure (HTTP) HTTP/2 server. +-- | +-- | The argument is the `createServer` options. +-- | See [`http2.createServer([options][, onRequestHandler])`](https://nodejs.org/docs/latest/api/http2.html#http2createserveroptions-onrequesthandler) +createServer + :: OptionsObject + -> Aff Http2Server +createServer options = makeAff \complete -> do + catchException (complete <<< Left) do + server <- Server.createServer options + complete (Right server) + pure nonCanceler + +-- | Open one listening socket for unencrypted connections. +-- | +-- | For each new client connection and request, the handler function will +-- | be invoked by `launchAff` and passed the request. +-- | This makes the handler function uncancellable. +-- | +-- | Will complete after the socket has stopped listening and closed. +-- | +-- | Errors will be thrown through the `Aff` `MonadThrow` instance. +-- | +-- | Listening may be stopped explicity by calling `closeServer` on the +-- | server, or implicitly by `killFiber`. +-- | +-- | For the `listen` options, +-- | see [`server.listen(options[, callback])`](https://nodejs.org/docs/latest/api/net.html#serverlistenoptions-callback) +listen + :: Http2Server + -> OptionsObject + -> (ServerHttp2Session -> HeadersObject -> ServerHttp2Stream -> Aff Unit) + -> Aff Unit +listen server options handler = makeAff \complete -> do + -- The Http2Server is a tls.Server + -- https://nodejs.org/docs/latest/api/tls.html#event-tlsclienterror + -- The Http2Server is a net.Server + -- https://nodejs.org/docs/latest/api/net.html#event-error + -- The Http2Server is an EventEmitter. + -- https://nodejs.org/docs/latest/api/events.html#class-eventemitter + + onStreamCancel <- Server.onStream server \stream headers _ -> do + launchAff_ $ handler (Server.session stream) headers stream + + -- https://nodejs.org/docs/latest/api/net.html#event-error + -- “the 'close' event will not be emitted directly following this event unless server.close() is manually called.” + onErrorCancel <- Server.onErrorServer server \err -> do + onStreamCancel + -- The socket must be closed when listening completes. + Server.closeServer server $ pure unit + complete (Left err) + + onceCloseCancel <- Server.onceCloseServer server do + onStreamCancel + onErrorCancel + complete (Right unit) + + Server.listen server options do + -- We don't want to complete here. + pure unit + + pure $ effectCanceler do + onStreamCancel + onErrorCancel + onceCloseCancel + Server.closeServer server $ pure unit + +-- | Create a secure (HTTPS) HTTP/2 server. +-- | +-- | The argument is the `createServer` options. +-- | See [`http2.createServer([options][, onRequestHandler])`](https://nodejs.org/docs/latest/api/http2.html#http2createserveroptions-onrequesthandler) +-- | +-- | Required options: `key :: String`, `cert :: String`. +createSecureServer + :: OptionsObject + -> Aff Http2SecureServer +createSecureServer options = makeAff \complete -> do + catchException (complete <<< Left) do + server <- Server.createSecureServer options + complete (Right server) + pure nonCanceler + +-- | Secure version of `listen`. Open one listening socket +-- | for encrypted connections. +-- | +-- | For each new client connection and request, the handler function will +-- | be invoked by `launchAff` and passed the request. +-- | This makes the handler function uncancellable. +-- | +-- | Will complete after the socket has stopped listening and closed. +-- | +-- | Errors will be thrown through the `Aff` `MonadThrow` instance. +-- | +-- | Listening may be stopped explicity by calling `closeSecureServer` on the +-- | server, or implicitly by `killFiber`. +-- | +-- | For the `listen` options, +-- | see [`server.listen(options[, callback])`](https://nodejs.org/docs/latest/api/net.html#serverlistenoptions-callback) +listenSecure + :: Http2SecureServer + -> OptionsObject + -> (ServerHttp2Session -> HeadersObject -> ServerHttp2Stream -> Aff Unit) + -> Aff Unit +listenSecure server options handler = makeAff \complete -> do + -- The Http2Server is a tls.Server + -- https://nodejs.org/docs/latest/api/tls.html#event-tlsclienterror + -- The Http2Server is a net.Server + -- https://nodejs.org/docs/latest/api/net.html#event-error + -- The Http2Server is an EventEmitter. + -- https://nodejs.org/docs/latest/api/events.html#class-eventemitter + + onStreamCancel <- Server.onStreamSecure server \stream headers _ -> do + launchAff_ $ handler (Server.session stream) headers stream + + -- https://nodejs.org/docs/latest/api/net.html#event-error + -- “the 'close' event will not be emitted directly following this event unless server.close() is manually called.” + onErrorCancel <- Server.onErrorServerSecure server \err -> do + onStreamCancel + -- The socket must be closed when listening completes. + Server.closeServerSecure server $ pure unit + complete (Left err) + + onceCloseCancel <- Server.onceCloseServerSecure server do + onStreamCancel + onErrorCancel + complete (Right unit) + + Server.listenSecure server options do + -- We don't want to complete here. + pure unit + + pure $ effectCanceler do + onStreamCancel + onErrorCancel + onceCloseCancel + Server.closeServerSecure server $ pure unit + +-- | Begin a server response. +-- | +-- | Follow this with calls to +-- | [`Node.Stream.Aff.write`](https://pursuit.purescript.org/packages/purescript-node-streams-aff/docs/Node.Stream.Aff#v:write) +-- | and +-- | [`Node.Stream.Aff.end`](https://pursuit.purescript.org/packages/purescript-node-streams-aff/docs/Node.Stream.Aff#v:end). +-- | +-- | See +-- | [`http2stream.respond([headers[, options]])`](https://nodejs.org/docs/latest/api/http2.html#http2streamrespondheaders-options) +respond :: ServerHttp2Stream -> OptionsObject -> HeadersObject -> Aff Unit +respond stream options headers = makeAff \complete -> do + catchException (complete <<< Left) do + Server.respond stream headers options + -- TODO wait for respond send? + complete (Right unit) + pure nonCanceler + +-- | Gracefully closes the `Http2Session`, allowing any existing streams +-- | to complete on their own and preventing new `Http2Stream` instances +-- | from being created. +-- | +-- | See [`http2session.close([callback])`](https://nodejs.org/docs/latest/api/http2.html#http2sessionclosecallback) +closeSession :: ServerHttp2Session -> Aff Unit +closeSession session = makeAff \complete -> do + catchException (complete <<< Left) do + Server.closeSession session do + (complete (Right unit)) + pure nonCanceler + +-- | Close the server listening socket. Will complete after socket is closed. +-- | +-- | See [`http2server.close([callback])`](https://nodejs.org/docs/latest/api/http2.html#serverclosecallback) +closeServer :: Http2Server -> Aff Unit +closeServer server = makeAff \complete -> do + catchException (complete <<< Left) do + Server.closeServer server $ complete (Right unit) + pure nonCanceler + +-- | Close the server listening socket. Will complete after socket is closed. +closeSecureServer :: Http2SecureServer -> Aff Unit +closeSecureServer server = makeAff \complete -> do + catchException (complete <<< Left) do + Server.closeServerSecure server $ complete (Right unit) + pure nonCanceler + +pushAllowed :: ServerHttp2Stream -> Aff Boolean +pushAllowed = liftEffect <<< Server.pushAllowed + +-- | Push a stream to the client, with the client request headers for a +-- | request which the client did not send but to which the server will respond. +-- | +-- | On the new pushed stream, it is mandatory to first call `respond`. +-- | +-- | Then call +-- | [`Node.Stream.Aff.write`](https://pursuit.purescript.org/packages/purescript-node-streams-aff/docs/Node.Stream.Aff#v:write) +-- | and +-- | [`Node.Stream.Aff.end`](https://pursuit.purescript.org/packages/purescript-node-streams-aff/docs/Node.Stream.Aff#v:end). +-- | +-- | See [`http2stream.pushStream(headers[, options], callback)`](https://nodejs.org/docs/latest/api/http2.html#http2streampushstreamheaders-options-callback) +-- | +-- | > Calling `http2stream.pushStream()` from within a pushed stream is not permitted and will throw an error. +pushStream :: ServerHttp2Stream -> OptionsObject -> HeadersObject -> Aff ServerHttp2Stream +pushStream stream options headersRequest = makeAff \complete -> do + Server.pushStream stream headersRequest options \nerr pushedstream _ -> do + case toMaybe nerr of + Just err -> complete (Left err) + Nothing -> complete (Right pushedstream) + pure nonCanceler + +-- | Send an additional informational `HEADERS` frame to the connected HTTP/2 peer. +sendHeadersAdditional :: ServerHttp2Stream -> HeadersObject -> Aff Unit +sendHeadersAdditional stream headers = do + liftEffect $ Server.additionalHeaders stream headers + +-- | Wait for the end of the `Readable` request stream from the client. +-- | Maybe return +-- | trailing header fields (“trailers”) if found at the end of the stream. +-- | +-- | This `waitEnd` function must be called concurrently with `readAll`, +-- | for some reason related to the timing of the `'end'` event from Node’s +-- | `ServerHttp2Stream`. +-- | That’s not true for the `HTTP2.Client.Aff.waitEnd` function. +-- | It’s only necessary to call this function when you need the request +-- | trailing headers. +-- | +-- | ``` +-- | parSequence_ +-- | [ do +-- | trailers <- waitEnd stream +-- | , do +-- | buffers <- readAll (Server.Aff.toDuplex stream) +-- | ] +-- | ``` +waitEnd :: ServerHttp2Stream -> Aff (Maybe HeadersObject) +waitEnd stream = do + result :: Either HeadersObject Unit <- sequential $ + alt + do + parallel do + Left <$> waitTrailers stream + do + parallel do + Right <$> waitEnd' stream + case result of + Left trailers -> do + waitEnd' stream + pure (Just trailers) + Right _ -> pure Nothing + where + + -- | Wait to receive a block of headers associated with trailing header fields. + -- | + -- | See + -- | [Event: `'trailers'`](https://nodejs.org/docs/latest/api/http2.html#event-trailers) + waitTrailers :: ServerHttp2Stream -> Aff HeadersObject + waitTrailers stream' = makeAff \complete -> do + onceErrorStreamCancel <- Server.onceErrorStream stream' (complete <<< Left) + onceTrailersCancel <- Server.onceTrailers stream' \headers _flags -> do + onceErrorStreamCancel + complete (Right headers) + pure $ effectCanceler do + onceErrorStreamCancel + onceTrailersCancel + + -- | Wait for the end of the `Readable` stream from the server. + waitEnd' :: ServerHttp2Stream -> Aff Unit + waitEnd' stream' = makeAff \complete -> do + readable <- Node.Stream.Aff.Internal.readable (toDuplex stream') + if readable then do + onceErrorStreamCancel <- Server.onceErrorStream stream' (complete <<< Left) + onceEndCancel <- Server.onceEnd stream' $ complete (Right unit) + pure $ effectCanceler do + onceErrorStreamCancel + onceEndCancel + else do + complete (Right unit) + pure nonCanceler + +-- | Close the server stream. +-- | +-- | See [`http2stream.close(code[, callback])`](https://nodejs.org/docs/latest/api/http2.html#http2streamclosecode-callback) +-- | +-- | Note that `http2stream.close()` __cannot__ be called instead of +-- | `http2stream.sendTrailers()` as suggested by this passage. +-- | +-- | https://nodejs.org/docs/latest/api/http2.html#clienthttp2sessionrequestheaders-options +-- | +-- | > When `options.waitForTrailers` is set, the `Http2Stream` will +-- | > not automatically close when the final `DATA` frame is +-- | > transmitted. User code must call either +-- | > `http2stream.sendTrailers()` or `http2stream.close()` to +-- | > close the `Http2Stream`. +-- | +closeStream :: ServerHttp2Stream -> Int -> Aff Unit +closeStream stream code = makeAff \complete -> do + catchException (complete <<< Left) do + Server.closeStream stream code $ complete (Right unit) + pure nonCanceler + +-- | Wait for the +-- | [`wantTrailers`](https://nodejs.org/docs/latest/api/http2.html#event-wanttrailers) +-- | event. +-- | +-- | > When initiating a `request` or `response`, the `waitForTrailers` option must +-- | > be set for this event to be emitted. +-- | +-- | Follow this with a call to `sendTrailers`. +waitWantTrailers :: ServerHttp2Stream -> Aff Unit +waitWantTrailers stream = makeAff \complete -> do + onceWantTrailersCancel <- Server.onceWantTrailers stream $ complete (Right unit) + pure $ effectCanceler do + onceWantTrailersCancel + +-- | Send a trailing `HEADERS` frame to the connected HTTP/2 peer. +-- | This will cause the `Http2Stream` to immediately close and must +-- | only be called after the final `DATA` frame is signalled with +-- | [`Node.Stream.Aff.end`](https://pursuit.purescript.org/packages/purescript-node-streams-aff/docs/Node.Stream.Aff#v:end). +-- | +-- | See [`http2stream.sendTrailers(headers)`](https://nodejs.org/docs/latest/api/http2.html#http2streamsendtrailersheaders) +-- | +-- | > When sending a request or sending a response, the +-- | > `options.waitForTrailers` option must be set in order to keep +-- | > the `Http2Stream` open after the final `DATA` frame so that +-- | > trailers can be sent. +sendTrailers :: ServerHttp2Stream -> HeadersObject -> Aff Unit +sendTrailers stream headers = makeAff \complete -> do + catchException (complete <<< Left) do + Server.sendTrailers stream headers + complete (Right unit) + pure nonCanceler diff --git a/test/HTTP2.purs b/test/HTTP2.purs new file mode 100644 index 0000000..f99203f --- /dev/null +++ b/test/HTTP2.purs @@ -0,0 +1,94 @@ +module Test.HTTP2 where + +import Prelude + +import Control.Monad.ST.Class (liftST) +import Control.Monad.ST.Ref as ST.Ref +import Data.Either (Either(..)) +import Data.Foldable (for_) +import Data.Maybe (Maybe(..), fromMaybe) +import Effect (Effect) +import Effect.Console as Console +import Effect.Exception (Error, throwException) +import Node.Encoding as Node.Encoding +import Node.HTTP2 (headerKeys, headerString, sensitiveHeaders, toHeaders, toOptions) +import Node.HTTP2.Client as HTTP2.Client +import Node.HTTP2.Server as HTTP2.Server +import Node.Stream as Node.Stream +import Node.URL as URL +import Test.MockCert (cert, key) +import Unsafe.Coerce (unsafeCoerce) + +basic_serverSecure :: (Either Error Unit -> Effect Unit) -> Effect Unit +basic_serverSecure complete = do + + server <- HTTP2.Server.createSecureServer + (toOptions { key: key, cert: cert }) + + void $ HTTP2.Server.onceStreamSecure server \stream _ _ -> do + HTTP2.Server.respond stream + ( toHeaders + { "content-type": "text/html; charset=utf-8" + , ":status": 200 + } + ) + (toOptions {}) + void + $ Node.Stream.writeString (HTTP2.Server.toDuplex stream) + Node.Encoding.UTF8 + "HTTP/2 Secure Body" + $ case _ of + Just err -> throwException err + Nothing -> + Node.Stream.end (HTTP2.Server.toDuplex stream) + $ case _ of + Just err -> throwException err + Nothing -> do + HTTP2.Server.closeServerSecure server (pure unit) + complete (Right unit) + + HTTP2.Server.listenSecure server + (toOptions { port: 8443 }) + (pure unit) + +basic_client :: Effect Unit +basic_client = do + + clientsession <- HTTP2.Client.connect + (URL.parse "https://localhost:8443") + (toOptions { ca: cert }) + (\_ _ -> pure unit) + + clientstream <- HTTP2.Client.request clientsession + (toHeaders { ":path": "/" }) + (toOptions {}) + + void $ HTTP2.Client.onceResponse clientstream + \headers _ -> + for_ (headerKeys headers) \name -> + Console.log $ + name <> ": " <> fromMaybe "" (headerString headers name) + + let req = HTTP2.Client.toDuplex clientstream + + dataRef <- liftST $ ST.Ref.new "" + Node.Stream.onDataString req Node.Encoding.UTF8 + \chunk -> void $ liftST $ ST.Ref.modify (_ <> chunk) dataRef + Node.Stream.onEnd req do + dataString <- liftST $ ST.Ref.read dataRef + Console.log $ "\n" <> dataString + HTTP2.Client.closeSession clientsession (pure unit) + +headers_sensitive :: Effect Unit +headers_sensitive = do + Console.log $ unsafeCoerce $ + toHeaders + { ":status": "200" + , "content-type": "text-plain" + , "ABC": [ "has", "more", "than", "one", "value" ] + } + <> + sensitiveHeaders + { "cookie": "some-cookie" + , "other-sensitive-header": "very secret data" + } diff --git a/test/HTTP2Aff.purs b/test/HTTP2Aff.purs new file mode 100644 index 0000000..c6d5e49 --- /dev/null +++ b/test/HTTP2Aff.purs @@ -0,0 +1,302 @@ +module Test.HTTP2Aff where + +import Prelude + +import Control.Alternative ((<|>)) +import Control.Parallel (parSequence_) +import Data.Either (either) +import Data.Maybe (fromJust, fromMaybe) +import Data.String as String +import Data.Tuple (fst) +import Effect.Aff (Aff, attempt, catchError, error, forkAff, killFiber, throwError) +import Effect.Class (liftEffect) +import Effect.Console as Console +import Node.HTTP2 (HeadersObject, headerArray, headerKeys, headerString, toHeaders, toOptions) +import Node.HTTP2.Client.Aff as Client.Aff +import Node.HTTP2.Server.Aff as Server.Aff +import Node.Stream.Aff (end, fromStringUTF8, readAll, toStringUTF8, write) +import Node.URL as URL +import Partial.Unsafe (unsafePartial) +import Test.MockCert (cert, key) +import Unsafe.Coerce (unsafeCoerce) + +-- | Print anything to the console. +console :: forall a. a -> Aff Unit +console x = liftEffect $ Console.log (unsafeCoerce x) + +push1_serverSecure :: Aff Unit +push1_serverSecure = do + + either (\err -> liftEffect $ Console.error (unsafeCoerce err)) pure =<< attempt do + -- 1. Start the server, wait for a connection. + server <- Server.Aff.createSecureServer + (toOptions { key: key, cert: cert }) + void $ Server.Aff.listenSecure server + (toOptions { port: 8444 }) + \_session _headers stream -> do + + -- 2. Wait to receive a request. + let s = Server.Aff.toDuplex stream + requestBody <- toStringUTF8 =<< (fst <$> readAll s) + console $ "SERVER Request body: " <> requestBody + + -- 3. Send a response stream. + Server.Aff.respond stream (toOptions {}) (toHeaders {}) + write s =<< fromStringUTF8 "HTTP/2 secure response body Aff" + + -- 4. Push a new stream. + stream2 <- Server.Aff.pushStream stream (toOptions {}) (toHeaders {}) + Server.Aff.respond stream2 (toOptions {}) (toHeaders {}) + let s2 = Server.Aff.toDuplex stream2 + write s2 =<< fromStringUTF8 "HTTP/2 secure push body Aff" + end s2 + + -- 5. End the response, end the session + end s + -- Server.Aff.closeSession session + + -- After one session, stop the server. + Server.Aff.closeSecureServer server + +push1_client :: Aff Unit +push1_client = do + + either (\err -> console err) pure =<< attempt do + -- 1. Begin the session, open a connection. + session <- Client.Aff.connect + (toOptions { ca: cert }) + (URL.parse "https://localhost:8444") + + -- 2. Send a request. + stream <- Client.Aff.request session + (toOptions { endStream: false }) + (toHeaders {}) + + let s = Client.Aff.toDuplex stream + + write s =<< fromStringUTF8 "HTTP/2 secure request body Aff" + end s + + -- 3. Wait for the response. + _ <- Client.Aff.waitResponse stream + + -- We have to do steps 4 and 5 concurrently because we don't know which of + -- `readAll` or `waitPush` will complete first. + parSequence_ + [ do + -- 4. Wait for the reponse body. + responseBody <- toStringUTF8 =<< (fst <$> readAll s) + console $ "CLIENT Response body: " <> responseBody + , do + -- 5. Receive a pushed stream. + { streamPushed } <- Client.Aff.waitPush session + bodyPushed <- toStringUTF8 =<< (fst <$> readAll (Client.Aff.toDuplex streamPushed)) + console $ "CLIENT Pushed body: " <> bodyPushed + ] + + -- 6. Close the session. + Client.Aff.closeSession session + +headers_serverSecure :: Aff Unit +headers_serverSecure = do + + -- 1. Start the server, wait for a connection. + server <- Server.Aff.createSecureServer + (toOptions { key: key, cert: cert }) + Server.Aff.listenSecure server + (toOptions { port: 8445 }) + \_session headers stream -> do + console $ "SERVER " <> headersShow headers + + -- 2. Receive a request. Wait for the end of the request. + _ <- readAll (Server.Aff.toDuplex stream) + _ <- Server.Aff.waitEnd stream + + -- 3. Send a response. + Server.Aff.respond stream (toOptions {}) $ toHeaders + { "normal": "server normal header" + } + -- TODO + -- Error [ERR_HTTP2_HEADERS_AFTER_RESPOND]: Cannot specify additional headers after response initiated + -- Server.Aff.sendHeadersAdditional stream $ toHeaders + -- { "additional": "server additional header" + -- } + + -- 4. Push a new stream. + stream2 <- Server.Aff.pushStream stream (toOptions {}) (toHeaders {}) + Server.Aff.respond stream2 (toOptions {}) + ( toHeaders + { "pushnormal": "server normal pushed header" + } + ) + end (Server.Aff.toDuplex stream2) + + -- 5. End the response. + end (Server.Aff.toDuplex stream) + + -- After one session, stop the server. + Server.Aff.closeSecureServer server + +headers_client :: Aff Unit +headers_client = do + + -- 1. Begin the session, open a connection. + session <- Client.Aff.connect + (toOptions { ca: cert }) + (URL.parse "https://localhost:8445") + + -- 2. Send a request. + stream <- Client.Aff.request session (toOptions {}) $ toHeaders + { "normal": "client normal header" + } + end (Client.Aff.toDuplex stream) + + -- 3. Wait for the response. + headers <- Client.Aff.waitResponse stream + console $ "CLIENT " <> headersShow headers + + -- 4. Receive a pushed stream. + { headersRequest, headersResponse } <- Client.Aff.waitPush session + console $ "CLIENT Pushed Request " <> headersShow headersRequest + console $ "CLIENT Pushed Response " <> headersShow headersResponse + + -- 5. Wait for the stream to end, then close the connection. + _ <- Client.Aff.waitEnd stream + Client.Aff.closeSession session + +trailers_serverSecure :: Aff Unit +trailers_serverSecure = do + + -- 1. Start the server, wait for a connection. + server <- Server.Aff.createSecureServer + (toOptions { key: key, cert: cert }) + Server.Aff.listenSecure server + (toOptions { port: 8446 }) + \_session _headers stream -> do + + -- 2. Wait for the end of the request. + parSequence_ + [ do + trailers <- unsafePartial $ fromJust <$> Server.Aff.waitEnd stream + console $ "SERVER Trailer " <> headersShow trailers + , do + _ <- readAll (Server.Aff.toDuplex stream) + pure unit + ] + + -- 3. Send a response + Server.Aff.respond stream + (toOptions { waitForTrailers: true }) + (toHeaders {}) + end (Server.Aff.toDuplex stream) + Server.Aff.waitWantTrailers stream + Server.Aff.sendTrailers stream (toHeaders { "trailer1": "response trailer" }) + + -- After one session, stop the server. + Server.Aff.closeSecureServer server + +trailers_client :: Aff Unit +trailers_client = do + + -- 1. Begin the session, open a connection. + session <- Client.Aff.connect + (toOptions { ca: cert }) + (URL.parse "https://localhost:8446") + + -- 2. Send a request. + stream <- Client.Aff.request session + (toOptions { waitForTrailers: true, endStream: false }) + (toHeaders {}) + end (Client.Aff.toDuplex stream) + Client.Aff.waitWantTrailers stream + Client.Aff.sendTrailers stream (toHeaders { "trailer1": "request trailer" }) + + -- 3. Wait for the response. + headers <- Client.Aff.waitResponse stream + console $ "CLIENT Header " <> headersShow headers + + -- 4. Wait for trailers. + trailers <- unsafePartial $ fromJust <$> Client.Aff.waitEnd stream + console $ "CLIENT Trailer " <> headersShow trailers + + -- 5. Close the connection. + Client.Aff.closeSession session + +headersShow :: HeadersObject -> String +headersShow headers = String.joinWith ", " $ headerKeys headers <#> \key -> + key <> ": " <> + ( fromMaybe "" $ + (headerString headers key) + <|> + (String.joinWith " " <$> headerArray headers key) + ) + +error1_serverSecure :: Aff Unit +error1_serverSecure = catchError + do + -- 1. Start the server, wait for a connection. + _ <- Server.Aff.createSecureServer + (toOptions { key: "bad key", cert: "bad cert" }) + pure unit + ( \e -> do + console e + throwError e + ) + +error2_serverSecure :: Aff Unit +error2_serverSecure = catchError + do + -- 1. Start the server, wait for a connection. + server <- Server.Aff.createSecureServer + (toOptions { key: key, cert: cert }) + void $ Server.Aff.listenSecure server + (toOptions { port: 1 }) + \_session _headers _stream -> pure unit + ( \e -> do + console e + throwError e + ) + +error1_client :: Aff Unit +error1_client = catchError + do + -- 1. Begin the session, open a connection. + _ <- Client.Aff.connect + (toOptions { ca: cert }) + (URL.parse "https://localhost:1") + pure unit + ( \e -> do + console e + throwError e + ) + +error2_client :: Aff Unit +error2_client = catchError + do + -- 1. Begin the session, open a connection. + session <- Client.Aff.connect + (toOptions {}) + (URL.parse "https://www.google.com:443") + stream <- Client.Aff.request session + (toOptions {}) + (toHeaders { "bad header": "bad header" }) + headers <- Client.Aff.waitResponse stream + console headers + ( \e -> do + console e + throwError e + ) + +cancel1_serverSecure :: Aff Unit +cancel1_serverSecure = do + + -- 1. Start the server, wait for a connection. + server <- Server.Aff.createSecureServer + (toOptions { key: key, cert: cert }) + fiber <- forkAff do + void $ Server.Aff.listenSecure server + (toOptions { port: 8447 }) + \_session _headers _stream -> do + pure unit + killFiber (error "no error") fiber + Server.Aff.closeSecureServer server 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..2d2380e 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -2,136 +2,146 @@ module Test.Main where import Prelude +import Control.Parallel (parSequence_) +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.Class (liftEffect) 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.HTTP2 as HTTP2 +import Test.HTTP2Aff as HTTP2Aff +import Test.MockCert (cert, key) +import Test.Spec (describe, it) +import Test.Spec.Assertions (expectError, 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 + describe "HTTP/2" do + it "test basic" do + flip shouldReturn unit $ makeAff \complete -> do + HTTP2.basic_serverSecure complete + HTTP2.basic_client + pure nonCanceler + it "headers_sensitive" do + flip shouldReturn unit $ liftEffect HTTP2.headers_sensitive + describe "HTTP/2 Aff" do + it "push1" do + flip shouldReturn unit $ + parSequence_ + [ HTTP2Aff.push1_serverSecure + , HTTP2Aff.push1_client + ] + it "error1_serverSecure" do + expectError HTTP2Aff.error1_serverSecure + it "error2 serverSecure" do + expectError HTTP2Aff.error2_serverSecure + it "error1_client" do + expectError HTTP2Aff.error1_client + it "error2_client" do + expectError HTTP2Aff.error2_client + it "headers" do + flip shouldReturn unit $ + parSequence_ + [ HTTP2Aff.headers_serverSecure + , HTTP2Aff.headers_client + ] + it "trailers" do + flip shouldReturn unit $ + parSequence_ + [ HTTP2Aff.trailers_serverSecure + , HTTP2Aff.trailers_client + ] + it "cancel1_secureServer" do + flip shouldReturn unit do + HTTP2Aff.cancel1_serverSecure 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 - -testHttps :: Effect Unit -testHttps = - simpleReq "https://pursuit.purescript.org/packages/purescript-node-http/badge" + sslOpts = + HTTPS.key := HTTPS.keyString key <> + HTTPS.cert := HTTPS.certString cert -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 +157,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 +167,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-----""" From c972482c9beec9ed1cd576da34bf7ccf4bfe20a1 Mon Sep 17 00:00:00 2001 From: James Brock Date: Tue, 10 Jan 2023 10:20:10 +0900 Subject: [PATCH 2/3] unsafeCoerce only when upcast a JavaScript object to its superclass. --- src/Node/HTTP2/Client.purs | 22 +++++++++---------- src/Node/HTTP2/Server.purs | 44 +++++++++++++++++++------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/Node/HTTP2/Client.purs b/src/Node/HTTP2/Client.purs index b3787c1..73da14c 100644 --- a/src/Node/HTTP2/Client.purs +++ b/src/Node/HTTP2/Client.purs @@ -97,7 +97,7 @@ foreign import destroy :: ClientHttp2Stream -> Effect Unit -- | https://nodejs.org/docs/latest/api/http2.html#http2sessionclosecallback closeSession :: ClientHttp2Session -> Effect Unit -> Effect Unit -closeSession = unsafeCoerce Internal.closeSession +closeSession http2session = Internal.closeSession (unsafeCoerce http2session) -- | https://nodejs.org/docs/latest/api/http2.html#event-response -- | @@ -124,7 +124,7 @@ foreign import onceHeaders :: ClientHttp2Stream -> (HeadersObject -> Flags -> Ef -- | Returns an effect for removing the event listener before the event -- | is raised. onceStream :: ClientHttp2Session -> (ClientHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -onceStream = unsafeCoerce Internal.onceStream +onceStream http2session callback = Internal.onceStream (unsafeCoerce http2session) (\http2stream -> callback (unsafeCoerce http2stream)) -- | https://nodejs.org/docs/latest/api/http2.html#event-stream -- | @@ -132,7 +132,7 @@ onceStream = unsafeCoerce Internal.onceStream -- | -- | Returns an effect for removing the event listener. onStream :: ClientHttp2Session -> (ClientHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -onStream = unsafeCoerce Internal.onStream +onStream http2session callback = Internal.onStream (unsafeCoerce http2session) (\http2stream -> callback (unsafeCoerce http2stream)) -- | https://nodejs.org/docs/latest/api/http2.html#event-error -- | @@ -141,7 +141,7 @@ onStream = unsafeCoerce Internal.onStream -- | Returns an effect for removing the event listener before the event -- | is raised. onceErrorSession :: ClientHttp2Session -> (Error -> Effect Unit) -> Effect (Effect Unit) -onceErrorSession = unsafeCoerce Internal.onceEmitterError +onceErrorSession http2session = Internal.onceEmitterError (unsafeCoerce http2session) -- | https://nodejs.org/docs/latest/api/http2.html#event-error_1 -- | @@ -150,7 +150,7 @@ onceErrorSession = unsafeCoerce Internal.onceEmitterError -- | Returns an effect for removing the event listener before the event -- | is raised. onceErrorStream :: ClientHttp2Stream -> (Error -> Effect Unit) -> Effect (Effect Unit) -onceErrorStream = unsafeCoerce Internal.onceEmitterError +onceErrorStream http2stream = Internal.onceEmitterError (unsafeCoerce http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#event-push -- | @@ -164,7 +164,7 @@ foreign import oncePush :: ClientHttp2Stream -> (HeadersObject -> Flags -> Effec -- | Returns an effect for removing the event listener before the event -- | is raised. onceTrailers :: ClientHttp2Stream -> (HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -onceTrailers = unsafeCoerce Internal.onceTrailers +onceTrailers http2stream = Internal.onceTrailers (unsafeCoerce http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#event-wanttrailers -- | @@ -173,19 +173,19 @@ onceTrailers = unsafeCoerce Internal.onceTrailers -- | Returns an effect for removing the event listener before the event -- | is raised. onceWantTrailers :: ClientHttp2Stream -> Effect Unit -> Effect (Effect Unit) -onceWantTrailers = unsafeCoerce Internal.onceWantTrailers +onceWantTrailers http2stream = Internal.onceWantTrailers (unsafeCoerce http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#http2streamsendtrailersheaders -- | -- | > When sending a request or sending a response, the `options.waitForTrailers` option must be set in order to keep the `Http2Stream` open after the final `DATA` frame so that trailers can be sent. sendTrailers :: ClientHttp2Stream -> HeadersObject -> Effect Unit -sendTrailers = unsafeCoerce Internal.sendTrailers +sendTrailers http2stream = Internal.sendTrailers (unsafeCoerce http2stream) -- | https://nodejs.org/docs/latest/api/stream.html#event-data -- | -- | Returns an effect for removing the event listener. onData :: ClientHttp2Stream -> (Buffer -> Effect Unit) -> Effect (Effect Unit) -onData = unsafeCoerce Internal.onData +onData http2stream = Internal.onData (unsafeCoerce http2stream) -- | https://nodejs.org/docs/latest/api/net.html#event-end -- | @@ -194,11 +194,11 @@ onData = unsafeCoerce Internal.onData -- | Returns an effect for removing the event listener before the event -- | is raised. onceEnd :: ClientHttp2Stream -> Effect Unit -> Effect (Effect Unit) -onceEnd = unsafeCoerce Internal.onceEnd +onceEnd http2stream = Internal.onceEnd (unsafeCoerce http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#http2streamclosecode-callback closeStream :: ClientHttp2Stream -> Int -> Effect Unit -> Effect Unit -closeStream stream code = Internal.closeStream (unsafeCoerce stream) code +closeStream stream = Internal.closeStream (unsafeCoerce stream) -- | Coerce to a duplex stream. toDuplex :: ClientHttp2Stream -> Duplex diff --git a/src/Node/HTTP2/Server.purs b/src/Node/HTTP2/Server.purs index 3f18b62..e7cb987 100644 --- a/src/Node/HTTP2/Server.purs +++ b/src/Node/HTTP2/Server.purs @@ -90,7 +90,7 @@ foreign import listen :: Http2Server -> OptionsObject -> Effect Unit -> Effect U -- | https://nodejs.org/docs/latest/api/http2.html#serverclosecallback closeServer :: Http2Server -> Effect Unit -> Effect Unit -closeServer = unsafeCoerce Internal.closeServer +closeServer http2server = Internal.closeServer (unsafeCoerce http2server) -- | https://nodejs.org/docs/latest/api/net.html#event-close -- | @@ -99,7 +99,7 @@ closeServer = unsafeCoerce Internal.closeServer -- | Returns an effect for removing the event listener before the event -- | is raised. onceCloseServer :: Http2Server -> Effect Unit -> Effect (Effect Unit) -onceCloseServer = unsafeCoerce Internal.onceServerClose +onceCloseServer http2server = Internal.onceServerClose (unsafeCoerce http2server) -- | An HTTP/2 server with one listening socket for encrypted connections. -- | @@ -113,11 +113,11 @@ foreign import createSecureServer :: OptionsObject -> Effect Http2SecureServer -- | https://nodejs.org/docs/latest/api/net.html#serverlistenoptions-callback listenSecure :: Http2SecureServer -> OptionsObject -> Effect Unit -> Effect Unit -listenSecure = unsafeCoerce listen +listenSecure http2server = listen (unsafeCoerce http2server) -- | https://nodejs.org/docs/latest/api/http2.html#serverclosecallback closeServerSecure :: Http2SecureServer -> Effect Unit -> Effect Unit -closeServerSecure = unsafeCoerce Internal.closeServer +closeServerSecure http2server = Internal.closeServer (unsafeCoerce http2server) -- | https://nodejs.org/docs/latest/api/net.html#event-close -- | @@ -126,7 +126,7 @@ closeServerSecure = unsafeCoerce Internal.closeServer -- | Returns an effect for removing the event listener before the event -- | is raised. onceCloseServerSecure :: Http2SecureServer -> Effect Unit -> Effect (Effect Unit) -onceCloseServerSecure = unsafeCoerce Internal.onceServerClose +onceCloseServerSecure http2server = Internal.onceServerClose (unsafeCoerce http2server) -- | https://nodejs.org/docs/latest/api/net.html#event-error -- | @@ -134,7 +134,7 @@ onceCloseServerSecure = unsafeCoerce Internal.onceServerClose -- | -- | Returns an effect for removing the event listener. onErrorServer :: Http2Server -> (Error -> Effect Unit) -> Effect (Effect Unit) -onErrorServer = unsafeCoerce Internal.onEmitterError +onErrorServer http2server = Internal.onEmitterError (unsafeCoerce http2server) -- | https://nodejs.org/docs/latest/api/net.html#event-error -- | @@ -142,7 +142,7 @@ onErrorServer = unsafeCoerce Internal.onEmitterError -- | -- | Returns an effect for removing the event listener. onErrorServerSecure :: Http2SecureServer -> (Error -> Effect Unit) -> Effect (Effect Unit) -onErrorServerSecure = unsafeCoerce Internal.onEmitterError +onErrorServerSecure http2server = Internal.onEmitterError (unsafeCoerce http2server) -- | https://nodejs.org/docs/latest/api/http2.html#class-serverhttp2session -- | @@ -169,11 +169,11 @@ foreign import onceSession :: Http2Server -> (ServerHttp2Session -> Effect Unit) -- | https://nodejs.org/api/http2.html#http2sessionlocalsettings localSettings :: ServerHttp2Session -> Effect SettingsObject -localSettings = unsafeCoerce Internal.localSettings +localSettings http2session = Internal.localSettings (unsafeCoerce http2session) -- | https://nodejs.org/docs/latest/api/http2.html#http2sessionclosecallback closeSession :: ServerHttp2Session -> Effect Unit -> Effect Unit -closeSession = unsafeCoerce Internal.closeSession +closeSession http2session = Internal.closeSession (unsafeCoerce http2session) -- | Listen for one event, call the callback, then remove -- | the event listener. @@ -183,13 +183,13 @@ closeSession = unsafeCoerce Internal.closeSession -- | -- | https://nodejs.org/docs/latest/api/http2.html#event-stream onceStream :: Http2Server -> (ServerHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -onceStream = unsafeCoerce Internal.onceStream +onceStream http2server callback = Internal.onceStream (unsafeCoerce http2server) (\http2stream -> callback (unsafeCoerce http2stream)) -- | https://nodejs.org/docs/latest/api/http2.html#event-stream -- | -- | Returns an effect for removing the event listener. onStream :: Http2Server -> (ServerHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -onStream = unsafeCoerce Internal.onStream +onStream http2server callback = Internal.onStream (unsafeCoerce http2server) (\http2stream -> callback (unsafeCoerce http2stream)) -- | Listen for one event, call the callback, then remove -- | the event listener. @@ -199,17 +199,17 @@ onStream = unsafeCoerce Internal.onStream -- | -- | https://nodejs.org/docs/latest/api/http2.html#event-stream onceStreamSecure :: Http2SecureServer -> (ServerHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -onceStreamSecure = unsafeCoerce Internal.onceStream +onceStreamSecure http2server callback = Internal.onceStream (unsafeCoerce http2server) (\http2stream -> callback (unsafeCoerce http2stream)) -- | https://nodejs.org/docs/latest/api/http2.html#event-stream -- | -- | Returns an effect for removing the event listener. onStreamSecure :: Http2SecureServer -> (ServerHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -onStreamSecure = unsafeCoerce Internal.onStream +onStreamSecure http2server callback = Internal.onStream (unsafeCoerce http2server) (\http2stream -> callback (unsafeCoerce http2stream)) -- | https://nodejs.org/docs/latest/api/http2.html#http2streamrespondheaders-options respond :: ServerHttp2Stream -> HeadersObject -> OptionsObject -> Effect Unit -respond = unsafeCoerce Internal.respond +respond http2stream = Internal.respond (unsafeCoerce http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#event-session_1 -- | @@ -217,8 +217,8 @@ respond = unsafeCoerce Internal.respond -- | -- | Returns an effect for removing the event listener before the event -- | is raised. -onceSessionSecure :: Http2SecureServer -> (ServerHttp2Session -> Effect Unit) -> Effect Unit -onceSessionSecure = unsafeCoerce onceSession +onceSessionSecure :: Http2SecureServer -> (ServerHttp2Session -> Effect Unit) -> Effect (Effect Unit) +onceSessionSecure http2server = onceSession (unsafeCoerce http2server) -- | An HTTP/2 server `Http2Stream` connected to a client. -- | @@ -242,7 +242,7 @@ foreign import pushStream :: ServerHttp2Stream -> HeadersObject -> OptionsObject -- | Returns an effect for removing the event listener before the event -- | is raised. onceTrailers :: ServerHttp2Stream -> (HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -onceTrailers = unsafeCoerce Internal.onceTrailers +onceTrailers http2stream = Internal.onceTrailers (unsafeCoerce http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#http2streamadditionalheadersheaders foreign import additionalHeaders :: ServerHttp2Stream -> HeadersObject -> Effect Unit @@ -254,7 +254,7 @@ foreign import additionalHeaders :: ServerHttp2Stream -> HeadersObject -> Effect -- | Returns an effect for removing the event listener before the event -- | is raised. onceErrorStream :: ServerHttp2Stream -> (Error -> Effect Unit) -> Effect (Effect Unit) -onceErrorStream = unsafeCoerce Internal.onceEmitterError +onceErrorStream http2stream = Internal.onceEmitterError (unsafeCoerce http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#event-wanttrailers -- | @@ -263,13 +263,13 @@ onceErrorStream = unsafeCoerce Internal.onceEmitterError -- | Returns an effect for removing the event listener before the event -- | is raised. onceWantTrailers :: ServerHttp2Stream -> Effect Unit -> Effect (Effect Unit) -onceWantTrailers = unsafeCoerce Internal.onceWantTrailers +onceWantTrailers http2stream = Internal.onceWantTrailers (unsafeCoerce http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#http2streamsendtrailersheaders -- | -- | > When sending a request or sending a response, the `options.waitForTrailers` option must be set in order to keep the `Http2Stream` open after the final `DATA` frame so that trailers can be sent. sendTrailers :: ServerHttp2Stream -> HeadersObject -> Effect Unit -sendTrailers = unsafeCoerce Internal.sendTrailers +sendTrailers http2stream = Internal.sendTrailers (unsafeCoerce http2stream) -- | https://nodejs.org/docs/latest/api/net.html#event-end -- | @@ -278,11 +278,11 @@ sendTrailers = unsafeCoerce Internal.sendTrailers -- | Returns an effect for removing the event listener before the event -- | is raised. onceEnd :: ServerHttp2Stream -> Effect Unit -> Effect (Effect Unit) -onceEnd = unsafeCoerce Internal.onceEnd +onceEnd http2stream = Internal.onceEnd (unsafeCoerce http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#http2streamclosecode-callback closeStream :: ServerHttp2Stream -> Int -> Effect Unit -> Effect Unit -closeStream stream code = Internal.closeStream (unsafeCoerce stream) code +closeStream stream = Internal.closeStream (unsafeCoerce stream) -- | Coerce to a duplex stream. toDuplex :: ServerHttp2Stream -> Duplex From 12792b26c8f27fbcdb3ac18d0a8a350ebd086a55 Mon Sep 17 00:00:00 2001 From: James Brock Date: Wed, 11 Jan 2023 19:17:38 +0900 Subject: [PATCH 3/3] Type-annotate all unsafeCoerce JavaScript object casts --- src/Node/HTTP2/Client.js | 7 +++ src/Node/HTTP2/Client.purs | 55 +++++++++-------- src/Node/HTTP2/Internal.js | 53 +++------------- src/Node/HTTP2/Internal.purs | 46 +------------- src/Node/HTTP2/Server.js | 31 ++++++++-- src/Node/HTTP2/Server.purs | 108 +++++++++++++-------------------- src/Node/HTTP2/Server/Aff.purs | 4 +- test/HTTP2.purs | 2 +- 8 files changed, 115 insertions(+), 191 deletions(-) diff --git a/src/Node/HTTP2/Client.js b/src/Node/HTTP2/Client.js index f59b8b2..d3cbff6 100644 --- a/src/Node/HTTP2/Client.js +++ b/src/Node/HTTP2/Client.js @@ -20,6 +20,13 @@ export const onceReady = socket => callback => () => { return () => socket.removeEventListener("ready", callback); }; +// https://nodejs.org/docs/latest/api/http2.html#event-stream +export const onceStream = foreign => callback => () => { + const cb = (stream, headers, flags) => callback(stream)(headers)(flags)(); + foreign.once("stream", cb); + return () => {foreign.removeListener("stream", cb);}; +}; + // https://nodejs.org/docs/latest/api/http2.html#clienthttp2sessionrequestheaders-options export const request = clienthttp2session => headers => options => () => { return clienthttp2session.request(headers, options); diff --git a/src/Node/HTTP2/Client.purs b/src/Node/HTTP2/Client.purs index 73da14c..dca98e4 100644 --- a/src/Node/HTTP2/Client.purs +++ b/src/Node/HTTP2/Client.purs @@ -41,7 +41,6 @@ module Node.HTTP2.Client , request , onceErrorSession , onceResponse - , onStream , onceStream , onceHeaders , closeSession @@ -64,6 +63,7 @@ import Effect (Effect) import Effect.Exception (Error) import Node.Buffer (Buffer) import Node.HTTP2 (Flags, HeadersObject, OptionsObject) +import Node.HTTP2.Internal (Http2Session, Http2Stream) import Node.HTTP2.Internal as Internal import Node.Net.Socket (Socket) import Node.Stream (Duplex) @@ -75,6 +75,9 @@ import Unsafe.Coerce (unsafeCoerce) -- | See [__Class: ClientHttp2Session__](https://nodejs.org/docs/latest/api/http2.html#class-clienthttp2session) foreign import data ClientHttp2Session :: Type +upcastClientHttp2Session :: ClientHttp2Session -> Http2Session +upcastClientHttp2Session = unsafeCoerce + -- | https://nodejs.org/docs/latest/api/http2.html#http2connectauthority-options-listener foreign import connect :: URL -> OptionsObject -> (ClientHttp2Session -> Socket -> Effect Unit) -> Effect ClientHttp2Session @@ -89,6 +92,9 @@ foreign import onceReady :: Socket -> (Effect Unit) -> Effect (Effect Unit) -- | See [__Class: ClientHttp2Stream__](https://nodejs.org/docs/latest/api/http2.html#class-clienthttp2stream) foreign import data ClientHttp2Stream :: Type +upcastClientHttp2Stream :: ClientHttp2Stream -> Http2Stream +upcastClientHttp2Stream = unsafeCoerce + -- |https://nodejs.org/docs/latest/api/http2.html#clienthttp2sessionrequestheaders-options foreign import request :: ClientHttp2Session -> HeadersObject -> OptionsObject -> Effect ClientHttp2Stream @@ -97,11 +103,11 @@ foreign import destroy :: ClientHttp2Stream -> Effect Unit -- | https://nodejs.org/docs/latest/api/http2.html#http2sessionclosecallback closeSession :: ClientHttp2Session -> Effect Unit -> Effect Unit -closeSession http2session = Internal.closeSession (unsafeCoerce http2session) +closeSession http2session = Internal.closeSession (upcastClientHttp2Session http2session) -- | https://nodejs.org/docs/latest/api/http2.html#event-response -- | --- | Listen for one event, then remove the event listener. +-- | Listen for one event, call the callback, then remove the event listener. -- | -- | Returns an effect for removing the event listener before the event -- | is raised. @@ -109,7 +115,7 @@ foreign import onceResponse :: ClientHttp2Stream -> (HeadersObject -> Flags -> E -- | https://nodejs.org/docs/latest/api/http2.html#event-headers -- | --- | Listen for one event, then remove the event listener. +-- | Listen for one event, call the callback, then remove the event listener. -- | -- | Returns an effect for removing the event listener before the event -- | is raised. @@ -119,38 +125,31 @@ foreign import onceHeaders :: ClientHttp2Stream -> (HeadersObject -> Flags -> Ef -- | -- | https://nodejs.org/docs/latest/api/http2.html#push-streams-on-the-client -- | --- | Listen for one event, then remove the event listener. +-- | Listen for one event, call the callback, then remove the event listener. -- | -- | Returns an effect for removing the event listener before the event --- | is raised. -onceStream :: ClientHttp2Session -> (ClientHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -onceStream http2session callback = Internal.onceStream (unsafeCoerce http2session) (\http2stream -> callback (unsafeCoerce http2stream)) - --- | https://nodejs.org/docs/latest/api/http2.html#event-stream -- | --- | https://nodejs.org/docs/latest/api/http2.html#push-streams-on-the-client --- | --- | Returns an effect for removing the event listener. -onStream :: ClientHttp2Session -> (ClientHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -onStream http2session callback = Internal.onStream (unsafeCoerce http2session) (\http2stream -> callback (unsafeCoerce http2stream)) +-- | https://nodejs.org/docs/latest/api/http2.html#event-stream +-- | is raised. +foreign import onceStream :: ClientHttp2Session -> (ClientHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -- | https://nodejs.org/docs/latest/api/http2.html#event-error -- | --- | Listen for one event, then remove the event listener. +-- | Listen for one event, call the callback, then remove the event listener. -- | -- | Returns an effect for removing the event listener before the event -- | is raised. onceErrorSession :: ClientHttp2Session -> (Error -> Effect Unit) -> Effect (Effect Unit) -onceErrorSession http2session = Internal.onceEmitterError (unsafeCoerce http2session) +onceErrorSession http2session = Internal.onceSessionEmitterError (upcastClientHttp2Session http2session) -- | https://nodejs.org/docs/latest/api/http2.html#event-error_1 -- | --- | Listen for one event, then remove the event listener. +-- | Listen for one event, call the callback, then remove the event listener. -- | -- | Returns an effect for removing the event listener before the event -- | is raised. onceErrorStream :: ClientHttp2Stream -> (Error -> Effect Unit) -> Effect (Effect Unit) -onceErrorStream http2stream = Internal.onceEmitterError (unsafeCoerce http2stream) +onceErrorStream http2stream = Internal.onceStreamEmitterError (upcastClientHttp2Stream http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#event-push -- | @@ -159,46 +158,46 @@ foreign import oncePush :: ClientHttp2Stream -> (HeadersObject -> Flags -> Effec -- | https://nodejs.org/docs/latest/api/http2.html#event-trailers -- | --- | Listen for one event, then remove the event listener. +-- | Listen for one event, call the callback, then remove the event listener. -- | -- | Returns an effect for removing the event listener before the event -- | is raised. onceTrailers :: ClientHttp2Stream -> (HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -onceTrailers http2stream = Internal.onceTrailers (unsafeCoerce http2stream) +onceTrailers http2stream = Internal.onceTrailers (upcastClientHttp2Stream http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#event-wanttrailers -- | --- | Listen for one event, then remove the event listener. +-- | Listen for one event, call the callback, then remove the event listener. -- | -- | Returns an effect for removing the event listener before the event -- | is raised. onceWantTrailers :: ClientHttp2Stream -> Effect Unit -> Effect (Effect Unit) -onceWantTrailers http2stream = Internal.onceWantTrailers (unsafeCoerce http2stream) +onceWantTrailers http2stream = Internal.onceWantTrailers (upcastClientHttp2Stream http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#http2streamsendtrailersheaders -- | -- | > When sending a request or sending a response, the `options.waitForTrailers` option must be set in order to keep the `Http2Stream` open after the final `DATA` frame so that trailers can be sent. sendTrailers :: ClientHttp2Stream -> HeadersObject -> Effect Unit -sendTrailers http2stream = Internal.sendTrailers (unsafeCoerce http2stream) +sendTrailers http2stream = Internal.sendTrailers (upcastClientHttp2Stream http2stream) -- | https://nodejs.org/docs/latest/api/stream.html#event-data -- | -- | Returns an effect for removing the event listener. onData :: ClientHttp2Stream -> (Buffer -> Effect Unit) -> Effect (Effect Unit) -onData http2stream = Internal.onData (unsafeCoerce http2stream) +onData http2stream = Internal.onData (upcastClientHttp2Stream http2stream) -- | https://nodejs.org/docs/latest/api/net.html#event-end -- | --- | Listen for one event, then remove the event listener. +-- | Listen for one event, call the callback, then remove the event listener. -- | -- | Returns an effect for removing the event listener before the event -- | is raised. onceEnd :: ClientHttp2Stream -> Effect Unit -> Effect (Effect Unit) -onceEnd http2stream = Internal.onceEnd (unsafeCoerce http2stream) +onceEnd http2stream = Internal.onceEnd (upcastClientHttp2Stream http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#http2streamclosecode-callback closeStream :: ClientHttp2Stream -> Int -> Effect Unit -> Effect Unit -closeStream stream = Internal.closeStream (unsafeCoerce stream) +closeStream stream = Internal.closeStream (upcastClientHttp2Stream stream) -- | Coerce to a duplex stream. toDuplex :: ClientHttp2Stream -> Duplex diff --git a/src/Node/HTTP2/Internal.js b/src/Node/HTTP2/Internal.js index 815d3c4..fced735 100644 --- a/src/Node/HTTP2/Internal.js +++ b/src/Node/HTTP2/Internal.js @@ -18,11 +18,6 @@ export const closeSession = http2session => callback => () => { } }; -// https://nodejs.org/docs/latest/api/http2.html#serverclosecallback -export const closeServer = http2server => callback => () => { - http2server.close(() => callback()); -}; - // https://nodejs.org/docs/latest/api/http2.html#event-close_1 export const onceClose = http2stream => callback => () => { const cb = () => callback(http2stream.rstCode)(); @@ -30,50 +25,20 @@ export const onceClose = http2stream => callback => () => { return () => {http2stream.removeEventListener("close", cb);}; }; -// https://nodejs.org/docs/latest/api/http2.html#event-stream -export const onceStream = foreign => callback => () => { - const cb = (stream, headers, flags) => callback(stream)(headers)(flags)(); - foreign.once("stream", cb); - return () => {foreign.removeListener("stream", cb);}; -}; - -// https://nodejs.org/docs/latest/api/http2.html#event-stream -export const onStream = foreign => callback => () => { - const cb = (stream, headers, flags) => callback(stream)(headers)(flags)(); - foreign.on("stream", cb); - return () => {foreign.removeListener("stream", cb);}; -}; - -// https://nodejs.org/docs/latest/api/events.html#nodeeventtargetoncetype-listener-options -export const onceError = eventtarget => callback => () => { - const cb = error => callback(error)(); - eventtarget.once("error", cb); - return () => {eventtarget.removeEventListener("error", cb);}; -}; - -// https://nodejs.org/docs/latest/api/net.html#event-close -export const onceServerClose = server => callback => () => { - const cb = () => callback(); - server.once("close", cb); - return () => {server.removeEventListener("close", cb);}; -}; - // https://nodejs.org/docs/latest/api/events.html#emitteronceeventname-listener -export const onceEmitterError = eventemitter => callback => () => { +const onceEmitterError = eventemitter => callback => () => { const cb = error => callback(error)(); eventemitter.once("error", cb); return () => {eventemitter.removeListener("error", cb);}; }; -export const onEmitterError = eventemitter => callback => () => { - const cb = error => callback(error)(); - eventemitter.on("error", cb); - return () => {eventemitter.removeListener("error", cb);}; -}; +// During PR review it was requested that there be no `unsafeCoerce`, so +// we unsafely coerce in JavaScript instead. +export const onceStreamEmitterError = onceEmitterError; -export const throwAllErrors = eventtarget => () => { - eventtarget.addEventListener("error", error => {throw error;}); -}; +// During PR review it was requested that there be no `unsafeCoerce`, so +// we unsafely coerce in JavaScript instead. +export const onceSessionEmitterError = onceEmitterError; export const onceWantTrailers = http2stream => callback => () => { const cb = () => callback(); @@ -104,10 +69,6 @@ export const onceEnd = netsocket => callback => () => { return () => {netsocket.removeListener("end", cb);}; }; -export const session = http2stream => { - return http2stream.session; -}; - // https://nodejs.org/docs/latest/api/http2.html#http2streamclosecode-callback export const closeStream = http2stream => code => callback => () => { http2stream.close(code, () => callback()); diff --git a/src/Node/HTTP2/Internal.purs b/src/Node/HTTP2/Internal.purs index 68d43b6..a279e00 100644 --- a/src/Node/HTTP2/Internal.purs +++ b/src/Node/HTTP2/Internal.purs @@ -7,7 +7,6 @@ import Prelude import Effect (Effect) import Effect.Exception (Error) -import Foreign (Foreign) import Node.Buffer (Buffer) import Node.HTTP2 (Flags, HeadersObject, OptionsObject, SettingsObject) import Node.HTTP2.Constants (NGHTTP2) @@ -25,59 +24,23 @@ foreign import data Http2Session :: Type -- | https://nodejs.org/api/http2.html#http2sessionlocalsettings foreign import localSettings :: Http2Session -> Effect SettingsObject --- | Listen for one event, call the callback, then remove +-- | Listen for one EventEmitter `'error'`, call the callback, then remove -- | the event listener. --- | Returns an effect for removing the event listener before the event --- | is raised. -- | --- | https://nodejs.org/docs/latest/api/http2.html#event-stream -foreign import onceStream :: Foreign -> (Http2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) - --- | https://nodejs.org/docs/latest/api/http2.html#event-stream -foreign import onStream :: Foreign -> (Http2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) - --- | Listen for one NodeEventTarget `'error'`, call the callback, then remove --- | the event listener. -- | Returns an effect for removing the event listener before the event -- | is raised. -foreign import onceError :: Foreign -> (Error -> Effect Unit) -> Effect (Effect Unit) +foreign import onceStreamEmitterError :: Http2Stream -> (Error -> Effect Unit) -> Effect (Effect Unit) -- | Listen for one EventEmitter `'error'`, call the callback, then remove -- | the event listener. --- | Returns an effect for removing the event listener before the event --- | is raised. -foreign import onceEmitterError :: Foreign -> (Error -> Effect Unit) -> Effect (Effect Unit) - --- | EventEmitter `on 'error'` -- | -- | Returns an effect for removing the event listener before the event -- | is raised. -foreign import onEmitterError :: Foreign -> (Error -> Effect Unit) -> Effect (Effect Unit) +foreign import onceSessionEmitterError :: Http2Session -> (Error -> Effect Unit) -> Effect (Effect Unit) -- | https://nodejs.org/docs/latest/api/http2.html#http2sessionclosecallback foreign import closeSession :: Http2Session -> Effect Unit -> Effect Unit --- | https://nodejs.org/docs/latest/api/http2.html#serverclosecallback -foreign import closeServer :: Foreign -> Effect Unit -> Effect Unit - --- | https://nodejs.org/docs/latest/api/net.html#event-close --- | --- | Returns an effect for removing the event listener before the event --- | is raised. -foreign import onceServerClose :: Foreign -> Effect Unit -> Effect (Effect Unit) - --- | To an `EventTarget` attach an `'error'` listener which will always throw --- | a synchronous `Error`. --- | --- | https://nodejs.org/docs/latest/api/http2.html#error-handling --- | --- | > (Errors) will be reported using either a synchronous throw or via --- | > an 'error' event on the `Http2Stream`, `Http2Session` or --- | > `Http2Server` objects, depending on where and when the error occurs. --- | --- | https://nodejs.org/api/events.html#eventtargetaddeventlistenertype-listener-options -foreign import throwAllErrors :: Foreign -> Effect Unit - -- | Private type which can be coerced into ClientHttp2Stream -- | or ServerHttp2Stream or Duplex. -- | @@ -105,8 +68,5 @@ foreign import onData :: Http2Stream -> (Buffer -> Effect Unit) -> Effect (Effec -- | https://nodejs.org/docs/latest/api/net.html#event-end foreign import onceEnd :: Http2Stream -> Effect Unit -> Effect (Effect Unit) --- | https://nodejs.org/api/http2.html#http2streamsession -foreign import session :: Http2Stream -> Http2Session - -- | https://nodejs.org/docs/latest/api/http2.html#http2streamclosecode-callback foreign import closeStream :: Http2Stream -> Int -> Effect Unit -> Effect Unit diff --git a/src/Node/HTTP2/Server.js b/src/Node/HTTP2/Server.js index b9c12f3..fa5bcae 100644 --- a/src/Node/HTTP2/Server.js +++ b/src/Node/HTTP2/Server.js @@ -16,10 +16,33 @@ export const listen = server => options => callback => () => { server.listen(options, () => callback()); }; -export const onceSession = http2server => callback => () => { - const cb = session => callback(session)(); - http2server.once("session", cb); - return () => http2server.removeEventListener("session", cb); +// https://nodejs.org/docs/latest/api/http2.html#serverclosecallback +export const closeServer = http2server => callback => () => { + http2server.close(() => callback()); +}; + +// https://nodejs.org/docs/latest/api/net.html#event-close +export const onceServerClose = server => callback => () => { + const cb = () => callback(); + server.once("close", cb); + return () => {server.removeEventListener("close", cb);}; +}; + +export const onEmitterError = eventemitter => callback => () => { + const cb = error => callback(error)(); + eventemitter.on("error", cb); + return () => {eventemitter.removeListener("error", cb);}; +}; + +export const session = http2stream => { + return http2stream.session; +}; + +// https://nodejs.org/docs/latest/api/http2.html#event-stream +export const onStream = http2server => callback => () => { + const cb = (stream, headers, flags) => callback(stream)(headers)(flags)(); + http2server.on("stream", cb); + return () => {http2server.removeListener("stream", cb);}; }; // https://nodejs.org/docs/latest/api/http2.html#http2streampushallowed diff --git a/src/Node/HTTP2/Server.purs b/src/Node/HTTP2/Server.purs index e7cb987..b0e16a4 100644 --- a/src/Node/HTTP2/Server.purs +++ b/src/Node/HTTP2/Server.purs @@ -34,21 +34,17 @@ module Node.HTTP2.Server ( Http2Server , createServer , listen - , onceSession , onStream - , onceStream , onErrorServer , closeServer - , onceCloseServer + , onceServerClose , Http2SecureServer , createSecureServer , listenSecure - , onceSessionSecure , onStreamSecure - , onceStreamSecure , onErrorServerSecure , closeServerSecure - , onceCloseServerSecure + , onceServerSecureClose , ServerHttp2Session , respond , localSettings @@ -73,6 +69,7 @@ import Data.Nullable (Nullable) import Effect (Effect) import Effect.Exception (Error) import Node.HTTP2 (Flags, HeadersObject, OptionsObject, SettingsObject) +import Node.HTTP2.Internal (Http2Session, Http2Stream) import Node.HTTP2.Internal as Internal import Node.Stream (Duplex) import Unsafe.Coerce (unsafeCoerce) @@ -82,6 +79,12 @@ import Unsafe.Coerce (unsafeCoerce) -- | See [__Class: Http2Server__](https://nodejs.org/docs/latest/api/http2.html#class-http2server) foreign import data Http2Server :: Type +-- | Http2Server inherits from net.Server. +-- | Http2ServerSecure inherits from tls.Server. +-- | But they have mostly the same methods. +castHttp2Server :: Http2SecureServer -> Http2Server +castHttp2Server = unsafeCoerce + -- | https://nodejs.org/docs/latest/api/http2.html#http2createserveroptions-onrequesthandler foreign import createServer :: OptionsObject -> Effect Http2Server @@ -89,8 +92,7 @@ foreign import createServer :: OptionsObject -> Effect Http2Server foreign import listen :: Http2Server -> OptionsObject -> Effect Unit -> Effect Unit -- | https://nodejs.org/docs/latest/api/http2.html#serverclosecallback -closeServer :: Http2Server -> Effect Unit -> Effect Unit -closeServer http2server = Internal.closeServer (unsafeCoerce http2server) +foreign import closeServer :: Http2Server -> Effect Unit -> Effect Unit -- | https://nodejs.org/docs/latest/api/net.html#event-close -- | @@ -98,8 +100,7 @@ closeServer http2server = Internal.closeServer (unsafeCoerce http2server) -- | -- | Returns an effect for removing the event listener before the event -- | is raised. -onceCloseServer :: Http2Server -> Effect Unit -> Effect (Effect Unit) -onceCloseServer http2server = Internal.onceServerClose (unsafeCoerce http2server) +foreign import onceServerClose :: Http2Server -> Effect Unit -> Effect (Effect Unit) -- | An HTTP/2 server with one listening socket for encrypted connections. -- | @@ -113,11 +114,11 @@ foreign import createSecureServer :: OptionsObject -> Effect Http2SecureServer -- | https://nodejs.org/docs/latest/api/net.html#serverlistenoptions-callback listenSecure :: Http2SecureServer -> OptionsObject -> Effect Unit -> Effect Unit -listenSecure http2server = listen (unsafeCoerce http2server) +listenSecure http2server = listen (castHttp2Server http2server) -- | https://nodejs.org/docs/latest/api/http2.html#serverclosecallback closeServerSecure :: Http2SecureServer -> Effect Unit -> Effect Unit -closeServerSecure http2server = Internal.closeServer (unsafeCoerce http2server) +closeServerSecure http2server = closeServer (castHttp2Server http2server) -- | https://nodejs.org/docs/latest/api/net.html#event-close -- | @@ -125,8 +126,14 @@ closeServerSecure http2server = Internal.closeServer (unsafeCoerce http2server) -- | -- | Returns an effect for removing the event listener before the event -- | is raised. -onceCloseServerSecure :: Http2SecureServer -> Effect Unit -> Effect (Effect Unit) -onceCloseServerSecure http2server = Internal.onceServerClose (unsafeCoerce http2server) +onceServerSecureClose :: Http2SecureServer -> Effect Unit -> Effect (Effect Unit) +onceServerSecureClose http2server = onceServerClose (castHttp2Server http2server) + +-- | EventEmitter `on 'error'` +-- | +-- | Returns an effect for removing the event listener before the event +-- | is raised. +foreign import onEmitterError :: Http2Server -> (Error -> Effect Unit) -> Effect (Effect Unit) -- | https://nodejs.org/docs/latest/api/net.html#event-error -- | @@ -134,7 +141,7 @@ onceCloseServerSecure http2server = Internal.onceServerClose (unsafeCoerce http2 -- | -- | Returns an effect for removing the event listener. onErrorServer :: Http2Server -> (Error -> Effect Unit) -> Effect (Effect Unit) -onErrorServer http2server = Internal.onEmitterError (unsafeCoerce http2server) +onErrorServer = onEmitterError -- | https://nodejs.org/docs/latest/api/net.html#event-error -- | @@ -142,7 +149,7 @@ onErrorServer http2server = Internal.onEmitterError (unsafeCoerce http2server) -- | -- | Returns an effect for removing the event listener. onErrorServerSecure :: Http2SecureServer -> (Error -> Effect Unit) -> Effect (Effect Unit) -onErrorServerSecure http2server = Internal.onEmitterError (unsafeCoerce http2server) +onErrorServerSecure http2server = onEmitterError (castHttp2Server http2server) -- | https://nodejs.org/docs/latest/api/http2.html#class-serverhttp2session -- | @@ -155,76 +162,43 @@ onErrorServerSecure http2server = Internal.onEmitterError (unsafeCoerce http2ser -- | > taken through interactions with either the `Http2Server` or `Http2Stream` objects. foreign import data ServerHttp2Session :: Type -session :: ServerHttp2Stream -> ServerHttp2Session -session = unsafeCoerce Internal.session +upcastServerHttp2Session :: ServerHttp2Session -> Http2Session +upcastServerHttp2Session = unsafeCoerce --- | https://nodejs.org/docs/latest/api/http2.html#event-session --- | --- | Listen for one event, call the callback, then remove --- | the event listener. --- | --- | Returns an effect for removing the event listener before the event --- | is raised. -foreign import onceSession :: Http2Server -> (ServerHttp2Session -> Effect Unit) -> Effect (Effect Unit) +-- | https://nodejs.org/api/http2.html#http2streamsession +foreign import session :: ServerHttp2Stream -> ServerHttp2Session -- | https://nodejs.org/api/http2.html#http2sessionlocalsettings localSettings :: ServerHttp2Session -> Effect SettingsObject -localSettings http2session = Internal.localSettings (unsafeCoerce http2session) +localSettings http2session = Internal.localSettings (upcastServerHttp2Session http2session) -- | https://nodejs.org/docs/latest/api/http2.html#http2sessionclosecallback closeSession :: ServerHttp2Session -> Effect Unit -> Effect Unit -closeSession http2session = Internal.closeSession (unsafeCoerce http2session) - --- | Listen for one event, call the callback, then remove --- | the event listener. --- | --- | Returns an effect for removing the event listener before the event --- | is raised. --- | --- | https://nodejs.org/docs/latest/api/http2.html#event-stream -onceStream :: Http2Server -> (ServerHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -onceStream http2server callback = Internal.onceStream (unsafeCoerce http2server) (\http2stream -> callback (unsafeCoerce http2stream)) +closeSession http2session = Internal.closeSession (upcastServerHttp2Session http2session) -- | https://nodejs.org/docs/latest/api/http2.html#event-stream -- | -- | Returns an effect for removing the event listener. -onStream :: Http2Server -> (ServerHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -onStream http2server callback = Internal.onStream (unsafeCoerce http2server) (\http2stream -> callback (unsafeCoerce http2stream)) - --- | Listen for one event, call the callback, then remove --- | the event listener. --- | --- | Returns an effect for removing the event listener before the event --- | is raised. --- | --- | https://nodejs.org/docs/latest/api/http2.html#event-stream -onceStreamSecure :: Http2SecureServer -> (ServerHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -onceStreamSecure http2server callback = Internal.onceStream (unsafeCoerce http2server) (\http2stream -> callback (unsafeCoerce http2stream)) +foreign import onStream :: Http2Server -> (ServerHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -- | https://nodejs.org/docs/latest/api/http2.html#event-stream -- | -- | Returns an effect for removing the event listener. onStreamSecure :: Http2SecureServer -> (ServerHttp2Stream -> HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -onStreamSecure http2server callback = Internal.onStream (unsafeCoerce http2server) (\http2stream -> callback (unsafeCoerce http2stream)) +onStreamSecure http2server callback = onStream (castHttp2Server http2server) (\http2stream -> callback http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#http2streamrespondheaders-options respond :: ServerHttp2Stream -> HeadersObject -> OptionsObject -> Effect Unit -respond http2stream = Internal.respond (unsafeCoerce http2stream) - --- | https://nodejs.org/docs/latest/api/http2.html#event-session_1 --- | --- | Listen for one event, then remove the event listener. --- | --- | Returns an effect for removing the event listener before the event --- | is raised. -onceSessionSecure :: Http2SecureServer -> (ServerHttp2Session -> Effect Unit) -> Effect (Effect Unit) -onceSessionSecure http2server = onceSession (unsafeCoerce http2server) +respond http2stream = Internal.respond (upcastServerHttp2Stream http2stream) -- | An HTTP/2 server `Http2Stream` connected to a client. -- | -- | See [__Class: ServerHttp2Stream__](https://nodejs.org/docs/latest/api/http2.html#class-serverhttp2stream) foreign import data ServerHttp2Stream :: Type +upcastServerHttp2Stream :: ServerHttp2Stream -> Http2Stream +upcastServerHttp2Stream = unsafeCoerce + -- | https://nodejs.org/docs/latest/api/http2.html#http2streampushallowed foreign import pushAllowed :: ServerHttp2Stream -> Effect Boolean @@ -242,7 +216,7 @@ foreign import pushStream :: ServerHttp2Stream -> HeadersObject -> OptionsObject -- | Returns an effect for removing the event listener before the event -- | is raised. onceTrailers :: ServerHttp2Stream -> (HeadersObject -> Flags -> Effect Unit) -> Effect (Effect Unit) -onceTrailers http2stream = Internal.onceTrailers (unsafeCoerce http2stream) +onceTrailers http2stream = Internal.onceTrailers (upcastServerHttp2Stream http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#http2streamadditionalheadersheaders foreign import additionalHeaders :: ServerHttp2Stream -> HeadersObject -> Effect Unit @@ -254,7 +228,7 @@ foreign import additionalHeaders :: ServerHttp2Stream -> HeadersObject -> Effect -- | Returns an effect for removing the event listener before the event -- | is raised. onceErrorStream :: ServerHttp2Stream -> (Error -> Effect Unit) -> Effect (Effect Unit) -onceErrorStream http2stream = Internal.onceEmitterError (unsafeCoerce http2stream) +onceErrorStream http2stream = Internal.onceStreamEmitterError (upcastServerHttp2Stream http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#event-wanttrailers -- | @@ -263,13 +237,13 @@ onceErrorStream http2stream = Internal.onceEmitterError (unsafeCoerce http2strea -- | Returns an effect for removing the event listener before the event -- | is raised. onceWantTrailers :: ServerHttp2Stream -> Effect Unit -> Effect (Effect Unit) -onceWantTrailers http2stream = Internal.onceWantTrailers (unsafeCoerce http2stream) +onceWantTrailers http2stream = Internal.onceWantTrailers (upcastServerHttp2Stream http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#http2streamsendtrailersheaders -- | -- | > When sending a request or sending a response, the `options.waitForTrailers` option must be set in order to keep the `Http2Stream` open after the final `DATA` frame so that trailers can be sent. sendTrailers :: ServerHttp2Stream -> HeadersObject -> Effect Unit -sendTrailers http2stream = Internal.sendTrailers (unsafeCoerce http2stream) +sendTrailers http2stream = Internal.sendTrailers (upcastServerHttp2Stream http2stream) -- | https://nodejs.org/docs/latest/api/net.html#event-end -- | @@ -278,11 +252,11 @@ sendTrailers http2stream = Internal.sendTrailers (unsafeCoerce http2stream) -- | Returns an effect for removing the event listener before the event -- | is raised. onceEnd :: ServerHttp2Stream -> Effect Unit -> Effect (Effect Unit) -onceEnd http2stream = Internal.onceEnd (unsafeCoerce http2stream) +onceEnd http2stream = Internal.onceEnd (upcastServerHttp2Stream http2stream) -- | https://nodejs.org/docs/latest/api/http2.html#http2streamclosecode-callback closeStream :: ServerHttp2Stream -> Int -> Effect Unit -> Effect Unit -closeStream stream = Internal.closeStream (unsafeCoerce stream) +closeStream stream = Internal.closeStream (upcastServerHttp2Stream stream) -- | Coerce to a duplex stream. toDuplex :: ServerHttp2Stream -> Duplex diff --git a/src/Node/HTTP2/Server/Aff.purs b/src/Node/HTTP2/Server/Aff.purs index cab371d..02c3363 100644 --- a/src/Node/HTTP2/Server/Aff.purs +++ b/src/Node/HTTP2/Server/Aff.purs @@ -112,7 +112,7 @@ listen server options handler = makeAff \complete -> do Server.closeServer server $ pure unit complete (Left err) - onceCloseCancel <- Server.onceCloseServer server do + onceCloseCancel <- Server.onceServerClose server do onStreamCancel onErrorCancel complete (Right unit) @@ -182,7 +182,7 @@ listenSecure server options handler = makeAff \complete -> do Server.closeServerSecure server $ pure unit complete (Left err) - onceCloseCancel <- Server.onceCloseServerSecure server do + onceCloseCancel <- Server.onceServerSecureClose server do onStreamCancel onErrorCancel complete (Right unit) diff --git a/test/HTTP2.purs b/test/HTTP2.purs index f99203f..e6c6b99 100644 --- a/test/HTTP2.purs +++ b/test/HTTP2.purs @@ -25,7 +25,7 @@ basic_serverSecure complete = do server <- HTTP2.Server.createSecureServer (toOptions { key: key, cert: cert }) - void $ HTTP2.Server.onceStreamSecure server \stream _ _ -> do + void $ HTTP2.Server.onStreamSecure server \stream _ _ -> do HTTP2.Server.respond stream ( toHeaders { "content-type": "text/html; charset=utf-8"