diff --git a/bower.json b/bower.json index 53ae436..408c2e7 100644 --- a/bower.json +++ b/bower.json @@ -19,6 +19,8 @@ "purescript-node-streams": "^3.0.0", "purescript-node-url": "^3.0.0", "purescript-options": "^3.0.0", - "purescript-unsafe-coerce": "^3.0.0" + "purescript-unsafe-coerce": "^3.0.0", + "purescript-node-buffer": "^3.0.1", + "purescript-arraybuffer-types": "^2.0.0" } } diff --git a/src/Node/HTTP/Secure.js b/src/Node/HTTP/Secure.js new file mode 100644 index 0000000..5912654 --- /dev/null +++ b/src/Node/HTTP/Secure.js @@ -0,0 +1,13 @@ +"use strict"; + +var https = require("https"); + +exports.createServerImpl = function (options) { + return function (handleRequest) { + return function () { + return https.createServer(options, function (req, res) { + handleRequest(req)(res)(); + }); + }; + }; +}; diff --git a/src/Node/HTTP/Secure.purs b/src/Node/HTTP/Secure.purs new file mode 100644 index 0000000..66fe288 --- /dev/null +++ b/src/Node/HTTP/Secure.purs @@ -0,0 +1,282 @@ +-- | This module defines low-level bindings to the Node HTTPS module. + +module Node.HTTP.Secure + ( createServer + + , SSLOptions + , handshakeTimeout + , requestCert + , rejectUnauthorized + + , NPNProtocols + , npnProtocolsString + , npnProtocolsBuffer + , npnProtocolsUint8Array + , npnProtocolsStringArray + , npnProtocolsBufferArray + , npnProtocolsUint8ArrayArray + , npnProtocols + + , ALPNProtocols + , alpnProtocolsString + , alpnProtocolsBuffer + , alpnProtocolsUint8Array + , alpnProtocolsStringArray + , alpnProtocolsBufferArray + , alpnProtocolsUint8ArrayArray + , alpnProtocols + + , sessionTimeout + , ticketKeys + + , PFX + , pfxString + , pfxBuffer + , pfx + + , Key + , keyString + , keyBuffer + , keyStringArray + , keyBufferArray + , key + + , passphrase + + , Cert + , certString + , certBuffer + , certStringArray + , certBufferArray + , cert + + , CA + , caString + , caBuffer + , caStringArray + , caBufferArray + , ca + + , CRL + , crlString + , crlBuffer + , crlStringArray + , crlBufferArray + , crl + + , ciphers + , honorCipherOrder + , ecdhCurve + + , DHParam + , dhparamString + , dhparamBuffer + , dhparam + + , secureProtocol + , secureOptions + , sessionIdContext + ) where + +import Prelude + +import Control.Monad.Eff (Eff) +import Data.ArrayBuffer.Types (Uint8Array) +import Data.Foreign (Foreign) +import Data.Options (Options, Option, options, opt) +import Node.Buffer (Buffer) +import Node.HTTP (Request, Response, Server, HTTP) +import Unsafe.Coerce (unsafeCoerce) + +-- | The type of HTTPS server options +data SSLOptions + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener) +handshakeTimeout :: Option SSLOptions Int +handshakeTimeout = opt "handshakeTimeout" + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener) +requestCert :: Option SSLOptions Boolean +requestCert = opt "requestCert" + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener) +rejectUnauthorized :: Option SSLOptions Boolean +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 + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener) +npnProtocols :: Option SSLOptions NPNProtocols +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 + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener) +alpnProtocols :: Option SSLOptions ALPNProtocols +alpnProtocols = opt "ALPNProtocols" + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener) +sessionTimeout :: Option SSLOptions Int +sessionTimeout = opt "sessionTimeout" + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener) +ticketKeys :: Option SSLOptions Buffer +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 + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) +pfx :: Option SSLOptions PFX +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 + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) +key :: Option SSLOptions Key +key = opt "key" + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) +passphrase :: Option SSLOptions String +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 + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) +cert :: Option SSLOptions Cert +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 + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) +ca :: Option SSLOptions CA +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 + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) +crl :: Option SSLOptions CRL +crl = opt "crl" + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) +ciphers :: Option SSLOptions String +ciphers = opt "ciphers" + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) +honorCipherOrder :: Option SSLOptions Boolean +honorCipherOrder = opt "honorCipherOrder" + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) +ecdhCurve :: Option SSLOptions String +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 + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) +dhparam :: Option SSLOptions DHParam +dhparam = opt "dhparam" + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) +secureProtocol :: Option SSLOptions String +secureProtocol = opt "secureProtocol" + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) +secureOptions :: Option SSLOptions Int +secureOptions = opt "secureOptions" + +-- | See the [node docs](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) +sessionIdContext :: Option SSLOptions String +sessionIdContext = opt "sessionIdContext" + +-- | Create an HTTPS server, given the SSL options and a function to be executed +-- | when a request is received. +foreign import createServerImpl :: + forall eff. + Foreign -> + (Request -> Response -> Eff (http :: HTTP | eff) Unit) -> + Eff (http :: HTTP | eff) Server + +-- | Create an HTTPS server, given the SSL options and a function to be executed +-- | when a request is received. +createServer :: forall eff. + Options SSLOptions -> + (Request -> Response -> Eff (http :: HTTP | eff) Unit) -> + Eff (http :: HTTP | eff) Server +createServer = createServerImpl <<< options diff --git a/test/Main.purs b/test/Main.purs index 979d776..e78d5a7 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -7,47 +7,123 @@ import Control.Monad.Eff.Console (CONSOLE, log, logShow) import Data.Foldable (foldMap) import Data.Maybe (Maybe(..)) +import Data.Options (Options, options, (:=)) import Node.Encoding (Encoding(..)) -import Node.HTTP (HTTP, listen, createServer, setHeader, requestMethod, requestURL, responseAsStream, requestAsStream, setStatusCode) +import Node.HTTP (HTTP, Request, Response, listen, createServer, setHeader, requestMethod, requestURL, responseAsStream, requestAsStream, setStatusCode) import Node.HTTP.Client as Client +import Node.HTTP.Secure as HTTPS import Node.Stream (Writable, end, pipe, writeString) import Partial.Unsafe (unsafeCrashWith) +import Unsafe.Coerce (unsafeCoerce) foreign import stdout :: forall eff r. Writable r eff main :: forall eff. Eff (console :: CONSOLE, http :: HTTP | eff) Unit main = do testBasic + testHttpsServer testHttps testCookies +respond :: forall eff. Request -> Response -> Eff (console :: CONSOLE, http :: HTTP | eff) Unit +respond req res = do + setStatusCode res 200 + let inputStream = requestAsStream req + outputStream = responseAsStream res + log (requestMethod req <> " " <> requestURL req) + case requestMethod req of + "GET" -> do + let html = foldMap (_ <> "\n") + [ "
" + ] + setHeader res "Content-Type" "text/html" + _ <- writeString outputStream UTF8 html (pure unit) + end outputStream (pure unit) + "POST" -> void $ pipe inputStream outputStream + _ -> unsafeCrashWith "Unexpected HTTP method" + testBasic :: forall eff. Eff (console :: CONSOLE, http :: HTTP | eff) Unit testBasic = do server <- createServer respond 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 :: forall eff. Eff (console :: CONSOLE, http :: HTTP | eff) Unit +testHttpsServer = do + server <- HTTPS.createServer sslOpts respond + 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 where - respond req res = do - setStatusCode res 200 - let inputStream = requestAsStream req - outputStream = responseAsStream res - log (requestMethod req <> " " <> requestURL req) - case requestMethod req of - "GET" -> do - let html = foldMap (_ <> "\n") - [ "" - ] - setHeader res "Content-Type" "text/html" - _ <- writeString outputStream UTF8 html (pure unit) - end outputStream (pure unit) - "POST" -> void $ pipe inputStream outputStream - _ -> unsafeCrashWith "Unexpected HTTP method" + sslOpts = + HTTPS.key := HTTPS.keyString mockKey <> + HTTPS.cert := HTTPS.certString mockCert testHttps :: forall eff. Eff (console :: CONSOLE, http :: HTTP | eff) Unit testHttps = @@ -61,12 +137,23 @@ testCookies = simpleReq :: forall eff. String -> Eff (console :: CONSOLE, http :: HTTP | eff) Unit simpleReq uri = do log ("GET " <> uri <> ":") - req <- Client.requestFromURI uri \response -> void do - log "Headers:" - logShow $ Client.responseHeaders response - log "Cookies:" - logShow $ Client.responseCookies response - log "Response:" - let responseStream = Client.responseAsStream response - pipe responseStream stdout + req <- Client.requestFromURI uri logResponse end (Client.requestAsStream req) (pure unit) + +complexReq :: forall eff. Options Client.RequestOptions -> Eff (console :: CONSOLE, http :: HTTP | eff) Unit +complexReq opts = do + log $ optsR.method <> " " <> optsR.protocol <> "//" <> optsR.hostname <> ":" <> optsR.port <> optsR.path <> ":" + req <- Client.request opts logResponse + end (Client.requestAsStream req) (pure unit) + where + optsR = unsafeCoerce $ options opts + +logResponse :: forall eff. Client.Response -> Eff (console :: CONSOLE, http :: HTTP | eff) Unit +logResponse response = void do + log "Headers:" + logShow $ Client.responseHeaders response + log "Cookies:" + logShow $ Client.responseCookies response + log "Response:" + let responseStream = Client.responseAsStream response + pipe responseStream stdout