From e10f6bae09f206a25c706df71081c8bee1844561 Mon Sep 17 00:00:00 2001 From: Apollon Oikonomopoulos Date: Sat, 5 Aug 2017 17:51:57 -0400 Subject: New upstream version 1.0.0 --- .gitignore | 12 + .travis.yml | 16 + CHANGELOG.md | 50 ++ CONTRIBUTING.md | 9 + LICENSE | 201 +++++++ README.md | 291 ++++++++++ dev-resources/config/jetty/ssl/certs/ca.pem | 30 + dev-resources/config/jetty/ssl/certs/localhost.pem | 33 ++ .../config/jetty/ssl/private_keys/localhost.pem | 51 ++ dev-resources/logback-test.xml | 11 + dev-resources/ssl/cert.pem | 31 + ext/travisci/test.sh | 3 + jenkins/deploy.sh | 15 + project.clj | 44 ++ src/puppetlabs/ring_middleware/common.clj | 53 ++ src/puppetlabs/ring_middleware/core.clj | 214 +++++++ src/puppetlabs/ring_middleware/utils.clj | 93 +++ test/puppetlabs/ring_middleware/core_test.clj | 635 +++++++++++++++++++++ .../ring_middleware/testutils/common.clj | 23 + test/puppetlabs/ring_middleware/utils_test.clj | 25 + 20 files changed, 1840 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 dev-resources/config/jetty/ssl/certs/ca.pem create mode 100644 dev-resources/config/jetty/ssl/certs/localhost.pem create mode 100644 dev-resources/config/jetty/ssl/private_keys/localhost.pem create mode 100644 dev-resources/logback-test.xml create mode 100644 dev-resources/ssl/cert.pem create mode 100755 ext/travisci/test.sh create mode 100755 jenkins/deploy.sh create mode 100644 project.clj create mode 100644 src/puppetlabs/ring_middleware/common.clj create mode 100644 src/puppetlabs/ring_middleware/core.clj create mode 100644 src/puppetlabs/ring_middleware/utils.clj create mode 100644 test/puppetlabs/ring_middleware/core_test.clj create mode 100644 test/puppetlabs/ring_middleware/testutils/common.clj create mode 100644 test/puppetlabs/ring_middleware/utils_test.clj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e0d8dff --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.nrepl-port +pom.xml +pom.xml.asc +*jar +/lib/ +/classes/ +/target/ +/checkouts/ +.lein-deps-sum +.lein-repl-history +.lein-plugins/ +.lein-failures diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..1bf409f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,16 @@ +language: clojure +lein: lein2 +jdk: +- oraclejdk7 +- openjdk7 +script: ./ext/travisci/test.sh +notifications: +email: false + +# workaround for buffer overflow issue, ref https://github.com/travis-ci/travis-ci/issues/522e +before_install: + - cat /etc/hosts # optionally check the content *before* + - sudo hostname "$(hostname | cut -c1-63)" + - sed -e "s/^\\(127\\.0\\.0\\.1.*\\)/\\1 $(hostname | cut -c1-63)/" /etc/hosts | sudo tee /etc/hosts + - cat /etc/hosts # optionally check the content *after* + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d95bb00 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,50 @@ +# 1.0.0 +#### Breaking Changes +* Moves from `{:type ... :message ...}` to `{:kind ... :msg ...}` for + exceptions and error responses. +* Moves schemas and helpers previously defined in `core` namespace into new `utils` namespace. + +# 0.3.1 +* This is a bug-fix release for a regression in wrap-proxy. +* All middleware now have the `:always-validate` metadata + set for schema validation. + +# 0.3.0 +* This version adds many middleware that are used in other + puppetlabs projects. These middleware are mostly for logging + and error handling, and they are all documented in the + [README](./README.md): + * `wrap-request-logging` + * `wrap-response-logging` + * `wrap-service-unavailable` + * `wrap-bad-request` + * `wrap-data-errors` + * `wrap-schema-errors` + * `wrap-uncaught-errors` +* Additionally, this version fixes + [an issue](https://tickets.puppetlabs.com/browse/TK-228) with the + behavior of `wrap-proxy` and its handling of redirects. + +# 0.2.1 +* Add wrap-with-certificate-cn middleware that adds a `:ssl-client-cn` key + to the request map if a `:ssl-client-cert` is present. +* Add wrap-with-x-frame-options-deny middleware that adds `X-Frame-Options: DENY` + +# 0.2.0 +* Modify behavior of regex support in the wrap-proxy function. + Now, when a regex is given for the `proxied-path` argument, + the entirety of the request uri's path will be appended onto + the path of `remote-uri-base`. +* Add a new utility middleware, `wrap-add-cache-headers`, + that adds `cache-control` headers to `GET` and `PUT` + requests. + +# 0.1.3 +* Log proxied requests +* Allow `proxied-path` argument in `wrap-proxy` function to + be a regular expression +* Bump http-client to v0.2.8 + +# 0.1.2 +* Add support for redirect following on proxy requests +* Fix issue where Gzipped proxy responses were being truncated diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8890fe7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +# How to contribute + +Third-party patches are essential for keeping puppet open-source projects +great. We want to keep it as easy as possible to contribute changes that +allow you to get the most out of our projects. There are a few guidelines +that we need contributors to follow so that we can have a chance of keeping on +top of things. For more info, see our canonical guide to contributing: + +[https://github.com/puppetlabs/puppet/blob/master/CONTRIBUTING.md](https://github.com/puppetlabs/puppet/blob/master/CONTRIBUTING.md) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5c304d1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..04c42c7 --- /dev/null +++ b/README.md @@ -0,0 +1,291 @@ +# ring-middleware + +[![Build Status](https://travis-ci.org/puppetlabs/ring-middleware.png?branch=master)](https://travis-ci.org/puppetlabs/ring-middleware) + +This project was originally adapted from tailrecursion's +[ring-proxy](https://github.com/tailrecursion/ring-proxy) middleware, and is +meant for use with the [Trapperkeeper Jetty9 Webservice](https://github.com/puppetlabs/trapperkeeper-webserver-jetty9). It also contains common ring middleware between Puppet projects and helpers to be used with the middleware. + +## Usage + + +To use `ring-middleware`, add this project as a dependency in your leiningen project file: + +[![Clojars Project](http://clojars.org/puppetlabs/ring-middleware/latest-version.svg)](https://clojars.org/puppetlabs/ring-middleware) + +## Schemas + + * `ResponseType` -- one of the two supported response types (`:json`, `:plain`) returned by many middleware. + * `RingRequest` -- a map containing at least a `:uri`, optionally a valid certificate, and any number of keyword-Any pairs. + * `RingResponse` -- a map with at least `:status`, `:headers`, and `:body` keys. + + +## Non-Middleware Helpers +### json-response +```clj +(json-response status body) +``` +Creates a basic ring response with `:status` of `status` and a `:body` of `body` serialized to json. + +### plain-response +```clj +(plain-response status body) +``` +Creates a basic ring response with `:status` of `status` and a `:body` of `body` set to UTF-8 plain text. + + +### throw-bad-request! +```clj +(throw-bad-request! "Error Message") +``` +Throws a :bad-request type slingshot error with the supplied message. +See `wrap-bad-request` for middleware designed to compliment this function, +also `bad-request?` for a function to help implement your own error handling. + +### throw-service-unavailable! +```clj +(throw-service-unavailable! "Error Message") +``` +Throws a :service-unavailable type slingshot error with the supplied message. +See `wrap-service-unavailable` for middleware designed to compliment this function, +also `service-unavailable?` for a function to help implement your own error handling. + +### throw-data-invalid! +```clj +(throw-data-invalid! "Error Message") +``` +Throws a :data-invalid type slingshot error with the supplied message. +See `wrap-data-errors` for middleware designed to compliment this function, +also `data-invalid?` for a function to help implement your own error handling. + + +### bad-request? +```clj +(try+ (handler request) + (catch bad-request? e + (...handle a bad request...))) +``` +Determines if the supplied slingshot error map is for a bad request. + +### service-unavailable? +```clj +(try+ (handler request) + (catch service-unavailable? e + (...handle service unavailability...))) +``` +Determines if the supplied slingshot error map is for the service being unavailable. + +### data-invalid? +```clj +(try+ (handler request) + (catch data-invalid? e + (...handle invalid data...))) +``` +Determines if the supplied slingshot error map is for invalid data. + +### schema-error? +```clj +(try+ (handler request) + (catch schema-error? e + (...handle schema error...))) +``` +Determines if the supplied slingshot error map is for a schema error. + + + +## Middleware +### wrap-request-logging +```clj +(wrap-request-logging handler) +``` +Logs the `:request-method` and `:uri` at debug level, the full request at trace. At the trace level, attempts to remove sensitive auth information and replace client certificate with the client's common name. + +### wrap-response-logging +```clj +(wrap-response-logging handler) +``` +Logs the response at the trace log level. + +### wrap-proxy +```clj +(wrap-proxy handler proxied-path remote-uri-base & [http-opts]) +``` + +This function returns a ring handler that, when given a URL with a certain prefix, proxies the request +to a remote URL specified by the `remote-uri-base` argument. + +The arguments are as follows: + +* `handler`: A ring-handler that will be used if the provided url does not begin with the proxied-path prefix +* `proxied-path`: The URL prefix of all requests that are to be proxied. This can be either a string or a + regular expression pattern. Note that, when this is a regular expression, the entire request URI + will be appended to `remote-uri-base` when the URI is being rewritten, whereas if this argument + is a string, the `proxied-path` will not be included. +* `remote-uri-base`: The base URL that you want to proxy requests with the `proxied-path` prefix to +* `http-opts`: An optional list of options for an http client. This is used by the handler returned by + `wrap-proxy` when it makes a proxied request to a remote URI. For a list of available options, please + see the options defined for [clj-http-client](https://github.com/puppetlabs/clj-http-client). + +For example, the following: + +```clj +(wrap-proxy handler "/hello-world" "http://localhost:9000/hello") +``` +would return a ring handler that proxies all requests with URL prefix "/hello-world" to +`http://localhost:9000/hello`. + +The following: + +```clj +(wrap-proxy handler #"^/hello-world" "http://localhost:9000/hello") +``` +would return a ring handler that proxies all requests with a URL path matching the regex +`#^/hello-world"` to `http://localhost:9000/hello/[url-path]`. + +#### Proxy Redirect Support + +By default, all proxy requests using `wrap-proxy` will follow any redirects, including on POST and PUT +requests. To allow redirects but restrict their use on POST and PUT requests, set the `:force-redirects` +option to `false` in the `http-opts` map. To disable redirect following on proxy requests, set the +`:follow-redirects` option to `false` in the `http-opts` map. Please not that if proxy redirect following +is disabled, you may have to disable it on the client making the proxy request as well if the location returned +by the redirect is relative. + +#### SSL Support + +`wrap-proxy` supports SSL. To add SSL support, you can set SSL options in the `http-opts` map as you would in +a request made with [clj-http-client](https://github.com/puppetlabs/clj-http-client). Simply set the +`:ssl-cert`, `:ssl-key`, and `:ssl-ca-cert` options in the `http-opts` map to be paths to your .pem files. + +### wrap-with-certificate-cn + +This middleware adds a `:ssl-client-cn` key to the request map if a +`:ssl-client-cert` is present. If no client certificate is present, + the key's value is set to nil. This makes for easier certificate +whitelisting (using the cert whitelisting function from pl/kitchensink) + +### wrap-add-cache-headers + +A utility middleware with the following signature: + +```clj +(wrap-add-cache-headers handler) +``` + +This middleware adds `cache-control` headers ("private, max-age=0, no-cache") to `GET` and `PUT` requests if they are handled by the handler. + +### wrap-add-x-frame-options-deny + +A utility middleware with the following signature: + +```clj +(wrap-add-x-frame-options-deny handler) +``` + +This middleware adds `X-Frame-Options: DENY` headers to requests if they are handled by the handler. + +### wrap-data-errors +```clj +(wrap-data-errors handler) +``` +Always returns a status code of 400 to the client and logs the error message at the "error" log level. +Catches and processes any exceptions thrown via `slingshot/throw+` with a `:type` of one of: + * `:request-data-invalid` + * `:user-data-invalid` + * `:data-invalid` + * `:service-status-version-not-found` + +Returns a basic ring response map with the `:body` set to the JSON serialized representation of the exception thrown wrapped in a map and accessible by the "error" key. + +Example return body: +```json +{ + "error": { + "type": "user-data-invalid", + "message": "Error Message From Thrower" + } +} +``` + +Returns valid [`ResponseType`](#schemas)s, eg: +```clj +(wrap-data-errors handler :plain) +``` + +### wrap-bad-request +```clj +(wrap-bad-request handler) +``` +Always returns a status code of 400 to the client and logs the error message at the "error" log level. +Catches and processes any exceptions thrown via `slingshot/throw+` with a `:type` of one of: + * `:bad-request` + +Returns a basic ring response map with the `:body` set to the JSON serialized representation of the exception thrown wrapped in a map and accessible by the "error" key. + +Example return body: +```json +{ + "error": { + "type": "bad-request", + "message": "Error Message From Thrower" + } +} +``` + +Returns valid [`ResponseType`](#schemas)s, eg: +```clj +(wrap-bad-request handler :plain) +``` + +### wrap-schema-errors +```clj +(wrap-schema-errors handler) +``` +Always returns a status code of 500 to the client and logs an message containing the schema error, expected value, and exception type at the "error" log level. + +Returns a basic ring response map with the `:body` as the JSON serialized representation of helpful exception information wrapped in a map and accessible by the "error" key. Always returns an error type of "application-error". + +Example return body: +```json +{ + "error": { + "type": "application-error", + "message": "Something unexpected happened: {:error ... :value ... :type :schema.core/error}" + } +} +``` + +Returns valid [`ResponseType`](#schemas)s, eg: +```clj +(wrap-schema-errors handler :plain) +``` + +### wrap-uncaught-errors +```clj +(wrap-uncaught-errors handler) +``` +Always returns a status code of 500 to the client and logs a message with the serialized Exception at the "error" log level. + +Returns a basic ring response map with the `:body` set as the JSON serialized representation of helpful exception information wrapped in a map and accessible by the "error" key. Always returns an error type of "application-error". + +Example return body: +```json +{ + "error": { + "type": "application-error", + "message": "Internal Server Error: " + } +} +``` + +Returns valid [`ResponseType`](#schemas)s, eg: +```clj +(wrap-uncaught-errors handler :plain) +``` + +## Support + +Please log tickets and issues at our [Trapperkeeper Issue Tracker](https://tickets.puppetlabs.com/browse/TK). +In addition there is a #trapperkeeper channel on Freenode. + +Maintainers: Justin Stoller , Kevin Corcoran , Nathaniel Smith diff --git a/dev-resources/config/jetty/ssl/certs/ca.pem b/dev-resources/config/jetty/ssl/certs/ca.pem new file mode 100644 index 0000000..e11d220 --- /dev/null +++ b/dev-resources/config/jetty/ssl/certs/ca.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRQdXBw +ZXQgQ0E6IGxvY2FsaG9zdDAeFw0xNDAyMTQxODA5MDdaFw0xOTAyMTQxODA5MDda +MB8xHTAbBgNVBAMMFFB1cHBldCBDQTogbG9jYWxob3N0MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA5vYnoJ85k6qcUFzWFOr9MN2ZWFlgA6nB0Adfm8Yg +ovg963NrauwTcoPqbfGhi53A8RWc5GT5x1OSjxQW/PAGGfGg3aHZuQMClYWsauod +CYG6YgG49WGZODCHZ+TbHMN1pNV+6S+tnZbUtfVKsN347XyHCyrymmb/OQNAxPyO +/76dDR4dB7kWKP4KMMOgDAnA+WDpD1d9/UfBPVZtw/sTyjeJGEOZqSXQW93PKumJ +9/DS4azsUMR1JwJA67yWffoHb6sL7QSAQEfp17gDs9asIzITZPPyHNslmY55zaSW +EslzFGqPvB7ugUbqH0rjp2TjQ/9Nw7ZKBxukFB9cBUr7v21D2Of2SHQorzRe9lXO +wO3BYEt/viypktDnH42qAeoLHDK1ay9ugByTm4ARj14CjslpkEKflk9t9XwUR3ku +Uaj3w+Y5ItZXFBxns1OQpDt3bMhJFemj7MxkZXt8tvWlUMKVmCUqbnG1eDdTF4Vd +OovdacMDpvKg438I8MCjaHQf90qIp8MaOjdfSoKBCQi6AP7cpjB+x9xhEuetPYcb +/bNjV1OFo4h77XJ+lIH58HBpEG0zkqhtS7JqICIzp3GK7ZG3bhfKKJz0Qr4ISxI0 +YsYWdmsG38SyMwcomMy4WoLiuQF2QusBSHwBS+p5qQyNkJmXEy8KSQmrrIB/bkiS +mgkCAwEAAaN5MHcwNQYJYIZIAYb4QgENBChQdXBwZXQgUnVieS9PcGVuU1NMIElu +dGVybmFsIENlcnRpZmljYXRlMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBRZy3D0vQGiBu3mnggq8jiEZERoXzANBgkqhkiG9w0BAQsF +AAOCAgEAZHcyah146g2vmuQZsGJtljCkWjM1WWjcPoLewdd4FG1YKkAgbGGPVnSS +yIS04tj4/Z3eePUlBNe3ZHs7yTQkq3SwWIhRSMJRA8xAjtL0rL7mvJ6PMV+Sujb7 ++ENTML76+oq1d7XTqAh8mnSJOIc6eMUQqBBYl/5BYa4IzkvkrS4ai4vg0ihKr6pf +C8XVINTjxChqK08dEr690sPD/3DwPPVGqG+qIyIv6u5buVLniLWiaq4HOG54i/yH +k21W6A5UizRxb5GwVxWfjsHcLr2pvPq/ipRP/sCNBgKdziaGQm+IqeJiI6bQegx3 +ZF5UX2CaIkZsEM0v2yQ7/t9rSCfpd/eEYMdcWcnSfEf/OOA/ti7jnpapENXturLo +6/TNJarHYQlkpEsaRfiiSmomAn3TNOOuqBhWtj0fdy21/fETxVVzPp/CL7EvkUnU +26SguZdBkGf22yZViKRq22eDnM/frsoSMTkIbkayDQ8Hx9JVpfPmdJWQyL86etsd +uCR5OXJtd7vxRZT15m2cFvdpRW3VZbqFyIwe0NgJUs3FRrjFZdqQiBsvWwQjK8jN +A3+IAAz4pk0A4xpQvNwvUzo4wyQB8/PTcmZoPBzeDaJScqSto2r3kvOSqvm1PSc6 +gneGkbPvQpQRH9HVxzaEEcCrcZYFZXpCkF0ACuB+smgfMVY2Sno= +-----END CERTIFICATE----- diff --git a/dev-resources/config/jetty/ssl/certs/localhost.pem b/dev-resources/config/jetty/ssl/certs/localhost.pem new file mode 100644 index 0000000..faa309c --- /dev/null +++ b/dev-resources/config/jetty/ssl/certs/localhost.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFnjCCA4agAwIBAgIBAjANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRQdXBw +ZXQgQ0E6IGxvY2FsaG9zdDAeFw0xNDAyMTQxODA5MDdaFw0xOTAyMTQxODA5MDda +MBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBALzn4xcbGhSX65fgSIo+NB4EOYX+zJxbHamv+hbQkhAxqHxUSszDD86+ +s9ZsN8cvpDIRex1/SfkNRKhgVUHGiUYbol+9RLpgpdHYLJCY0S0kOyBUgWqEcQrA +tVDYpwaNC5qMneiZkcNvQ74pnuFOa0/bzbpLyOjYKEQq3BXK32yGLLHseb/PIX1o +bqcuYiR6yI7S3wK9hJzSo6BRPSz0cNZNpnXbW/yLRtHbthBtzTifM2MW2jtWuFvg +OW1ly9vxwTHCrJv5KxMubVDrqAx+RmmOsn327APO3r6NUr2CzV1vG8CMqLApse6m +dKpCZ5UNLm4dwCUe2zWAQ0Q2Eba88O480bH8k/t8NUGXlWt25B8BUE8rklY0jwSw +v8Dyjda0moeoxgMMZb+ogPjTY/Ds/h2hgFzy/DUuGrv6kyVxzH0/pOg29HeDoUaK +IaqjAsN8h/oYTJ6CiOKAqWfbi8JZRmiEVTekskS5eySsPqPCegfAkfpIlz4EU4FN +0A2vORI7epW4mNiJcCLb09v5jogK+DhfeSscDsrYgIF8eAPLw6r9h1am+vXoD6vt +tIwZBu15pnCWXdDcKWBNjx+zYU648iZ9V/qFG31uTJaw0z18eFPyTcJN8StTnoGZ +zjKI2AyjG5F6ov/S7mnU3I/wv0B5Vq6fPjSvMrsBlbBdI3Pbr3w/AgMBAAGjge8w +gewwNQYJYIZIAYb4QgENBChQdXBwZXQgUnVieS9PcGVuU1NMIEludGVybmFsIENl +cnRpZmljYXRlMFQGA1UdEQRNMEuCG2Rqcm9vbWJhLnZwbi5wdXBwZXRsYWJzLm5l +dIIJbG9jYWxob3N0ggZwdXBwZXSCGXB1cHBldC52cG4ucHVwcGV0bGFicy5uZXQw +DgYDVR0PAQH/BAQDAgWgMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcD +AjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRZy3D0vQGiBu3mnggq8jiEZERoXzAN +BgkqhkiG9w0BAQsFAAOCAgEAJwDAK7UKZvLtSJkdprO/Z0qALdUSliO0+I6lsSr/ +A8SyilfMQuxOoq6uWA6j7uMU4SAHwT14QD9c6BJJiBhWLo6HoynZfYK8Smn1q2Xy +vNMnUEUEjcWIgIqEDP98RTUZldgR4aWQkQVHB9XY8g6F0qWzBq64qLWjfrsIUijF +Z3Ex1OMU7ZMjHhoKk2J3oBcMau47mqU49C9MoWkBH+0fLLr+lxoa40DPcFr+KzhI +BHdTKuKAEJEkiE5QNVWl3+M7psFZzdTd+YFz9Vn/9L3aQr8KCb4oMietp84KM0yR +6GwodvIjh/3owsnvNvl28HeZGWQMCNIlG0aWx3JIfCOHSYlXfQ55FCtLRqMflJty +M4MqRkPHKykZMZmNoiTtXRSz3vMW8JnIyZPsNtfIGltnpjHd4Y4EEVxdZhlL2wBv +YycI6PN1oyhv/9ZcIbSaVY4jEpxx6mrsG8iuaT1YHlO2HOwuqvXJ48WkT1Q6go9k +A775N00y4jLJXqWchiuP9Hp7AGYWzoKXFDmQbUyl0jpZsrfoTskb90xp6R7+IO7K +6opzTahhO9QeURqc7lkdvwjiYMw7uIiTT6IAKtYeK4f52siiJ/LBWucte5FLCjvq +KkdWF7aQOKZq+L7Cs2nJ/qQQzlDYTQLpAbBWJbVAscoYYGPVPdKrwI4UEbKGIsWs +wMA= +-----END CERTIFICATE----- diff --git a/dev-resources/config/jetty/ssl/private_keys/localhost.pem b/dev-resources/config/jetty/ssl/private_keys/localhost.pem new file mode 100644 index 0000000..231da6a --- /dev/null +++ b/dev-resources/config/jetty/ssl/private_keys/localhost.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/dev-resources/logback-test.xml b/dev-resources/logback-test.xml new file mode 100644 index 0000000..b13ebc2 --- /dev/null +++ b/dev-resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + %d %-5p [%c{2}] %m%n + + + + + + + diff --git a/dev-resources/ssl/cert.pem b/dev-resources/ssl/cert.pem new file mode 100644 index 0000000..4d9b583 --- /dev/null +++ b/dev-resources/ssl/cert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFSjCCAzKgAwIBAgIBAzANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRQdXBw +ZXQgQ0E6IGV4cGxvc2l2bzAeFw0xMzAxMDEyMTQ0MjhaFw0xODAxMDEyMTQ0Mjha +MBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBANCEYBvEIdTXFOMwz18wahs6tg26C+LT2XOwQspb/Aj5WT2EBwnG2leD +CzfOAKyhHDL6jRqrYU32jqoqBzmzkeVsHzqOMNFosyvcBLU8zyLZU+IP1rjJCyE8 +xx9HsdhPKJj93f/gSNR5NQlRcZfqahhOwh/nYdY3pFiNgjUoRwhV2Q01n+ku8WJw +kLVT1TREW9TiSWk7cHWF/ZltPOMMxvJ9q0kXh8sVYK4Gtt3pphTUW0qgXQ2NnNWT +W+7vciRjnHxeoY3q6ZG7vZ8HewYKR4W8D6FA32xCsWELSsWlAABt1lBjKGas/fiY +SeDqSfIxknFn/CIM9AIp2PLS3wh5e0o98qey9AN2WRyG7Qs0ijhwKx9bsMxbM0LR +5jXuXjBnjGQ69fwCjwlUsOSpNPWLibM+GmxvhghJgAlH5dD4+GqN77WLncQTWYXX +GnOw5efVivS4bgU3t8l8mHLH6quLolR1KLfCv+HuqkvRposAqqLwKH+dhlbq1Y+i +4siPxfYV5NZ092Z9R0F4BPEmLhKngkK+/eQXxLY2zfaR6Ns83yRJfMXRyElECX/+ +RBT1LyIRZg+MbsRg7DsKWI0plzxso/4CgSmYSfPku5nkekrMN34YhUtcxsdHSmY1 +5/p2olvKpTJj3e5fa2KVswcv77FsC17gIfMXqvN3tITP+q1LLJHNAgMBAAGjgZsw +gZgwNwYJYIZIAYb4QgENBCoWKFB1cHBldCBSdWJ5L09wZW5TU0wgSW50ZXJuYWwg +Q2VydGlmaWNhdGUwDgYDVR0PAQH/BAQDAgWgMCAGA1UdJQEB/wQWMBQGCCsGAQUF +BwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBT3awXrPXSWNszZ +tlLKoXQ8LRjJ3DANBgkqhkiG9w0BAQsFAAOCAgEAYTFx++uptZxgptFmkPfT1f2W +6djOOVULmlLGPC6Ovbe5v0ksA2hbLW3eSmfL28Ku0WC8gRl0/PhyiyW77M1jp9dV +ztsFkXjMiIIcY0B7Hgqh1kpK1CFvSbsD3piXcDLlZ1CwSAXuohp+J2fUblHRfAUD +Th9qrm3g4uNFp0wXxO1+GgXeDrGRqYosb0wAhB7/BhW2WbOFtVYdFoyyXJFJYx/3 +Gj7ZTE3rvGGxOEEuww0pFmuGCflZYxEu15Rynej7soGaE80+wRk+gMS26WKwRQ/0 +TGovOHSLo/fpOjjHIoqbQLH3S08jUfAYjjP7Rd01SiztUjZMILC2WCnpdYN+2O9O +rGTj2Zl3oRtE67NwxgKlo2GIFghSF366XOF8O4z1e9id6u5XEdoz8uGFkHyMu79N +cdYcUtmAqLvJ0Ubewg+TfNDcfk1akNtHtIJDNqFwHlZ9R1GIupHQs10R4YxJv3I2 +LojJbtcgWcDg9StwCHRA0SuLrWnHPnm+glzXM5HTNJZ6vcrM0SrcnZ59p3o3ZULL +JTJikA+pcs+WAWz1yTNg/ywxYPnFrs8A4MEC43XFe2dS96a7VcNqpMMya+DAWWCS ++51lDlMLuz+q5WHDGh3ouTNYMdcSLo8bHzKInDYnK/DzUpdJ5OKYwqfxv5n0bJs6 +fRA2buaQo0zJeid7G2Q= +-----END CERTIFICATE----- diff --git a/ext/travisci/test.sh b/ext/travisci/test.sh new file mode 100755 index 0000000..db011da --- /dev/null +++ b/ext/travisci/test.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +lein2 test diff --git a/jenkins/deploy.sh b/jenkins/deploy.sh new file mode 100755 index 0000000..3bc667f --- /dev/null +++ b/jenkins/deploy.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -e +set -x + +git fetch --tags + +lein test +echo "Tests passed!" + +lein release +echo "Release plugin successful, pushing changes to git" + +git push origin --tags HEAD:$RING_MIDDLEWARE_BRANCH +echo "git push successful." diff --git a/project.clj b/project.clj new file mode 100644 index 0000000..ad8f7b2 --- /dev/null +++ b/project.clj @@ -0,0 +1,44 @@ +(def clj-version "1.7.0") +(def ks-version "1.3.0") +(def tk-version "1.3.1") + +(defproject puppetlabs/ring-middleware "1.0.0" + :dependencies [[org.clojure/clojure ~clj-version] + + ;; begin version conflict resolution dependencies + [clj-time "0.11.0"] + ;; end version conflict resolution dependencies + + [cheshire "5.6.1"] + [org.clojure/tools.logging "0.3.1"] + [prismatic/schema "1.1.0"] + + [puppetlabs/http-client "0.5.0"] + [puppetlabs/kitchensink ~ks-version] + [puppetlabs/ssl-utils "0.8.1"] + [ring "1.4.0"] + [slingshot "0.12.2"]] + + ;; Abort when version ranges or version conflicts are detected in + ;; dependencies. Also supports :warn to simply emit warnings. + ;; requires lein 2.2.0+. + :pedantic? :abort + + :plugins [[lein-release "1.0.5"]] + + :deploy-repositories [["releases" {:url "https://clojars.org/repo" + :username :env/clojars_jenkins_username + :password :env/clojars_jenkins_password + :sign-releases false}] + ["snapshots" "http://nexus.delivery.puppetlabs.net/content/repositories/snapshots/"]] + + :profiles {:dev {:dependencies [ + ;; begin version conflict resolution dependencies + [org.clojure/tools.reader "1.0.0-beta1"] + [org.clojure/tools.macro "0.1.5"] + ;; begin version conflict resolution dependencies + + [puppetlabs/trapperkeeper-webserver-jetty9 "1.5.5"] + [puppetlabs/kitchensink ~ks-version :classifier "test" :scope "test"] + [puppetlabs/trapperkeeper ~tk-version :classifier "test" :scope "test"] + [compojure "1.5.0"]]}}) diff --git a/src/puppetlabs/ring_middleware/common.clj b/src/puppetlabs/ring_middleware/common.clj new file mode 100644 index 0000000..d34cf3e --- /dev/null +++ b/src/puppetlabs/ring_middleware/common.clj @@ -0,0 +1,53 @@ +(ns puppetlabs.ring-middleware.common + (:require [clojure.tools.logging :as log] + [clojure.string :refer [join split replace-first]] + [puppetlabs.http.client.sync :refer [request]]) + (:import (java.net URI))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Private utility functions + +(defn prepare-cookies + "Removes the :domain and :secure keys and converts the :expires key (a Date) + to a string in the ring response map resp. Returns resp with cookies properly + munged." + [resp] + (let [prepare #(-> (update-in % [1 :expires] str) + (update-in [1] dissoc :domain :secure))] + (assoc resp :cookies (into {} (map prepare (:cookies resp)))))) + +(defn strip-trailing-slash + [url] + (if (.endsWith url "/") + (.substring url 0 (- (count url) 1)) + url)) + +(defn proxy-request + [req proxied-path remote-uri-base & [http-opts]] + ; Remove :decompress-body from the options map, as if this is + ; ever set to true, the response returned to the client making the + ; proxy request will be truncated + (let [http-opts (dissoc http-opts :decompress-body) + uri (URI. (strip-trailing-slash remote-uri-base)) + remote-uri (URI. (.getScheme uri) + (.getAuthority uri) + (str (.getPath uri) + (if (instance? java.util.regex.Pattern proxied-path) + (:uri req) + (replace-first (:uri req) proxied-path ""))) + nil + nil) + response (-> (merge {:method (:request-method req) + :url (str remote-uri "?" (:query-string req)) + :headers (dissoc (:headers req) "host" "content-length") + :body (not-empty (slurp (:body req))) + :as :stream + :force-redirects false + :follow-redirects false + :decompress-body false} + http-opts) + request + prepare-cookies)] + (log/debug "Proxying request to" (:uri req) "to remote url" (str remote-uri) + ". Remote server responded with status" (:status response)) + response)) diff --git a/src/puppetlabs/ring_middleware/core.clj b/src/puppetlabs/ring_middleware/core.clj new file mode 100644 index 0000000..f125a25 --- /dev/null +++ b/src/puppetlabs/ring_middleware/core.clj @@ -0,0 +1,214 @@ +(ns puppetlabs.ring-middleware.core + (:require [cheshire.core :as json] + [clojure.tools.logging :as log] + [puppetlabs.kitchensink.core :as ks] + [puppetlabs.ring-middleware.common :as common] + [puppetlabs.ring-middleware.utils :as utils] + [puppetlabs.ssl-utils.core :as ssl-utils] + [ring.middleware.cookies :as cookies] + [ring.util.response :as rr] + [schema.core :as schema] + [slingshot.slingshot :as sling]) + (:import (clojure.lang IFn ExceptionInfo) + (java.lang Exception) + (java.util.regex Pattern) + (java.security.cert X509Certificate))) + + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; Private + +(defn sanitize-client-cert + "Given a ring request, return a map which replaces the :ssl-client-cert with + just the certificate's Common Name at :ssl-client-cert-cn. Also, remove the + copy of the certificate put on the request by TK-auth." + [req] + (-> (if-let [client-cert (:ssl-client-cert req)] + (-> req + (dissoc :ssl-client-cert) + (assoc :ssl-client-cert-cn (ssl-utils/get-cn-from-x509-certificate client-cert))) + req) + (ks/dissoc-in [:authorization :certificate]))) + + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; Middleware + +(schema/defn ^:always-validate wrap-request-logging :- IFn + "A ring middleware that logs the request." + [handler :- IFn] + (fn [{:keys [request-method uri] :as req}] + (log/debug "Processing" request-method uri) + (log/trace (str "Full request:\n" (ks/pprint-to-string (sanitize-client-cert req)))) + (handler req))) + +(schema/defn ^:always-validate wrap-response-logging :- IFn + "A ring middleware that logs the response." + [handler :- IFn] + (fn [req] + (let [resp (handler req)] + (log/trace "Computed response:" resp) + resp))) + +(schema/defn ^:always-validate wrap-proxy :- IFn + "Proxies requests to proxied-path, a local URI, to the remote URI at + remote-uri-base, also a string." + [handler :- IFn + proxied-path :- (schema/either Pattern schema/Str) + remote-uri-base :- schema/Str + & [http-opts]] + (let [proxied-path (if (instance? Pattern proxied-path) + (re-pattern (str "^" (.pattern proxied-path))) + proxied-path)] + (cookies/wrap-cookies + (fn [req] + (if (or (and (string? proxied-path) (.startsWith ^String (:uri req) (str proxied-path "/"))) + (and (instance? Pattern proxied-path) (re-find proxied-path (:uri req)))) + (common/proxy-request req proxied-path remote-uri-base http-opts) + (handler req)))))) + +(schema/defn ^:always-validate wrap-add-cache-headers :- IFn + "Adds cache control invalidation headers to GET and PUT requests if they are handled by the handler" + [handler :- IFn] + (fn [request] + (let [request-method (:request-method request) + response (handler request)] + (when-not (nil? response) + (if (or + (= request-method :get) + (= request-method :put)) + (assoc-in response [:headers "cache-control"] "private, max-age=0, no-cache") + response))))) + +(schema/defn ^:always-validate wrap-add-x-frame-options-deny :- IFn + "Adds 'X-Frame-Options: DENY' headers to requests if they are handled by the handler" + [handler :- IFn] + (fn [request] + (let [response (handler request)] + (when response + (assoc-in response [:headers "X-Frame-Options"] "DENY"))))) + +(schema/defn ^:always-validate wrap-with-certificate-cn :- IFn + "Ring middleware that will annotate the request with an + :ssl-client-cn key representing the CN contained in the client + certificate of the request. If no client certificate is present, + the key's value is set to nil." + [handler :- IFn] + (fn [{:keys [ssl-client-cert] :as req}] + (let [cn (some-> ssl-client-cert + ssl-utils/get-cn-from-x509-certificate) + req (assoc req :ssl-client-cn cn)] + (handler req)))) + +(schema/defn ^:always-validate wrap-data-errors :- IFn + "A ring middleware that catches a slingshot error thrown by + throw-data-invalid! or a :kind of slingshot error of one of: + :request-data-invalid + :user-data-invalid + :data-invalid + :service-status-version-not-found + logs the error and returns a 400 ring response." + ([handler :- IFn] + (wrap-data-errors handler :json)) + ([handler :- IFn + type :- utils/ResponseType] + (let [code 400 + response (fn [e] + (log/error "Submitted data is invalid:" (:msg e)) + (case type + :json (utils/json-response code e) + :plain (utils/plain-response code (:msg e))))] + (fn [request] + (sling/try+ (handler request) + (catch + #(contains? #{:request-data-invalid + :user-data-invalid + :data-invalid + :service-status-version-not-found} + (:kind %)) + e + (response e))))))) + +(schema/defn ^:always-validate wrap-service-unavailable :- IFn + "A ring middleware that catches slingshot errors thrown by + utils/throw-service-unavailabe!, logs the error and returns a 503 ring + response." + ([handler :- IFn] + (wrap-service-unavailable handler :json)) + ([handler :- IFn + type :- utils/ResponseType] + (let [code 503 + response (fn [e] + (log/error "Service Unavailable:" (:msg e)) + (case type + :json (utils/json-response code e) + :plain (utils/plain-response code (:msg e))))] + (fn [request] + (sling/try+ (handler request) + (catch utils/service-unavailable? e + (response e))))))) + +(schema/defn ^:always-validate wrap-bad-request :- IFn + "A ring middleware that catches slingshot errors thrown by + utils/throw-bad-request!, logs the error and returns a 503 ring + response." + ([handler :- IFn] + (wrap-bad-request handler :json)) + ([handler :- IFn + type :- utils/ResponseType] + (let [code 400 + response (fn [e] + (log/error "Bad Request:" (:msg e)) + (case type + :json (utils/json-response code e) + :plain (utils/plain-response code (:msg e))))] + (fn [request] + (sling/try+ (handler request) + (catch utils/bad-request? e + (response e))))))) + +(schema/defn ^:always-validate wrap-schema-errors :- IFn + "A ring middleware that catches schema errors and returns a 500 + response with the details" + ([handler :- IFn] + (wrap-schema-errors handler :json)) + ([handler :- IFn + type :- utils/ResponseType] + (let [code 500 + response (fn [e] + (let [msg (str "Something unexpected happened: " + (select-keys e [:error :value :type]))] + (log/error msg) + (case type + :json (utils/json-response code + {:kind :application-error + :msg msg}) + :plain (utils/plain-response code msg))))] + (fn [request] + (sling/try+ (handler request) + (catch utils/schema-error? e + (response e))))))) + +(schema/defn ^:always-validate wrap-uncaught-errors :- IFn + "A ring middleware that catches all otherwise uncaught errors and + returns a 500 response with the error message" + ([handler :- IFn] + (wrap-uncaught-errors handler :json)) + ([handler :- IFn + type :- utils/ResponseType] + (let [code 500 + response (fn [e] + (let [msg (str "Internal Server Error: " e)] + (log/error msg) + (case type + :json (utils/json-response code + {:kind :application-error + :msg msg}) + :plain (utils/plain-response code msg))))] + (fn [request] + (sling/try+ (handler request) + (catch Exception e + (response e))))))) + diff --git a/src/puppetlabs/ring_middleware/utils.clj b/src/puppetlabs/ring_middleware/utils.clj new file mode 100644 index 0000000..93937ca --- /dev/null +++ b/src/puppetlabs/ring_middleware/utils.clj @@ -0,0 +1,93 @@ +(ns puppetlabs.ring-middleware.utils + (:require [schema.core :as schema] + [ring.util.response :as rr] + [slingshot.slingshot :as sling] + [cheshire.core :as json]) + (:import (java.security.cert X509Certificate))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; Schemas + +(def ResponseType + (schema/enum :json :plain)) + +(def RingRequest + {:uri schema/Str + (schema/optional-key :ssl-client-cert) (schema/maybe X509Certificate) + schema/Keyword schema/Any}) + +(def RingResponse + {:status schema/Int + :headers {schema/Str schema/Any} + :body schema/Any + schema/Keyword schema/Any}) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; Helpers + +(schema/defn ^:always-validate json-response + :- RingResponse + [status :- schema/Int + body :- schema/Any] + (-> body + json/encode + rr/response + (rr/status status) + (rr/content-type "application/json; charset=utf-8"))) + +(schema/defn ^:always-validate plain-response + :- RingResponse + [status :- schema/Int + body :- schema/Str] + (-> body + rr/response + (rr/status status) + (rr/content-type "text/plain; charset=utf-8"))) + +(defn throw-bad-request! + "Throw a :bad-request type slingshot error with the supplied message" + [message] + (sling/throw+ {:kind :bad-request + :msg message})) + +(defn bad-request? + [e] + "Determine if the supplied slingshot error is for a bad request" + (when (map? e) + (= (:kind e) + :bad-request))) + +(defn throw-service-unavailable! + "Throw a :service-unavailable type slingshot error with the supplied message" + [message] + (sling/throw+ {:kind :service-unavailable + :msg message})) + +(defn service-unavailable? + [e] + "Determine if the supplied slingshot error is for an unavailable service" + (when (map? e) + (= (:kind e) + :service-unavailable))) + +(defn throw-data-invalid! + "Throw a :data-invalid type slingshot error with the supplied message" + [message] + (sling/throw+ {:kind :data-invalid + :msg message})) + +(defn data-invalid? + [e] + "Determine if the supplied slingshot error is for invalid data" + (when (map? e) + (= (:kind e) + :data-invalid))) + +(defn schema-error? + [e] + "Determine if the supplied slingshot error is for a schema mismatch" + (when (map? e) + (= (:type e) + :schema.core/error))) + diff --git a/test/puppetlabs/ring_middleware/core_test.clj b/test/puppetlabs/ring_middleware/core_test.clj new file mode 100644 index 0000000..a1d00a1 --- /dev/null +++ b/test/puppetlabs/ring_middleware/core_test.clj @@ -0,0 +1,635 @@ +(ns puppetlabs.ring-middleware.core-test + (:require [cheshire.core :as json] + [clojure.test :refer :all] + [compojure.core :refer :all] + [compojure.handler :as handler] + [compojure.route :as route] + [puppetlabs.ring-middleware.core :as core] + [puppetlabs.ring-middleware.utils :as utils] + [puppetlabs.ring-middleware.testutils.common :refer :all] + [puppetlabs.ssl-utils.core :refer [pem->cert]] + [puppetlabs.ssl-utils.simple :as ssl-simple] + [puppetlabs.trapperkeeper.app :refer [get-service]] + [puppetlabs.trapperkeeper.services.webserver.jetty9-service :refer :all] + [puppetlabs.trapperkeeper.testutils.bootstrap :refer [with-app-with-config]] + [puppetlabs.trapperkeeper.testutils.logging :as logutils] + [ring.util.response :as rr] + [schema.core :as schema] + [slingshot.slingshot :as slingshot])) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; Testing Helpers + + +(def WackSchema + [schema/Str]) + +(schema/defn ^:always-validate cause-schema-error + [request :- WackSchema] + (throw (IllegalStateException. "The test should have never gotten here..."))) + +(defn throwing-handler + [kind msg] + (fn [_] (slingshot/throw+ {:kind kind :msg msg}))) + +(defn basic-request + ([] (basic-request "foo-agent" :get "https://example.com")) + ([subject method uri] + {:request-method method + :uri uri + :ssl-client-cert (:cert (ssl-simple/gen-self-signed-cert subject 1)) + :authorization {:certificate "foo"}})) + +(defn post-target-handler + [req] + (if (= (:request-method req) :post) + {:status 200 :body (slurp (:body req))} + {:status 404 :body "Z'oh"})) + +(defn proxy-target-handler + [req] + (condp = (:uri req) + "/hello" {:status 302 :headers {"Location" "/hello/world"}} + "/hello/" {:status 302 :headers {"Location" "/hello/world"}} + "/hello/world" {:status 200 :body "Hello, World!"} + "/hello/wrong-host" {:status 302 :headers {"Location" "http://localhost:4/fake"}} + "/hello/fully-qualified" {:status 302 :headers {"Location" "http://localhost:9000/hello/world"}} + "/hello/different-path" {:status 302 :headers {"Location" "http://localhost:9000/different/"}} + {:status 404 :body "D'oh"})) + +(defn non-proxy-target + [_] + {:status 200 :body "Non-proxied path"}) + +(def gzip-body + (apply str (repeat 1000 "f"))) + +(defn proxy-gzip-response + [_] + (-> gzip-body + (rr/response) + (rr/status 200) + (rr/content-type "text/plain") + (rr/charset "UTF-8"))) + +(defn proxy-error-handler + [_] + {:status 404 :body "N'oh"}) + +(defn proxy-regex-response + [req] + {:status 200 :body (str "Proxied to " (:uri req))}) + +(defroutes fallthrough-routes + (GET "/hello/world" [] "Hello, World! (fallthrough)") + (GET "/goodbye/world" [] "Goodbye, World! (fallthrough)") + (route/not-found "Not Found (fallthrough)")) + +(def proxy-regex-fallthrough + (handler/site fallthrough-routes)) + +(def proxy-wrapped-app + (-> proxy-error-handler + (core/wrap-proxy "/hello-proxy" "http://localhost:9000/hello"))) + +(def proxy-wrapped-app-ssl + (-> proxy-error-handler + (core/wrap-proxy "/hello-proxy" "https://localhost:9001/hello" + {:ssl-cert "./dev-resources/config/jetty/ssl/certs/localhost.pem" + :ssl-key "./dev-resources/config/jetty/ssl/private_keys/localhost.pem" + :ssl-ca-cert "./dev-resources/config/jetty/ssl/certs/ca.pem"}))) + +(def proxy-wrapped-app-redirects + (-> proxy-error-handler + (core/wrap-proxy "/hello-proxy" "http://localhost:9000/hello" + {:force-redirects true + :follow-redirects true}))) + +(def proxy-wrapped-app-regex + (-> proxy-regex-fallthrough + (core/wrap-proxy #"^/([^/]+/certificate.*)$" "http://localhost:9000/hello"))) + +(def proxy-wrapped-app-regex-alt + (-> proxy-regex-fallthrough + (core/wrap-proxy #"/hello-proxy" "http://localhost:9000/hello"))) + +(def proxy-wrapped-app-regex-no-prepend + (-> proxy-regex-fallthrough + (core/wrap-proxy #"^/([^/]+/certificate.*)$" "http://localhost:9000"))) + +(def proxy-wrapped-app-regex-trailing-slash + (-> proxy-regex-fallthrough + (core/wrap-proxy #"^/([^/]+/certificate.*)$" "http://localhost:9000/"))) + +(defmacro with-target-and-proxy-servers + [{:keys [target proxy proxy-handler ring-handler endpoint target-endpoint]} & body] + `(with-app-with-config proxy-target-app# + [jetty9-service] + {:webserver ~target} + (let [target-webserver# (get-service proxy-target-app# :WebserverService)] + (add-ring-handler + target-webserver# + ~ring-handler + ~target-endpoint) + (add-ring-handler + target-webserver# + non-proxy-target + "/different") + (add-ring-handler + target-webserver# + post-target-handler + "/hello/post/")) + (with-app-with-config proxy-app# + [jetty9-service] + {:webserver ~proxy} + (let [proxy-webserver# (get-service proxy-app# :WebserverService)] + (add-ring-handler proxy-webserver# ~proxy-handler ~endpoint)) + ~@body))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; Core Helpers + +(deftest sanitize-client-cert-test + (testing "sanitize-client-cert" + (let [subject "foo-client" + cert (:cert (ssl-simple/gen-self-signed-cert subject 1)) + request {:ssl-client-cert cert :authorization {:certificate "stuff"}} + response (core/sanitize-client-cert request)] + (testing "adds the CN at :ssl-client-cert-cn" + (is (= subject (response :ssl-client-cert-cn)))) + (testing "removes :ssl-client-cert key from response" + (is (nil? (response :ssl-client-cert)))) + (testing "remove tk-auth cert info" + (is (nil? (get-in response [:authorization :certificate]))))))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; Core Middleware + +(deftest test-proxy + (let [common-ssl-config {:ssl-cert "./dev-resources/config/jetty/ssl/certs/localhost.pem" + :ssl-key "./dev-resources/config/jetty/ssl/private_keys/localhost.pem" + :ssl-ca-cert "./dev-resources/config/jetty/ssl/certs/ca.pem"}] + + (testing "basic proxy support" + (with-target-and-proxy-servers + {:target {:host "0.0.0.0" + :port 9000} + :proxy {:host "0.0.0.0" + :port 10000} + :proxy-handler proxy-wrapped-app + :ring-handler proxy-target-handler + :endpoint "/hello-proxy" + :target-endpoint "/hello"} + (let [response (http-get "http://localhost:9000/hello/world")] + (is (= (:status response) 200)) + (is (= (:body response) "Hello, World!"))) + (let [response (http-get "http://localhost:10000/hello-proxy/world")] + (is (= (:status response) 200)) + (is (= (:body response) "Hello, World!"))) + (let [response (http-get "http://localhost:10000/hello-proxy/world" {:as :stream})] + (is (= (slurp (:body response)) "Hello, World!"))) + (let [response (http-post "http://localhost:10000/hello-proxy/post/" {:as :stream :body "I'm posted!"})] + (is (= (:status response) 200)) + (is (= (slurp (:body response)) "I'm posted!"))))) + + (testing "basic https proxy support" + (with-target-and-proxy-servers + {:target (merge common-ssl-config + {:ssl-host "0.0.0.0" + :ssl-port 9001}) + :proxy (merge common-ssl-config + {:ssl-host "0.0.0.0" + :ssl-port 10001}) + :proxy-handler proxy-wrapped-app-ssl + :ring-handler proxy-target-handler + :endpoint "/hello-proxy" + :target-endpoint "/hello"} + (let [response (http-get "https://localhost:9001/hello/world" default-options-for-https-client)] + (is (= (:status response) 200)) + (is (= (:body response) "Hello, World!"))) + (let [response (http-get "https://localhost:10001/hello-proxy/world" default-options-for-https-client)] + (is (= (:status response) 200)) + (is (= (:body response) "Hello, World!"))))) + + (testing "basic http->https proxy support" + (with-target-and-proxy-servers + {:target (merge common-ssl-config + {:ssl-host "0.0.0.0" + :ssl-port 9001}) + :proxy {:host "0.0.0.0" + :port 10000} + :proxy-handler proxy-wrapped-app-ssl + :ring-handler proxy-target-handler + :endpoint "/hello-proxy" + :target-endpoint "/hello"} + (let [response (http-get "https://localhost:9001/hello/world" default-options-for-https-client)] + (is (= (:status response) 200)) + (is (= (:body response) "Hello, World!"))) + (let [response (http-get "http://localhost:10000/hello-proxy/world")] + (is (= (:status response) 200)) + (is (= (:body response) "Hello, World!"))))) + + (testing "basic https->http proxy support" + (with-target-and-proxy-servers + {:target {:host "0.0.0.0" + :port 9000} + :proxy (merge common-ssl-config + {:ssl-host "0.0.0.0" + :ssl-port 10001}) + :proxy-handler proxy-wrapped-app + :ring-handler proxy-target-handler + :endpoint "/hello-proxy" + :target-endpoint "/hello"} + (let [response (http-get "http://localhost:9000/hello/world")] + (is (= (:status response) 200)) + (is (= (:body response) "Hello, World!"))) + (let [response (http-get "https://localhost:10001/hello-proxy/world" default-options-for-https-client)] + (is (= (:status response) 200)) + (is (= (:body response) "Hello, World!"))))) + (testing "redirect test with proxy" + (with-target-and-proxy-servers + {:target {:host "0.0.0.0" + :port 9000} + :proxy {:host "0.0.0.0" + :port 10000} + :proxy-handler proxy-wrapped-app + :ring-handler proxy-target-handler + :endpoint "/hello-proxy" + :target-endpoint "/hello"} + (let [response (http-get "http://localhost:9000/hello")] + (is (= (:status response) 200)) + (is (= (:body response) "Hello, World!"))) + (let [response (http-get "http://localhost:9000/hello/world")] + (is (= (:status response) 200)) + (is (= (:body response) "Hello, World!"))) + (let [response (http-get "http://localhost:10000/hello-proxy/" + {:follow-redirects false + :as :text})] + (is (= (:status response) 302)) + (is (= "/hello/world" (get-in response [:headers "location"])))) + (let [response (http-post "http://localhost:10000/hello-proxy/" + {:follow-redirects false + :as :text})] + (is (= (:status response) 302)) + (is (= "/hello/world" (get-in response [:headers "location"])))) + (let [response (http-get "http://localhost:10000/hello-proxy/world")] + (is (= (:status response) 200)) + (is (= (:body response) "Hello, World!"))))) + + (testing "proxy redirect succeeds on POST if :force-redirects set true" + (with-target-and-proxy-servers + {:target {:host "0.0.0.0" + :port 9000} + :proxy {:host "0.0.0.0" + :port 10000} + :proxy-handler proxy-wrapped-app-redirects + :ring-handler proxy-target-handler + :endpoint "/hello-proxy" + :target-endpoint "/hello"} + (let [response (http-get "http://localhost:10000/hello-proxy/" + {:follow-redirects false + :as :text})] + (is (= (:status response) 200)) + (is (= (:body response) "Hello, World!"))) + (let [response (http-post "http://localhost:10000/hello-proxy/" + {:follow-redirects false})] + (is (= (:status response) 200))))) + + (testing "redirect test with fully qualified url, correct host, and proxied path" + (with-target-and-proxy-servers + {:target {:host "0.0.0.0" + :port 9000} + :proxy {:host "0.0.0.0" + :port 10000} + :proxy-handler proxy-wrapped-app-redirects + :ring-handler proxy-target-handler + :endpoint "/hello-proxy" + :target-endpoint "/hello"} + (let [response (http-get "http://localhost:10000/hello-proxy/fully-qualified" + {:follow-redirects false + :as :text})] + (is (= (:status response) 200)) + (is (= (:body response) "Hello, World!"))))) + + (testing "redirect test with correct host on non-proxied path" + (with-target-and-proxy-servers + {:target {:host "0.0.0.0" + :port 9000} + :proxy {:host "0.0.0.0" + :port 10000} + :proxy-handler proxy-wrapped-app-redirects + :ring-handler proxy-target-handler + :endpoint "/hello-proxy" + :target-endpoint "/hello"} + (let [response (http-get "http://localhost:9000/different")] + (is (= (:status response) 200)) + (is (= (:body response) "Non-proxied path"))) + (let [response (http-get "http://localhost:10000/different")] + (is (= (:status response) 404))) + (let [response (http-get "http://localhost:10000/hello-proxy/different-path" + {:follow-redirects false + :as :text})] + (is (= (:status response) 200)) + (is (= (:body response) "Non-proxied path"))))) + + (testing "gzipped responses not truncated" + (with-target-and-proxy-servers + {:target {:host "0.0.0.0" + :port 9000} + :proxy {:host "0.0.0.0" + :port 10000} + :proxy-handler proxy-wrapped-app + :ring-handler proxy-gzip-response + :endpoint "/hello-proxy" + :target-endpoint "/hello"} + (let [response (http-get "http://localhost:9000/hello")] + (is (= gzip-body (:body response))) + (is (= "gzip" (:orig-content-encoding response)))) + (let [response (http-get "http://localhost:10000/hello-proxy/")] + (is (= gzip-body (:body response))) + (is (= "gzip" (:orig-content-encoding response)))))) + + (testing "proxy works with regex" + (with-target-and-proxy-servers + {:target {:host "0.0.0.0" + :port 9000} + :proxy {:host "0.0.0.0" + :port 10000} + :proxy-handler proxy-wrapped-app-regex + :ring-handler proxy-regex-response + :endpoint "/" + :target-endpoint "/hello"} + (let [response (http-get "http://localhost:10000/production/certificate/foo")] + (is (= (:status response) 200)) + (is (= (:body response) "Proxied to /hello/production/certificate/foo"))) + (let [response (http-get "http://localhost:10000/hello/world")] + (is (= (:status response) 200)) + (is (= (:body response) "Hello, World! (fallthrough)"))) + (let [response (http-get "http://localhost:10000/goodbye/world")] + (is (= (:status response) 200)) + (is (= (:body response) "Goodbye, World! (fallthrough)"))) + (let [response (http-get "http://localhost:10000/production/cert/foo")] + (is (= (:status response) 404)) + (is (= (:body response) "Not Found (fallthrough)"))))) + + (testing "proxy regex matches beginning of string" + (with-target-and-proxy-servers + {:target {:host "0.0.0.0" + :port 9000} + :proxy {:host "0.0.0.0" + :port 10000} + :proxy-handler proxy-wrapped-app-regex-alt + :ring-handler proxy-regex-response + :endpoint "/" + :target-endpoint "/hello"} + (let [response (http-get "http://localhost:10000/hello-proxy")] + (is (= (:status response) 200)) + (is (= (:body response) "Proxied to /hello/hello-proxy"))) + (let [response (http-get "http://localhost:10000/production/hello-proxy")] + (is (= (:status response) 404)) + (is (= (:body response) "Not Found (fallthrough)"))))) + + (testing "proxy regex does not need to match entire request uri" + (with-target-and-proxy-servers + {:target {:host "0.0.0.0" + :port 9000} + :proxy {:host "0.0.0.0" + :port 10000} + :proxy-handler proxy-wrapped-app-regex-alt + :ring-handler proxy-regex-response + :endpoint "/" + :target-endpoint "/hello"} + (let [response (http-get "http://localhost:10000/hello-proxy/world")] + (is (= (:status response) 200)) + (is (= (:body response) "Proxied to /hello/hello-proxy/world"))))) + + (testing "proxy works with regex and no prepended path" + (with-target-and-proxy-servers + {:target {:host "0.0.0.0" + :port 9000} + :proxy {:host "0.0.0.0" + :port 10000} + :proxy-handler proxy-wrapped-app-regex-no-prepend + :ring-handler proxy-regex-response + :endpoint "/" + :target-endpoint "/"} + (let [response (http-get "http://localhost:10000/production/certificate/foo")] + (is (= (:status response) 200)) + (is (= (:body response) "Proxied to /production/certificate/foo"))))) + + (testing "no repeat slashes exist in rewritten uri" + (with-target-and-proxy-servers + {:target {:host "0.0.0.0" + :port 9000} + :proxy {:host "0.0.0.0" + :port 10000} + :proxy-handler proxy-wrapped-app-regex-trailing-slash + :ring-handler proxy-regex-response + :endpoint "/" + :target-endpoint "/"} + (let [response (http-get "http://localhost:10000/production/certificate/foo")] + (is (= (:status response) 200)) + (is (= (:body response) "Proxied to /production/certificate/foo"))))))) + +(deftest test-wrap-add-cache-headers + (let [put-request {:request-method :put} + get-request {:request-method :get} + post-request {:request-method :post} + delete-request {:request-method :delete} + no-cache-header "private, max-age=0, no-cache"] + (testing "wrap-add-cache-headers ignores nil response" + (let [handler (constantly nil) + wrapped-handler (core/wrap-add-cache-headers handler)] + (is (nil? (wrapped-handler put-request))) + (is (nil? (wrapped-handler get-request))) + (is (nil? (wrapped-handler post-request))) + (is (nil? (wrapped-handler delete-request))))) + (testing "wrap-add-cache-headers observes handled response" + (let [handler (constantly {}) + wrapped-handler (core/wrap-add-cache-headers handler) + handled-response {:headers {"cache-control" no-cache-header}} + not-handled-response {}] + (is (= handled-response (wrapped-handler get-request))) + (is (= handled-response (wrapped-handler put-request))) + (is (= not-handled-response (wrapped-handler post-request))) + (is (= not-handled-response (wrapped-handler delete-request))))) + (testing "wrap-add-cache-headers doesn't stomp on existing headers" + (let [fake-response {:headers {:something "Hi mom"}} + handler (constantly fake-response) + wrapped-handler (core/wrap-add-cache-headers handler) + handled-response {:headers {:something "Hi mom" + "cache-control" no-cache-header}} + not-handled-response fake-response] + (is (= handled-response (wrapped-handler get-request))) + (is (= handled-response (wrapped-handler put-request))) + (is (= not-handled-response (wrapped-handler post-request))) + (is (= not-handled-response (wrapped-handler delete-request))))))) + +(deftest test-wrap-with-cn + (testing "When extracting a CN from a cert" + (testing "and there is no cert" + (let [mw-fn (core/wrap-with-certificate-cn identity) + post-req (mw-fn {})] + (testing "ssl-client-cn is set to nil" + (is (= post-req {:ssl-client-cn nil}))))) + + (testing "and there is a cert" + (let [mw-fn (core/wrap-with-certificate-cn identity) + post-req (mw-fn {:ssl-client-cert (pem->cert "dev-resources/ssl/cert.pem")})] + (testing "ssl-client-cn is set properly" + (is (= (:ssl-client-cn post-req) "localhost"))))))) + +(deftest test-wrap-add-x-frame-options-deny + (let [get-request {:request-method :get} + put-request {:request-method :put} + post-request {:request-method :post} + delete-request {:request-method :delete} + x-frame-header "DENY"] + (testing "wrap-add-x-frame-options-deny ignores nil response" + (let [handler (constantly nil) + wrapped-handler (core/wrap-add-x-frame-options-deny handler)] + (is (nil? (wrapped-handler get-request))) + (is (nil? (wrapped-handler put-request))) + (is (nil? (wrapped-handler post-request))) + (is (nil? (wrapped-handler delete-request))))) + (testing "wrap-add-x-frame-options-deny observes handled response" + (let [handler (constantly {}) + wrapped-handler (core/wrap-add-x-frame-options-deny handler) + handled-response {:headers {"X-Frame-Options" x-frame-header}} + not-handled-response {}] + (is (= handled-response (wrapped-handler get-request))) + (is (= handled-response (wrapped-handler put-request))) + (is (= handled-response (wrapped-handler post-request))) + (is (= handled-response (wrapped-handler delete-request))))) + (testing "wrap-add-x-frame-options-deny doesn't stomp on existing headers" + (let [fake-response {:headers {:something "Hi mom"}} + handler (constantly fake-response) + wrapped-handler (core/wrap-add-x-frame-options-deny handler) + handled-response {:headers {:something "Hi mom" + "X-Frame-Options" x-frame-header}}] + (is (= handled-response (wrapped-handler get-request))) + (is (= handled-response (wrapped-handler put-request))) + (is (= handled-response (wrapped-handler post-request))) + (is (= handled-response (wrapped-handler delete-request))))))) + +(deftest wrap-response-logging-test + (testing "wrap-response-logging" + (logutils/with-test-logging + (let [stack (core/wrap-response-logging identity) + response (stack (basic-request))] + (is (logged? #"Computed response.*" :trace)))))) + +(deftest wrap-request-logging-test + (testing "wrap-request-logging" + (logutils/with-test-logging + (let [subject "foo-agent" + method :get + uri "https://example.com" + stack (core/wrap-request-logging identity) + request (basic-request subject method uri) + response (stack request)] + (is (logged? (format "Processing %s %s" method uri) :debug)) + (is (logged? #"Full request" :trace)))))) + +(deftest wrap-data-errors-test + (testing "wrap-data-errors" + (testing "default behavior" + (logutils/with-test-logging + (let [stack (core/wrap-data-errors (throwing-handler :user-data-invalid "Error Message")) + response (stack (basic-request)) + json-body (json/parse-string (response :body))] + (is (= 400 (response :status))) + (is (= "Error Message" (get json-body "msg"))) + (is (logged? #"Error Message" :error)) + (is (logged? #"Submitted data is invalid" :error))))) + (doseq [error [:request-data-invalid :user-data-invalid :service-status-version-not-found]] + (testing (str "handles errors of " error) + (logutils/with-test-logging + (let [stack (core/wrap-data-errors (throwing-handler error "Error Message")) + response (stack (basic-request)) + json-body (json/parse-string (response :body))] + (is (= 400 (response :status))) + (is (= (name error) (get json-body "kind"))))))) + (testing "handles errors thrown by `throw-data-invalid!`" + (logutils/with-test-logging + (let [stack (core/wrap-data-errors (fn [_] (utils/throw-data-invalid! "Error Message"))) + response (stack (basic-request)) + json-body (json/parse-string (response :body))] + (is (= 400 (response :status))) + (is (= (name "data-invalid") (get json-body "kind")))))) + (testing "can be plain text" + (logutils/with-test-logging + (let [stack (core/wrap-data-errors + (throwing-handler :user-data-invalid "Error Message") :plain) + response (stack (basic-request))] + (is (re-matches #"text/plain.*" (get-in response [:headers "Content-Type"])))))))) + +(deftest wrap-bad-request-test + (testing "wrap-bad-request" + (testing "default behavior" + (logutils/with-test-logging + (let [stack (core/wrap-bad-request (fn [_] (utils/throw-bad-request! "Error Message"))) + response (stack (basic-request)) + json-body (json/parse-string (response :body))] + (is (= 400 (response :status))) + (is (logged? #".*Bad Request.*" :error)) + (is (re-matches #"Error Message.*" (get json-body "msg" ""))) + (is (= "bad-request" (get json-body "kind")))))) + (testing "can be plain text" + (logutils/with-test-logging + (let [stack (core/wrap-bad-request (fn [_] (utils/throw-bad-request! "Error Message")) :plain) + response (stack (basic-request))] + (is (re-matches #"text/plain.*" (get-in response [:headers "Content-Type"])))))))) + +(deftest wrap-schema-errors-test + (testing "wrap-schema-errors" + (testing "default behavior" + (logutils/with-test-logging + (let [stack (core/wrap-schema-errors cause-schema-error) + response (stack (basic-request)) + json-body (json/parse-string (response :body))] + (is (= 500 (response :status))) + (is (logged? #".*Something unexpected.*" :error)) + (is (re-matches #"Something unexpected.*" (get json-body "msg" ""))) + (is (= "application-error" (get json-body "kind")))))) + (testing "can be plain text" + (logutils/with-test-logging + (let [stack (core/wrap-schema-errors cause-schema-error :plain) + response (stack (basic-request))] + (is (re-matches #"text/plain.*" (get-in response [:headers "Content-Type"])))))))) + +(deftest wrap-service-unavailable-test + (testing "wrap-service-unavailable" + (testing "default behavior" + (logutils/with-test-logging + (let [stack (core/wrap-service-unavailable (fn [_] (utils/throw-service-unavailable! "Test Service is DOWN!"))) + response (stack (basic-request)) + json-body (json/parse-string (response :body))] + (is (= 503 (response :status))) + (is (logged? #".*Service Unavailable.*" :error)) + (is (= "Test Service is DOWN!" (get json-body "msg"))) + (is (= "service-unavailable" (get json-body "kind")))))) + (testing "can be plain text" + (logutils/with-test-logging + (let [stack (core/wrap-service-unavailable (fn [_] (utils/throw-service-unavailable! "Test Service is DOWN!")) :plain) + response (stack (basic-request))] + (is (re-matches #"text/plain.*" (get-in response [:headers "Content-Type"])))))))) + +(deftest wrap-uncaught-errors-test + (testing "wrap-uncaught-errors" + (testing "default behavior" + (logutils/with-test-logging + (let [stack (core/wrap-uncaught-errors (fn [_] (throw (IllegalStateException. "Woah...")))) + response (stack (basic-request)) + json-body (json/parse-string (response :body))] + (is (= 500 (response :status))) + (is (logged? #".*Internal Server Error.*" :warn)) + (is (re-matches #"Internal Server Error.*" (get json-body "msg" "")))))) + (testing "can be plain text" + (logutils/with-test-logging + (let [stack (core/wrap-uncaught-errors (fn [_] (throw (IllegalStateException. "Woah..."))) :plain) + response (stack (basic-request))] + (is (re-matches #"text/plain.*" (get-in response [:headers "Content-Type"])))))))) diff --git a/test/puppetlabs/ring_middleware/testutils/common.clj b/test/puppetlabs/ring_middleware/testutils/common.clj new file mode 100644 index 0000000..f9420c0 --- /dev/null +++ b/test/puppetlabs/ring_middleware/testutils/common.clj @@ -0,0 +1,23 @@ +(ns puppetlabs.ring-middleware.testutils.common + (:require [puppetlabs.http.client.sync :as http-client])) + +(def default-options-for-https-client + {:ssl-cert "./dev-resources/config/jetty/ssl/certs/localhost.pem" + :ssl-key "./dev-resources/config/jetty/ssl/private_keys/localhost.pem" + :ssl-ca-cert "./dev-resources/config/jetty/ssl/certs/ca.pem" + :as :text}) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Testing utility functions + +(defn http-get + ([url] + (http-get url {:as :text})) + ([url options] + (http-client/get url options))) + +(defn http-post + ([url] + (http-post url {:as :text})) + ([url options] + (http-client/post url options))) \ No newline at end of file diff --git a/test/puppetlabs/ring_middleware/utils_test.clj b/test/puppetlabs/ring_middleware/utils_test.clj new file mode 100644 index 0000000..faa21ce --- /dev/null +++ b/test/puppetlabs/ring_middleware/utils_test.clj @@ -0,0 +1,25 @@ +(ns puppetlabs.ring-middleware.utils-test + (:require [cheshire.core :as json] + [clojure.test :refer :all] + [puppetlabs.ring-middleware.utils :as utils])) + +(deftest json-response-test + (testing "json response" + (let [source {:key 1} + response (utils/json-response 200 source)] + (testing "has 200 status code" + (is (= 200 (:status response)))) + (testing "has json content-type" + (is (re-matches #"application/json.*" (get-in response [:headers "Content-Type"])))) + (testing "is properly converted to a json string" + (is (= 1 ((json/parse-string (:body response)) "key"))))))) + +(deftest plain-response-test + (testing "json response" + (let [message "Response message" + response (utils/plain-response 200 message)] + (testing "has 200 status code" + (is (= 200 (:status response)))) + (testing "has plain content-type" + (is (re-matches #"text/plain.*" (get-in response [:headers "Content-Type"]))))))) + -- cgit v1.2.3