├── CODEOWNERS
├── Makefile
├── ext
└── travisci
│ └── test.sh
├── .gitignore
├── jenkins
└── deploy.sh
├── .travis.yml
├── dev-resources
├── logback-test.xml
├── exts.cnf
├── gen-pki.sh
├── ssl
│ └── cert.pem
├── config
│ └── jetty
│ │ └── ssl
│ │ ├── certs
│ │ ├── localhost.pem
│ │ └── ca.pem
│ │ └── private_keys
│ │ └── localhost.pem
└── Makefile.i18n
├── CONTRIBUTING.md
├── test
└── puppetlabs
│ └── ring_middleware
│ ├── testutils
│ └── common.clj
│ ├── utils_test.clj
│ └── core_test.clj
├── project.clj
├── locales
├── eo.po
└── ring-middleware.pot
├── .github
└── workflows
│ └── mend.yml
├── src
└── puppetlabs
│ └── ring_middleware
│ ├── common.clj
│ ├── utils.clj
│ ├── params.clj
│ └── core.clj
├── CHANGELOG.md
├── LICENSE
└── README.md
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @puppetlabs/dumpling
2 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | include dev-resources/Makefile.i18n
2 |
--------------------------------------------------------------------------------
/ext/travisci/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | lein test
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .nrepl-port
2 | pom.xml
3 | pom.xml.asc
4 | *jar
5 | /lib/
6 | /classes/
7 | /target/
8 | /checkouts/
9 | .lein-deps-sum
10 | .lein-repl-history
11 | .lein-plugins/
12 | .lein-failures
13 | /resources/locales.clj
14 | /dev-resources/i18n/bin
15 | /resources/**/Messages*.class
16 |
--------------------------------------------------------------------------------
/jenkins/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 | set -x
5 |
6 | git fetch --tags
7 |
8 | lein test
9 | echo "Tests passed!"
10 |
11 | lein release
12 | echo "Release plugin successful, pushing changes to git"
13 |
14 | git push origin --tags HEAD:$RING_MIDDLEWARE_BRANCH
15 | echo "git push successful."
16 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: clojure
2 | lein: 2.7.1
3 | jdk:
4 | - openjdk8
5 | script: ./ext/travisci/test.sh
6 | notifications:
7 | email: false
8 |
9 | # workaround for buffer overflow issue, ref https://github.com/travis-ci/travis-ci/issues/522e
10 | addons:
11 | hosts:
12 | - myshorthost
13 | hostname: myshorthost
14 |
--------------------------------------------------------------------------------
/dev-resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d %-5p [%c{2}] %m%n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute
2 |
3 | Third-party patches are essential for keeping puppet open-source projects
4 | great. We want to keep it as easy as possible to contribute changes that
5 | allow you to get the most out of our projects. There are a few guidelines
6 | that we need contributors to follow so that we can have a chance of keeping on
7 | top of things. For more info, see our canonical guide to contributing:
8 |
9 | [https://github.com/puppetlabs/puppet/blob/master/CONTRIBUTING.md](https://github.com/puppetlabs/puppet/blob/master/CONTRIBUTING.md)
10 |
--------------------------------------------------------------------------------
/test/puppetlabs/ring_middleware/testutils/common.clj:
--------------------------------------------------------------------------------
1 | (ns puppetlabs.ring-middleware.testutils.common
2 | (:require [puppetlabs.http.client.sync :as http-client]))
3 |
4 | (def default-options-for-https-client
5 | {:ssl-cert "./dev-resources/config/jetty/ssl/certs/localhost.pem"
6 | :ssl-key "./dev-resources/config/jetty/ssl/private_keys/localhost.pem"
7 | :ssl-ca-cert "./dev-resources/config/jetty/ssl/certs/ca.pem"
8 | :as :text})
9 |
10 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
11 | ;;; Testing utility functions
12 |
13 | (defn http-get
14 | ([url]
15 | (http-get url {:as :text}))
16 | ([url options]
17 | (http-client/get url options)))
18 |
19 | (defn http-post
20 | ([url]
21 | (http-post url {:as :text}))
22 | ([url options]
23 | (http-client/post url options)))
--------------------------------------------------------------------------------
/dev-resources/exts.cnf:
--------------------------------------------------------------------------------
1 | [ req ]
2 | #default_bits = 2048
3 | #default_md = sha256
4 | #default_keyfile = privkey.pem
5 | distinguished_name = req_distinguished_name
6 | attributes = req_attributes
7 |
8 | [ req_distinguished_name ]
9 | countryName = Country Name (2 letter code)
10 | countryName_min = 2
11 | countryName_max = 2
12 | stateOrProvinceName = State or Province Name (full name)
13 | localityName = Locality Name (eg, city)
14 | 0.organizationName = Organization Name (eg, company)
15 | organizationalUnitName = Organizational Unit Name (eg, section)
16 | commonName = Common Name (eg, fully qualified host name)
17 | commonName_max = 64
18 |
19 | [ req_attributes ]
20 |
21 | # This section should be referenced when building an x509v3 CA
22 | # Certificate.
23 | # The default path length and the key usage can be overridden
24 | # modified by setting the CERTPATHLEN and CERTUSAGE environment
25 | # variables.
26 | [x509v3_CA]
27 | subjectKeyIdentifier = hash
28 | basicConstraints=critical,CA:true
29 | keyUsage=digitalSignature,keyCertSign,cRLSign
30 | authorityKeyIdentifier=keyid
31 |
32 |
--------------------------------------------------------------------------------
/test/puppetlabs/ring_middleware/utils_test.clj:
--------------------------------------------------------------------------------
1 | (ns puppetlabs.ring-middleware.utils-test
2 | (:require [cheshire.core :as json]
3 | [clojure.test :refer :all]
4 | [puppetlabs.ring-middleware.utils :as utils]))
5 |
6 | (deftest json-response-test
7 | (testing "json response"
8 | (let [source {:key 1}
9 | response (utils/json-response 200 source)]
10 | (testing "has 200 status code"
11 | (is (= 200 (:status response))))
12 | (testing "has json content-type"
13 | (is (re-matches #"application/json.*" (get-in response [:headers "Content-Type"]))))
14 | (testing "is properly converted to a json string"
15 | (is (= 1 ((json/parse-string (:body response)) "key")))))))
16 |
17 | (deftest plain-response-test
18 | (testing "json response"
19 | (let [message "Response message"
20 | response (utils/plain-response 200 message)]
21 | (testing "has 200 status code"
22 | (is (= 200 (:status response))))
23 | (testing "has plain content-type"
24 | (is (re-matches #"text/plain.*" (get-in response [:headers "Content-Type"])))))))
25 |
26 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject puppetlabs/ring-middleware "2.0.5-SNAPSHOT"
2 | :dependencies [[cheshire]]
3 |
4 | :min-lein-version "2.7.1"
5 |
6 | :parent-project {:coords [puppetlabs/clj-parent "7.3.31"]
7 | :inherit [:managed-dependencies]}
8 | :license {:name "Apache-2.0"
9 | :url "https://www.apache.org/licenses/LICENSE-2.0.txt"}
10 |
11 | ;; Abort when version ranges or version conflicts are detected in
12 | ;; dependencies. Also supports :warn to simply emit warnings.
13 | ;; requires lein 2.2.0+.
14 | :pedantic? :abort
15 |
16 | :plugins [[lein-parent "0.3.7"]
17 | [puppetlabs/i18n "0.7.1"]]
18 |
19 | :deploy-repositories [["releases" {:url "https://clojars.org/repo"
20 | :username :env/clojars_jenkins_username
21 | :password :env/clojars_jenkins_password
22 | :sign-releases false}]
23 | ["snapshots" "http://nexus.delivery.puppetlabs.net/content/repositories/snapshots/"]]
24 |
25 | :profiles {:dev {:dependencies [[com.puppetlabs/trapperkeeper-webserver-jetty10]
26 | [org.bouncycastle/bcpkix-jdk18on]
27 | [puppetlabs/kitchensink nil :classifier "test" :scope "test"]
28 | [puppetlabs/trapperkeeper nil :classifier "test" :scope "test"]
29 | [compojure]]}})
30 |
--------------------------------------------------------------------------------
/dev-resources/gen-pki.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if ! [[ -d dev-resources/ssl ]]; then
4 | echo "This script must be called from the root of the project and dev-resources/ssl must already exist"
5 | exit 1
6 | fi
7 |
8 | echo
9 | echo "Generating primary self-signed CA"
10 | openssl req -x509 \
11 | -newkey rsa:4096 \
12 | -keyout dev-resources/ssl/ca.key \
13 | -out dev-resources/ssl/ca.pem \
14 | -days 1825 -nodes \
15 | -extensions x509v3_CA \
16 | -config dev-resources/exts.cnf \
17 | -subj "/C=US/ST=OR/L=Portland/O=Puppet, Inc/CN=puppet"
18 |
19 | echo
20 | echo "Generating node cert"
21 | openssl genrsa -out dev-resources/ssl/key.pem 2048
22 |
23 | echo
24 | echo "Creating node CSR"
25 | openssl req -new -sha256 \
26 | -key dev-resources/ssl/key.pem \
27 | -out dev-resources/ssl/csr.pem \
28 | -subj "/C=US/ST=OR/L=Portland/O=Puppet, Inc/CN=localhost"
29 |
30 | echo
31 | echo "Signing node CSR"
32 | openssl x509 -req \
33 | -in dev-resources/ssl/csr.pem \
34 | -CA dev-resources/ssl/ca.pem \
35 | -CAkey dev-resources/ssl/ca.key \
36 | -CAcreateserial \
37 | -out dev-resources/ssl/cert.pem \
38 | -days 1825 -sha256
39 |
40 | echo
41 | echo "Generating alternate self-signed CA"
42 | openssl req -x509 \
43 | -newkey rsa:4096 \
44 | -keyout dev-resources/ssl/alternate-ca.key \
45 | -out dev-resources/ssl/alternate-ca.pem \
46 | -days 1825 -nodes \
47 | -extensions x509v3_CA \
48 | -config dev-resources/exts.cnf \
49 | -subj "/C=US/ST=OR/L=Portland/O=Puppet, Inc/CN=alternate"
50 |
51 |
52 | echo
53 | echo "Cleaning up files that will not be used by the tests"
54 | rm dev-resources/ssl/{alternate-ca.key,ca.key,ca.srl,csr.pem}
55 |
--------------------------------------------------------------------------------
/locales/eo.po:
--------------------------------------------------------------------------------
1 | # Esperanto translations for puppetlabs.ring_middleware package.
2 | # Copyright (C) 2017 Puppet
3 | # This file is distributed under the same license as the puppetlabs.ring_middleware package.
4 | # Automatically generated, 2017.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: puppetlabs.ring_middleware \n"
9 | "Report-Msgid-Bugs-To: docs@puppet.com\n"
10 | "POT-Creation-Date: \n"
11 | "PO-Revision-Date: \n"
12 | "Last-Translator: Automatically generated\n"
13 | "Language-Team: none\n"
14 | "Language: eo\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
19 |
20 | #: src/puppetlabs/ring_middleware/common.clj
21 | msgid ""
22 | "Proxying request to {0} to remote url {1}. Remote server responded with "
23 | "status {2}"
24 | msgstr ""
25 |
26 | #: src/puppetlabs/ring_middleware/core.clj
27 | msgid "Processing {0} {1}"
28 | msgstr ""
29 |
30 | #: src/puppetlabs/ring_middleware/core.clj
31 | msgid "Full request:"
32 | msgstr ""
33 |
34 | #: src/puppetlabs/ring_middleware/core.clj
35 | msgid "Computed response: {0}"
36 | msgstr ""
37 |
38 | #: src/puppetlabs/ring_middleware/core.clj
39 | msgid "Submitted data is invalid: {0}"
40 | msgstr ""
41 |
42 | #: src/puppetlabs/ring_middleware/core.clj
43 | msgid "Service Unavailable:"
44 | msgstr ""
45 |
46 | #: src/puppetlabs/ring_middleware/core.clj
47 | msgid "Bad Request:"
48 | msgstr ""
49 |
50 | #: src/puppetlabs/ring_middleware/core.clj
51 | msgid "Something unexpected happened: {0}"
52 | msgstr ""
53 |
54 | #: src/puppetlabs/ring_middleware/core.clj
55 | msgid "Internal Server Error: {0}"
56 | msgstr ""
57 |
--------------------------------------------------------------------------------
/dev-resources/ssl/cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIEezCCAmOgAwIBAgIUJLNMHcPnF90Y6dhr39MCIhnkwiswDQYJKoZIhvcNAQEL
3 | BQAwVDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk9SMREwDwYDVQQHDAhQb3J0bGFu
4 | ZDEUMBIGA1UECgwLUHVwcGV0LCBJbmMxDzANBgNVBAMMBnB1cHBldDAeFw0yNDA3
5 | MDkyMzQ3NDVaFw0yOTA3MDgyMzQ3NDVaMFcxCzAJBgNVBAYTAlVTMQswCQYDVQQI
6 | DAJPUjERMA8GA1UEBwwIUG9ydGxhbmQxFDASBgNVBAoMC1B1cHBldCwgSW5jMRIw
7 | EAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
8 | AQCs1R5uSRH5xV6qxvMiz9X7lvAxwI55nZZmgxKHZz7EqWcVtR1VKlKW4K3khGAr
9 | 0Sm9ihpM2KvaPEhGBQq6hhLBRDMKLkt1JaUmSyTeoz2EGGYXWMLZaoFEMPlBDY2t
10 | Hz03CF4WSDPp8MIKW7PBwspmHdkizFrrtQqLRYDtf01w4tgMflqgwWhLVPuO2eOf
11 | 0XmGVScRlQnAcS8WVFPAKJCstZNF3aqK5cGzhRkGeiDYqEfq29HyRbjPSO3M9vyg
12 | WIGSetR9DBM8VIpaGO457iEBZOGdgHqG84Ma8pjBj4pdPuTwjavh9coYQp0Tongt
13 | xF5Undf+fmkiPUfEVC5cZy2jAgMBAAGjQjBAMB0GA1UdDgQWBBQpfykU4gAMP6Pm
14 | mBsC1LUYCcM96zAfBgNVHSMEGDAWgBQu69ueqs80Ioqd8f+hI2YUdTswFDANBgkq
15 | hkiG9w0BAQsFAAOCAgEAeM5ME8mvkznGiepHQqDojpxaNIT9N4QDCAxW8y1LVfEF
16 | S2ufMva22q4loK+NJetaTNjDDv0lcCwUtdAqGQxngrSW8mmVL3qZrm/fFKDsRp0T
17 | 7r7qp0+nZIM+QkovxqPvDO39G70yKATOeXo5ureUK7bbXoU3h8SuPjZNT58XMjKN
18 | VDZ7f70ejaxjA45wU8jJ2kkotm62jyY7Osh5hA4doE6zLhGYrZZLoE6+kHTiTW1x
19 | UVnLcm0+MHBGFtydnK3BnnB4muWlWJggJhsmvOxTXK4kuIt1SMxp4fI4F21A/FUi
20 | qnVG9+pJ/+bm3vFAH5a+fxy/1Jq3yEgKKmn/bVRc+1cP0zgNMFSx7IvC+h2AgbpL
21 | 74lMKd74a5YqbnQraV0FGVY2q0b9W7Ce95kOm//aoFtlxGiAnftwwuFgzOe/pIdb
22 | r0AtiNkzfBOvqnXAwTHBU16042LtD9kO4aNjw7E2C7E8v5IyUwrYQxBM9pcj0waB
23 | PvjaT6aVrepQl5+17YzuQGd9YaTucS2ikGMJCQuaFY6ZupNZsQY3AeD8FKWkAgLU
24 | lKEBbbNZlCLmHBIxk/DGUyiBy6Q3BsBMivOzdWDpkhnHwhwpumFPJCF8yh9BXeba
25 | Xu89BP1D0Dme9r0dD4daCsa9UOqkmaSELd9c8sstojcRK7rD0tCBQI8fFzdwGNo=
26 | -----END CERTIFICATE-----
27 |
--------------------------------------------------------------------------------
/dev-resources/config/jetty/ssl/certs/localhost.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIEezCCAmOgAwIBAgIUJLNMHcPnF90Y6dhr39MCIhnkwiswDQYJKoZIhvcNAQEL
3 | BQAwVDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk9SMREwDwYDVQQHDAhQb3J0bGFu
4 | ZDEUMBIGA1UECgwLUHVwcGV0LCBJbmMxDzANBgNVBAMMBnB1cHBldDAeFw0yNDA3
5 | MDkyMzQ3NDVaFw0yOTA3MDgyMzQ3NDVaMFcxCzAJBgNVBAYTAlVTMQswCQYDVQQI
6 | DAJPUjERMA8GA1UEBwwIUG9ydGxhbmQxFDASBgNVBAoMC1B1cHBldCwgSW5jMRIw
7 | EAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
8 | AQCs1R5uSRH5xV6qxvMiz9X7lvAxwI55nZZmgxKHZz7EqWcVtR1VKlKW4K3khGAr
9 | 0Sm9ihpM2KvaPEhGBQq6hhLBRDMKLkt1JaUmSyTeoz2EGGYXWMLZaoFEMPlBDY2t
10 | Hz03CF4WSDPp8MIKW7PBwspmHdkizFrrtQqLRYDtf01w4tgMflqgwWhLVPuO2eOf
11 | 0XmGVScRlQnAcS8WVFPAKJCstZNF3aqK5cGzhRkGeiDYqEfq29HyRbjPSO3M9vyg
12 | WIGSetR9DBM8VIpaGO457iEBZOGdgHqG84Ma8pjBj4pdPuTwjavh9coYQp0Tongt
13 | xF5Undf+fmkiPUfEVC5cZy2jAgMBAAGjQjBAMB0GA1UdDgQWBBQpfykU4gAMP6Pm
14 | mBsC1LUYCcM96zAfBgNVHSMEGDAWgBQu69ueqs80Ioqd8f+hI2YUdTswFDANBgkq
15 | hkiG9w0BAQsFAAOCAgEAeM5ME8mvkznGiepHQqDojpxaNIT9N4QDCAxW8y1LVfEF
16 | S2ufMva22q4loK+NJetaTNjDDv0lcCwUtdAqGQxngrSW8mmVL3qZrm/fFKDsRp0T
17 | 7r7qp0+nZIM+QkovxqPvDO39G70yKATOeXo5ureUK7bbXoU3h8SuPjZNT58XMjKN
18 | VDZ7f70ejaxjA45wU8jJ2kkotm62jyY7Osh5hA4doE6zLhGYrZZLoE6+kHTiTW1x
19 | UVnLcm0+MHBGFtydnK3BnnB4muWlWJggJhsmvOxTXK4kuIt1SMxp4fI4F21A/FUi
20 | qnVG9+pJ/+bm3vFAH5a+fxy/1Jq3yEgKKmn/bVRc+1cP0zgNMFSx7IvC+h2AgbpL
21 | 74lMKd74a5YqbnQraV0FGVY2q0b9W7Ce95kOm//aoFtlxGiAnftwwuFgzOe/pIdb
22 | r0AtiNkzfBOvqnXAwTHBU16042LtD9kO4aNjw7E2C7E8v5IyUwrYQxBM9pcj0waB
23 | PvjaT6aVrepQl5+17YzuQGd9YaTucS2ikGMJCQuaFY6ZupNZsQY3AeD8FKWkAgLU
24 | lKEBbbNZlCLmHBIxk/DGUyiBy6Q3BsBMivOzdWDpkhnHwhwpumFPJCF8yh9BXeba
25 | Xu89BP1D0Dme9r0dD4daCsa9UOqkmaSELd9c8sstojcRK7rD0tCBQI8fFzdwGNo=
26 | -----END CERTIFICATE-----
27 |
--------------------------------------------------------------------------------
/dev-resources/config/jetty/ssl/private_keys/localhost.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCs1R5uSRH5xV6q
3 | xvMiz9X7lvAxwI55nZZmgxKHZz7EqWcVtR1VKlKW4K3khGAr0Sm9ihpM2KvaPEhG
4 | BQq6hhLBRDMKLkt1JaUmSyTeoz2EGGYXWMLZaoFEMPlBDY2tHz03CF4WSDPp8MIK
5 | W7PBwspmHdkizFrrtQqLRYDtf01w4tgMflqgwWhLVPuO2eOf0XmGVScRlQnAcS8W
6 | VFPAKJCstZNF3aqK5cGzhRkGeiDYqEfq29HyRbjPSO3M9vygWIGSetR9DBM8VIpa
7 | GO457iEBZOGdgHqG84Ma8pjBj4pdPuTwjavh9coYQp0TongtxF5Undf+fmkiPUfE
8 | VC5cZy2jAgMBAAECggEAEuU1737DnVgLsoYPvOWWEmx9FCNmMDufXtPDqdQK07tl
9 | jsT/UPlQkDg+KraiQQgcFSHNIEur9i8TA7y3YI8Z69FF9z36d/NGq/oZLNIR/qgg
10 | OTs2CkkPmuHzzj3qGFxK+AJNLwhzzIbK4BEIhQ2DzUhEHf7TjeN8JJ/TqaN6VvX3
11 | r8j5G++uc1XKvDs4j5l/TbGfDKCNc6SPMCuI4LpuKMx+egpUBZE5HXGZhlhIL7OH
12 | 91m8YjWd0N0YXgsORof8nXzfzlYowDs9q2ZEtFOxiKyUzGQEnAReu4Y3ruANq3BS
13 | 0J6jv6F2Az+6CCJsOtooaSTKrGOkgQhmqtvoDG25cQKBgQDtIVtNZY6lWpNwbdYH
14 | jHU9vQ2cZL/xjYKhjzpavYNdILlQchWDMV19qxIUMtelAq9H8Xaimgpf9G7aA7Kt
15 | EMyTJimAG5JNPfZftp0ASJDp6YlT+uEYMqmfsZ0Mnx8UeQ7sn4qV/4WcGbfw6cU8
16 | FD3hodMm58Ek3SG6fNAhZ+Cu+QKBgQC6le9RzIQ+/eVOUSTYXb8ZjQHFjEwbFgbI
17 | hsEKePg0Ux3ofBLTWqGSyspzDap31bJpKlG41S1N+d42sFAN57eNL9ipVl15BNXK
18 | paFMbObQQFWx1Hiwd8S8AYG8r+++jAso6QYS1n9FKD888YDBJUQzMU1Av+fuY3H0
19 | tMqDEGb8ewKBgQDJFctOA7wGPpve8FVaS2K0exgKsmkOlpjbFhE/F4xJMdHUBRp3
20 | CSqlwabwF/lERdWL5Zhb5NK9chN6rz4agq9obSkuKLNU6yF9IudacS7qHQ9Gdu3g
21 | zj2HXV+3b0w02T+tqtEjx+5uZGTWV/bYrrWXG9pqGIdyEk9izCrW2TbwSQKBgAad
22 | F+2LVUiyUTV0dNzifcqXD/ADqBLxte3XsPIBFbMtGwtJkpVBSibc304yts8mmPtX
23 | T6xAiimQaMsBduT3SK9Ned2OvSN0A2v6cPw3g/rvvNnf0SNYK3YKi6G3jsTvS9n4
24 | YIm8Zqh547vyR4ERJBi4b6eS5dKyXbCx09fPdgcPAoGACqY7TwE1SFf19GxDDudd
25 | sstRyphb6uuQyZHqvQFtriHti2GtaETIjfHz/wO5exjLGwo9RfCMzimhbxtbzOPx
26 | DSj7QiBHWlQrw1PD4x3n2SXTvj5BV9+Fe95mO1/+dH2BPCSdNw/sWSn0wMJ3csOA
27 | a7h0CZrZsfivsBQcvUXq5WY=
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/.github/workflows/mend.yml:
--------------------------------------------------------------------------------
1 | name: mend_scan
2 | on:
3 | workflow_dispatch:
4 | push:
5 | branches:
6 | - main
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: connect_twingate
12 | uses: twingate/github-action@v1
13 | with:
14 | service-key: ${{ secrets.TWINGATE_PUBLIC_REPO_KEY }}
15 | - name: checkout repo content
16 | uses: actions/checkout@v4 # checkout the repository content to github runner.
17 | with:
18 | fetch-depth: 1
19 | # install java which is required for mend and clojure
20 | - name: setup java
21 | uses: actions/setup-java@v4
22 | with:
23 | distribution: temurin
24 | java-version: 17
25 | # install clojure tools
26 | - name: Install Clojure tools
27 | uses: DeLaGuardo/setup-clojure@12.5
28 | with:
29 | # Install just one or all simultaneously
30 | # The value must indicate a particular version of the tool, or use 'latest'
31 | # to always provision the latest version
32 | cli: latest # Clojure CLI based on tools.deps
33 | lein: latest # Leiningen
34 | boot: latest # Boot.clj
35 | bb: latest # Babashka
36 | clj-kondo: latest # Clj-kondo
37 | cljstyle: latest # cljstyle
38 | zprint: latest # zprint
39 | # run lein gen
40 | - name: create pom.xml
41 | run: lein pom
42 | # download mend
43 | - name: download_mend
44 | run: curl -o wss-unified-agent.jar https://unified-agent.s3.amazonaws.com/wss-unified-agent.jar
45 | - name: run mend
46 | run: env WS_INCLUDES=pom.xml java -jar wss-unified-agent.jar
47 | env:
48 | WS_APIKEY: ${{ secrets.MEND_API_KEY }}
49 | WS_WSS_URL: https://saas-eu.whitesourcesoftware.com/agent
50 | WS_USERKEY: ${{ secrets.MEND_TOKEN }}
51 | WS_PRODUCTNAME: Puppet Enterprise
52 | WS_PROJECTNAME: ${{ github.event.repository.name }}
53 |
--------------------------------------------------------------------------------
/dev-resources/config/jetty/ssl/certs/ca.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIFdTCCA12gAwIBAgIUbkMWXOFBEfh6IByiV3WgnglzkwowDQYJKoZIhvcNAQEL
3 | BQAwVDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk9SMREwDwYDVQQHDAhQb3J0bGFu
4 | ZDEUMBIGA1UECgwLUHVwcGV0LCBJbmMxDzANBgNVBAMMBnB1cHBldDAeFw0yNDA3
5 | MDkyMzQ3NDVaFw0yOTA3MDgyMzQ3NDVaMFQxCzAJBgNVBAYTAlVTMQswCQYDVQQI
6 | DAJPUjERMA8GA1UEBwwIUG9ydGxhbmQxFDASBgNVBAoMC1B1cHBldCwgSW5jMQ8w
7 | DQYDVQQDDAZwdXBwZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCm
8 | 3AHJwjrQW//zu3u3uQ9CRhIMp4jk8dJn8ASBDz5njcbu2L2YxOSYDEttPWIJmZ9f
9 | rNTvDsaQI7sqTCpPxBRKaOiIMKV7h1PgGagXfxUHYIycu3ktX+X0CRdSOFWwyXAX
10 | 9L+wRKvUvUdTIUgyNA/XvRTsvN8RDedKPuAfKGWY/b9kbYqz8YQG6ipgP3b4yAK7
11 | Mrtq57X0eLDIqB59cmMFMl9u45L8dOGQSgSCxWhSSF+jyfnBeXLB1NXIMswQZoBV
12 | K7moUuKStWAPSbiBh3pPsELmSIm/KkbqouYQ3jLbI3iEeDC/aVtbYrxnmzYvonPo
13 | TkaiIQattLfv2ZxEUzbPGGL//sb1oOFIFtsuQUqiqtZ3lbGAV3AZH6EmcVsgBpN6
14 | 4uDgY3FcFYJqaLnWPN8EKTyzZQmgyNi+VV6TXFRjREQOZH9fKeC8axNqws+dC6E0
15 | qjzooJSSO/n745GJ9YTOZxhscLib4tZV9jUNQ5qMV6gJeJcYE8GlK4TyHukZHXZU
16 | E3hul7owHR/e39SUsOhhShgEJ4+6gWpdwFDOe6g9RbYIDapOu011DwT75M9yn522
17 | 1UWX54MCNea0wlgiMOla92SdsioiLue757b1kyzzytAfZr6jJTem8INyAESsiqlD
18 | DdIZXfJIlGQr/y2kHPVPabPg2eUb0XCHQswvvlGkywIDAQABoz8wPTAdBgNVHQ4E
19 | FgQULuvbnqrPNCKKnfH/oSNmFHU7MBQwDwYDVR0TAQH/BAUwAwEB/zALBgNVHQ8E
20 | BAMCAYYwDQYJKoZIhvcNAQELBQADggIBADBsOYGdQ7he5yqf3BIenRx3xAfXIFbM
21 | H74uqF3q3xfPkQbDgDdPbNpgCdyzUl5+hLWSOnyKSykl5FBVvnQUk07pQJvQdWcy
22 | KusK6fivUpzRVXvWuaB5ngXSsYP3SBrvS3SuFL2t7NzZnswHyTJiKH7H0b6Kt9wn
23 | POuWttD9ysfkACF9zHKCJyKXMjOQ+G+wK1/5oKyhJsNE71JxeN51KDugyPwY9aC7
24 | TwEUykL1ua7DP6IiJme8exIWEr6LN0D1Gp47Lj/NQF8b960tqoPeB0YpB5aG+ew1
25 | 2hETZiJb1aBrn58v6QPodd3yj/bjz83FZKIy6VZKAiyK2ml+p+4bLOF3PBgQqIvp
26 | 2nxXgQNyWJY6AzjMqiaAV2i9HG1a8zjlMNfw4fofQqsaK6x/+0Sr6KyXZaIhLG8G
27 | 3tQJKrWB9MPNjUHgsjyK8d4SpaPcHrzpW5afInAu38DrVuJzj9MffSRVa7DvDu4A
28 | SLGFzrx+OKOQ6OX2oZMszioiHXPXZtYjFlvMtLiJMqCM2GtjVe3tjb0q4w8HcLLd
29 | zp9BwIb+GnJS3oL0qD8BuJNquo7JeShdOAPJ81baTePDOELeK6W6UcVNaBZgJw28
30 | NsDaGyXFgPy2cHDB1gl9GuyeAieKnrWCkbiH1l/NRotEW/r4DBbNrYe43C4pdfY+
31 | wl8jJh/tD/b9
32 | -----END CERTIFICATE-----
33 |
--------------------------------------------------------------------------------
/locales/ring-middleware.pot:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR Puppet
3 | # This file is distributed under the same license as the puppetlabs.ring_middleware package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: puppetlabs.ring_middleware \n"
10 | "X-Git-Ref: ca6f15d0850241780dfadbfe464525e788a2b834\n"
11 | "Report-Msgid-Bugs-To: docs@puppet.com\n"
12 | "POT-Creation-Date: \n"
13 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
14 | "Last-Translator: FULL NAME \n"
15 | "Language-Team: LANGUAGE \n"
16 | "Language: \n"
17 | "MIME-Version: 1.0\n"
18 | "Content-Type: text/plain; charset=UTF-8\n"
19 | "Content-Transfer-Encoding: 8bit\n"
20 |
21 | #: src/puppetlabs/ring_middleware/common.clj
22 | msgid ""
23 | "Proxying request to {0} to remote url {1}. Remote server responded with "
24 | "status {2}"
25 | msgstr ""
26 |
27 | #: src/puppetlabs/ring_middleware/core.clj
28 | msgid "Processing {0} {1}"
29 | msgstr ""
30 |
31 | #: src/puppetlabs/ring_middleware/core.clj
32 | msgid "Full request:"
33 | msgstr ""
34 |
35 | #: src/puppetlabs/ring_middleware/core.clj
36 | msgid "Computed response: {0}"
37 | msgstr ""
38 |
39 | #: src/puppetlabs/ring_middleware/core.clj
40 | msgid "Submitted data is invalid: {0}"
41 | msgstr ""
42 |
43 | #: src/puppetlabs/ring_middleware/core.clj
44 | msgid "Service Unavailable:"
45 | msgstr ""
46 |
47 | #: src/puppetlabs/ring_middleware/core.clj
48 | msgid "Bad Request:"
49 | msgstr ""
50 |
51 | #: src/puppetlabs/ring_middleware/core.clj
52 | msgid "Something unexpected happened: {0}"
53 | msgstr ""
54 |
55 | #: src/puppetlabs/ring_middleware/core.clj
56 | msgid "Internal Server Error: {0}"
57 | msgstr ""
58 |
59 | #: src/puppetlabs/ring_middleware/core.clj
60 | msgid "Internal Server Error for {1} {2}: {0}"
61 | msgstr ""
62 |
63 | #: src/puppetlabs/ring_middleware/core.clj
64 | msgid "accept header must include {0}"
65 | msgstr ""
66 |
67 | #: src/puppetlabs/ring_middleware/core.clj
68 | msgid "content-type {0} is not a supported type for request of type {1} at {2}"
69 | msgstr ""
70 |
71 | #: src/puppetlabs/ring_middleware/core.clj
72 | msgid "Failed to parse json for request of type {0} at {1}"
73 | msgstr ""
74 |
--------------------------------------------------------------------------------
/src/puppetlabs/ring_middleware/common.clj:
--------------------------------------------------------------------------------
1 | (ns puppetlabs.ring-middleware.common
2 | (:require [clojure.string :refer [replace-first]]
3 | [clojure.tools.logging :as log]
4 | [puppetlabs.http.client.sync :refer [request]]
5 | [puppetlabs.i18n.core :refer [trs]])
6 | (:import (java.net URI)
7 | (java.util.regex Pattern)))
8 |
9 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
10 | ;;; Private utility functions
11 |
12 | (defn prepare-cookies
13 | "Removes the :domain and :secure keys and converts the :expires key (a Date)
14 | to a string in the ring response map resp. Returns resp with cookies properly
15 | munged."
16 | [resp]
17 | (let [prepare #(-> (update-in % [1 :expires] str)
18 | (update-in [1] dissoc :domain :secure))]
19 | (assoc resp :cookies (into {} (map prepare (:cookies resp))))))
20 |
21 | (defn strip-trailing-slash
22 | [url]
23 | (if (.endsWith url "/")
24 | (.substring url 0 (- (count url) 1))
25 | url))
26 |
27 | (defn proxy-request
28 | [req proxied-path remote-uri-base & [http-opts]]
29 | ; Remove :decompress-body from the options map, as if this is
30 | ; ever set to true, the response returned to the client making the
31 | ; proxy request will be truncated
32 | (let [http-opts (dissoc http-opts :decompress-body)
33 | uri (URI. (strip-trailing-slash remote-uri-base))
34 | remote-uri (URI. (.getScheme uri)
35 | (.getAuthority uri)
36 | (str (.getPath uri)
37 | (if (instance? Pattern proxied-path)
38 | (:uri req)
39 | (replace-first (:uri req) proxied-path "")))
40 | nil
41 | nil)
42 | response (-> (merge {:method (:request-method req)
43 | :url (str remote-uri "?" (:query-string req))
44 | :headers (dissoc (:headers req) "host" "content-length")
45 | :body (not-empty (slurp (:body req)))
46 | :as :stream
47 | :force-redirects false
48 | :follow-redirects false
49 | :decompress-body false}
50 | http-opts)
51 | request
52 | prepare-cookies)]
53 | (log/debug (trs "Proxying request to {0} to remote url {1}. Remote server responded with status {2}" (:uri req) (str remote-uri) (:status response)))
54 | response))
55 |
--------------------------------------------------------------------------------
/src/puppetlabs/ring_middleware/utils.clj:
--------------------------------------------------------------------------------
1 | (ns puppetlabs.ring-middleware.utils
2 | (:require [schema.core :as schema]
3 | [ring.util.response :as rr]
4 | [slingshot.slingshot :as sling]
5 | [cheshire.core :as json])
6 | (:import (java.security.cert X509Certificate)))
7 |
8 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
9 | ;;;; Schemas
10 |
11 | (def ResponseType
12 | (schema/enum :json :plain))
13 |
14 | (def RingRequest
15 | {:uri schema/Str
16 | (schema/optional-key :ssl-client-cert) (schema/maybe X509Certificate)
17 | schema/Keyword schema/Any})
18 |
19 | (def RingResponse
20 | {:status schema/Int
21 | :headers {schema/Str schema/Any}
22 | :body schema/Any
23 | schema/Keyword schema/Any})
24 |
25 |
26 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
27 | ;;;; Helpers
28 |
29 | (schema/defn ^:always-validate json-response
30 | :- RingResponse
31 | [status :- schema/Int
32 | body :- schema/Any]
33 | (-> body
34 | json/encode
35 | rr/response
36 | (rr/status status)
37 | (rr/content-type "application/json; charset=utf-8")))
38 |
39 | (schema/defn ^:always-validate plain-response
40 | :- RingResponse
41 | [status :- schema/Int
42 | body :- schema/Str]
43 | (-> body
44 | rr/response
45 | (rr/status status)
46 | (rr/content-type "text/plain; charset=utf-8")))
47 |
48 | (defn throw-bad-request!
49 | "Throw a :bad-request type slingshot error with the supplied message"
50 | [message]
51 | (sling/throw+ {:kind :bad-request
52 | :msg message}))
53 |
54 | (defn bad-request?
55 | [e]
56 | "Determine if the supplied slingshot error is for a bad request"
57 | (when (map? e)
58 | (= (:kind e)
59 | :bad-request)))
60 |
61 | (defn throw-service-unavailable!
62 | "Throw a :service-unavailable type slingshot error with the supplied message"
63 | [message]
64 | (sling/throw+ {:kind :service-unavailable
65 | :msg message}))
66 |
67 | (defn service-unavailable?
68 | [e]
69 | "Determine if the supplied slingshot error is for an unavailable service"
70 | (when (map? e)
71 | (= (:kind e)
72 | :service-unavailable)))
73 |
74 | (defn throw-data-invalid!
75 | "Throw a :data-invalid type slingshot error with the supplied message"
76 | [message]
77 | (sling/throw+ {:kind :data-invalid
78 | :msg message}))
79 |
80 | (defn data-invalid?
81 | [e]
82 | "Determine if the supplied slingshot error is for invalid data"
83 | (when (map? e)
84 | (= (:kind e)
85 | :data-invalid)))
86 |
87 | (defn schema-error?
88 | [e]
89 | "Determine if the supplied slingshot error is for a schema mismatch"
90 | (when (map? e)
91 | (= (:type e)
92 | :schema.core/error)))
93 |
94 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 2.0.4
2 | * add calling method and uri to unhandled exception logging
3 | # 2.0.3
4 | * released with no changes due to issues with licensing in project.clj
5 | # 2.0.2 - unreleased
6 | * update to clj parent 7.3.31
7 | * optimize the `wrap-uncaught-errors` to avoid anonymous functions, and ensure streams are closed
8 | * add new wrappers:
9 | * `wrap-accepts-content-type`
10 | * `wrap-accepts-json`
11 | * `wrap-content-type`
12 | * `wrap-content-type-json`
13 | * `wrap-json-parse-exception-handler`
14 |
15 | # 2.0.1
16 | * Updates tk-jetty-10 to 1.0.7 which includes a fix for ring handler's getRequestCharacterEncoding() function.
17 |
18 | # 2.0.0
19 |
20 | * This version updates testing in the repo to use trapperkeeper-webserver-jetty10. The repo
21 | is now intended to be used with Jetty 10.
22 |
23 | # 1.3.0
24 | * This version updates the `wrap-add-cache-headers` middleware so that
25 | it adds a `cache-control` header with the "no-store" directive instead of
26 | the "private, max-age=0, no-cache" directives.
27 |
28 | # 1.2.0
29 | * This version adds `wrap-params` middleware with custom implementation of the
30 | `params-request` function. This was copied from the puppetserver repo to this
31 | more-central location. It is documented in the [README](./README.md).
32 |
33 | # 1.1.0
34 | * This version adds two new middleware used in other
35 | puppetlabs projects, documented in the [README](./README.md):
36 | * `wrap-add-x-content-nosniff`
37 | * `wrap-add-csp`
38 |
39 | # 1.0.1
40 | * This is a bug fix release that ensure stacktraces are correctly printed
41 | to the log when handling otherwise uncaught exceptions.
42 |
43 | # 1.0.0
44 | #### Breaking Changes
45 | * Moves from `{:type ... :message ...}` to `{:kind ... :msg ...}` for
46 | exceptions and error responses.
47 | * Moves schemas and helpers previously defined in `core` namespace into new `utils` namespace.
48 |
49 | # 0.3.1
50 | * This is a bug-fix release for a regression in wrap-proxy.
51 | * All middleware now have the `:always-validate` metadata
52 | set for schema validation.
53 |
54 | # 0.3.0
55 | * This version adds many middleware that are used in other
56 | puppetlabs projects. These middleware are mostly for logging
57 | and error handling, and they are all documented in the
58 | [README](./README.md):
59 | * `wrap-request-logging`
60 | * `wrap-response-logging`
61 | * `wrap-service-unavailable`
62 | * `wrap-bad-request`
63 | * `wrap-data-errors`
64 | * `wrap-schema-errors`
65 | * `wrap-uncaught-errors`
66 | * Additionally, this version fixes
67 | [an issue](https://tickets.puppetlabs.com/browse/TK-228) with the
68 | behavior of `wrap-proxy` and its handling of redirects.
69 |
70 | # 0.2.1
71 | * Add wrap-with-certificate-cn middleware that adds a `:ssl-client-cn` key
72 | to the request map if a `:ssl-client-cert` is present.
73 | * Add wrap-with-x-frame-options-deny middleware that adds `X-Frame-Options: DENY`
74 |
75 | # 0.2.0
76 | * Modify behavior of regex support in the wrap-proxy function.
77 | Now, when a regex is given for the `proxied-path` argument,
78 | the entirety of the request uri's path will be appended onto
79 | the path of `remote-uri-base`.
80 | * Add a new utility middleware, `wrap-add-cache-headers`,
81 | that adds `cache-control` headers to `GET` and `PUT`
82 | requests.
83 |
84 | # 0.1.3
85 | * Log proxied requests
86 | * Allow `proxied-path` argument in `wrap-proxy` function to
87 | be a regular expression
88 | * Bump http-client to v0.2.8
89 |
90 | # 0.1.2
91 | * Add support for redirect following on proxy requests
92 | * Fix issue where Gzipped proxy responses were being truncated
93 |
--------------------------------------------------------------------------------
/src/puppetlabs/ring_middleware/params.clj:
--------------------------------------------------------------------------------
1 | (ns puppetlabs.ring-middleware.params
2 | (:require [ring.util.codec :as codec]
3 | [ring.util.request :as req]))
4 |
5 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
6 | ;; COPY OF RELEVANT FUNCTIONS FROM UPSTREAM ring.middleware.params LIBRARY
7 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
8 | ;;
9 | ;; This namespace is basically just here to provide an implementation of
10 | ;; the `params-request` middleware function that supports a String representation
11 | ;; of the body of a request. The upstream library requires the body to be of
12 | ;; a type that is compatible with Clojure's IOFactory, which forces us to read
13 | ;; the request body into memory twice for requests that we have to pass down
14 | ;; into the JRuby layer. (Technically, the Ring specification states that the
15 | ;; body must be an InputStream, so the maintainer of the upstream library was
16 | ;; reluctant to accept any sort of upstream PR to work around this issue.)
17 | ;;
18 | ;; All of this code is copied from the upstream library, and there is just
19 | ;; one very slight modification (see comment in `assoc-form-params` function)
20 | ;; that allows us to avoid reading the body into memory twice.
21 | ;;
22 | ;; In the future, if we can handle the query parameter parsing strictly on the
23 | ;; Clojure side and remove that code from the Ruby side, we should be able to
24 | ;; get rid of this. That will be much easier to consider doing once we're able
25 | ;; to get rid of the Rack/Webrick support.
26 | ;;
27 | ;; If that happens, we should delete this namespace :)
28 | ;;
29 |
30 | (defn parse-params [params encoding]
31 | (let [params (codec/form-decode params encoding)]
32 | (if (map? params) params {})))
33 |
34 | (defn content-type
35 | "Return the content-type of the request, or nil if no content-type is set."
36 | [request]
37 | ;; NOTE: in the latest version of ring-core, they only look in
38 | ;; the headers map for the content type. They no longer fall
39 | ;; back to looking for it in the main request map.
40 | (if-let [type (or (get-in request [:headers "content-type"])
41 | (get request :content-type))]
42 | (second (re-find #"^(.*?)(?:;|$)" type))))
43 |
44 | (defn urlencoded-form?
45 | "True if a request contains a urlencoded form in the body."
46 | [request]
47 | (if-let [^String type (content-type request)]
48 | (.startsWith type "application/x-www-form-urlencoded")))
49 |
50 | (defn assoc-query-params
51 | "Parse and assoc parameters from the query string with the request."
52 | [request encoding]
53 | (merge-with merge request
54 | (if-let [query-string (:query-string request)]
55 | (let [params (parse-params query-string encoding)]
56 | {:query-params params, :params params})
57 | {:query-params {}, :params {}})))
58 |
59 | (defn assoc-form-params
60 | "Parse and assoc parameters from the request body with the request."
61 | [request encoding]
62 | (merge-with merge request
63 | (if-let [body (and (urlencoded-form? request) (:body request))]
64 | (let [params (parse-params
65 | ;; NOTE: this is the main difference between our
66 | ;; copy of this code and the upstream version:
67 | ;; the upstream always does a slurp here, while
68 | ;; we only do a slurp if the body is not already
69 | ;; a string.
70 | (if (string? body)
71 | body
72 | (slurp body :encoding encoding))
73 | encoding)]
74 | {:form-params params, :params params})
75 | {:form-params {}, :params {}})))
76 |
77 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
78 | ;;; Public
79 |
80 | (defn params-request
81 | "Adds parameters from the query string and the request body to the request
82 | map. See: wrap-params."
83 | {:arglists '([request] [request options])}
84 | [request & [opts]]
85 | (let [encoding (or (:encoding opts)
86 | (req/character-encoding request)
87 | "UTF-8")
88 | request (if (:form-params request)
89 | request
90 | (assoc-form-params request encoding))]
91 | (if (:query-params request)
92 | request
93 | (assoc-query-params request encoding))))
94 |
95 | (defn wrap-params
96 | "Middleware to parse urlencoded parameters from the query string and form
97 | body (if the request is a url-encoded form). Adds the following keys to
98 | the request map:
99 | :query-params - a map of parameters from the query string
100 | :form-params - a map of parameters from the body
101 | :params - a merged map of all types of parameter
102 | Accepts the following options:
103 | :encoding - encoding to use for url-decoding. If not specified, uses
104 | the request character encoding, or \"UTF-8\" if no request
105 | character encoding is set."
106 | {:arglists '([handler] [handler options])}
107 | [handler & [options]]
108 | (fn [request]
109 | (handler (params-request request options))))
110 |
--------------------------------------------------------------------------------
/dev-resources/Makefile.i18n:
--------------------------------------------------------------------------------
1 | # -*- Makefile -*-
2 | # This file was generated by the i18n leiningen plugin
3 | # Do not edit this file; it will be overwritten the next time you run
4 | # lein i18n init
5 | #
6 |
7 | # The name of the package into which the translations bundle will be placed
8 | BUNDLE=puppetlabs.ring_middleware
9 |
10 | # The name of the POT file into which the gettext code strings (msgid) will be placed
11 | POT_NAME=ring-middleware.pot
12 |
13 | # The list of names of packages covered by the translation bundle;
14 | # by default it contains a single package - the same where the translations
15 | # bundle itself is placed - but this can be overridden - preferably in
16 | # the top level Makefile
17 | PACKAGES?=$(BUNDLE)
18 | LOCALES=$(basename $(notdir $(wildcard locales/*.po)))
19 | BUNDLE_DIR=$(subst .,/,$(BUNDLE))
20 | BUNDLE_FILES=$(patsubst %,resources/$(BUNDLE_DIR)/Messages_%.class,$(LOCALES))
21 | FIND_SOURCES=find src -name \*.clj
22 | # xgettext before 0.19 does not understand --add-location=file. Even CentOS
23 | # 7 ships with an older gettext. We will therefore generate full location
24 | # info on those systems, and only file names where xgettext supports it
25 | LOC_OPT=$(shell xgettext --add-location=file -f - /dev/null 2>&1 && echo --add-location=file || echo --add-location)
26 |
27 | LOCALES_CLJ=resources/locales.clj
28 | define LOCALES_CLJ_CONTENTS
29 | {
30 | :locales #{$(patsubst %,"%",$(LOCALES))}
31 | :packages [$(patsubst %,"%",$(PACKAGES))]
32 | :bundle $(patsubst %,"%",$(BUNDLE).Messages)
33 | }
34 | endef
35 | export LOCALES_CLJ_CONTENTS
36 |
37 |
38 | i18n: msgfmt
39 |
40 | # Update locales/.pot
41 | update-pot: locales/$(POT_NAME)
42 |
43 | locales/$(POT_NAME): $(shell $(FIND_SOURCES)) | locales
44 | @tmp=$$(mktemp $@.tmp.XXXX); \
45 | $(FIND_SOURCES) \
46 | | xgettext --from-code=UTF-8 --language=lisp \
47 | --copyright-holder='Puppet ' \
48 | --package-name="$(BUNDLE)" \
49 | --package-version="$(BUNDLE_VERSION)" \
50 | --msgid-bugs-address="docs@puppet.com" \
51 | -k \
52 | -kmark:1 -ki18n/mark:1 \
53 | -ktrs:1 -ki18n/trs:1 \
54 | -ktru:1 -ki18n/tru:1 \
55 | -ktrun:1,2 -ki18n/trun:1,2 \
56 | -ktrsn:1,2 -ki18n/trsn:1,2 \
57 | $(LOC_OPT) \
58 | --add-comments --sort-by-file \
59 | -o $$tmp -f -; \
60 | sed -i.bak -e 's/charset=CHARSET/charset=UTF-8/' $$tmp; \
61 | sed -i.bak -e 's/POT-Creation-Date: [^\\]*/POT-Creation-Date: /' $$tmp; \
62 | rm -f $$tmp.bak; \
63 | if ! diff -q -I POT-Creation-Date $$tmp $@ >/dev/null 2>&1; then \
64 | mv $$tmp $@; \
65 | else \
66 | rm $$tmp; touch $@; \
67 | fi
68 |
69 | # Run msgfmt over all .po files to generate Java resource bundles
70 | # and create the locales.clj file
71 | msgfmt: $(BUNDLE_FILES) $(LOCALES_CLJ) clean-orphaned-bundles
72 |
73 | # Force rebuild of locales.clj if its contents is not the the desired one. The
74 | # shell echo is used to add a trailing newline to match the one from `cat`
75 | ifneq ($(shell cat $(LOCALES_CLJ) 2> /dev/null),$(shell echo '$(LOCALES_CLJ_CONTENTS)'))
76 | .PHONY: $(LOCALES_CLJ)
77 | endif
78 | $(LOCALES_CLJ): | resources
79 | @echo "Writing $@"
80 | @echo "$$LOCALES_CLJ_CONTENTS" > $@
81 |
82 | # Remove every resource bundle that wasn't generated from a PO file.
83 | # We do this because we used to generate the english bundle directly from the POT.
84 | .PHONY: clean-orphaned-bundles
85 | clean-orphaned-bundles:
86 | @for bundle in resources/$(BUNDLE_DIR)/Messages_*.class; do \
87 | locale=$$(basename "$$bundle" | sed -E -e 's/\$$?1?\.class$$/_class/' | cut -d '_' -f 2;); \
88 | if [ ! -f "locales/$$locale.po" ]; then \
89 | rm "$$bundle"; \
90 | fi \
91 | done
92 |
93 | resources/$(BUNDLE_DIR)/Messages_%.class: locales/%.po | resources
94 | msgfmt --java2 -d resources -r $(BUNDLE).Messages -l $(*F) $<
95 |
96 | # Use this to initialize translations. Updating the PO files is done
97 | # automatically through a CI job that utilizes the scripts in the project's
98 | # `bin` file, which themselves come from the `clj-i18n` project.
99 | locales/%.po: | locales
100 | @if [ ! -f $@ ]; then \
101 | touch $@ && msginit --no-translator -l $(*F) -o $@ -i locales/$(POT_NAME); \
102 | fi
103 |
104 | resources locales:
105 | @mkdir $@
106 |
107 | help:
108 | $(info $(HELP))
109 | @echo
110 |
111 | .PHONY: help
112 |
113 | define HELP
114 | This Makefile assists in handling i18n related tasks during development. Files
115 | that need to be checked into source control are put into the locales/ directory.
116 | They are
117 |
118 | locales/$(POT_NAME) - the POT file generated by 'make update-pot'
119 | locales/$$LANG.po - the translations for $$LANG
120 |
121 | Only the $$LANG.po files should be edited manually; this is usually done by
122 | translators.
123 |
124 | You can use the following targets:
125 |
126 | i18n: refresh all the files in locales/ and recompile resources
127 | update-pot: extract strings and update locales/$(POT_NAME)
128 | locales/LANG.po: create translations for LANG
129 | msgfmt: compile the translations into Java classes; this step is
130 | needed to make translations available to the Clojure code
131 | and produces Java class files in resources/
132 | endef
133 | # @todo lutter 2015-04-20: for projects that use libraries with their own
134 | # translation, we need to combine all their translations into one big po
135 | # file and then run msgfmt over that so that we only have to deal with one
136 | # resource bundle
137 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ring-middleware
2 |
3 | [](https://travis-ci.org/puppetlabs/ring-middleware)
4 |
5 | This project was originally adapted from tailrecursion's
6 | [ring-proxy](https://github.com/tailrecursion/ring-proxy) middleware, and is
7 | meant for use with the [Trapperkeeper Jetty10 Webservice](https://github.com/puppetlabs/trapperkeeper-webserver-jetty10). It also contains common ring middleware between Puppet projects and helpers to be used with the middleware.
8 |
9 | ## Usage
10 |
11 |
12 | To use `ring-middleware`, add this project as a dependency in your leiningen project file:
13 |
14 | [](https://clojars.org/puppetlabs/ring-middleware)
15 |
16 | ## Schemas
17 |
18 | * `ResponseType` -- one of the two supported response types (`:json`, `:plain`) returned by many middleware.
19 | * `RingRequest` -- a map containing at least a `:uri`, optionally a valid certificate, and any number of keyword-Any pairs.
20 | * `RingResponse` -- a map with at least `:status`, `:headers`, and `:body` keys.
21 |
22 |
23 | ## Non-Middleware Helpers
24 | ### json-response
25 | ```clj
26 | (json-response status body)
27 | ```
28 | Creates a basic ring response with `:status` of `status` and a `:body` of `body` serialized to json.
29 |
30 | ### plain-response
31 | ```clj
32 | (plain-response status body)
33 | ```
34 | Creates a basic ring response with `:status` of `status` and a `:body` of `body` set to UTF-8 plain text.
35 |
36 |
37 | ### throw-bad-request!
38 | ```clj
39 | (throw-bad-request! "Error Message")
40 | ```
41 | Throws a :bad-request type slingshot error with the supplied message.
42 | See `wrap-bad-request` for middleware designed to compliment this function,
43 | also `bad-request?` for a function to help implement your own error handling.
44 |
45 | ### throw-service-unavailable!
46 | ```clj
47 | (throw-service-unavailable! "Error Message")
48 | ```
49 | Throws a :service-unavailable type slingshot error with the supplied message.
50 | See `wrap-service-unavailable` for middleware designed to compliment this function,
51 | also `service-unavailable?` for a function to help implement your own error handling.
52 |
53 | ### throw-data-invalid!
54 | ```clj
55 | (throw-data-invalid! "Error Message")
56 | ```
57 | Throws a :data-invalid type slingshot error with the supplied message.
58 | See `wrap-data-errors` for middleware designed to compliment this function,
59 | also `data-invalid?` for a function to help implement your own error handling.
60 |
61 |
62 | ### bad-request?
63 | ```clj
64 | (try+ (handler request)
65 | (catch bad-request? e
66 | (...handle a bad request...)))
67 | ```
68 | Determines if the supplied slingshot error map is for a bad request.
69 |
70 | ### service-unavailable?
71 | ```clj
72 | (try+ (handler request)
73 | (catch service-unavailable? e
74 | (...handle service unavailability...)))
75 | ```
76 | Determines if the supplied slingshot error map is for the service being unavailable.
77 |
78 | ### data-invalid?
79 | ```clj
80 | (try+ (handler request)
81 | (catch data-invalid? e
82 | (...handle invalid data...)))
83 | ```
84 | Determines if the supplied slingshot error map is for invalid data.
85 |
86 | ### schema-error?
87 | ```clj
88 | (try+ (handler request)
89 | (catch schema-error? e
90 | (...handle schema error...)))
91 | ```
92 | Determines if the supplied slingshot error map is for a schema error.
93 |
94 |
95 |
96 | ## Middleware
97 | ### wrap-request-logging
98 | ```clj
99 | (wrap-request-logging handler)
100 | ```
101 | 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.
102 |
103 | ### wrap-response-logging
104 | ```clj
105 | (wrap-response-logging handler)
106 | ```
107 | Logs the response at the trace log level.
108 |
109 | ### wrap-proxy
110 | ```clj
111 | (wrap-proxy handler proxied-path remote-uri-base & [http-opts])
112 | ```
113 |
114 | This function returns a ring handler that, when given a URL with a certain prefix, proxies the request
115 | to a remote URL specified by the `remote-uri-base` argument.
116 |
117 | The arguments are as follows:
118 |
119 | * `handler`: A ring-handler that will be used if the provided url does not begin with the proxied-path prefix
120 | * `proxied-path`: The URL prefix of all requests that are to be proxied. This can be either a string or a
121 | regular expression pattern. Note that, when this is a regular expression, the entire request URI
122 | will be appended to `remote-uri-base` when the URI is being rewritten, whereas if this argument
123 | is a string, the `proxied-path` will not be included.
124 | * `remote-uri-base`: The base URL that you want to proxy requests with the `proxied-path` prefix to
125 | * `http-opts`: An optional list of options for an http client. This is used by the handler returned by
126 | `wrap-proxy` when it makes a proxied request to a remote URI. For a list of available options, please
127 | see the options defined for [clj-http-client](https://github.com/puppetlabs/clj-http-client).
128 |
129 | For example, the following:
130 |
131 | ```clj
132 | (wrap-proxy handler "/hello-world" "http://localhost:9000/hello")
133 | ```
134 | would return a ring handler that proxies all requests with URL prefix "/hello-world" to
135 | `http://localhost:9000/hello`.
136 |
137 | The following:
138 |
139 | ```clj
140 | (wrap-proxy handler #"^/hello-world" "http://localhost:9000/hello")
141 | ```
142 | would return a ring handler that proxies all requests with a URL path matching the regex
143 | `#^/hello-world"` to `http://localhost:9000/hello/[url-path]`.
144 |
145 | #### Proxy Redirect Support
146 |
147 | By default, all proxy requests using `wrap-proxy` will follow any redirects, including on POST and PUT
148 | requests. To allow redirects but restrict their use on POST and PUT requests, set the `:force-redirects`
149 | option to `false` in the `http-opts` map. To disable redirect following on proxy requests, set the
150 | `:follow-redirects` option to `false` in the `http-opts` map. Please not that if proxy redirect following
151 | is disabled, you may have to disable it on the client making the proxy request as well if the location returned
152 | by the redirect is relative.
153 |
154 | #### SSL Support
155 |
156 | `wrap-proxy` supports SSL. To add SSL support, you can set SSL options in the `http-opts` map as you would in
157 | a request made with [clj-http-client](https://github.com/puppetlabs/clj-http-client). Simply set the
158 | `:ssl-cert`, `:ssl-key`, and `:ssl-ca-cert` options in the `http-opts` map to be paths to your .pem files.
159 |
160 | ### wrap-with-certificate-cn
161 |
162 | This middleware adds a `:ssl-client-cn` key to the request map if a
163 | `:ssl-client-cert` is present. If no client certificate is present,
164 | the key's value is set to nil. This makes for easier certificate
165 | whitelisting (using the cert whitelisting function from pl/kitchensink)
166 |
167 | ### wrap-add-cache-headers
168 |
169 | A utility middleware with the following signature:
170 |
171 | ```clj
172 | (wrap-add-cache-headers handler)
173 | ```
174 |
175 | This middleware adds `Cache-Control: no-store` headers to `GET` and `PUT` requests if they are handled by the handler.
176 |
177 | ### wrap-add-x-frame-options-deny
178 |
179 | A utility middleware with the following signature:
180 |
181 | ```clj
182 | (wrap-add-x-frame-options-deny handler)
183 | ```
184 |
185 | This middleware adds `X-Frame-Options: DENY` headers to requests if they are handled by the handler.
186 |
187 | ### wrap-add-x-content-nosniff
188 |
189 | A utility middleware with the following signature:
190 |
191 | ```clj
192 | (wrap-add-x-content-nosniff handler)
193 | ```
194 |
195 | This middleware adds `X-Content-Type-Options: nosniff` headers to requests if they are handled by the handler.
196 |
197 | ### wrap-add-csp
198 |
199 | A utility middleware with the following signature:
200 |
201 | ```clj
202 | (wrap-add-csp handler csp-val)
203 | ```
204 |
205 | This middleware adds `Content-Security-Policy` headers to requests if they are handled by the handler.
206 | The value of the header will be equivalent to the second argument passed, `csp-val`.
207 |
208 | ### wrap-params
209 |
210 | A utility middleware with the following signature:
211 |
212 | ```clj
213 | (wrap-params handler & [options])
214 | ```
215 |
216 | This middleware parses url-encoded parameters from the query string and form body
217 | and adds the following keys to the request map:
218 | * `:query-params` - a map of parameters from the query string
219 | * `:form-params` - a map of parameters from the body
220 | * `:params` - a map of all types of parameter
221 |
222 | Accepts the following options:
223 | * `:encoding` - encoding to use for url-decoding. If not specified, uses the request
224 | character encoding, or "UTF-8" if no request charater encoding is set.
225 |
226 | ### wrap-data-errors
227 | ```clj
228 | (wrap-data-errors handler)
229 | ```
230 | Always returns a status code of 400 to the client and logs the error message at the "error" log level.
231 | Catches and processes any exceptions thrown via `slingshot/throw+` with a `:type` of one of:
232 | * `:request-data-invalid`
233 | * `:user-data-invalid`
234 | * `:data-invalid`
235 | * `:service-status-version-not-found`
236 |
237 | 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.
238 |
239 | Example return body:
240 | ```json
241 | {
242 | "error": {
243 | "type": "user-data-invalid",
244 | "message": "Error Message From Thrower"
245 | }
246 | }
247 | ```
248 |
249 | Returns valid [`ResponseType`](#schemas)s, eg:
250 | ```clj
251 | (wrap-data-errors handler :plain)
252 | ```
253 |
254 | ### wrap-bad-request
255 | ```clj
256 | (wrap-bad-request handler)
257 | ```
258 | Always returns a status code of 400 to the client and logs the error message at the "error" log level.
259 | Catches and processes any exceptions thrown via `slingshot/throw+` with a `:type` of one of:
260 | * `:bad-request`
261 |
262 | 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.
263 |
264 | Example return body:
265 | ```json
266 | {
267 | "error": {
268 | "type": "bad-request",
269 | "message": "Error Message From Thrower"
270 | }
271 | }
272 | ```
273 |
274 | Returns valid [`ResponseType`](#schemas)s, eg:
275 | ```clj
276 | (wrap-bad-request handler :plain)
277 | ```
278 |
279 | ### wrap-schema-errors
280 | ```clj
281 | (wrap-schema-errors handler)
282 | ```
283 | 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.
284 |
285 | 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".
286 |
287 | Example return body:
288 | ```json
289 | {
290 | "error": {
291 | "type": "application-error",
292 | "message": "Something unexpected happened: {:error ... :value ... :type :schema.core/error}"
293 | }
294 | }
295 | ```
296 |
297 | Returns valid [`ResponseType`](#schemas)s, eg:
298 | ```clj
299 | (wrap-schema-errors handler :plain)
300 | ```
301 |
302 | ### wrap-uncaught-errors
303 | ```clj
304 | (wrap-uncaught-errors handler)
305 | ```
306 | Always returns a status code of 500 to the client and logs a message with the serialized Exception at the "error" log level.
307 |
308 | 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".
309 |
310 | Example return body:
311 | ```json
312 | {
313 | "error": {
314 | "type": "application-error",
315 | "message": "Internal Server Error: "
316 | }
317 | }
318 | ```
319 |
320 | Returns valid [`ResponseType`](#schemas)s, eg:
321 | ```clj
322 | (wrap-uncaught-errors handler :plain)
323 | ```
324 | ### wrap-accepts-content-type
325 | ```clj
326 | (wrap-accepts-content-type handler content-type)
327 | ```
328 | Returns a wrapper that evaluates the accept header of the incoming request and validate that it accepts a type
329 | that this endpoint will produce.
330 |
331 | For example, if the endpoint will accept json
332 | ```clj
333 | (wrap-accepts-content-type handler "application/json")
334 | ```
335 |
336 | If the request supplies a result with `text/plain` a `406 not-acceptable` error will be returned
337 |
338 | ### wrap-accepts-json
339 | ```clj
340 | (wrap-accepts-json handler)
341 | ```
342 | Returns a wrapper that evaluates the accept header of the incoming request and validates
343 | that it matches `application/json`
344 |
345 | If it doesn't, a response with a `406 not acceptable` is returned.
346 |
347 | ### wrap-content-type
348 | ```clj
349 | (wrap-content-type handler )
350 | ```
351 |
352 | Ensure that the content type of the body of the incoming request matches the acceptable
353 | content types this endpoint supports.
354 | For example:
355 | ```clj
356 | (wrap-content-type handler [json-encoding-type])
357 | ```
358 | If the content type is not matched, a `415 Unsupported Media Type` is returned.
359 |
360 | ### wrap-content-type-json
361 | ```clj
362 | (wrap-content-type-json handler)
363 | ```
364 | Ensure that the content type of the body of the incoming request matches the a json type.
365 | If the content type is not matched, a `415 Unsupported Media Type` is returned.
366 |
367 | ### wrap-json-parse-exception-handler
368 | ```clj
369 | (wrap-json-parse-exception-handler handler)
370 | ```
371 | Ensure that if a JsonParseException is thrown while the handler is being executed, an
372 | appropriate error is returned. a `400 Bad Request` with appropriate messaging will be returned.
373 |
374 | ## Support
375 |
376 | Please log tickets and issues at our [Trapperkeeper Issue Tracker](https://tickets.puppetlabs.com/browse/TK).
377 | In addition there is a #trapperkeeper channel on Freenode.
378 |
379 |
--------------------------------------------------------------------------------
/src/puppetlabs/ring_middleware/core.clj:
--------------------------------------------------------------------------------
1 | (ns puppetlabs.ring-middleware.core
2 | (:require [clojure.string :as str]
3 | [clojure.tools.logging :as log]
4 | [puppetlabs.i18n.core :refer [trs]]
5 | [puppetlabs.kitchensink.core :as ks]
6 | [puppetlabs.ring-middleware.common :as common]
7 | [puppetlabs.ring-middleware.utils :as utils]
8 | [puppetlabs.ssl-utils.core :as ssl-utils]
9 | [ring.middleware.cookies :as cookies]
10 | [schema.core :as schema]
11 | [slingshot.slingshot :as sling])
12 | (:import (clojure.lang IFn)
13 | (com.fasterxml.jackson.core JsonParseException)
14 | (java.io ByteArrayOutputStream PrintStream)
15 | (java.util.regex Pattern)))
16 |
17 | (def json-encoding-type "application/json")
18 | ;; HTTP error codes
19 | (def InternalServiceError 500)
20 | (def ServiceUnavailable 503)
21 | (def BadRequest 400)
22 | (def NotAcceptable 406)
23 | (def UnsupportedMediaType 415)
24 |
25 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
26 | ;;;; Private
27 |
28 | (defn sanitize-client-cert
29 | "Given a ring request, return a map which replaces the :ssl-client-cert with
30 | just the certificate's Common Name at :ssl-client-cert-cn. Also, remove the
31 | copy of the certificate put on the request by TK-auth."
32 | [req]
33 | (-> (if-let [client-cert (:ssl-client-cert req)]
34 | (-> req
35 | (dissoc :ssl-client-cert)
36 | (assoc :ssl-client-cert-cn (ssl-utils/get-cn-from-x509-certificate client-cert)))
37 | req)
38 | (ks/dissoc-in [:authorization :certificate])))
39 |
40 |
41 |
42 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
43 | ;;;; Middleware
44 |
45 | (schema/defn ^:always-validate wrap-request-logging :- IFn
46 | "A ring middleware that logs the request."
47 | [handler :- IFn]
48 | (fn [{:keys [request-method uri] :as req}]
49 | (log/debug (trs "Processing {0} {1}" request-method uri))
50 | (log/trace (format "%s\n%s" (trs "Full request:") (ks/pprint-to-string (sanitize-client-cert req))))
51 | (handler req)))
52 |
53 | (schema/defn ^:always-validate wrap-response-logging :- IFn
54 | "A ring middleware that logs the response."
55 | [handler :- IFn]
56 | (fn [req]
57 | (let [resp (handler req)]
58 | (log/trace (trs "Computed response: {0}" resp))
59 | resp)))
60 |
61 | (schema/defn ^:always-validate wrap-proxy :- IFn
62 | "Proxies requests to proxied-path, a local URI, to the remote URI at
63 | remote-uri-base, also a string."
64 | [handler :- IFn
65 | proxied-path :- (schema/either Pattern schema/Str)
66 | remote-uri-base :- schema/Str
67 | & [http-opts]]
68 | (let [proxied-path (if (instance? Pattern proxied-path)
69 | (re-pattern (str "^" (.pattern proxied-path)))
70 | proxied-path)]
71 | (cookies/wrap-cookies
72 | (fn [req]
73 | (if (or (and (string? proxied-path) (.startsWith ^String (:uri req) (str proxied-path "/")))
74 | (and (instance? Pattern proxied-path) (re-find proxied-path (:uri req))))
75 | (common/proxy-request req proxied-path remote-uri-base http-opts)
76 | (handler req))))))
77 |
78 | (schema/defn ^:always-validate wrap-add-cache-headers :- IFn
79 | "Adds cache control invalidation headers to GET and PUT requests if they are handled by the handler"
80 | [handler :- IFn]
81 | (fn [request]
82 | (let [request-method (:request-method request)
83 | response (handler request)]
84 | (when-not (nil? response)
85 | (if (or
86 | (= request-method :get)
87 | (= request-method :put))
88 | (assoc-in response [:headers "cache-control"] "no-store")
89 | response)))))
90 |
91 | (schema/defn ^:always-validate wrap-add-x-frame-options-deny :- IFn
92 | "Adds 'X-Frame-Options: DENY' headers to requests if they are handled by the handler"
93 | [handler :- IFn]
94 | (fn [request]
95 | (let [response (handler request)]
96 | (when response
97 | (assoc-in response [:headers "X-Frame-Options"] "DENY")))))
98 |
99 | (schema/defn ^:always-validate wrap-add-x-content-nosniff :- IFn
100 | "Adds 'X-Content-Type-Options: nosniff' headers to request."
101 | [handler :- IFn]
102 | (fn [request]
103 | (let [response (handler request)]
104 | (when response
105 | (assoc-in response [:headers "X-Content-Type-Options"] "nosniff")))))
106 |
107 | (schema/defn ^:always-validate wrap-add-csp :- IFn
108 | "Adds 'Content-Security-Policy: default-src 'self'' headers to request."
109 | [handler :- IFn
110 | csp-val]
111 | (fn [request]
112 | (let [response (handler request)]
113 | (when response
114 | (assoc-in response [:headers "Content-Security-Policy"] csp-val)))))
115 |
116 | (schema/defn ^:always-validate wrap-with-certificate-cn :- IFn
117 | "Ring middleware that will annotate the request with an
118 | :ssl-client-cn key representing the CN contained in the client
119 | certificate of the request. If no client certificate is present,
120 | the key's value is set to nil."
121 | [handler :- IFn]
122 | (fn [{:keys [ssl-client-cert] :as req}]
123 | (let [cn (some-> ssl-client-cert
124 | ssl-utils/get-cn-from-x509-certificate)
125 | req (assoc req :ssl-client-cn cn)]
126 | (handler req))))
127 |
128 | (schema/defn ^:always-validate wrap-data-errors :- IFn
129 | "A ring middleware that catches a slingshot error thrown by
130 | throw-data-invalid! or a :kind of slingshot error of one of:
131 | :request-data-invalid
132 | :user-data-invalid
133 | :data-invalid
134 | :service-status-version-not-found
135 | logs the error and returns a BadRequest ring response."
136 | ([handler :- IFn]
137 | (wrap-data-errors handler :json))
138 | ([handler :- IFn
139 | type :- utils/ResponseType]
140 | (let [response (fn [e]
141 | (log/error e (trs "Submitted data is invalid: {0}" (:msg e)))
142 | (case type
143 | :json (utils/json-response BadRequest e)
144 | :plain (utils/plain-response BadRequest (:msg e))))]
145 | (fn [request]
146 | (sling/try+ (handler request)
147 | (catch
148 | #(contains? #{:request-data-invalid
149 | :user-data-invalid
150 | :data-invalid
151 | :service-status-version-not-found}
152 | (:kind %))
153 | e
154 | (response e)))))))
155 |
156 | (schema/defn ^:always-validate wrap-service-unavailable :- IFn
157 | "A ring middleware that catches slingshot errors thrown by
158 | utils/throw-service-unavailabe!, logs the error and returns a ServiceUnavailable ring
159 | response."
160 | ([handler :- IFn]
161 | (wrap-service-unavailable handler :json))
162 | ([handler :- IFn
163 | type :- utils/ResponseType]
164 | (let [response (fn [e]
165 | (log/error e (trs "Service Unavailable:" (:msg e)))
166 | (case type
167 | :json (utils/json-response ServiceUnavailable e)
168 | :plain (utils/plain-response ServiceUnavailable (:msg e))))]
169 | (fn [request]
170 | (sling/try+ (handler request)
171 | (catch utils/service-unavailable? e
172 | (response e)))))))
173 |
174 | (schema/defn ^:always-validate wrap-bad-request :- IFn
175 | "A ring middleware that catches slingshot errors thrown by
176 | utils/throw-bad-request!, logs the error and returns a BadRequest ring
177 | response."
178 | ([handler :- IFn]
179 | (wrap-bad-request handler :json))
180 | ([handler :- IFn
181 | type :- utils/ResponseType]
182 | (let [response (fn [e]
183 | (log/error e (trs "Bad Request:" (:msg e)))
184 | (case type
185 | :json (utils/json-response BadRequest e)
186 | :plain (utils/plain-response BadRequest (:msg e))))]
187 | (fn [request]
188 | (sling/try+ (handler request)
189 | (catch utils/bad-request? e
190 | (response e)))))))
191 |
192 | (schema/defn ^:always-validate wrap-schema-errors :- IFn
193 | "A ring middleware that catches schema errors and returns a InternalServiceError
194 | response with the details"
195 | ([handler :- IFn]
196 | (wrap-schema-errors handler :json))
197 | ([handler :- IFn
198 | type :- utils/ResponseType]
199 | (let [response (fn [e]
200 | (let [msg (trs "Something unexpected happened: {0}"
201 | (select-keys e [:error :value :type]))]
202 | (log/error e msg)
203 | (case type
204 | :json (utils/json-response InternalServiceError
205 | {:kind :application-error
206 | :msg msg})
207 | :plain (utils/plain-response InternalServiceError msg))))]
208 | (fn [request]
209 | (sling/try+ (handler request)
210 | (catch utils/schema-error? e
211 | (response e)))))))
212 |
213 | (defn- handle-error-response
214 | [request e type]
215 | (with-open [baos (ByteArrayOutputStream.)
216 | print-stream (PrintStream. baos)]
217 | (.printStackTrace e print-stream)
218 | (let [msg (trs "Internal Server Error: {0}" (.toString e))
219 | method (-> request :request-method name str/upper-case)]
220 | (log/error (trs "Internal Server Error for {1} {2}: {0}" (.toString baos) method (:uri request)))
221 | (case type
222 | :json (utils/json-response InternalServiceError
223 | {:kind :application-error
224 | :msg msg})
225 | :plain (utils/plain-response InternalServiceError msg)))))
226 |
227 | (schema/defn ^:always-validate wrap-uncaught-errors :- IFn
228 | "A ring middleware that catches all otherwise uncaught errors and
229 | returns a InternalServiceError response with the error message"
230 | ([handler :- IFn]
231 | (wrap-uncaught-errors handler :json))
232 | ([handler :- IFn
233 | type :- utils/ResponseType]
234 | (fn [request]
235 | (try
236 | (handler request)
237 | (catch Throwable e
238 | (handle-error-response request e type))))))
239 |
240 | (schema/defn ^:always-validate wrap-add-referrer-policy :- IFn
241 | "Adds referrer policy to the header as 'Referrer-Policy: no-referrer' or 'Referrer-Policy: same-origin'"
242 | [policy-option :- schema/Str
243 | handler :- IFn]
244 | (fn [request]
245 | (let [response (handler request)]
246 | (when-not (nil? response)
247 | (assoc-in response [:headers "Referrer-Policy"] policy-option)))))
248 |
249 | (def superwildcard "*/*")
250 |
251 | (defn acceptable-content-type
252 | "Returns a boolean indicating whether the `candidate` mime type
253 | matches any of those listed in `header`, an Accept header."
254 | [candidate header]
255 | (if-not (string? header)
256 | true
257 | (let [[prefix] (.split ^String candidate "/")
258 | wildcard (str prefix "/*")
259 | types (->> (str/split header #",")
260 | (map #(.trim ^String %))
261 | (set))]
262 | (or (types superwildcard)
263 | (types wildcard)
264 | (types candidate)))))
265 |
266 | (defn wrap-accepts-content-type
267 | "Ring middleware that requires a request for the wrapped `handler` to accept the
268 | provided `content-type`. If the content type isn't acceptable, a 406 Not
269 | Acceptable status is returned, with a message informing the client it must
270 | accept the content type."
271 | [handler content-type]
272 | (fn [{:keys [headers] :as req}]
273 | (if (acceptable-content-type
274 | content-type
275 | (get headers "accept"))
276 | (handler req)
277 | (utils/json-response NotAcceptable
278 | {:kind "not-acceptable"
279 | :msg (trs "accept header must include {0}" content-type)}))))
280 |
281 | (def wrap-accepts-json
282 | "Ring middleware which requires a request for `handler` to accept
283 | application/json as a content type. If the request doesn't accept
284 | application/json, a 406 Not Acceptable status is returned with an error
285 | message indicating the problem."
286 | (fn [handler]
287 | (wrap-accepts-content-type handler json-encoding-type)))
288 |
289 | (defn wrap-content-type
290 | "Verification for the specified list of content-types."
291 | [handler content-types]
292 | {:pre [(coll? content-types)
293 | (every? string? content-types)]}
294 | (fn [{:keys [headers] :as req}]
295 | (if (or (= (:request-method req) :post) (= (:request-method req) :put))
296 | (let [content-type (get headers "content-type")
297 | media-type (when-not (nil? content-type)
298 | (ks/base-type content-type))]
299 | (if (or (nil? media-type) (some #{media-type} content-types))
300 | (handler req)
301 | (utils/json-response UnsupportedMediaType
302 | {:kind "unsupported-type"
303 | :msg (trs "content-type {0} is not a supported type for request of type {1} at {2}"
304 | media-type (:request-method req) (:uri req))})))
305 | (handler req))))
306 |
307 | (def wrap-content-type-json
308 | "Ring middleware which requires a request for `handler` to accept
309 | application/json as a content-type. If the request doesn't specify
310 | a content-type of application/json a 415 Unsupported Type status is returned."
311 | (fn [handler]
312 | (wrap-content-type handler [json-encoding-type])))
313 |
314 | (def wrap-json-parse-exception-handler
315 | "Ring middleware which catches JsonParseExceptions and returns a predictable result"
316 | (fn [handler]
317 | (fn [req]
318 | (try
319 | (handler req)
320 | (catch JsonParseException e
321 | (log/debug e (trs "Failed to parse json for request of type {0} at {1}" (:request-method req) (:uri req)))
322 | (utils/json-response BadRequest
323 | {:kind :json-parse-exception
324 | :msg (.getMessage e)}))))))
325 |
--------------------------------------------------------------------------------
/test/puppetlabs/ring_middleware/core_test.clj:
--------------------------------------------------------------------------------
1 | (ns puppetlabs.ring-middleware.core-test
2 | (:require [cheshire.core :as json]
3 | [clojure.test :refer :all]
4 | [compojure.core :refer :all]
5 | [compojure.handler :as handler]
6 | [compojure.route :as route]
7 | [puppetlabs.ring-middleware.core :as core]
8 | [puppetlabs.ring-middleware.utils :as utils]
9 | [puppetlabs.ring-middleware.testutils.common :refer :all]
10 | [puppetlabs.ssl-utils.core :refer [pem->cert]]
11 | [puppetlabs.ssl-utils.simple :as ssl-simple]
12 | [puppetlabs.trapperkeeper.app :refer [get-service]]
13 | [puppetlabs.trapperkeeper.services.webserver.jetty10-service :refer :all]
14 | [puppetlabs.trapperkeeper.testutils.bootstrap :refer [with-app-with-config]]
15 | [puppetlabs.trapperkeeper.testutils.logging :as logutils]
16 | [ring.util.response :as rr]
17 | [schema.core :as schema]
18 | [slingshot.slingshot :as slingshot])
19 | (:import (com.fasterxml.jackson.core JsonFactory JsonLocation JsonParseException JsonParser)))
20 |
21 |
22 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
23 | ;;;; Testing Helpers
24 |
25 |
26 | (def WackSchema
27 | [schema/Str])
28 |
29 | (schema/defn ^:always-validate cause-schema-error
30 | [request :- WackSchema]
31 | (throw (IllegalStateException. "The test should have never gotten here...")))
32 |
33 | (defn throwing-handler
34 | [kind msg]
35 | (fn [_] (slingshot/throw+ {:kind kind :msg msg})))
36 |
37 | (defn basic-request
38 | ([] (basic-request "foo-agent" :get "https://example.com"))
39 | ([subject method uri]
40 | {:request-method method
41 | :uri uri
42 | :ssl-client-cert (:cert (ssl-simple/gen-self-signed-cert subject
43 | 1
44 | {:keylength 512}))
45 | :authorization {:certificate "foo"}}))
46 |
47 | (defn post-target-handler
48 | [req]
49 | (if (= (:request-method req) :post)
50 | {:status 200 :body (slurp (:body req))}
51 | {:status 404 :body "Z'oh"}))
52 |
53 | (defn proxy-target-handler
54 | [req]
55 | (condp = (:uri req)
56 | "/hello" {:status 302 :headers {"Location" "/hello/world"}}
57 | "/hello/" {:status 302 :headers {"Location" "/hello/world"}}
58 | "/hello/world" {:status 200 :body "Hello, World!"}
59 | "/hello/wrong-host" {:status 302 :headers {"Location" "http://localhost:4/fake"}}
60 | "/hello/fully-qualified" {:status 302 :headers {"Location" "http://localhost:9000/hello/world"}}
61 | "/hello/different-path" {:status 302 :headers {"Location" "http://localhost:9000/different/"}}
62 | {:status 404 :body "D'oh"}))
63 |
64 | (defn non-proxy-target
65 | [_]
66 | {:status 200 :body "Non-proxied path"})
67 |
68 | (def gzip-body
69 | (apply str (repeat 1000 "f")))
70 |
71 | (defn proxy-gzip-response
72 | [_]
73 | (-> gzip-body
74 | (rr/response)
75 | (rr/status 200)
76 | (rr/content-type "text/plain")
77 | (rr/charset "UTF-8")))
78 |
79 | (defn proxy-error-handler
80 | [_]
81 | {:status 404 :body "N'oh"})
82 |
83 | (defn proxy-regex-response
84 | [req]
85 | {:status 200 :body (str "Proxied to " (:uri req))})
86 |
87 | (defroutes fallthrough-routes
88 | (GET "/hello/world" [] "Hello, World! (fallthrough)")
89 | (GET "/goodbye/world" [] "Goodbye, World! (fallthrough)")
90 | (route/not-found "Not Found (fallthrough)"))
91 |
92 | (def proxy-regex-fallthrough
93 | (handler/site fallthrough-routes))
94 |
95 | (def proxy-wrapped-app
96 | (-> proxy-error-handler
97 | (core/wrap-proxy "/hello-proxy" "http://localhost:9000/hello")))
98 |
99 | (def proxy-wrapped-app-ssl
100 | (-> proxy-error-handler
101 | (core/wrap-proxy "/hello-proxy" "https://localhost:9001/hello"
102 | {:ssl-cert "./dev-resources/config/jetty/ssl/certs/localhost.pem"
103 | :ssl-key "./dev-resources/config/jetty/ssl/private_keys/localhost.pem"
104 | :ssl-ca-cert "./dev-resources/config/jetty/ssl/certs/ca.pem"})))
105 |
106 | (def proxy-wrapped-app-redirects
107 | (-> proxy-error-handler
108 | (core/wrap-proxy "/hello-proxy" "http://localhost:9000/hello"
109 | {:force-redirects true
110 | :follow-redirects true})))
111 |
112 | (def proxy-wrapped-app-regex
113 | (-> proxy-regex-fallthrough
114 | (core/wrap-proxy #"^/([^/]+/certificate.*)$" "http://localhost:9000/hello")))
115 |
116 | (def proxy-wrapped-app-regex-alt
117 | (-> proxy-regex-fallthrough
118 | (core/wrap-proxy #"/hello-proxy" "http://localhost:9000/hello")))
119 |
120 | (def proxy-wrapped-app-regex-no-prepend
121 | (-> proxy-regex-fallthrough
122 | (core/wrap-proxy #"^/([^/]+/certificate.*)$" "http://localhost:9000")))
123 |
124 | (def proxy-wrapped-app-regex-trailing-slash
125 | (-> proxy-regex-fallthrough
126 | (core/wrap-proxy #"^/([^/]+/certificate.*)$" "http://localhost:9000/")))
127 |
128 | (defmacro with-target-and-proxy-servers
129 | [{:keys [target proxy proxy-handler ring-handler endpoint target-endpoint]} & body]
130 | `(with-app-with-config proxy-target-app#
131 | [jetty10-service]
132 | {:webserver ~target}
133 | (let [target-webserver# (get-service proxy-target-app# :WebserverService)]
134 | (add-ring-handler
135 | target-webserver#
136 | ~ring-handler
137 | ~target-endpoint)
138 | (add-ring-handler
139 | target-webserver#
140 | non-proxy-target
141 | "/different")
142 | (add-ring-handler
143 | target-webserver#
144 | post-target-handler
145 | "/hello/post"))
146 | (with-app-with-config proxy-app#
147 | [jetty10-service]
148 | {:webserver ~proxy}
149 | (let [proxy-webserver# (get-service proxy-app# :WebserverService)]
150 | (add-ring-handler proxy-webserver# ~proxy-handler ~endpoint))
151 | ~@body)))
152 |
153 |
154 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
155 | ;;;; Core Helpers
156 |
157 | (deftest sanitize-client-cert-test
158 | (testing "sanitize-client-cert"
159 | (let [subject "foo-client"
160 | cert (:cert (ssl-simple/gen-self-signed-cert subject 1))
161 | request {:ssl-client-cert cert :authorization {:certificate "stuff"}}
162 | response (core/sanitize-client-cert request)]
163 | (testing "adds the CN at :ssl-client-cert-cn"
164 | (is (= subject (response :ssl-client-cert-cn))))
165 | (testing "removes :ssl-client-cert key from response"
166 | (is (nil? (response :ssl-client-cert))))
167 | (testing "remove tk-auth cert info"
168 | (is (nil? (get-in response [:authorization :certificate])))))))
169 |
170 |
171 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
172 | ;;;; Core Middleware
173 |
174 | (deftest test-proxy
175 | (let [common-ssl-config {:ssl-cert "./dev-resources/config/jetty/ssl/certs/localhost.pem"
176 | :ssl-key "./dev-resources/config/jetty/ssl/private_keys/localhost.pem"
177 | :ssl-ca-cert "./dev-resources/config/jetty/ssl/certs/ca.pem"}]
178 |
179 | (testing "basic proxy support"
180 | (with-target-and-proxy-servers
181 | {:target {:host "0.0.0.0"
182 | :port 9000}
183 | :proxy {:host "0.0.0.0"
184 | :port 10000}
185 | :proxy-handler proxy-wrapped-app
186 | :ring-handler proxy-target-handler
187 | :endpoint "/hello-proxy"
188 | :target-endpoint "/hello"}
189 | (let [response (http-get "http://localhost:9000/hello/world")]
190 | (is (= (:status response) 200))
191 | (is (= (:body response) "Hello, World!")))
192 | (let [response (http-get "http://localhost:10000/hello-proxy/world")]
193 | (is (= (:status response) 200))
194 | (is (= (:body response) "Hello, World!")))
195 | (let [response (http-get "http://localhost:10000/hello-proxy/world" {:as :stream})]
196 | (is (= (slurp (:body response)) "Hello, World!")))
197 | (let [response (http-post "http://localhost:10000/hello-proxy/post/" {:as :stream :body "I'm posted!"})]
198 | (is (= (:status response) 200))
199 | (is (= (slurp (:body response)) "I'm posted!")))))
200 |
201 | (testing "basic https proxy support"
202 | (with-target-and-proxy-servers
203 | {:target (merge common-ssl-config
204 | {:ssl-host "0.0.0.0"
205 | :ssl-port 9001})
206 | :proxy (merge common-ssl-config
207 | {:ssl-host "0.0.0.0"
208 | :ssl-port 10001})
209 | :proxy-handler proxy-wrapped-app-ssl
210 | :ring-handler proxy-target-handler
211 | :endpoint "/hello-proxy"
212 | :target-endpoint "/hello"}
213 | (let [response (http-get "https://localhost:9001/hello/world" default-options-for-https-client)]
214 | (is (= (:status response) 200))
215 | (is (= (:body response) "Hello, World!")))
216 | (let [response (http-get "https://localhost:10001/hello-proxy/world" default-options-for-https-client)]
217 | (is (= (:status response) 200))
218 | (is (= (:body response) "Hello, World!")))))
219 |
220 | (testing "basic http->https proxy support"
221 | (with-target-and-proxy-servers
222 | {:target (merge common-ssl-config
223 | {:ssl-host "0.0.0.0"
224 | :ssl-port 9001})
225 | :proxy {:host "0.0.0.0"
226 | :port 10000}
227 | :proxy-handler proxy-wrapped-app-ssl
228 | :ring-handler proxy-target-handler
229 | :endpoint "/hello-proxy"
230 | :target-endpoint "/hello"}
231 | (let [response (http-get "https://localhost:9001/hello/world" default-options-for-https-client)]
232 | (is (= (:status response) 200))
233 | (is (= (:body response) "Hello, World!")))
234 | (let [response (http-get "http://localhost:10000/hello-proxy/world")]
235 | (is (= (:status response) 200))
236 | (is (= (:body response) "Hello, World!")))))
237 |
238 | (testing "basic https->http proxy support"
239 | (with-target-and-proxy-servers
240 | {:target {:host "0.0.0.0"
241 | :port 9000}
242 | :proxy (merge common-ssl-config
243 | {:ssl-host "0.0.0.0"
244 | :ssl-port 10001})
245 | :proxy-handler proxy-wrapped-app
246 | :ring-handler proxy-target-handler
247 | :endpoint "/hello-proxy"
248 | :target-endpoint "/hello"}
249 | (let [response (http-get "http://localhost:9000/hello/world")]
250 | (is (= (:status response) 200))
251 | (is (= (:body response) "Hello, World!")))
252 | (let [response (http-get "https://localhost:10001/hello-proxy/world" default-options-for-https-client)]
253 | (is (= (:status response) 200))
254 | (is (= (:body response) "Hello, World!")))))
255 | (testing "redirect test with proxy"
256 | (with-target-and-proxy-servers
257 | {:target {:host "0.0.0.0"
258 | :port 9000}
259 | :proxy {:host "0.0.0.0"
260 | :port 10000}
261 | :proxy-handler proxy-wrapped-app
262 | :ring-handler proxy-target-handler
263 | :endpoint "/hello-proxy"
264 | :target-endpoint "/hello"}
265 | (let [response (http-get "http://localhost:9000/hello")]
266 | (is (= (:status response) 200))
267 | (is (= (:body response) "Hello, World!")))
268 | (let [response (http-get "http://localhost:9000/hello/world")]
269 | (is (= (:status response) 200))
270 | (is (= (:body response) "Hello, World!")))
271 | (let [response (http-get "http://localhost:10000/hello-proxy/"
272 | {:follow-redirects false
273 | :as :text})]
274 | (is (= (:status response) 302))
275 | (is (= "/hello/world" (get-in response [:headers "location"]))))
276 | (let [response (http-post "http://localhost:10000/hello-proxy/"
277 | {:follow-redirects false
278 | :as :text})]
279 | (is (= (:status response) 302))
280 | (is (= "/hello/world" (get-in response [:headers "location"]))))
281 | (let [response (http-get "http://localhost:10000/hello-proxy/world")]
282 | (is (= (:status response) 200))
283 | (is (= (:body response) "Hello, World!")))))
284 |
285 | (testing "proxy redirect succeeds on POST if :force-redirects set true"
286 | (with-target-and-proxy-servers
287 | {:target {:host "0.0.0.0"
288 | :port 9000}
289 | :proxy {:host "0.0.0.0"
290 | :port 10000}
291 | :proxy-handler proxy-wrapped-app-redirects
292 | :ring-handler proxy-target-handler
293 | :endpoint "/hello-proxy"
294 | :target-endpoint "/hello"}
295 | (let [response (http-get "http://localhost:10000/hello-proxy/"
296 | {:follow-redirects false
297 | :as :text})]
298 | (is (= (:status response) 200))
299 | (is (= (:body response) "Hello, World!")))
300 | (let [response (http-post "http://localhost:10000/hello-proxy/"
301 | {:follow-redirects false})]
302 | (is (= (:status response) 200)))))
303 |
304 | (testing "redirect test with fully qualified url, correct host, and proxied path"
305 | (with-target-and-proxy-servers
306 | {:target {:host "0.0.0.0"
307 | :port 9000}
308 | :proxy {:host "0.0.0.0"
309 | :port 10000}
310 | :proxy-handler proxy-wrapped-app-redirects
311 | :ring-handler proxy-target-handler
312 | :endpoint "/hello-proxy"
313 | :target-endpoint "/hello"}
314 | (let [response (http-get "http://localhost:10000/hello-proxy/fully-qualified"
315 | {:follow-redirects false
316 | :as :text})]
317 | (is (= (:status response) 200))
318 | (is (= (:body response) "Hello, World!")))))
319 |
320 | (testing "redirect test with correct host on non-proxied path"
321 | (with-target-and-proxy-servers
322 | {:target {:host "0.0.0.0"
323 | :port 9000}
324 | :proxy {:host "0.0.0.0"
325 | :port 10000}
326 | :proxy-handler proxy-wrapped-app-redirects
327 | :ring-handler proxy-target-handler
328 | :endpoint "/hello-proxy"
329 | :target-endpoint "/hello"}
330 | (let [response (http-get "http://localhost:9000/different")]
331 | (is (= (:status response) 200))
332 | (is (= (:body response) "Non-proxied path")))
333 | (let [response (http-get "http://localhost:10000/different")]
334 | (is (= (:status response) 404)))
335 | (let [response (http-get "http://localhost:10000/hello-proxy/different-path"
336 | {:follow-redirects false
337 | :as :text})]
338 | (is (= (:status response) 200))
339 | (is (= (:body response) "Non-proxied path")))))
340 |
341 | (testing "gzipped responses not truncated"
342 | (with-target-and-proxy-servers
343 | {:target {:host "0.0.0.0"
344 | :port 9000}
345 | :proxy {:host "0.0.0.0"
346 | :port 10000}
347 | :proxy-handler proxy-wrapped-app
348 | :ring-handler proxy-gzip-response
349 | :endpoint "/hello-proxy"
350 | :target-endpoint "/hello"}
351 | (let [response (http-get "http://localhost:9000/hello")]
352 | (is (= gzip-body (:body response)))
353 | (is (= "gzip" (:orig-content-encoding response))))
354 | (let [response (http-get "http://localhost:10000/hello-proxy/")]
355 | (is (= gzip-body (:body response)))
356 | (is (= "gzip" (:orig-content-encoding response))))))
357 |
358 | (testing "proxy works with regex"
359 | (with-target-and-proxy-servers
360 | {:target {:host "0.0.0.0"
361 | :port 9000}
362 | :proxy {:host "0.0.0.0"
363 | :port 10000}
364 | :proxy-handler proxy-wrapped-app-regex
365 | :ring-handler proxy-regex-response
366 | :endpoint "/"
367 | :target-endpoint "/hello"}
368 | (let [response (http-get "http://localhost:10000/production/certificate/foo")]
369 | (is (= (:status response) 200))
370 | (is (= (:body response) "Proxied to /hello/production/certificate/foo")))
371 | (let [response (http-get "http://localhost:10000/hello/world")]
372 | (is (= (:status response) 200))
373 | (is (= (:body response) "Hello, World! (fallthrough)")))
374 | (let [response (http-get "http://localhost:10000/goodbye/world")]
375 | (is (= (:status response) 200))
376 | (is (= (:body response) "Goodbye, World! (fallthrough)")))
377 | (let [response (http-get "http://localhost:10000/production/cert/foo")]
378 | (is (= (:status response) 404))
379 | (is (= (:body response) "Not Found (fallthrough)")))))
380 |
381 | (testing "proxy regex matches beginning of string"
382 | (with-target-and-proxy-servers
383 | {:target {:host "0.0.0.0"
384 | :port 9000}
385 | :proxy {:host "0.0.0.0"
386 | :port 10000}
387 | :proxy-handler proxy-wrapped-app-regex-alt
388 | :ring-handler proxy-regex-response
389 | :endpoint "/"
390 | :target-endpoint "/hello"}
391 | (let [response (http-get "http://localhost:10000/hello-proxy")]
392 | (is (= (:status response) 200))
393 | (is (= (:body response) "Proxied to /hello/hello-proxy")))
394 | (let [response (http-get "http://localhost:10000/production/hello-proxy")]
395 | (is (= (:status response) 404))
396 | (is (= (:body response) "Not Found (fallthrough)")))))
397 |
398 | (testing "proxy regex does not need to match entire request uri"
399 | (with-target-and-proxy-servers
400 | {:target {:host "0.0.0.0"
401 | :port 9000}
402 | :proxy {:host "0.0.0.0"
403 | :port 10000}
404 | :proxy-handler proxy-wrapped-app-regex-alt
405 | :ring-handler proxy-regex-response
406 | :endpoint "/"
407 | :target-endpoint "/hello"}
408 | (let [response (http-get "http://localhost:10000/hello-proxy/world")]
409 | (is (= (:status response) 200))
410 | (is (= (:body response) "Proxied to /hello/hello-proxy/world")))))
411 |
412 | (testing "proxy works with regex and no prepended path"
413 | (with-target-and-proxy-servers
414 | {:target {:host "0.0.0.0"
415 | :port 9000}
416 | :proxy {:host "0.0.0.0"
417 | :port 10000}
418 | :proxy-handler proxy-wrapped-app-regex-no-prepend
419 | :ring-handler proxy-regex-response
420 | :endpoint "/"
421 | :target-endpoint "/"}
422 | (let [response (http-get "http://localhost:10000/production/certificate/foo")]
423 | (is (= (:status response) 200))
424 | (is (= (:body response) "Proxied to /production/certificate/foo")))))
425 |
426 | (testing "no repeat slashes exist in rewritten uri"
427 | (with-target-and-proxy-servers
428 | {:target {:host "0.0.0.0"
429 | :port 9000}
430 | :proxy {:host "0.0.0.0"
431 | :port 10000}
432 | :proxy-handler proxy-wrapped-app-regex-trailing-slash
433 | :ring-handler proxy-regex-response
434 | :endpoint "/"
435 | :target-endpoint "/"}
436 | (let [response (http-get "http://localhost:10000/production/certificate/foo")]
437 | (is (= (:status response) 200))
438 | (is (= (:body response) "Proxied to /production/certificate/foo")))))))
439 |
440 | (deftest test-wrap-add-cache-headers
441 | (let [put-request {:request-method :put}
442 | get-request {:request-method :get}
443 | post-request {:request-method :post}
444 | delete-request {:request-method :delete}
445 | no-cache-header "no-store"]
446 | (testing "wrap-add-cache-headers ignores nil response"
447 | (let [handler (constantly nil)
448 | wrapped-handler (core/wrap-add-cache-headers handler)]
449 | (is (nil? (wrapped-handler put-request)))
450 | (is (nil? (wrapped-handler get-request)))
451 | (is (nil? (wrapped-handler post-request)))
452 | (is (nil? (wrapped-handler delete-request)))))
453 | (testing "wrap-add-cache-headers observes handled response"
454 | (let [handler (constantly {})
455 | wrapped-handler (core/wrap-add-cache-headers handler)
456 | handled-response {:headers {"cache-control" no-cache-header}}
457 | not-handled-response {}]
458 | (is (= handled-response (wrapped-handler get-request)))
459 | (is (= handled-response (wrapped-handler put-request)))
460 | (is (= not-handled-response (wrapped-handler post-request)))
461 | (is (= not-handled-response (wrapped-handler delete-request)))))
462 | (testing "wrap-add-cache-headers doesn't stomp on existing headers"
463 | (let [fake-response {:headers {:something "Hi mom"}}
464 | handler (constantly fake-response)
465 | wrapped-handler (core/wrap-add-cache-headers handler)
466 | handled-response {:headers {:something "Hi mom"
467 | "cache-control" no-cache-header}}
468 | not-handled-response fake-response]
469 | (is (= handled-response (wrapped-handler get-request)))
470 | (is (= handled-response (wrapped-handler put-request)))
471 | (is (= not-handled-response (wrapped-handler post-request)))
472 | (is (= not-handled-response (wrapped-handler delete-request)))))))
473 |
474 | (deftest test-wrap-add-x-content-nosniff
475 | (let [put-request {:request-method :put}
476 | get-request {:request-method :get}
477 | post-request {:request-method :post}
478 | delete-request {:request-method :delete}]
479 | (testing "wrap-add-x-content-nosniff ignores nil response"
480 | (let [handler (constantly nil)
481 | wrapped-handler (core/wrap-add-x-content-nosniff handler)]
482 | (is (nil? (wrapped-handler put-request)))
483 | (is (nil? (wrapped-handler get-request)))
484 | (is (nil? (wrapped-handler post-request)))
485 | (is (nil? (wrapped-handler delete-request)))))
486 | (testing "wrap-add-x-content-nosniff observes handled response"
487 | (let [handler (constantly {})
488 | wrapped-handler (core/wrap-add-x-content-nosniff handler)
489 | handled-response {:headers {"X-Content-Type-Options" "nosniff"}}]
490 | (is (= handled-response (wrapped-handler get-request)))
491 | (is (= handled-response (wrapped-handler put-request)))
492 | (is (= handled-response (wrapped-handler post-request)))
493 | (is (= handled-response (wrapped-handler delete-request)))))
494 | (testing "wrap-add-x-content-nosniff doesn't stomp on existing headers"
495 | (let [fake-response {:headers {:something "Hi mom"}}
496 | handler (constantly fake-response)
497 | wrapped-handler (core/wrap-add-x-content-nosniff handler)
498 | handled-response {:headers {:something "Hi mom"
499 | "X-Content-Type-Options" "nosniff"}}]
500 | (is (= handled-response (wrapped-handler get-request)))
501 | (is (= handled-response (wrapped-handler put-request)))
502 | (is (= handled-response (wrapped-handler post-request)))
503 | (is (= handled-response (wrapped-handler delete-request)))))))
504 |
505 | (deftest test-wrap-add-csp
506 | (let [put-request {:request-method :put}
507 | get-request {:request-method :get}
508 | post-request {:request-method :post}
509 | delete-request {:request-method :delete}]
510 | (testing "wrap-add-csp ignores nil response"
511 | (let [handler (constantly nil)
512 | wrapped-handler (core/wrap-add-csp handler "foo")]
513 | (is (nil? (wrapped-handler put-request)))
514 | (is (nil? (wrapped-handler get-request)))
515 | (is (nil? (wrapped-handler post-request)))
516 | (is (nil? (wrapped-handler delete-request)))))
517 | (testing "wrap-add-csp observes handled response"
518 | (let [handler (constantly {})
519 | wrapped-handler (core/wrap-add-csp handler "foo")
520 | handled-response {:headers {"Content-Security-Policy" "foo"}}]
521 | (is (= handled-response (wrapped-handler get-request)))
522 | (is (= handled-response (wrapped-handler put-request)))
523 | (is (= handled-response (wrapped-handler post-request)))
524 | (is (= handled-response (wrapped-handler delete-request)))))
525 | (testing "wrap-add-csp doesn't stomp on existing headers"
526 | (let [fake-response {:headers {:something "Hi mom"}}
527 | handler (constantly fake-response)
528 | wrapped-handler (core/wrap-add-csp handler "foo")
529 | handled-response {:headers {:something "Hi mom"
530 | "Content-Security-Policy" "foo"}}]
531 | (is (= handled-response (wrapped-handler get-request)))
532 | (is (= handled-response (wrapped-handler put-request)))
533 | (is (= handled-response (wrapped-handler post-request)))
534 | (is (= handled-response (wrapped-handler delete-request)))))
535 | (testing "wrap-add-csp takes arg for header value"
536 | (let [handler (constantly {})
537 | wrapped-handler (core/wrap-add-csp handler "bar")
538 | handled-response {:headers {"Content-Security-Policy" "bar"}}]
539 | (is (= handled-response (wrapped-handler get-request)))
540 | (is (= handled-response (wrapped-handler put-request)))
541 | (is (= handled-response (wrapped-handler post-request)))
542 | (is (= handled-response (wrapped-handler delete-request)))))))
543 |
544 | (deftest test-wrap-with-cn
545 | (testing "When extracting a CN from a cert"
546 | (testing "and there is no cert"
547 | (let [mw-fn (core/wrap-with-certificate-cn identity)
548 | post-req (mw-fn {})]
549 | (testing "ssl-client-cn is set to nil"
550 | (is (= post-req {:ssl-client-cn nil})))))
551 |
552 | (testing "and there is a cert"
553 | (let [mw-fn (core/wrap-with-certificate-cn identity)
554 | post-req (mw-fn {:ssl-client-cert (pem->cert "dev-resources/ssl/cert.pem")})]
555 | (testing "ssl-client-cn is set properly"
556 | (is (= (:ssl-client-cn post-req) "localhost")))))))
557 |
558 | (deftest test-wrap-add-x-frame-options-deny
559 | (let [get-request {:request-method :get}
560 | put-request {:request-method :put}
561 | post-request {:request-method :post}
562 | delete-request {:request-method :delete}
563 | x-frame-header "DENY"]
564 | (testing "wrap-add-x-frame-options-deny ignores nil response"
565 | (let [handler (constantly nil)
566 | wrapped-handler (core/wrap-add-x-frame-options-deny handler)]
567 | (is (nil? (wrapped-handler get-request)))
568 | (is (nil? (wrapped-handler put-request)))
569 | (is (nil? (wrapped-handler post-request)))
570 | (is (nil? (wrapped-handler delete-request)))))
571 | (testing "wrap-add-x-frame-options-deny observes handled response"
572 | (let [handler (constantly {})
573 | wrapped-handler (core/wrap-add-x-frame-options-deny handler)
574 | handled-response {:headers {"X-Frame-Options" x-frame-header}}
575 | not-handled-response {}]
576 | (is (= handled-response (wrapped-handler get-request)))
577 | (is (= handled-response (wrapped-handler put-request)))
578 | (is (= handled-response (wrapped-handler post-request)))
579 | (is (= handled-response (wrapped-handler delete-request)))))
580 | (testing "wrap-add-x-frame-options-deny doesn't stomp on existing headers"
581 | (let [fake-response {:headers {:something "Hi mom"}}
582 | handler (constantly fake-response)
583 | wrapped-handler (core/wrap-add-x-frame-options-deny handler)
584 | handled-response {:headers {:something "Hi mom"
585 | "X-Frame-Options" x-frame-header}}]
586 | (is (= handled-response (wrapped-handler get-request)))
587 | (is (= handled-response (wrapped-handler put-request)))
588 | (is (= handled-response (wrapped-handler post-request)))
589 | (is (= handled-response (wrapped-handler delete-request)))))))
590 |
591 | (deftest wrap-response-logging-test
592 | (testing "wrap-response-logging"
593 | (logutils/with-test-logging
594 | (let [stack (core/wrap-response-logging identity)
595 | response (stack (basic-request))]
596 | (is (logged? #"Computed response.*" :trace))))))
597 |
598 | (deftest wrap-request-logging-test
599 | (testing "wrap-request-logging"
600 | (logutils/with-test-logging
601 | (let [subject "foo-agent"
602 | method :get
603 | uri "https://example.com"
604 | stack (core/wrap-request-logging identity)
605 | request (basic-request subject method uri)
606 | response (stack request)]
607 | (is (logged? (format "Processing %s %s" method uri) :debug))
608 | (is (logged? #"Full request" :trace))))))
609 |
610 | (deftest wrap-data-errors-test
611 | (testing "wrap-data-errors"
612 | (testing "default behavior"
613 | (logutils/with-test-logging
614 | (let [stack (core/wrap-data-errors (throwing-handler :user-data-invalid "Error Message"))
615 | response (stack (basic-request))
616 | json-body (json/parse-string (response :body))]
617 | (is (= 400 (response :status)))
618 | (is (= "Error Message" (get json-body "msg")))
619 | (is (logged? #"Error Message" :error))
620 | (is (logged? #"Submitted data is invalid" :error)))))
621 | (doseq [error [:request-data-invalid :user-data-invalid :service-status-version-not-found]]
622 | (testing (str "handles errors of " error)
623 | (logutils/with-test-logging
624 | (let [stack (core/wrap-data-errors (throwing-handler error "Error Message"))
625 | response (stack (basic-request))
626 | json-body (json/parse-string (response :body))]
627 | (is (= 400 (response :status)))
628 | (is (= (name error) (get json-body "kind")))))))
629 | (testing "handles errors thrown by `throw-data-invalid!`"
630 | (logutils/with-test-logging
631 | (let [stack (core/wrap-data-errors (fn [_] (utils/throw-data-invalid! "Error Message")))
632 | response (stack (basic-request))
633 | json-body (json/parse-string (response :body))]
634 | (is (= 400 (response :status)))
635 | (is (= (name "data-invalid") (get json-body "kind"))))))
636 | (testing "can be plain text"
637 | (logutils/with-test-logging
638 | (let [stack (core/wrap-data-errors
639 | (throwing-handler :user-data-invalid "Error Message") :plain)
640 | response (stack (basic-request))]
641 | (is (re-matches #"text/plain.*" (get-in response [:headers "Content-Type"]))))))))
642 |
643 | (deftest wrap-bad-request-test
644 | (testing "wrap-bad-request"
645 | (testing "default behavior"
646 | (logutils/with-test-logging
647 | (let [stack (core/wrap-bad-request (fn [_] (utils/throw-bad-request! "Error Message")))
648 | response (stack (basic-request))
649 | json-body (json/parse-string (response :body))]
650 | (is (= 400 (response :status)))
651 | (is (logged? #".*Bad Request.*" :error))
652 | (is (re-matches #"Error Message.*" (get json-body "msg" "")))
653 | (is (= "bad-request" (get json-body "kind"))))))
654 | (testing "can be plain text"
655 | (logutils/with-test-logging
656 | (let [stack (core/wrap-bad-request (fn [_] (utils/throw-bad-request! "Error Message")) :plain)
657 | response (stack (basic-request))]
658 | (is (re-matches #"text/plain.*" (get-in response [:headers "Content-Type"]))))))))
659 |
660 | (deftest wrap-schema-errors-test
661 | (testing "wrap-schema-errors"
662 | (testing "default behavior"
663 | (logutils/with-test-logging
664 | (let [stack (core/wrap-schema-errors cause-schema-error)
665 | response (stack (basic-request))
666 | json-body (json/parse-string (response :body))]
667 | (is (= 500 (response :status)))
668 | (is (logged? #".*Something unexpected.*" :error))
669 | (is (re-matches #"Something unexpected.*" (get json-body "msg" "")))
670 | (is (= "application-error" (get json-body "kind"))))))
671 | (testing "can be plain text"
672 | (logutils/with-test-logging
673 | (let [stack (core/wrap-schema-errors cause-schema-error :plain)
674 | response (stack (basic-request))]
675 | (is (re-matches #"text/plain.*" (get-in response [:headers "Content-Type"]))))))))
676 |
677 | (deftest wrap-service-unavailable-test
678 | (testing "wrap-service-unavailable"
679 | (testing "default behavior"
680 | (logutils/with-test-logging
681 | (let [stack (core/wrap-service-unavailable (fn [_] (utils/throw-service-unavailable! "Test Service is DOWN!")))
682 | response (stack (basic-request))
683 | json-body (json/parse-string (response :body))]
684 | (is (= 503 (response :status)))
685 | (is (logged? #".*Service Unavailable.*" :error))
686 | (is (= "Test Service is DOWN!" (get json-body "msg")))
687 | (is (= "service-unavailable" (get json-body "kind"))))))
688 | (testing "can be plain text"
689 | (logutils/with-test-logging
690 | (let [stack (core/wrap-service-unavailable (fn [_] (utils/throw-service-unavailable! "Test Service is DOWN!")) :plain)
691 | response (stack (basic-request))]
692 | (is (re-matches #"text/plain.*" (get-in response [:headers "Content-Type"]))))))))
693 |
694 | (deftest wrap-uncaught-errors-test
695 | (testing "wrap-uncaught-errors"
696 | (testing "default behavior"
697 | (logutils/with-test-logging
698 | (let [stack (core/wrap-uncaught-errors (fn [_] (throw (IllegalStateException. "Woah..."))))
699 | response (stack (basic-request))
700 | json-body (json/parse-string (response :body))]
701 | (is (= 500 (response :status)))
702 | ;; Note the "(?s)" uses Java's DOTALL matching mode, so that a "." will match newlines
703 | ;; This with the "at puppetlabs" that should be in a stacktrace ensures we're logging
704 | ;; the stacktrace...
705 | (is (logged? #"(?s).*Internal Server Error.*at puppetlabs.*" :error))
706 | ;; ...while this ending anchor and lack of DOTALL ensures that we are not sending a
707 | ;; stacktrace (technically nothing with new lines).
708 | (is (re-matches #"Internal Server Error.*$" (get json-body "msg" ""))))))
709 | (testing "can be plain text"
710 | (logutils/with-test-logging
711 | (let [stack (core/wrap-uncaught-errors (fn [_] (throw (IllegalStateException. "Woah..."))) :plain)
712 | response (stack (basic-request))]
713 | (is (re-matches #"text/plain.*" (get-in response [:headers "Content-Type"]))))))))
714 |
715 | (deftest test-wrap-add-referrer-policy
716 | (let [get-request {:request-method :get}
717 | put-request {:request-method :put}
718 | post-request {:request-method :post}
719 | delete-request {:request-method :delete}]
720 | (testing "wrap-add-referrer-policy observes handled response"
721 | (let [handler (constantly {})
722 | wrapped-handler (core/wrap-add-referrer-policy "no-referrer" handler)
723 | handled-response {:headers {"Referrer-Policy" "no-referrer"}}]
724 | (is (= handled-response (wrapped-handler get-request)))
725 | (is (= handled-response (wrapped-handler put-request)))
726 | (is (= handled-response (wrapped-handler post-request)))
727 | (is (= handled-response (wrapped-handler delete-request)))))
728 | (testing "wrap-add-referrer-policy observes handled dynamic response"
729 | (let [handler (constantly {})
730 | wrapped-handler (core/wrap-add-referrer-policy "same-origin" handler)
731 | handled-response {:headers {"Referrer-Policy" "same-origin"}}]
732 | (is (= handled-response (wrapped-handler get-request)))
733 | (is (= handled-response (wrapped-handler put-request)))
734 | (is (= handled-response (wrapped-handler post-request)))
735 | (is (= handled-response (wrapped-handler delete-request)))))
736 | (testing "wrap-add-referrer-policy ignores nil response"
737 | (let [handler (constantly nil)
738 | wrapped-handler (core/wrap-add-referrer-policy "same-origin" handler)]
739 | (is (nil? (wrapped-handler get-request)))
740 | (is (nil? (wrapped-handler put-request)))
741 | (is (nil? (wrapped-handler post-request)))
742 | (is (nil? (wrapped-handler delete-request)))))
743 | (testing "wrap-add-referrer-policy doesn't stomp existing headers"
744 | (let [fake-response {:headers {:something "hi there"}}
745 | handler (constantly fake-response)
746 | wrapped-handler (core/wrap-add-referrer-policy "same-origin" handler)
747 | handled-response {:headers {:something "hi there"
748 | "Referrer-Policy" "same-origin"}}]
749 | (is (= handled-response (wrapped-handler get-request)))
750 | (is (= handled-response (wrapped-handler put-request)))
751 | (is (= handled-response (wrapped-handler post-request)))
752 | (is (= handled-response (wrapped-handler delete-request)))))))
753 |
754 | (deftest wrap-accepts-json-test
755 | (let [request (fn [method a] {:request-method method :headers {"accept" a}})
756 | handler (constantly nil)
757 | wrapped-handler (core/wrap-accepts-json handler)]
758 | (testing "wrap-accepts-json allows json"
759 | (let []
760 | (doseq [method [:get :put :post :delete]]
761 | (is (nil? (wrapped-handler (request method "application/json"))))
762 | (is (nil? (wrapped-handler (request method "text/html, application/json"))))
763 | (is (nil? (wrapped-handler (request method "application/*"))))
764 | (is (nil? (wrapped-handler (request method "*/*")))))))
765 | (testing "wrap-accepts-json rejects non-json"
766 | (let [rejection-expectation
767 | {:body "{\"kind\":\"not-acceptable\",\"msg\":\"accept header must include application/json\"}"
768 | :headers {"Content-Type" "application/json; charset=utf-8"}
769 | :status 406}]
770 | (doseq [method [:get :put :post :delete]]
771 | (let [result (wrapped-handler (request method "text/html"))]
772 | (is (= rejection-expectation result))))))))
773 |
774 | (deftest wrap-content-type-json-test
775 | (let [request (fn [method a] {:request-method method :headers {"content-type" a} :uri "http://example.com"})
776 | handler (constantly nil)
777 | wrapped-handler (core/wrap-content-type-json handler)]
778 | (testing "wrap-content-type-json allows json"
779 | (let []
780 | (doseq [method [:put :post]]
781 | (is (nil? (wrapped-handler (request method "application/json")))))
782 | ;; content-type is ignored for get and delete
783 | (doseq [method [:get :delete]]
784 | (is (nil? (wrapped-handler (request method "text/html, application/json"))))
785 | (is (nil? (wrapped-handler (request method "application/*"))))
786 | (is (nil? (wrapped-handler (request method "*/*"))))))
787 | (testing "wrap-content-type-json rejects non-json"
788 | (let [rejection-expectation
789 | (fn [method]
790 | {:body (str "{\"kind\":\"unsupported-type\",\"msg\":\"content-type text/html is not a supported type for request of type " method " at http://example.com\"}")
791 | :headers {"Content-Type" "application/json; charset=utf-8"}
792 | :status 415})]
793 | (doseq [method [:put :post]]
794 | (let [result (wrapped-handler (request method "text/html"))]
795 | (is (= (rejection-expectation method) result)))))))))
796 |
797 | (deftest wrap-json-parse-exception-handler-test
798 | (let [factory (JsonFactory.)
799 | wrapped-handler (core/wrap-json-parse-exception-handler (fn [_] (throw (JsonParseException. (.createParser factory "") "Error Message" ))))
800 | response (wrapped-handler (basic-request))]
801 | (is (= {:body "{\"kind\":\"json-parse-exception\",\"msg\":\"Error Message\\n at [Source: (String)\\\"\\\"; line: 1, column: 1]\"}"
802 | :headers {"Content-Type" "application/json; charset=utf-8"}
803 | :status 400}
804 | response))))
--------------------------------------------------------------------------------