├── ring-core ├── test │ ├── ring │ │ ├── assets │ │ │ ├── foo.html │ │ │ ├── bars │ │ │ │ ├── backlink │ │ │ │ └── foo.html │ │ │ ├── index.html │ │ │ ├── plain.txt │ │ │ ├── random.xyz │ │ │ ├── index.asp │ │ │ ├── hello world.txt │ │ │ ├── index.js │ │ │ └── test-resource.jar │ │ ├── middleware │ │ │ ├── multipart_params │ │ │ │ └── test │ │ │ │ │ ├── request_context.clj │ │ │ │ │ ├── byte_array.clj │ │ │ │ │ └── temp_file.clj │ │ │ ├── test │ │ │ │ ├── keyword_params.clj │ │ │ │ ├── head.clj │ │ │ │ ├── params.clj │ │ │ │ ├── content_type.clj │ │ │ │ ├── flash.clj │ │ │ │ ├── resource.clj │ │ │ │ ├── nested_params.clj │ │ │ │ ├── file.clj │ │ │ │ ├── file_info.clj │ │ │ │ ├── not_modified.clj │ │ │ │ ├── multipart_params.clj │ │ │ │ ├── cookies.clj │ │ │ │ └── session.clj │ │ │ └── session │ │ │ │ └── test │ │ │ │ ├── memory.clj │ │ │ │ └── cookie.clj │ │ ├── util │ │ │ └── test │ │ │ │ ├── time.clj │ │ │ │ ├── mime_type.clj │ │ │ │ ├── io.clj │ │ │ │ ├── request.clj │ │ │ │ └── response.clj │ │ └── core │ │ │ └── test │ │ │ └── protocols.clj │ └── resource.jar ├── src │ └── ring │ │ ├── util │ │ ├── test.clj │ │ ├── parsing.clj │ │ ├── time.clj │ │ ├── io.clj │ │ ├── request.clj │ │ ├── mime_type.clj │ │ └── response.clj │ │ ├── middleware │ │ ├── multipart_params │ │ │ ├── byte_array.clj │ │ │ └── temp_file.clj │ │ ├── session │ │ │ ├── memory.clj │ │ │ ├── store.clj │ │ │ └── cookie.clj │ │ ├── head.clj │ │ ├── keyword_params.clj │ │ ├── content_type.clj │ │ ├── resource.clj │ │ ├── flash.clj │ │ ├── not_modified.clj │ │ ├── file.clj │ │ ├── params.clj │ │ ├── nested_params.clj │ │ ├── file_info.clj │ │ ├── session.clj │ │ ├── multipart_params.clj │ │ └── cookies.clj │ │ └── core │ │ └── protocols.clj └── project.clj ├── .travis.yml ├── ring-jetty-adapter ├── test │ └── keystore.jks ├── project.clj └── src │ └── ring │ └── adapter │ └── jetty.clj ├── .gitignore ├── ring-devel ├── test │ └── ring │ │ ├── middleware │ │ └── test │ │ │ ├── reload.clj │ │ │ ├── stacktrace.clj │ │ │ └── lint.clj │ │ └── handler │ │ └── test │ │ └── dump.clj ├── project.clj ├── resources │ └── ring │ │ └── css │ │ ├── dump.css │ │ └── stacktrace.css └── src │ └── ring │ ├── middleware │ ├── reload.clj │ ├── stacktrace.clj │ └── lint.clj │ └── handler │ └── dump.clj ├── ring-servlet ├── project.clj ├── src │ └── ring │ │ └── util │ │ └── servlet.clj └── test │ └── ring │ └── util │ └── test │ └── servlet.clj ├── project.clj ├── LICENSE ├── CONTRIBUTORS.md ├── README.md └── SPEC /ring-core/test/ring/assets/foo.html: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /ring-core/test/ring/assets/bars/backlink: -------------------------------------------------------------------------------- 1 | ../ -------------------------------------------------------------------------------- /ring-core/test/ring/assets/bars/foo.html: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /ring-core/test/ring/assets/index.html: -------------------------------------------------------------------------------- 1 | index -------------------------------------------------------------------------------- /ring-core/test/ring/assets/plain.txt: -------------------------------------------------------------------------------- 1 | plain 2 | -------------------------------------------------------------------------------- /ring-core/test/ring/assets/random.xyz: -------------------------------------------------------------------------------- 1 | random 2 | -------------------------------------------------------------------------------- /ring-core/test/ring/assets/index.asp: -------------------------------------------------------------------------------- 1 | asp index 2 | -------------------------------------------------------------------------------- /ring-core/test/ring/assets/hello world.txt: -------------------------------------------------------------------------------- 1 | Hello World 2 | -------------------------------------------------------------------------------- /ring-core/test/ring/assets/index.js: -------------------------------------------------------------------------------- 1 | javascript index 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | script: lein sub with-profile default:+1.6:+1.7:+1.8 test 3 | -------------------------------------------------------------------------------- /ring-core/test/resource.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clojure-china/ring/HEAD/ring-core/test/resource.jar -------------------------------------------------------------------------------- /ring-jetty-adapter/test/keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clojure-china/ring/HEAD/ring-jetty-adapter/test/keystore.jks -------------------------------------------------------------------------------- /ring-core/test/ring/assets/test-resource.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clojure-china/ring/HEAD/ring-core/test/ring/assets/test-resource.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | classes 3 | ring-*.jar 4 | ring.jar 5 | ring-standalone.jar 6 | pom.xml 7 | pom.xml.asc 8 | autodoc 9 | .lein-failures 10 | codox 11 | .lein-deps-sum 12 | .lein-env 13 | target 14 | checkouts 15 | .lein-repl-history 16 | .nrepl-port 17 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/multipart_params/test/request_context.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.multipart-params.test.request-context 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.multipart-params :as mp])) 4 | 5 | (deftest test-default-content-length 6 | (is (= -1 7 | (.getContentLength (#'mp/request-context {} nil))))) 8 | -------------------------------------------------------------------------------- /ring-core/src/ring/util/test.clj: -------------------------------------------------------------------------------- 1 | (ns ring.util.test 2 | "Utilities for testing Ring components. 3 | 4 | All functions in this namespace are currently deprecated." 5 | {:deprecated "1.1"} 6 | (:require [ring.util.io :as io])) 7 | 8 | (def ^{:doc "Returns a ByteArrayInputStream for the given String. 9 | 10 | See: ring.util.io/string-input-stream." 11 | :deprecated "1.1"} 12 | string-input-stream 13 | io/string-input-stream) 14 | -------------------------------------------------------------------------------- /ring-core/test/ring/util/test/time.clj: -------------------------------------------------------------------------------- 1 | (ns ring.util.test.time 2 | (:require [clojure.test :refer :all] 3 | [ring.util.time :refer :all] 4 | [clj-time.core :refer [date-time]] 5 | [clj-time.coerce :refer [to-date]])) 6 | 7 | (deftest test-parse-date 8 | (are [x y] (= (parse-date x) (to-date y)) 9 | "Sun, 06 Nov 1994 08:49:37 GMT" (date-time 1994 11 6 8 49 37) 10 | "Sunday, 06-Nov-94 08:49:37 GMT" (date-time 1994 11 6 8 49 37) 11 | "Sun Nov 6 08:49:37 1994" (date-time 1994 11 6 8 49 37) 12 | "'Sun, 06 Nov 1994 08:49:37 GMT'" (date-time 1994 11 6 8 49 37))) 13 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/multipart_params/test/byte_array.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.multipart-params.test.byte-array 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.multipart-params.byte-array :refer :all] 4 | [ring.util.io :refer [string-input-stream]])) 5 | 6 | (deftest test-byte-array-store 7 | (let [store (byte-array-store) 8 | result (store 9 | {:filename "foo.txt" 10 | :content-type "text/plain" 11 | :stream (string-input-stream "foo")})] 12 | (is (= (:filename result) "foo.txt")) 13 | (is (= (:content-type result) "text/plain")) 14 | (is (= (String. (:bytes result)) "foo")))) 15 | -------------------------------------------------------------------------------- /ring-devel/test/ring/middleware/test/reload.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.test.reload 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.reload :refer :all])) 4 | 5 | (deftest wrap-reload-smoke-test 6 | (let [handler (wrap-reload identity) 7 | request {:http-method :get, :uri "/"}] 8 | (is (= (handler request) request)))) 9 | 10 | (deftest wrap-reload-cps-test 11 | (let [handler (wrap-reload (fn [req respond _] (respond req))) 12 | request {:http-method :get, :uri "/"} 13 | response (promise) 14 | exception (promise)] 15 | (handler request response exception) 16 | (is (= request @response)) 17 | (is (not (realized? exception))))) 18 | -------------------------------------------------------------------------------- /ring-core/test/ring/util/test/mime_type.clj: -------------------------------------------------------------------------------- 1 | (ns ring.util.test.mime-type 2 | (:require [clojure.test :refer :all] 3 | [ring.util.mime-type :refer :all])) 4 | 5 | (deftest ext-mime-type-test 6 | (testing "default mime types" 7 | (are [f m] (= (ext-mime-type f) m) 8 | "foo.txt" "text/plain" 9 | "foo.html" "text/html" 10 | "foo.png" "image/png")) 11 | (testing "custom mime types" 12 | (is (= (ext-mime-type "foo.bar" {"bar" "application/bar"}) 13 | "application/bar")) 14 | (is (= (ext-mime-type "foo.txt" {"txt" "application/text"}) 15 | "application/text"))) 16 | (testing "case insensitivity" 17 | (is (= (ext-mime-type "FOO.TXT") "text/plain")))) 18 | -------------------------------------------------------------------------------- /ring-devel/project.clj: -------------------------------------------------------------------------------- 1 | (defproject ring/ring-devel "1.6.0-beta4" 2 | :description "Ring development and debugging libraries." 3 | :url "https://github.com/ring-clojure/ring" 4 | :scm {:dir ".."} 5 | :license {:name "The MIT License" 6 | :url "http://opensource.org/licenses/MIT"} 7 | :dependencies [[org.clojure/clojure "1.5.1"] 8 | [ring/ring-core "1.6.0-beta4"] 9 | [hiccup "1.0.5"] 10 | [clj-stacktrace "0.2.8"] 11 | [ns-tracker "0.3.0"]] 12 | :profiles 13 | {:1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} 14 | :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]} 15 | :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]}}) 16 | -------------------------------------------------------------------------------- /ring-servlet/project.clj: -------------------------------------------------------------------------------- 1 | (defproject ring/ring-servlet "1.6.0-beta4" 2 | :description "Ring servlet utilities." 3 | :url "https://github.com/ring-clojure/ring" 4 | :scm {:dir ".."} 5 | :license {:name "The MIT License" 6 | :url "http://opensource.org/licenses/MIT"} 7 | :dependencies [[org.clojure/clojure "1.5.1"] 8 | [ring/ring-core "1.6.0-beta4"]] 9 | :profiles 10 | {:provided {:dependencies [[javax.servlet/javax.servlet-api "3.1.0"]]} 11 | :dev {:dependencies [[javax.servlet/javax.servlet-api "3.1.0"]]} 12 | :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} 13 | :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]} 14 | :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]}}) 15 | -------------------------------------------------------------------------------- /ring-jetty-adapter/project.clj: -------------------------------------------------------------------------------- 1 | (defproject ring/ring-jetty-adapter "1.6.0-beta4" 2 | :description "Ring Jetty adapter." 3 | :url "https://github.com/ring-clojure/ring" 4 | :scm {:dir ".."} 5 | :license {:name "The MIT License" 6 | :url "http://opensource.org/licenses/MIT"} 7 | :dependencies [[org.clojure/clojure "1.5.1"] 8 | [ring/ring-core "1.6.0-beta4"] 9 | [ring/ring-servlet "1.6.0-beta4"] 10 | [org.eclipse.jetty/jetty-server "9.2.17.v20160517"]] 11 | :profiles 12 | {:dev {:dependencies [[clj-http "2.2.0"]]} 13 | :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} 14 | :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]} 15 | :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]}}) 16 | -------------------------------------------------------------------------------- /ring-core/src/ring/util/parsing.clj: -------------------------------------------------------------------------------- 1 | (ns ring.util.parsing 2 | "Regular expressions for parsing HTTP. 3 | 4 | For internal use.") 5 | 6 | (def ^{:doc "HTTP token: 1*. See RFC2068" 7 | :added "1.3"} 8 | re-token 9 | #"[!#$%&'*\-+.0-9A-Z\^_`a-z\|~]+") 10 | 11 | (def ^{:doc "HTTP quoted-string: <\"> * <\">. See RFC2068." 12 | :added "1.3"} 13 | re-quoted 14 | #"\"(\\\"|[^\"])*\"") 15 | 16 | (def ^{:doc "HTTP value: token | quoted-string. See RFC2109" 17 | :added "1.3"} 18 | re-value 19 | (str re-token "|" re-quoted)) 20 | 21 | (def ^{:doc "Pattern for pulling the charset out of the content-type header" 22 | :added "1.6"} 23 | re-charset 24 | (re-pattern (str ";(?:.*\\s)?(?i:charset)=(" re-value ")\\s*(?:;|$)"))) 25 | -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/multipart_params/byte_array.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.multipart-params.byte-array 2 | "A multipart storage engine for storing uploads as in-memory byte arrays." 3 | (:import [java.io InputStream] 4 | [org.apache.commons.io IOUtils])) 5 | 6 | (defn byte-array-store 7 | "Returns a function that stores multipart file parameters as an array of 8 | bytes. The multipart parameters will be stored as maps with the following 9 | keys: 10 | 11 | :filename - the name of the uploaded file 12 | :content-type - the content type of the uploaded file 13 | :bytes - an array of bytes containing the uploaded content" 14 | [] 15 | (fn [item] 16 | (-> (select-keys item [:filename :content-type]) 17 | (assoc :bytes (IOUtils/toByteArray ^InputStream (:stream item)))))) 18 | -------------------------------------------------------------------------------- /ring-core/test/ring/util/test/io.clj: -------------------------------------------------------------------------------- 1 | (ns ring.util.test.io 2 | (:require [clojure.test :refer :all] 3 | [clojure.java.io :as io] 4 | [ring.util.io :refer :all]) 5 | (:import [java.io IOException])) 6 | 7 | (deftest test-piped-input-stream 8 | (let [stream (piped-input-stream #(spit % "Hello World"))] 9 | (is (= (slurp stream) "Hello World")))) 10 | 11 | (deftest test-string-input-stream 12 | (let [stream (string-input-stream "Hello World")] 13 | (is (= (slurp stream) "Hello World")))) 14 | 15 | (deftest test-close! 16 | (testing "non-streams" 17 | (is (nil? (close! "foo")))) 18 | (testing "streams" 19 | (let [stream (piped-input-stream #(spit % "Hello World"))] 20 | (close! stream) 21 | (is (thrown? IOException (slurp stream))) 22 | (is (nil? (close! stream)))))) 23 | -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/session/memory.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.session.memory 2 | "A session storage engine that stores session data in memory." 3 | (:require [ring.middleware.session.store :refer [SessionStore]]) 4 | (:import [java.util UUID])) 5 | 6 | (deftype MemoryStore [session-map] 7 | SessionStore 8 | (read-session [_ key] 9 | (@session-map key)) 10 | (write-session [_ key data] 11 | (let [key (or key (str (UUID/randomUUID)))] 12 | (swap! session-map assoc key data) 13 | key)) 14 | (delete-session [_ key] 15 | (swap! session-map dissoc key) 16 | nil)) 17 | 18 | (ns-unmap *ns* '->MemoryStore) 19 | 20 | (defn memory-store 21 | "Creates an in-memory session storage engine. Accepts an atom as an optional 22 | argument; if supplied, the atom is used to hold the session data." 23 | ([] (memory-store (atom {}))) 24 | ([session-atom] (MemoryStore. session-atom))) 25 | -------------------------------------------------------------------------------- /ring-core/project.clj: -------------------------------------------------------------------------------- 1 | (defproject ring/ring-core "1.6.0-beta4" 2 | :description "Ring core libraries." 3 | :url "https://github.com/ring-clojure/ring" 4 | :scm {:dir ".."} 5 | :license {:name "The MIT License" 6 | :url "http://opensource.org/licenses/MIT"} 7 | :dependencies [[org.clojure/clojure "1.5.1"] 8 | [ring/ring-codec "1.0.1"] 9 | [commons-io "2.5"] 10 | [commons-fileupload "1.3.2"] 11 | [clj-time "0.11.0"] 12 | [crypto-random "1.2.0"] 13 | [crypto-equality "1.0.0"]] 14 | :profiles 15 | {:provided {:dependencies [[javax.servlet/servlet-api "2.5"]]} 16 | :dev {:dependencies [[javax.servlet/servlet-api "2.5"]]} 17 | :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} 18 | :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]} 19 | :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]}}) 20 | -------------------------------------------------------------------------------- /ring-devel/test/ring/handler/test/dump.clj: -------------------------------------------------------------------------------- 1 | (ns ring.handler.test.dump 2 | (:require [clojure.test :refer :all] 3 | [ring.handler.dump :refer :all] 4 | [ring.util.io :refer [string-input-stream]])) 5 | 6 | (def post-req 7 | {:uri "/foo/bar" 8 | :request-method :post 9 | :body (string-input-stream "post body")}) 10 | 11 | (def get-req 12 | {:uri "/foo/bar" 13 | :request-method :get}) 14 | 15 | (deftest test-handle-dump 16 | (binding [*out* (java.io.StringWriter.)] 17 | (let [{:keys [status]} (handle-dump post-req)] 18 | (is (= 200 status))) 19 | (let [{:keys [status]} (handle-dump get-req)] 20 | (is (= 200 status))))) 21 | 22 | (deftest handle-dump-cps-test 23 | (binding [*out* (java.io.StringWriter.)] 24 | (let [response (promise) 25 | exception (promise)] 26 | (handle-dump post-req response exception) 27 | (is (= 200 (:status @response))) 28 | (is (not (realized? exception)))))) 29 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject ring "1.6.0-beta4" 2 | :description "A Clojure web applications library." 3 | :url "https://github.com/ring-clojure/ring" 4 | :license {:name "The MIT License" 5 | :url "http://opensource.org/licenses/MIT"} 6 | :dependencies [[org.clojure/clojure "1.5.1"] 7 | [ring/ring-core "1.6.0-beta4"] 8 | [ring/ring-devel "1.6.0-beta4"] 9 | [ring/ring-jetty-adapter "1.6.0-beta4"] 10 | [ring/ring-servlet "1.6.0-beta4"]] 11 | :plugins [[lein-sub "0.2.4"] 12 | [lein-codox "0.9.5"]] 13 | :sub ["ring-core" 14 | "ring-devel" 15 | "ring-jetty-adapter" 16 | "ring-servlet"] 17 | :codox {:output-path "codox" 18 | :source-uri "http://github.com/ring-clojure/ring/blob/{version}/{filepath}#L{line}" 19 | :source-paths ["ring-core/src" 20 | "ring-devel/src" 21 | "ring-jetty-adapter/src" 22 | "ring-servlet/src"]}) 23 | -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/session/store.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.session.store 2 | "Contains the protocol used to define all Ring session storage engines.") 3 | 4 | (defprotocol SessionStore 5 | "An interface to a session storage engine. Implementing this protocol allows 6 | Ring session data to be stored in different places. 7 | 8 | Session keys are exposed to end users via a cookie, and therefore must be 9 | unguessable. A random UUID is a good choice for a session key. 10 | 11 | Session stores should come with a mechanism for expiring old session data." 12 | (read-session [store key] 13 | "Read a session map from the store. If the key is not found, nil 14 | is returned.") 15 | (write-session [store key data] 16 | "Write a session map to the store. Returns the (possibly changed) key under 17 | which the data was stored. If the key is nil, the session is considered 18 | to be new, and a fresh key should be generated.") 19 | (delete-session [store key] 20 | "Delete a session map from the store, and returns the session key. If the 21 | returned key is nil, the session cookie will be removed.")) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2010 Mark McGranaghan 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/head.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.head 2 | "Middleware to simplify replying to HEAD requests. 3 | 4 | A response to a HEAD request should be identical to a GET request, with the 5 | exception that a response to a HEAD request should have an empty body.") 6 | 7 | (defn head-request 8 | "Turns a HEAD request into a GET." 9 | {:added "1.2"} 10 | [request] 11 | (if (= :head (:request-method request)) 12 | (assoc request :request-method :get) 13 | request)) 14 | 15 | (defn head-response 16 | "Returns a nil body if original request was a HEAD." 17 | {:added "1.2"} 18 | [response request] 19 | (if (and response (= :head (:request-method request))) 20 | (assoc response :body nil) 21 | response)) 22 | 23 | (defn wrap-head 24 | "Middleware that turns any HEAD request into a GET, and then sets the response 25 | body to nil." 26 | {:added "1.1"} 27 | [handler] 28 | (fn 29 | ([request] 30 | (-> request 31 | head-request 32 | handler 33 | (head-response request))) 34 | ([request respond raise] 35 | (handler (head-request request) 36 | (fn [response] (respond (head-response response request))) 37 | raise)))) 38 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/test/keyword_params.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.test.keyword-params 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.keyword-params :refer :all])) 4 | 5 | (def wrapped-echo (wrap-keyword-params identity)) 6 | 7 | (deftest test-wrap-keyword-params 8 | (are [in out] (= out (:params (wrapped-echo {:params in}))) 9 | {"foo" "bar" "biz" "bat"} 10 | {:foo "bar" :biz "bat"} 11 | {"foo" "bar" "biz" [{"bat" "one"} {"bat" "two"}]} 12 | {:foo "bar" :biz [{:bat "one"} {:bat "two"}]} 13 | {"foo" 1} 14 | {:foo 1} 15 | {"foo" 1 "1bar" 2 "baz*" 3 "quz-buz" 4 "biz.bang" 5} 16 | {:foo 1 "1bar" 2 :baz* 3 :quz-buz 4 "biz.bang" 5} 17 | {:foo "bar"} 18 | {:foo "bar"} 19 | {"foo" {:bar "baz"}} 20 | {:foo {:bar "baz"}})) 21 | 22 | (deftest wrap-keyword-params-cps-test 23 | (let [handler (wrap-keyword-params (fn [req respond _] (respond (:params req)))) 24 | response (promise) 25 | exception (promise)] 26 | (handler {:params {"foo" "bar" :baz "quz"}} response exception) 27 | (is (= {:foo "bar" :baz "quz"} @response)) 28 | (is (not (realized? exception))))) 29 | 30 | (deftest keyword-params-request-test 31 | (is (fn? keyword-params-request))) 32 | -------------------------------------------------------------------------------- /ring-core/src/ring/util/time.clj: -------------------------------------------------------------------------------- 1 | (ns ring.util.time 2 | "Functions for dealing with time and dates in HTTP requests." 3 | (:require [clojure.string :as str]) 4 | (:import [java.text ParseException SimpleDateFormat] 5 | [java.util Locale TimeZone])) 6 | 7 | (def ^:no-doc http-date-formats 8 | {:rfc1123 "EEE, dd MMM yyyy HH:mm:ss zzz" 9 | :rfc1036 "EEEE, dd-MMM-yy HH:mm:ss zzz" 10 | :asctime "EEE MMM d HH:mm:ss yyyy"}) 11 | 12 | (defn- ^SimpleDateFormat formatter [format] 13 | (doto (SimpleDateFormat. ^String (http-date-formats format) Locale/US) 14 | (.setTimeZone (TimeZone/getTimeZone "GMT")))) 15 | 16 | (defn- attempt-parse [date format] 17 | (try 18 | (.parse (formatter format) date) 19 | (catch ParseException _ nil))) 20 | 21 | (defn- trim-quotes [s] 22 | (str/replace s #"^'|'$" "")) 23 | 24 | (defn parse-date 25 | "Attempt to parse a HTTP date. Returns nil if unsuccessful." 26 | {:added "1.2"} 27 | [http-date] 28 | (->> (keys http-date-formats) 29 | (map (partial attempt-parse (trim-quotes http-date))) 30 | (remove nil?) 31 | (first))) 32 | 33 | (defn format-date 34 | "Format a date as RFC1123 format." 35 | {:added "1.2"} 36 | [^java.util.Date date] 37 | (.format (formatter :rfc1123) date)) 38 | -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/keyword_params.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.keyword-params 2 | "Middleware that converts parameter keys in the request to keywords.") 3 | 4 | (defn- keyword-syntax? [s] 5 | (re-matches #"[A-Za-z*+!_?-][A-Za-z0-9*+!_?-]*" s)) 6 | 7 | (defn- keyify-params [target] 8 | (cond 9 | (map? target) 10 | (into {} 11 | (for [[k v] target] 12 | [(if (and (string? k) (keyword-syntax? k)) 13 | (keyword k) 14 | k) 15 | (keyify-params v)])) 16 | (vector? target) 17 | (vec (map keyify-params target)) 18 | :else 19 | target)) 20 | 21 | (defn keyword-params-request 22 | "Converts string keys in :params map to keywords. See: wrap-keyword-params." 23 | {:added "1.2"} 24 | [request] 25 | (update-in request [:params] keyify-params)) 26 | 27 | (defn wrap-keyword-params 28 | "Middleware that converts the any string keys in the :params map to keywords. 29 | Only keys that can be turned into valid keywords are converted. 30 | 31 | This middleware does not alter the maps under :*-params keys. These are left 32 | as strings." 33 | [handler] 34 | (fn 35 | ([request] 36 | (handler (keyword-params-request request))) 37 | ([request respond raise] 38 | (handler (keyword-params-request request) respond raise)))) 39 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | * Mark McGranaghan 2 | * James Reeves 3 | * Christophe Grand 4 | * Richard Newman 5 | * Seth Buntin 6 | * David Santiago 7 | * Aaron Bedra 8 | * Aaron France 9 | * Aku Kotkavuo 10 | * Alexander Solovyov 11 | * Allen Rohner 12 | * Andrew Cholakian 13 | * Andy Fingerhut 14 | * Antti Rasinen 15 | * Baishampayan Ghose 16 | * Bill Caputo 17 | * Christoffer Sawicki 18 | * Colin Jones 19 | * Daniel Janus 20 | * David Powell 21 | * Gabriel Horner 22 | * Giacomo Ritucci 23 | * Günter Glück 24 | * Herwig Hochleitner 25 | * Ignacio Thayer 26 | * Iwan van der Kleijn 27 | * Jesper André Lyngesen Pedersen 28 | * Joseph Wilk 29 | * Josh Comer 30 | * Jude Chao 31 | * Juergen Hoetzel 32 | * Kevin J. Lynagh 33 | * Kurman Karabukaev 34 | * Kushal Pisavadia 35 | * Lake Denman 36 | * Gabriel Horner 37 | * Marko Kocic 38 | * Marko Topolnik 39 | * Marshall Bockrath-Vandegrift 40 | * Matt Furden 41 | * Matthew Courtney 42 | * Max Riveiro 43 | * Nahuel Greco 44 | * Nate Young 45 | * Nick Zalabak 46 | * Oliver Powell 47 | * Paul Schorfheide 48 | * Pepijn de Vos 49 | * Peter Garbers 50 | * Petr Gladkikh 51 | * Pierre-Yves Ritschard 52 | * Roman Scherer 53 | * Ryan Fowler 54 | * Sebastián Bernardo Galkin 55 | * Steven Degutis 56 | * Stuart Halloway 57 | * Tero Parviainen 58 | * Tobias Löfgren 59 | * Tom Denley 60 | * Tommi Reiman 61 | * Trent Ogren 62 | * Trevor Wennblom 63 | * Tyler Hobbs 64 | * Vadim Platonov 65 | * Yuri Niyazov 66 | -------------------------------------------------------------------------------- /ring-devel/resources/ring/css/dump.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2008, Yahoo! Inc. All rights reserved. 3 | Code licensed under the BSD License: 4 | http://developer.yahoo.net/yui/license.txt 5 | version: 2.6.0 6 | */ 7 | html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:text-top;}sub{vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;}del,ins{text-decoration:none;} 8 | 9 | h3.info { 10 | font-size: 1.6em; 11 | margin-left: 1em; 12 | padding-top: .5em; 13 | padding-bottom: .5em; 14 | } 15 | 16 | table.request { 17 | font-size: 1.1em; 18 | width: 800px; 19 | margin-left: 1em; 20 | margin-right: 1em; 21 | background: lightgrey; 22 | } 23 | 24 | table.request tr { 25 | line-height: 1.4em; 26 | } 27 | 28 | table.request td.key { 29 | padding-left: .5em; 30 | text-aligh: left; 31 | width: 150px; 32 | } 33 | 34 | table.request td.val { 35 | text-align: left; 36 | } 37 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/session/test/memory.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.session.test.memory 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.session.store :refer :all] 4 | [ring.middleware.session.memory :refer :all])) 5 | 6 | (deftest memory-session-read-not-exist 7 | (let [store (memory-store)] 8 | (is (nil? (read-session store "non-existent"))))) 9 | 10 | (deftest memory-session-create 11 | (let [store (memory-store) 12 | sess-key (write-session store nil {:foo "bar"})] 13 | (is (not (nil? sess-key))) 14 | (is (= (read-session store sess-key) 15 | {:foo "bar"})))) 16 | 17 | (deftest memory-session-update 18 | (let [store (memory-store) 19 | sess-key (write-session store nil {:foo "bar"}) 20 | sess-key* (write-session store sess-key {:bar "baz"})] 21 | (is (= sess-key sess-key*)) 22 | (is (= (read-session store sess-key) 23 | {:bar "baz"})))) 24 | 25 | (deftest memory-session-delete 26 | (let [store (memory-store) 27 | sess-key (write-session store nil {:foo "bar"})] 28 | (is (nil? (delete-session store sess-key))) 29 | (is (nil? (read-session store sess-key))))) 30 | 31 | (deftest memory-session-custom-atom 32 | (let [session (atom {}) 33 | store (memory-store session) 34 | sess-key (write-session store nil {:foo "bar"})] 35 | (is (= (@session sess-key) {:foo "bar"})) 36 | (swap! session assoc sess-key {:foo "baz"}) 37 | (is (= (read-session store sess-key) 38 | {:foo "baz"})))) 39 | -------------------------------------------------------------------------------- /ring-core/src/ring/core/protocols.clj: -------------------------------------------------------------------------------- 1 | (ns ring.core.protocols 2 | "Protocols necessary for Ring." 3 | {:added "1.6"} 4 | (:require [clojure.java.io :as io] 5 | [ring.util.response :as response])) 6 | 7 | (defprotocol ^{:added "1.6"} StreamableResponseBody 8 | "A protocol for writing data to the response body via an output stream." 9 | (write-body-to-stream [body response output-stream] 10 | "Write a value representing a response body to an output stream. The stream 11 | will be closed after the value had been written.")) 12 | 13 | (defn- response-writer [response output-stream] 14 | (if-let [charset (response/get-charset response)] 15 | (io/writer output-stream :encoding charset) 16 | (io/writer output-stream))) 17 | 18 | (extend-protocol StreamableResponseBody 19 | String 20 | (write-body-to-stream [body response output-stream] 21 | (with-open [writer (response-writer response output-stream)] 22 | (.write writer body))) 23 | clojure.lang.ISeq 24 | (write-body-to-stream [body response output-stream] 25 | (with-open [writer (response-writer response output-stream)] 26 | (doseq [chunk body] 27 | (.write writer (str chunk))))) 28 | java.io.InputStream 29 | (write-body-to-stream [body _ output-stream] 30 | (with-open [out output-stream, body body] 31 | (io/copy body out))) 32 | java.io.File 33 | (write-body-to-stream [body _ output-stream] 34 | (with-open [out output-stream] 35 | (io/copy body out))) 36 | nil 37 | (write-body-to-stream [_ _ ^java.io.OutputStream output-stream] 38 | (.close output-stream))) 39 | -------------------------------------------------------------------------------- /ring-devel/src/ring/middleware/reload.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.reload 2 | "Middleware that reloads modified namespaces on each request. 3 | 4 | This middleware should be limited to use in development environments." 5 | (:require [ns-tracker.core :refer [ns-tracker]])) 6 | 7 | (defn- reloader [dirs retry?] 8 | (let [modified-namespaces (ns-tracker dirs) 9 | load-queue (java.util.concurrent.LinkedBlockingQueue.)] 10 | (fn [] 11 | (locking load-queue 12 | (doseq [ns-sym (modified-namespaces)] 13 | (.offer load-queue ns-sym)) 14 | (loop [] 15 | (when-let [ns-sym (.peek load-queue)] 16 | (if retry? 17 | (do (require ns-sym :reload) (.remove load-queue)) 18 | (do (.remove load-queue) (require ns-sym :reload))) 19 | (recur))))))) 20 | 21 | (defn wrap-reload 22 | "Reload namespaces of modified files before the request is passed to the 23 | supplied handler. 24 | 25 | Accepts the following options: 26 | 27 | :dirs - A list of directories that contain the source files. 28 | Defaults to [\"src\"]. 29 | :reload-compile-errors? - If true, keep attempting to reload namespaces 30 | that have compile errors. Defaults to true." 31 | ([handler] 32 | (wrap-reload handler {})) 33 | ([handler options] 34 | (let [reload! (reloader (:dirs options ["src"]) 35 | (:reload-compile-errors? options true))] 36 | (fn 37 | ([request] 38 | (reload!) 39 | (handler request)) 40 | ([request respond raise] 41 | (reload!) 42 | (handler request respond raise)))))) 43 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/test/head.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.test.head 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.head :refer :all])) 4 | 5 | (defn- handler 6 | ([req] 7 | {:status 200 8 | :headers {"X-method" (name (:request-method req))} 9 | :body "Foobar"}) 10 | ([req cont raise] 11 | (cont 12 | {:status 200 13 | :headers {"X-method" (name (:request-method req))} 14 | :body "Foobar"}))) 15 | 16 | (deftest test-wrap-head 17 | (let [resp ((wrap-head handler) {:request-method :head})] 18 | (is (nil? (:body resp))) 19 | (is (= "get" (get-in resp [:headers "X-method"])))) 20 | (let [resp ((wrap-head handler) {:request-method :post})] 21 | (is (= (:body resp) "Foobar")) 22 | (is (= "post" (get-in resp [:headers "X-method"]))))) 23 | 24 | (deftest wrap-head-cps-test 25 | (testing "HEAD request" 26 | (let [response (promise) 27 | exception (promise)] 28 | ((wrap-head handler) {:request-method :head} response exception) 29 | (is (nil? (:body @response))) 30 | (is (= "get" (get-in @response [:headers "X-method"]))) 31 | (is (not (realized? exception))))) 32 | 33 | (testing "POST request" 34 | (let [response (promise) 35 | exception (promise)] 36 | ((wrap-head handler) {:request-method :post} response exception) 37 | (is (= "Foobar" (:body @response))) 38 | (is (= "post" (get-in @response [:headers "X-method"]))) 39 | (is (not (realized? exception)))))) 40 | 41 | (deftest head-request-test 42 | (is (fn? head-request))) 43 | 44 | (deftest head-response-test 45 | (is (fn? head-response)) 46 | (is (nil? (head-response nil {:request-method :head})))) 47 | -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/content_type.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.content-type 2 | "Middleware for automatically adding a content type to response maps." 3 | (:require [ring.util.mime-type :refer [ext-mime-type]] 4 | [ring.util.response :refer [content-type get-header]])) 5 | 6 | (defn content-type-response 7 | "Adds a content-type header to response. See: wrap-content-type." 8 | {:added "1.2"} 9 | ([response request] 10 | (content-type-response response request {})) 11 | ([response request options] 12 | (if response 13 | (if (get-header response "Content-Type") 14 | response 15 | (let [mime-type (ext-mime-type (:uri request) (:mime-types options))] 16 | (content-type response (or mime-type "application/octet-stream"))))))) 17 | 18 | (defn wrap-content-type 19 | "Middleware that adds a content-type header to the response if one is not 20 | set by the handler. Uses the ring.util.mime-type/ext-mime-type function to 21 | guess the content-type from the file extension in the URI. If no 22 | content-type can be found, it defaults to 'application/octet-stream'. 23 | 24 | Accepts the following options: 25 | 26 | :mime-types - a map of filename extensions to mime-types that will be 27 | used in addition to the ones defined in 28 | ring.util.mime-types/default-mime-types" 29 | ([handler] 30 | (wrap-content-type handler {})) 31 | ([handler options] 32 | (fn 33 | ([request] 34 | (-> (handler request) (content-type-response request options))) 35 | ([request respond raise] 36 | (handler request 37 | (fn [response] (respond (content-type-response response request options))) 38 | raise))))) 39 | -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/resource.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.resource 2 | "Middleware for serving static resources." 3 | (:require [ring.util.codec :as codec] 4 | [ring.util.response :as response] 5 | [ring.util.request :as request] 6 | [ring.middleware.head :as head])) 7 | 8 | (defn resource-request 9 | "If request matches a static resource, returns it in a response map. 10 | Otherwise returns nil." 11 | {:added "1.2"} 12 | ([request root-path] 13 | (resource-request request root-path {})) 14 | ([request root-path options] 15 | (if (#{:head :get} (:request-method request)) 16 | (let [path (subs (codec/url-decode (request/path-info request)) 1)] 17 | (-> (response/resource-response path {:root root-path :loader (:loader options)}) 18 | (head/head-response request)))))) 19 | 20 | (defn wrap-resource 21 | "Middleware that first checks to see whether the request map matches a static 22 | resource. If it does, the resource is returned in a response map, otherwise 23 | the request map is passed onto the handler. The root-path argument will be 24 | added to the beginning of the resource path. If the optional :loader key is 25 | provided in the option map, the resource will be resolved by the given class 26 | loader instead of the context class loader." 27 | ([handler root-path] 28 | (wrap-resource handler root-path {})) 29 | ([handler root-path options] 30 | (fn 31 | ([request] 32 | (or (resource-request request root-path options) 33 | (handler request))) 34 | ([request respond raise] 35 | (if-let [response (resource-request request root-path options)] 36 | (respond response) 37 | (handler request respond raise)))))) 38 | -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/flash.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.flash 2 | "Middleware that adds session-based flash store that persists only to the 3 | next request in the same session.") 4 | 5 | (defn flash-request 6 | "Adds :flash key to request from :_flash in session." 7 | {:added "1.2"} 8 | [request] 9 | (let [session (:session request) 10 | flash (:_flash session) 11 | session (dissoc session :_flash)] 12 | (assoc request :session session, :flash flash))) 13 | 14 | (defn flash-response 15 | "If response has a :flash key, saves it in :_flash of session for next 16 | request." 17 | {:added "1.2"} 18 | [response request] 19 | (let [{:keys [session flash]} request] 20 | (if response 21 | (let [session (if (contains? response :session) 22 | (response :session) 23 | session) 24 | session (if-let [flash (response :flash)] 25 | (assoc (response :session session) :_flash flash) 26 | session)] 27 | (if (or flash (response :flash) (contains? response :session)) 28 | (assoc response :session session) 29 | response))))) 30 | 31 | (defn wrap-flash 32 | "If a :flash key is set on the response by the handler, a :flash key with 33 | the same value will be set on the next request that shares the same session. 34 | This is useful for small messages that persist across redirects." 35 | [handler] 36 | (fn 37 | ([request] 38 | (let [request (flash-request request)] 39 | (-> (handler request) (flash-response request)))) 40 | ([request respond raise] 41 | (let [request (flash-request request)] 42 | (handler request 43 | (fn [response] (respond (flash-response response request))) 44 | raise))))) 45 | -------------------------------------------------------------------------------- /ring-core/src/ring/util/io.clj: -------------------------------------------------------------------------------- 1 | (ns ring.util.io 2 | "Utility functions for handling I/O." 3 | (:require [clojure.java.io :as io]) 4 | (:import [java.io PipedInputStream 5 | PipedOutputStream 6 | ByteArrayInputStream 7 | File 8 | Closeable 9 | IOException])) 10 | 11 | (defn piped-input-stream 12 | "Create an input stream from a function that takes an output stream as its 13 | argument. The function will be executed in a separate thread. The stream 14 | will be automatically closed after the function finishes. 15 | 16 | For example: 17 | 18 | (piped-input-stream 19 | (fn [ostream] 20 | (spit ostream \"Hello\")))" 21 | {:added "1.1"} 22 | [func] 23 | (let [input (PipedInputStream.) 24 | output (PipedOutputStream.)] 25 | (.connect input output) 26 | (future 27 | (try 28 | (func output) 29 | (finally (.close output)))) 30 | input)) 31 | 32 | (defn string-input-stream 33 | "Returns a ByteArrayInputStream for the given String." 34 | {:added "1.1"} 35 | ([^String s] 36 | (ByteArrayInputStream. (.getBytes s))) 37 | ([^String s ^String encoding] 38 | (ByteArrayInputStream. (.getBytes s encoding)))) 39 | 40 | (defn close! 41 | "Ensure a stream is closed, swallowing any exceptions." 42 | {:added "1.2"} 43 | [stream] 44 | (when (instance? java.io.Closeable stream) 45 | (try 46 | (.close ^java.io.Closeable stream) 47 | (catch IOException _ nil)))) 48 | 49 | (defn last-modified-date 50 | "Returns the last modified date for a file, rounded down to the nearest 51 | second." 52 | {:added "1.2"} 53 | [^File file] 54 | (-> (.lastModified file) 55 | (/ 1000) (long) (* 1000) 56 | (java.util.Date.))) 57 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/multipart_params/test/temp_file.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.multipart-params.test.temp-file 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.multipart-params.temp-file :refer :all] 4 | [ring.util.io :refer [string-input-stream]])) 5 | 6 | (deftest test-temp-file-store 7 | (let [store (temp-file-store) 8 | result (store 9 | {:filename "foo.txt" 10 | :content-type "text/plain" 11 | :stream (string-input-stream "foo")})] 12 | (is (= (:filename result) "foo.txt")) 13 | (is (= (:content-type result) "text/plain")) 14 | (is (= (:size result) 3)) 15 | (is (instance? java.io.File (:tempfile result))) 16 | (is (.exists (:tempfile result))) 17 | (is (= (slurp (:tempfile result)) "foo")))) 18 | 19 | (defn eventually [check n d] 20 | (loop [i n] 21 | (if (check) 22 | true 23 | (when (pos? i) 24 | (Thread/sleep d) 25 | (recur (dec i)))))) 26 | 27 | (deftest test-temp-file-expiry 28 | (let [store (temp-file-store {:expires-in 2}) 29 | result (store 30 | {:filename "foo.txt" 31 | :content-type "text/plain" 32 | :stream (string-input-stream "foo")})] 33 | (is (.exists (:tempfile result))) 34 | (Thread/sleep 2000) 35 | (let [deleted? (eventually #(not (.exists (:tempfile result))) 120 250)] 36 | (is deleted?)))) 37 | 38 | (defn all-threads [] 39 | (.keySet (Thread/getAllStackTraces))) 40 | 41 | (deftest test-temp-file-threads 42 | (let [threads0 (all-threads) 43 | store (temp-file-store) 44 | threads1 (all-threads)] 45 | (is (= (count threads0) 46 | (count threads1))) 47 | (dotimes [_ 200] 48 | (store {:filename "foo.txt" 49 | :content-type "text/plain" 50 | :stream (string-input-stream "foo")})) 51 | (is (< (count (all-threads)) 52 | 100)))) 53 | -------------------------------------------------------------------------------- /ring-devel/src/ring/handler/dump.clj: -------------------------------------------------------------------------------- 1 | (ns ring.handler.dump 2 | "A handler that displays the received request map. 3 | 4 | This is useful for debugging new adapters." 5 | (:require [clojure.set :as set] 6 | [clojure.pprint :as pprint] 7 | [clojure.java.io :as io] 8 | [hiccup.core :refer [html h]] 9 | [hiccup.page :refer [doctype]] 10 | [hiccup.def :refer [defhtml]] 11 | [ring.util.response :refer [content-type response]])) 12 | 13 | (def ^:no-doc ring-keys 14 | '(:server-port :server-name :remote-addr :uri :query-string :scheme 15 | :request-method :content-type :content-length :character-encoding 16 | :ssl-client-cert :headers :body)) 17 | 18 | (defn- style-resource [path] 19 | (html [:style {:type "text/css"} (slurp (io/resource path))])) 20 | 21 | (defn- req-pair [key req] 22 | (html 23 | [:tr 24 | [:td.key (h (str key))] 25 | [:td.val (h (pr-str (key req)))]])) 26 | 27 | (defhtml ^:no-doc template 28 | [req] 29 | (doctype :xhtml-transitional) 30 | [:html {:xmlns "http://www.w3.org/1999/xhtml"} 31 | [:head 32 | [:title "Ring: Request Dump"] 33 | (style-resource "ring/css/dump.css")] 34 | [:body 35 | [:div#content 36 | [:h3.info "Ring Request Values"] 37 | [:table.request 38 | [:tbody 39 | (for [key ring-keys] 40 | (req-pair key req))]] 41 | (if-let [user-keys (set/difference (set (keys req)) (set ring-keys))] 42 | (html 43 | [:br] 44 | [:table.request.user 45 | [:tbody [:tr 46 | (for [key (sort user-keys)] 47 | (req-pair key req))]]]))]]]) 48 | 49 | (defn handle-dump 50 | "Returns a HTML response that shows the information in the request map. 51 | Also prints the request map to STDOUT." 52 | ([request] 53 | (pprint/pprint request) 54 | (println) 55 | (-> (response (template request)) 56 | (content-type "text/html"))) 57 | ([request respond raise] 58 | (respond (handle-dump request)))) 59 | -------------------------------------------------------------------------------- /ring-devel/test/ring/middleware/test/stacktrace.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.test.stacktrace 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.stacktrace :refer :all])) 4 | 5 | (def exception-app (wrap-stacktrace (fn [_] (throw (Exception. "fail"))))) 6 | (def assert-app (wrap-stacktrace (fn [_] (assert (= 1 2))))) 7 | 8 | 9 | (def html-req {}) 10 | (def js-req {:headers {"accept" "text/javascript"}}) 11 | 12 | (deftest wrap-stacktrace-smoke 13 | (binding [*err* (java.io.StringWriter.)] 14 | (let [{:keys [status headers] :as response} (exception-app html-req)] 15 | (is (= 500 status)) 16 | (is (= {"Content-Type" "text/html"} headers))) 17 | (let [{:keys [status headers]} (exception-app js-req)] 18 | (is (= 500 status)) 19 | (is (= {"Content-Type" "text/javascript"} headers))) 20 | (let [{:keys [status headers] :as response} (assert-app html-req)] 21 | (is (= 500 status)) 22 | (is (= {"Content-Type" "text/html"} headers))) 23 | (let [{:keys [status headers]} (assert-app js-req)] 24 | (is (= 500 status)) 25 | (is (= {"Content-Type" "text/javascript"} headers))))) 26 | 27 | (deftest wrap-stacktrace-cps-test 28 | (testing "no exception" 29 | (let [handler (wrap-stacktrace (fn [_ respond _] (respond :ok))) 30 | response (promise) 31 | exception (promise)] 32 | (handler {} response exception) 33 | (is (= :ok @response)) 34 | (is (not (realized? exception))))) 35 | 36 | (testing "thrown exception" 37 | (let [handler (wrap-stacktrace (fn [_ _ _] (throw (Exception. "fail")))) 38 | response (promise) 39 | exception (promise)] 40 | (binding [*err* (java.io.StringWriter.)] 41 | (handler {} response exception)) 42 | (is (= 500 (:status @response))) 43 | (is (not (realized? exception))))) 44 | 45 | (testing "raised exception" 46 | (let [handler (wrap-stacktrace (fn [_ _ raise] (raise (Exception. "fail")))) 47 | response (promise) 48 | exception (promise)] 49 | (binding [*err* (java.io.StringWriter.)] 50 | (handler {} response exception)) 51 | (is (= 500 (:status @response))) 52 | (is (not (realized? exception)))))) 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ring 2 | 3 | [![Build Status](https://travis-ci.org/ring-clojure/ring.svg?branch=master)](https://travis-ci.org/ring-clojure/ring) 4 | 5 | Ring is a Clojure web applications library inspired by Python's WSGI 6 | and Ruby's Rack. By abstracting the details of HTTP into a simple, 7 | unified API, Ring allows web applications to be constructed of modular 8 | components that can be shared among a variety of applications, web 9 | servers, and web frameworks. 10 | 11 | The [SPEC][1] file at the root of this distribution provides a 12 | complete description of the Ring interface. 13 | 14 | [1]: https://github.com/ring-clojure/ring/blob/master/SPEC 15 | 16 | ## Upgrade Notice 17 | 18 | From version 1.2.1 onward, the ring/ring-core package no longer comes 19 | with the `javax.servlet/servlet-api` package as a dependency (see 20 | issue [#89][2]). 21 | 22 | If you are using the `ring/ring-core` namespace on its own, you may 23 | run into errors when executing tests or running alternative adapters. 24 | To resolve this, include the following dependency in your dev profile: 25 | 26 | [javax.servlet/servlet-api "2.5"] 27 | 28 | [2]: https://github.com/ring-clojure/ring/pull/89 29 | 30 | ## Libraries 31 | 32 | * ring-core - essential functions for handling parameters, cookies and more 33 | * ring-devel - functions for developing and debugging Ring applications 34 | * ring-servlet - construct Java servlets from Ring handlers 35 | * ring-jetty-adapter - a Ring adapter that uses the Jetty webserver 36 | 37 | ## Installation 38 | 39 | To include one of the above libraries, for example `ring-core`, add 40 | the following to your `:dependencies`: 41 | 42 | [ring/ring-core "1.5.0"] 43 | 44 | To include all of them: 45 | 46 | [ring "1.5.0"] 47 | 48 | ## Documentation 49 | 50 | * [Wiki](https://github.com/ring-clojure/ring/wiki) 51 | * [API docs](http://ring-clojure.github.com/ring) 52 | 53 | ## Community 54 | 55 | * [Google group](http://groups.google.com/group/ring-clojure) 56 | 57 | ## Thanks 58 | 59 | This project borrows heavily from Ruby's Rack and Python's WSGI; 60 | thanks to those communities for their work. 61 | 62 | ## License 63 | 64 | Copyright © 2009-2016 Mark McGranaghan, James Reeves & contributors. 65 | 66 | Released under the MIT license. 67 | -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/not_modified.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.not-modified 2 | "Middleware that returns a 304 Not Modified response for responses with 3 | Last-Modified headers." 4 | (:require [ring.util.time :refer [parse-date]] 5 | [ring.util.response :refer [status get-header header]] 6 | [ring.util.io :refer [close!]])) 7 | 8 | (defn- etag-match? [request response] 9 | (if-let [etag (get-header response "ETag")] 10 | (= etag (get-header request "if-none-match")))) 11 | 12 | (defn- ^java.util.Date date-header [response header] 13 | (if-let [http-date (get-header response header)] 14 | (parse-date http-date))) 15 | 16 | (defn- not-modified-since? [request response] 17 | (let [modified-date (date-header response "Last-Modified") 18 | modified-since (date-header request "if-modified-since")] 19 | (and modified-date 20 | modified-since 21 | (not (.before modified-since modified-date))))) 22 | 23 | (defn- read-request? [request] 24 | (#{:get :head} (:request-method request))) 25 | 26 | (defn- ok-response? [response] 27 | (= (:status response) 200)) 28 | 29 | (defn not-modified-response 30 | "Returns 304 or original response based on response and request. 31 | See: wrap-not-modified." 32 | {:added "1.2"} 33 | [response request] 34 | (if (and (read-request? request) 35 | (ok-response? response) 36 | (or (etag-match? request response) 37 | (not-modified-since? request response))) 38 | (do (close! (:body response)) 39 | (-> response 40 | (assoc :status 304) 41 | (header "Content-Length" 0) 42 | (assoc :body nil))) 43 | response)) 44 | 45 | (defn wrap-not-modified 46 | "Middleware that returns a 304 Not Modified from the wrapped handler if the 47 | handler response has an ETag or Last-Modified header, and the request has a 48 | If-None-Match or If-Modified-Since header that matches the response." 49 | {:added "1.2"} 50 | [handler] 51 | (fn 52 | ([request] 53 | (-> (handler request) (not-modified-response request))) 54 | ([request respond raise] 55 | (handler request 56 | (fn [response] (respond (not-modified-response response request))) 57 | raise)))) 58 | -------------------------------------------------------------------------------- /ring-core/test/ring/core/test/protocols.clj: -------------------------------------------------------------------------------- 1 | (ns ring.core.test.protocols 2 | (:require [clojure.test :refer :all] 3 | [clojure.java.io :as io] 4 | [ring.core.protocols :refer :all])) 5 | 6 | (deftest test-write-body-defaults 7 | (testing "strings" 8 | (let [output (java.io.ByteArrayOutputStream.) 9 | response {:body "Hello World"}] 10 | (write-body-to-stream (:body response) response output) 11 | (is (= "Hello World" (.toString output))))) 12 | 13 | (testing "strings with encoding" 14 | (let [output (java.io.ByteArrayOutputStream.) 15 | response {:headers {"Content-Type" "text/plain; charset=UTF-16"} 16 | :body "Hello World"}] 17 | (write-body-to-stream (:body response) response output) 18 | (is (= "Hello World" (.toString output "UTF-16"))))) 19 | 20 | (testing "seqs" 21 | (let [output (java.io.ByteArrayOutputStream.) 22 | response {:body (list "Hello" " " "World")}] 23 | (write-body-to-stream (:body response) response output) 24 | (is (= "Hello World" (.toString output))))) 25 | 26 | (testing "seqs with encoding" 27 | (let [output (java.io.ByteArrayOutputStream.) 28 | response {:headers {"Content-Type" "text/plain; charset=UTF-16"} 29 | :body (list "Hello" " " "World")}] 30 | (write-body-to-stream (:body response) response output) 31 | (is (= "Hello World" (.toString output "UTF-16"))))) 32 | 33 | (testing "input streams" 34 | (let [output (java.io.ByteArrayOutputStream.) 35 | response {:body (io/input-stream (io/resource "ring/assets/hello world.txt"))}] 36 | (write-body-to-stream (:body response) response output) 37 | (is (= "Hello World\n" (.toString output))))) 38 | 39 | (testing "files" 40 | (let [output (java.io.ByteArrayOutputStream.) 41 | response {:body (io/file "test/ring/assets/hello world.txt")}] 42 | (write-body-to-stream (:body response) response output) 43 | (is (= "Hello World\n" (.toString output))))) 44 | 45 | (testing "nil" 46 | (let [output (java.io.ByteArrayOutputStream.) 47 | response {:body nil}] 48 | (write-body-to-stream (:body response) response output) 49 | (is (= "" (.toString output)))))) 50 | -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/file.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.file 2 | "Middleware to serve files from a directory. 3 | 4 | Most of the time you should prefer ring.middleware.resource instead, as this 5 | middleware will not work with files in jar or war files." 6 | (:require [ring.util.codec :as codec] 7 | [ring.util.response :as response] 8 | [ring.util.request :as request] 9 | [ring.middleware.head :as head] 10 | [clojure.java.io :as io])) 11 | 12 | (defn- ensure-dir 13 | "Ensures that a directory exists at the given path, throwing if one does not." 14 | [dir-path] 15 | (let [dir (io/as-file dir-path)] 16 | (if-not (.exists dir) 17 | (throw (Exception. (format "Directory does not exist: %s" dir-path)))))) 18 | 19 | (defn file-request 20 | "If request matches a static file, returns it in a response. Otherwise 21 | returns nil. See: wrap-file." 22 | {:added "1.2"} 23 | ([request root-path] 24 | (file-request request root-path {})) 25 | ([request root-path options] 26 | (let [options (merge {:root (str root-path) 27 | :index-files? true 28 | :allow-symlinks? false} 29 | options)] 30 | (if (#{:get :head} (:request-method request)) 31 | (let [path (subs (codec/url-decode (request/path-info request)) 1)] 32 | (-> (response/file-response path options) 33 | (head/head-response request))))))) 34 | 35 | (defn wrap-file 36 | "Wrap an handler such that the directory at the given root-path is checked for 37 | a static file with which to respond to the request, proxying the request to 38 | the wrapped handler if such a file does not exist. 39 | 40 | Accepts the following options: 41 | 42 | :index-files? - look for index.* files in directories, defaults to true 43 | :allow-symlinks? - serve files through symbolic links, defaults to false" 44 | ([handler root-path] 45 | (wrap-file handler root-path {})) 46 | ([handler root-path options] 47 | (ensure-dir root-path) 48 | (fn 49 | ([request] 50 | (or (file-request request root-path options) (handler request))) 51 | ([request respond raise] 52 | (if-let [response (file-request request root-path options)] 53 | (respond response) 54 | (handler request respond raise)))))) 55 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/session/test/cookie.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.session.test.cookie 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.session.store :refer :all] 4 | [ring.middleware.session.cookie :as cookie :refer [cookie-store]] 5 | [ring.util.codec :as codec] 6 | [crypto.random :as random])) 7 | 8 | (deftest cookie-session-read-not-exist 9 | (let [store (cookie-store)] 10 | (is (nil? (read-session store "non-existent"))))) 11 | 12 | (deftest cookie-session-create 13 | (let [store (cookie-store) 14 | sess-key (write-session store nil {:foo "bar"})] 15 | (is (not (nil? sess-key))) 16 | (is (= (read-session store sess-key) 17 | {:foo "bar"})))) 18 | 19 | (deftest cookie-session-update 20 | (let [store (cookie-store) 21 | sess-key (write-session store nil {:foo "bar"}) 22 | sess-key* (write-session store sess-key {:bar "baz"})] 23 | (is (not (nil? sess-key*))) 24 | (is (not= sess-key sess-key*)) 25 | (is (= (read-session store sess-key*) 26 | {:bar "baz"})))) 27 | 28 | (deftest cookie-session-delete 29 | (let [store (cookie-store) 30 | sess-key (write-session store nil {:foo "bar"}) 31 | sess-key* (delete-session store sess-key)] 32 | (is (not (nil? sess-key*))) 33 | (is (not= sess-key sess-key*)) 34 | (is (= (read-session store sess-key*) 35 | {})))) 36 | 37 | (defn seal-code-injection [key code] 38 | (let [data (#'cookie/encrypt key (.getBytes (str "#=" (pr-str code))))] 39 | (str (codec/base64-encode data) "--" (#'cookie/hmac key data)))) 40 | 41 | (deftest cookie-session-code-injection 42 | (let [secret-key (random/bytes 16) 43 | store (cookie-store {:key secret-key}) 44 | session (seal-code-injection secret-key `(+ 1 1))] 45 | (is (thrown? Exception (read-session store session))))) 46 | 47 | (deftest cookie-session-keyword-injection 48 | (let [store (cookie-store) 49 | bad-data {:foo 1 (keyword "bar 3 :baz ") 2}] 50 | (is (thrown? AssertionError (write-session store nil bad-data))))) 51 | 52 | (deftest cookie-session-invalid-key 53 | (is (thrown-with-msg? 54 | AssertionError #"the secret key must be exactly 16 bytes" 55 | (cookie-store {:key (.getBytes "abcd")}))) 56 | (is (thrown-with-msg? 57 | AssertionError #"the secret key must be exactly 16 bytes" 58 | (cookie-store {:key (.getBytes "012345678901234567890")})))) 59 | -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/multipart_params/temp_file.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.multipart-params.temp-file 2 | "A multipart storage engine for storing uploads in temporary files." 3 | (:require [clojure.java.io :as io]) 4 | (:import [java.io File])) 5 | 6 | (defn- background-thread [^Runnable f] 7 | (doto (Thread. f) 8 | (.setDaemon true) 9 | (.start))) 10 | 11 | (defmacro ^{:private true} do-every [delay & body] 12 | `(background-thread 13 | #(while true 14 | (Thread/sleep (* ~delay 1000)) 15 | (try ~@body 16 | (catch Exception ex#))))) 17 | 18 | (defn- expired? [^File file expiry-time] 19 | (< (.lastModified file) 20 | (- (System/currentTimeMillis) 21 | (* expiry-time 1000)))) 22 | 23 | (defn- remove-old-files [file-set expiry-time] 24 | (doseq [^File file @file-set] 25 | (when (expired? file expiry-time) 26 | (.delete file) 27 | (swap! file-set disj file)))) 28 | 29 | (defn- ^File make-temp-file [file-set] 30 | (let [temp-file (File/createTempFile "ring-multipart-" nil)] 31 | (swap! file-set conj temp-file) 32 | temp-file)) 33 | 34 | (defn- start-clean-up [file-set expires-in] 35 | (when expires-in 36 | (do-every expires-in 37 | (remove-old-files file-set expires-in)))) 38 | 39 | (defn- ensure-shutdown-clean-up [file-set] 40 | (.addShutdownHook 41 | (Runtime/getRuntime) 42 | (Thread. 43 | #(doseq [^File file @file-set] 44 | (.delete file))))) 45 | 46 | (defn temp-file-store 47 | "Returns a function that stores multipart file parameters as temporary files. 48 | Accepts the following options: 49 | 50 | :expires-in - delete temporary files older than this many seconds 51 | (defaults to 3600 - 1 hour) 52 | 53 | The multipart parameters will be stored as maps with the following keys: 54 | 55 | :filename - the name of the uploaded file 56 | :content-type - the content type of the upload file 57 | :tempfile - a File object that points to the temporary file containing 58 | the uploaded data 59 | :size - the size in bytes of the uploaded data" 60 | {:arglists '([] [options])} 61 | ([] (temp-file-store {:expires-in 3600})) 62 | ([{:keys [expires-in]}] 63 | (let [file-set (atom #{}) 64 | clean-up (delay (start-clean-up file-set expires-in))] 65 | (ensure-shutdown-clean-up file-set) 66 | (fn [item] 67 | (force clean-up) 68 | (let [temp-file (make-temp-file file-set)] 69 | (io/copy (:stream item) temp-file) 70 | (-> (select-keys item [:filename :content-type]) 71 | (assoc :tempfile temp-file 72 | :size (.length temp-file)))))))) 73 | -------------------------------------------------------------------------------- /ring-devel/resources/ring/css/stacktrace.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2008, Yahoo! Inc. All rights reserved. 3 | Code licensed under the BSD License: 4 | http://developer.yahoo.net/yui/license.txt 5 | version: 2.6.0 6 | */ 7 | html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:text-top;}sub{vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;}del,ins{text-decoration:none;} 8 | 9 | body { 10 | font-family: sans-serif; 11 | background: #a00; 12 | padding: 1em; 13 | } 14 | 15 | #exception { 16 | background: #f2f2f2; 17 | color: #333; 18 | padding: 1em; 19 | } 20 | 21 | h1 { 22 | color: #800; 23 | font-size: 32pt; 24 | text-align: center; 25 | margin-bottom: 0.3em; 26 | } 27 | 28 | .message { 29 | font-size: 16pt; 30 | text-align: center; 31 | margin-bottom: 1em; 32 | } 33 | 34 | #causes h2 { 35 | font-size: 22pt; 36 | text-align: center; 37 | margin-bottom: 0.3em; 38 | } 39 | 40 | #causes h2 .class { 41 | color: #800; 42 | } 43 | 44 | #causes .message { 45 | font-size: 14pt; 46 | } 47 | 48 | .trace { 49 | width: 90%; 50 | margin: auto; 51 | } 52 | 53 | .trace table { 54 | width: 100%; 55 | font-size: 12pt; 56 | background: #dadada; 57 | border: 0.8em solid #dadada; 58 | margin-bottom: 1.5em; 59 | } 60 | 61 | .trace table tr.clojure { 62 | color: #222; 63 | } 64 | 65 | .trace table tr.java { 66 | color: #6a6a6a; 67 | } 68 | 69 | .trace td { 70 | padding-top: 0.4em; 71 | padding-bottom: 0.4em; 72 | } 73 | 74 | .trace td.method { 75 | padding-left: 1em; 76 | padding-right: 0.2em; 77 | text-aligh: left; 78 | } 79 | 80 | .trace td.source { 81 | padding-left: 0.2em; 82 | text-align: right; 83 | } 84 | 85 | .trace .views { 86 | width: 100%; 87 | background: #bcbcbc; 88 | padding: 0.5em 0; 89 | } 90 | 91 | .views .label, .views ul, .views li { 92 | display: inline-block; 93 | } 94 | 95 | .trace .views .label { 96 | padding: 0 1em; 97 | } 98 | 99 | .trace .views li { 100 | padding: 0 2em; 101 | cursor: pointer; 102 | } 103 | -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/params.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.params 2 | "Middleware to parse url-encoded parameters from the query string and request 3 | body." 4 | (:require [ring.util.codec :as codec] 5 | [ring.util.request :as req])) 6 | 7 | (defn- parse-params [params encoding] 8 | (let [params (codec/form-decode params encoding)] 9 | (if (map? params) params {}))) 10 | 11 | (defn assoc-query-params 12 | "Parse and assoc parameters from the query string with the request." 13 | {:added "1.3"} 14 | [request encoding] 15 | (merge-with merge request 16 | (if-let [query-string (:query-string request)] 17 | (let [params (parse-params query-string encoding)] 18 | {:query-params params, :params params}) 19 | {:query-params {}, :params {}}))) 20 | 21 | (defn assoc-form-params 22 | "Parse and assoc parameters from the request body with the request." 23 | {:added "1.2"} 24 | [request encoding] 25 | (merge-with merge request 26 | (if-let [body (and (req/urlencoded-form? request) (:body request))] 27 | (let [params (parse-params (slurp body :encoding encoding) encoding)] 28 | {:form-params params, :params params}) 29 | {:form-params {}, :params {}}))) 30 | 31 | (defn params-request 32 | "Adds parameters from the query string and the request body to the request 33 | map. See: wrap-params." 34 | {:added "1.2"} 35 | ([request] 36 | (params-request request {})) 37 | ([request options] 38 | (let [encoding (or (:encoding options) 39 | (req/character-encoding request) 40 | "UTF-8") 41 | request (if (:form-params request) 42 | request 43 | (assoc-form-params request encoding))] 44 | (if (:query-params request) 45 | request 46 | (assoc-query-params request encoding))))) 47 | 48 | (defn wrap-params 49 | "Middleware to parse urlencoded parameters from the query string and form 50 | body (if the request is a url-encoded form). Adds the following keys to 51 | the request map: 52 | 53 | :query-params - a map of parameters from the query string 54 | :form-params - a map of parameters from the body 55 | :params - a merged map of all types of parameter 56 | 57 | Accepts the following options: 58 | 59 | :encoding - encoding to use for url-decoding. If not specified, uses 60 | the request character encoding, or \"UTF-8\" if no request 61 | character encoding is set." 62 | ([handler] 63 | (wrap-params handler {})) 64 | ([handler options] 65 | (fn 66 | ([request] 67 | (handler (params-request request options))) 68 | ([request respond raise] 69 | (handler (params-request request options) respond raise))))) 70 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/test/params.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.test.params 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.params :refer :all] 4 | [ring.util.io :refer [string-input-stream]])) 5 | 6 | (def wrapped-echo (wrap-params identity)) 7 | 8 | (deftest wrap-params-query-params-only 9 | (let [req {:query-string "foo=bar&biz=bat%25"} 10 | resp (wrapped-echo req)] 11 | (is (= {"foo" "bar" "biz" "bat%"} (:query-params resp))) 12 | (is (empty? (:form-params resp))) 13 | (is (= {"foo" "bar" "biz" "bat%"} (:params resp))))) 14 | 15 | (deftest wrap-params-query-and-form-params 16 | (let [req {:query-string "foo=bar" 17 | :headers {"content-type" "application/x-www-form-urlencoded"} 18 | :body (string-input-stream "biz=bat%25")} 19 | resp (wrapped-echo req)] 20 | (is (= {"foo" "bar"} (:query-params resp))) 21 | (is (= {"biz" "bat%"} (:form-params resp))) 22 | (is (= {"foo" "bar" "biz" "bat%"} (:params resp))))) 23 | 24 | (deftest wrap-params-not-form-encoded 25 | (let [req {:headers {"content-type" "application/json"} 26 | :body (string-input-stream "{foo: \"bar\"}")} 27 | resp (wrapped-echo req)] 28 | (is (empty? (:form-params resp))) 29 | (is (empty? (:params resp))))) 30 | 31 | (deftest wrap-params-always-assocs-maps 32 | (let [req {:query-string "" 33 | :headers {"content-type" "application/x-www-form-urlencoded"} 34 | :body (string-input-stream "")} 35 | resp (wrapped-echo req)] 36 | (is (= {} (:query-params resp))) 37 | (is (= {} (:form-params resp))) 38 | (is (= {} (:params resp))))) 39 | 40 | (deftest wrap-params-encoding 41 | (let [req {:headers {"content-type" "application/x-www-form-urlencoded;charset=UTF-16"} 42 | :body (string-input-stream "hello=world" "UTF-16")} 43 | resp (wrapped-echo req)] 44 | (is (= (:params resp) {"hello" "world"})) 45 | (is (= (:form-params resp) {"hello" "world"})))) 46 | 47 | (deftest wrap-params-cps-test 48 | (let [handler (wrap-params (fn [req respond _] (respond req))) 49 | request {:query-string "foo=bar" 50 | :headers {"content-type" "application/x-www-form-urlencoded"} 51 | :body (string-input-stream "biz=bat%25")} 52 | response (promise) 53 | exception (promise)] 54 | (handler request response exception) 55 | (is (= {"foo" "bar"} (:query-params @response))) 56 | (is (= {"biz" "bat%"} (:form-params @response))) 57 | (is (= {"foo" "bar" "biz" "bat%"} (:params @response))) 58 | (is (not (realized? exception))))) 59 | 60 | (deftest params-request-test 61 | (is (fn? params-request))) 62 | 63 | (deftest assoc-form-params-test 64 | (is (fn? assoc-form-params))) 65 | -------------------------------------------------------------------------------- /ring-core/src/ring/util/request.clj: -------------------------------------------------------------------------------- 1 | (ns ring.util.request 2 | "Functions for augmenting and pulling information from request maps." 3 | (:require [ring.util.parsing :refer [re-value]])) 4 | 5 | (defn request-url 6 | "Return the full URL of the request." 7 | {:added "1.2"} 8 | [request] 9 | (str (-> request :scheme name) 10 | "://" 11 | (get-in request [:headers "host"]) 12 | (:uri request) 13 | (if-let [query (:query-string request)] 14 | (str "?" query)))) 15 | 16 | (defn content-type 17 | "Return the content-type of the request, or nil if no content-type is set." 18 | {:added "1.3"} 19 | [request] 20 | (if-let [type (get-in request [:headers "content-type"])] 21 | (second (re-find #"^(.*?)(?:;|$)" type)))) 22 | 23 | (defn content-length 24 | "Return the content-length of the request, or nil no content-length is set." 25 | {:added "1.3"} 26 | [request] 27 | (if-let [^String length (get-in request [:headers "content-length"])] 28 | (Long. length))) 29 | 30 | (def ^:private charset-pattern 31 | (re-pattern (str ";(?:.*\\s)?(?i:charset)=(" re-value ")\\s*(?:;|$)"))) 32 | 33 | (defn character-encoding 34 | "Return the character encoding for the request, or nil if it is not set." 35 | {:added "1.3"} 36 | [request] 37 | (if-let [type (get-in request [:headers "content-type"])] 38 | (second (re-find charset-pattern type)))) 39 | 40 | (defn urlencoded-form? 41 | "True if a request contains a urlencoded form in the body." 42 | {:added "1.3"} 43 | [request] 44 | (if-let [^String type (content-type request)] 45 | (.startsWith type "application/x-www-form-urlencoded"))) 46 | 47 | (defmulti body-string 48 | "Return the request body as a string." 49 | {:arglists '([request]), :added "1.2"} 50 | (comp class :body)) 51 | 52 | (defmethod body-string nil [_] nil) 53 | 54 | (defmethod body-string String [request] 55 | (:body request)) 56 | 57 | (defmethod body-string clojure.lang.ISeq [request] 58 | (apply str (:body request))) 59 | 60 | (defmethod body-string java.io.File [request] 61 | (slurp (:body request))) 62 | 63 | (defmethod body-string java.io.InputStream [request] 64 | (slurp (:body request))) 65 | 66 | (defn path-info 67 | "Returns the relative path of the request." 68 | {:added "1.2"} 69 | [request] 70 | (or (:path-info request) 71 | (:uri request))) 72 | 73 | (defn in-context? 74 | "Returns true if the URI of the request is a subpath of the supplied context." 75 | {:added "1.2"} 76 | [request context] 77 | (.startsWith ^String (:uri request) context)) 78 | 79 | (defn set-context 80 | "Associate a context and path-info with the request. The request URI must be 81 | a subpath of the supplied context." 82 | {:added "1.2"} 83 | [request ^String context] 84 | {:pre [(in-context? request context)]} 85 | (assoc request 86 | :context context 87 | :path-info (subs (:uri request) (.length context)))) 88 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/test/content_type.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.test.content-type 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.content-type :refer :all])) 4 | 5 | (deftest wrap-content-type-test 6 | (testing "response without content-type" 7 | (let [response {:headers {}} 8 | handler (wrap-content-type (constantly response))] 9 | (is (= (handler {:uri "/foo/bar.png"}) 10 | {:headers {"Content-Type" "image/png"}})) 11 | (is (= (handler {:uri "/foo/bar.txt"}) 12 | {:headers {"Content-Type" "text/plain"}})))) 13 | 14 | (testing "response with content-type" 15 | (let [response {:headers {"Content-Type" "application/x-foo"}} 16 | handler (wrap-content-type (constantly response))] 17 | (is (= (handler {:uri "/foo/bar.png"}) 18 | {:headers {"Content-Type" "application/x-foo"}})))) 19 | 20 | (testing "unknown file extension" 21 | (let [response {:headers {}} 22 | handler (wrap-content-type (constantly response))] 23 | (is (= (handler {:uri "/foo/bar.xxxaaa"}) 24 | {:headers {"Content-Type" "application/octet-stream"}})) 25 | (is (= (handler {:uri "/foo/bar"}) 26 | {:headers {"Content-Type" "application/octet-stream"}})))) 27 | 28 | (testing "response with mime-types option" 29 | (let [response {:headers {}} 30 | handler (wrap-content-type (constantly response) {:mime-types {"edn" "application/edn"}})] 31 | (is (= (handler {:uri "/all.edn"}) 32 | {:headers {"Content-Type" "application/edn"}})))) 33 | 34 | (testing "nil response" 35 | (let [handler (wrap-content-type (constantly nil))] 36 | (is (nil? (handler {:uri "/foo/bar.txt"}))))) 37 | 38 | (testing "response header case insensitivity" 39 | (let [response {:headers {"CoNteNt-typE" "application/x-overridden"}} 40 | handler (wrap-content-type (constantly response))] 41 | (is (= (handler {:uri "/foo/bar.png"}) 42 | {:headers {"CoNteNt-typE" "application/x-overridden"}}))))) 43 | 44 | (deftest wrap-content-type-cps-test 45 | (testing "response without content-type" 46 | (let [handler (wrap-content-type (fn [_ respond _] (respond {:headers {}}))) 47 | response (promise) 48 | exception (promise)] 49 | (handler {:uri "/foo/bar.png"} response exception) 50 | (is (= @response {:headers {"Content-Type" "image/png"}})) 51 | (is (not (realized? exception))))) 52 | 53 | (testing "nil response" 54 | (let [handler (wrap-content-type (fn [_ respond _] (respond nil))) 55 | response (promise) 56 | exception (promise)] 57 | (handler {:uri "/foo/bar.png"} response exception) 58 | (is (nil? @response)) 59 | (is (not (realized? exception)))))) 60 | 61 | (deftest content-type-response-test 62 | (testing "function exists" 63 | (is (fn? content-type-response))) 64 | 65 | (testing "nil response" 66 | (is (nil? (content-type-response nil {}))))) 67 | -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/nested_params.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.nested-params 2 | "Middleware to convert a single-depth map of parameters to a nested map." 3 | (:require [ring.util.codec :refer [assoc-conj]])) 4 | 5 | (defn parse-nested-keys 6 | "Parse a parameter name into a list of keys using a 'C'-like index 7 | notation. 8 | 9 | For example: 10 | 11 | \"foo[bar][][baz]\" 12 | => [\"foo\" \"bar\" \"\" \"baz\"]" 13 | [param-name] 14 | (let [[_ k ks] (re-matches #"(?s)(.*?)((?:\[.*?\])*)" (name param-name)) 15 | keys (if ks (map second (re-seq #"\[(.*?)\]" ks)))] 16 | (cons k keys))) 17 | 18 | (defn- assoc-vec [m k v] 19 | (let [m (if (contains? m k) m (assoc m k []))] 20 | (assoc-conj m k v))) 21 | 22 | (defn- assoc-nested 23 | "Similar to assoc-in, but treats values of blank keys as elements in a 24 | list." 25 | [m [k & ks] v] 26 | (if k 27 | (if ks 28 | (let [[j & js] ks] 29 | (if (= j "") 30 | (assoc-vec m k (assoc-nested {} js v)) 31 | (assoc m k (assoc-nested (get m k {}) ks v)))) 32 | (if (map? m) 33 | (assoc-conj m k v) 34 | {k v})) 35 | v)) 36 | 37 | (defn- param-pairs 38 | "Return a list of name-value pairs for a parameter map." 39 | [params] 40 | (mapcat 41 | (fn [[name value]] 42 | (if (and (sequential? value) (not (coll? (first value)))) 43 | (for [v value] [name v]) 44 | [[name value]])) 45 | params)) 46 | 47 | (defn- nest-params 48 | "Takes a flat map of parameters and turns it into a nested map of 49 | parameters, using the function parse to split the parameter names 50 | into keys." 51 | [params parse] 52 | (reduce 53 | (fn [m [k v]] 54 | (assoc-nested m (parse k) v)) 55 | {} 56 | (param-pairs params))) 57 | 58 | (defn nested-params-request 59 | "Converts a request with a flat map of parameters to a nested map. 60 | See: wrap-nested-params." 61 | {:added "1.2"} 62 | ([request] 63 | (nested-params-request request {})) 64 | ([request options] 65 | (let [parse (:key-parser options parse-nested-keys)] 66 | (update-in request [:params] nest-params parse)))) 67 | 68 | (defn wrap-nested-params 69 | "Middleware to converts a flat map of parameters into a nested map. 70 | Accepts the following options: 71 | 72 | :key-parser - the function to use to parse the parameter names into a list 73 | of keys. Keys that are empty strings are treated as elements in 74 | a vector, non-empty keys are treated as elements in a map. 75 | Defaults to the parse-nested-keys function. 76 | 77 | For example: 78 | 79 | {\"foo[bar]\" \"baz\"} 80 | => {\"foo\" {\"bar\" \"baz\"}} 81 | 82 | {\"foo[]\" \"bar\"} 83 | => {\"foo\" [\"bar\"]}" 84 | ([handler] 85 | (wrap-nested-params handler {})) 86 | ([handler options] 87 | (fn 88 | ([request] 89 | (handler (nested-params-request request options))) 90 | ([request respond raise] 91 | (handler (nested-params-request request options) respond raise))))) 92 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/test/flash.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.test.flash 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.flash :refer :all])) 4 | 5 | (deftest flash-is-added-to-session 6 | (let [message {:error "Could not save"} 7 | handler (wrap-flash (constantly {:flash message})) 8 | response (handler {:session {}})] 9 | (is (= (:session response) {:_flash message})))) 10 | 11 | (deftest flash-is-retrieved-from-session 12 | (let [message {:error "Could not save"} 13 | handler (wrap-flash 14 | (fn [request] 15 | (is (= (:flash request) message)) 16 | {}))] 17 | (handler {:session {:_flash message}}))) 18 | 19 | (deftest flash-is-removed-after-read 20 | (let [message {:error "Could not save"} 21 | handler (wrap-flash (constantly {:session {:foo "bar"}})) 22 | response (handler {:session {:_flash message}})] 23 | (is (nil? (:flash response))) 24 | (is (= (:session response) {:foo "bar"})))) 25 | 26 | (deftest flash-is-removed-after-read-not-touching-session-explicitly 27 | (let [message {:error "Could not save"} 28 | handler (wrap-flash (constantly {:status 200})) 29 | response (handler {:session {:_flash message :foo "bar"}})] 30 | (is (nil? (:flash response))) 31 | (is (= (:session response) {:foo "bar"})))) 32 | 33 | (deftest flash-doesnt-wipe-session 34 | (let [message {:error "Could not save"} 35 | handler (wrap-flash (constantly {:flash message})) 36 | response (handler {:session {:foo "bar"}})] 37 | (is (= (:session response) {:foo "bar", :_flash message})))) 38 | 39 | (deftest flash-overwrites-nil-session 40 | (let [message {:error "Could not save"} 41 | handler (wrap-flash (constantly {:flash message, :session nil})) 42 | response (handler {:session {:foo "bar"}})] 43 | (is (= (:session response) {:_flash message})))) 44 | 45 | (deftest flash-not-except-on-nil-response 46 | (let [handler (wrap-flash (constantly nil))] 47 | (is (nil? (handler {}))))) 48 | 49 | (deftest wrap-flash-cps-test 50 | (testing "flash added to session" 51 | (let [message {:error "Could not save"} 52 | handler (wrap-flash (fn [_ respond _] (respond {:flash message}))) 53 | response (promise) 54 | exception (promise)] 55 | (handler {:session {}} response exception) 56 | (is (= (:session @response) {:_flash message})) 57 | (is (not (realized? exception))))) 58 | 59 | (testing "flash retrieved from session" 60 | (let [message {:error "Could not save"} 61 | handler (wrap-flash (fn [req respond _] (respond {:result (:flash req)}))) 62 | response (promise) 63 | exception (promise)] 64 | (handler {:session {:_flash message}} response exception) 65 | (is (= (:result @response) message)) 66 | (is (not (realized? exception)))))) 67 | 68 | (deftest flash-request-test 69 | (is (fn? flash-request))) 70 | 71 | (deftest flash-response-test 72 | (is (fn? flash-response)) 73 | (is (nil? (flash-response nil {})))) 74 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/test/resource.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.test.resource 2 | (:require [clojure.test :refer :all] 3 | [clojure.java.io :as io] 4 | [ring.middleware.resource :refer :all] 5 | [ring.util.io :refer [string-input-stream]]) 6 | (:import [java.net URL URLClassLoader])) 7 | 8 | (defn test-handler [request] 9 | {:status 200 10 | :headers {} 11 | :body (string-input-stream "handler")}) 12 | 13 | (deftest resource-test 14 | (let [handler (wrap-resource test-handler "/ring/assets")] 15 | (are [request body] (= (slurp (:body (handler request))) body) 16 | {:request-method :get, :uri "/foo.html"} "foo" 17 | {:request-method :get, :uri "/index.html"} "index" 18 | {:request-method :get, :uri "/bars/foo.html"} "foo" 19 | {:request-method :get, :uri "/handler"} "handler" 20 | {:request-method :post, :uri "/foo.html"} "handler" 21 | {:request-method :get, :uri "/pre/foo.html" 22 | :path-info "/foo.html", :context "/pre"} "foo"))) 23 | 24 | (deftest resource-loader-test 25 | (let [root-loader (->> (Thread/currentThread) 26 | .getContextClassLoader 27 | (iterate (memfn getParent)) 28 | (take-while identity) 29 | last) 30 | jarfile (io/file "test/resource.jar") 31 | urls (into-array URL [(.toURL (.toURI jarfile))]) 32 | loader (URLClassLoader. urls root-loader) 33 | no-loader (wrap-resource test-handler "/ring/assets") 34 | with-loader (wrap-resource test-handler "/ring/assets" {:loader loader})] 35 | (are [request body] (= (slurp (:body (no-loader request))) body) 36 | {:request-method :get, :uri "/foo.html"} "foo") 37 | (are [request body] (= (slurp (:body (with-loader request))) body) 38 | {:request-method :get, :uri "/foo.html"} "foo-in-jar"))) 39 | 40 | (deftest wrap-resource-cps-test 41 | (let [handler (-> (fn [req respond _] (respond (test-handler req))) 42 | (wrap-resource "/ring/assets"))] 43 | (testing "resource response" 44 | (let [response (promise) 45 | exception (promise)] 46 | (handler {:request-method :get, :uri "/foo.html"} response exception) 47 | (is (= "foo" (-> @response :body slurp))) 48 | (is (not (realized? exception))))) 49 | 50 | (testing "non-resource response" 51 | (let [response (promise) 52 | exception (promise)] 53 | (handler {:request-method :get, :uri "/handler"} response exception) 54 | (is (= "handler" (-> @response :body slurp))) 55 | (is (not (realized? exception))))))) 56 | 57 | (deftest resource-request-test 58 | (is (fn? resource-request))) 59 | 60 | (deftest test-head-request 61 | (testing "middleware" 62 | (let [handler (wrap-resource test-handler "/ring/assets") 63 | response (handler {:request-method :head, :uri "/foo.html"})] 64 | (is (= (:status response) 200)) 65 | (is (nil? (:body response))))) 66 | (testing "request fn" 67 | (let [request {:request-method :head, :uri "/foo.html"} 68 | response (resource-request request "/ring/assets")] 69 | (is (= (:status response) 200)) 70 | (is (nil? (:body response)))))) 71 | -------------------------------------------------------------------------------- /ring-core/test/ring/util/test/request.clj: -------------------------------------------------------------------------------- 1 | (ns ring.util.test.request 2 | (:require [clojure.test :refer :all] 3 | [ring.util.request :refer :all] 4 | [ring.util.io :refer [string-input-stream]]) 5 | (:import [java.io File])) 6 | 7 | (deftest test-request-url 8 | (is (= (request-url {:scheme :http 9 | :uri "/foo/bar" 10 | :headers {"host" "example.com"} 11 | :query-string "x=y"}) 12 | "http://example.com/foo/bar?x=y")) 13 | (is (= (request-url {:scheme :http 14 | :uri "/" 15 | :headers {"host" "localhost:8080"}}) 16 | "http://localhost:8080/")) 17 | (is (= (request-url {:scheme :https 18 | :uri "/index.html" 19 | :headers {"host" "www.example.com"}}) 20 | "https://www.example.com/index.html"))) 21 | 22 | (deftest test-content-type 23 | (testing "no content-type" 24 | (is (= (content-type {:headers {}}) nil))) 25 | (testing "content type with no charset" 26 | (is (= (content-type {:headers {"content-type" "text/plain"}}) "text/plain"))) 27 | (testing "content type with charset" 28 | (is (= (content-type {:headers {"content-type" "text/plain; charset=UTF-8"}}) 29 | "text/plain")))) 30 | 31 | (deftest test-content-length 32 | (testing "no content-length header" 33 | (is (= (content-length {:headers {}}) nil))) 34 | (testing "a content-length header" 35 | (is (= (content-length {:headers {"content-length" "1337"}}) 1337)))) 36 | 37 | (deftest test-character-encoding 38 | (testing "no content-type" 39 | (is (= (character-encoding {:headers {}}) nil))) 40 | (testing "content-type with no charset" 41 | (is (= (character-encoding {:headers {"content-type" "text/plain"}}) nil))) 42 | (testing "content-type with charset" 43 | (is (= (character-encoding {:headers {"content-type" "text/plain; charset=UTF-8"}}) 44 | "UTF-8")) 45 | (is (= (character-encoding {:headers {"content-type" "text/plain;charset=UTF-8"}}) 46 | "UTF-8")))) 47 | 48 | (deftest test-urlencoded-form? 49 | (testing "urlencoded form" 50 | (is (urlencoded-form? {:headers {"content-type" "application/x-www-form-urlencoded"}})) 51 | (is (urlencoded-form? 52 | {:headers {"content-type" "application/x-www-form-urlencoded; charset=UTF-8"}}))) 53 | (testing "other content type" 54 | (is (not (urlencoded-form? {:headers {"content-type" "application/json"}})))) 55 | (testing "no content type" 56 | (is (not (urlencoded-form? {:headers {}}))))) 57 | 58 | (deftest test-body-string 59 | (testing "nil body" 60 | (is (= (body-string {:body nil}) nil))) 61 | (testing "string body" 62 | (is (= (body-string {:body "foo"}) "foo"))) 63 | (testing "file body" 64 | (let [f (File/createTempFile "ring-test" "")] 65 | (spit f "bar") 66 | (is (= (body-string {:body f}) "bar")) 67 | (.delete f))) 68 | (testing "input-stream body" 69 | (is (= (body-string {:body (string-input-stream "baz")}) "baz")))) 70 | 71 | (deftest test-in-context? 72 | (is (in-context? {:uri "/foo/bar"} "/foo")) 73 | (is (not (in-context? {:uri "/foo/bar"} "/bar")))) 74 | 75 | (deftest test-set-context 76 | (is (= (set-context {:uri "/foo/bar"} "/foo") 77 | {:uri "/foo/bar" 78 | :context "/foo" 79 | :path-info "/bar"})) 80 | (is (thrown? AssertionError (set-context {:uri "/foo/bar"} "/bar")))) 81 | -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/file_info.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.file-info 2 | "Middleware to add Last-Modified and Content-Type headers to file responses. 3 | 4 | This middleware is deprecated. Prefer the ring.middleware.content-type and 5 | ring.middleware.not-modified middleware instead." 6 | (:require [ring.util.response :as res] 7 | [ring.util.mime-type :refer [ext-mime-type]] 8 | [ring.util.io :refer [last-modified-date]]) 9 | (:import [java.io File] 10 | [java.util Date Locale TimeZone] 11 | [java.text SimpleDateFormat])) 12 | 13 | (defn- guess-mime-type 14 | "Returns a String corresponding to the guessed mime type for the given file, 15 | or application/octet-stream if a type cannot be guessed." 16 | [^File file mime-types] 17 | (or (ext-mime-type (.getPath file) mime-types) 18 | "application/octet-stream")) 19 | 20 | (defn- ^SimpleDateFormat make-http-format 21 | "Formats or parses dates into HTTP date format (RFC 822/1123)." 22 | [] 23 | ;; SimpleDateFormat is not threadsafe, so return a new instance each time 24 | (doto (SimpleDateFormat. "EEE, dd MMM yyyy HH:mm:ss ZZZ" Locale/US) 25 | (.setTimeZone (TimeZone/getTimeZone "UTC")))) 26 | 27 | (defn- not-modified-since? 28 | "Has the file been modified since the last request from the client?" 29 | [{headers :headers :as req} last-modified] 30 | (if-let [modified-since (headers "if-modified-since")] 31 | (not (.before (.parse (make-http-format) modified-since) 32 | last-modified)))) 33 | 34 | (defn file-info-response 35 | "Adds headers to response as described in wrap-file-info." 36 | {:added "1.2", :deprecated "1.2"} 37 | ([response request] 38 | (file-info-response response request {})) 39 | ([response request mime-types] 40 | (let [body (:body response)] 41 | (if (instance? File body) 42 | (let [file-type (guess-mime-type body mime-types) 43 | file-length (.length ^File body) 44 | lmodified (last-modified-date body) 45 | response (-> response 46 | (res/content-type file-type) 47 | (res/header 48 | "Last-Modified" 49 | (.format (make-http-format) lmodified)))] 50 | (if (not-modified-since? request lmodified) 51 | (-> response 52 | (res/status 304) 53 | (res/header "Content-Length" 0) 54 | (assoc :body "")) 55 | (-> response (res/header "Content-Length" file-length)))) 56 | response)))) 57 | 58 | (defn wrap-file-info 59 | "Wrap a handler such that responses with a file for a body will have 60 | corresponding Content-Type, Content-Length, and Last Modified headers added if 61 | they can be determined from the file. 62 | 63 | If the request specifies a If-Modified-Since header that matches the last 64 | modification date of the file, a 304 Not Modified response is returned. 65 | If two arguments are given, the second is taken to be a map of file extensions 66 | to content types that will supplement the default, built-in map." 67 | {:deprecated "1.2"} 68 | ([handler] 69 | (wrap-file-info handler {})) 70 | ([handler mime-types] 71 | (fn 72 | ([request] 73 | (-> (handler request) 74 | (file-info-response request mime-types))) 75 | ([request respond raise] 76 | (handler request 77 | (fn [response] 78 | (respond (file-info-response response request mime-types))) 79 | raise))))) 80 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/test/nested_params.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.test.nested-params 2 | (:require [clojure.test :refer :all] 3 | [clojure.string :as string] 4 | [ring.middleware.nested-params :refer :all])) 5 | 6 | (deftest nested-params-test 7 | (let [handler (wrap-nested-params :params)] 8 | (testing "nested parameter maps" 9 | (are [p r] (= (handler {:params p}) r) 10 | {"foo" "bar"} {"foo" "bar"} 11 | {"x[y]" "z"} {"x" {"y" "z"}} 12 | {"a[b][c]" "d"} {"a" {"b" {"c" "d"}}} 13 | {"a" "b", "c" "d"} {"a" "b", "c" "d"})) 14 | (testing "nested parameter lists" 15 | (are [p r] (= (handler {:params p}) r) 16 | {"foo[]" "bar"} {"foo" ["bar"]} 17 | {"foo[]" ["bar" "baz"]} {"foo" ["bar" "baz"]}) 18 | (let [params (handler {:params {"a[x][]" ["b"], "a[x][][y]" "c"}})] 19 | (is (= (keys params) ["a"])) 20 | (is (= (keys (params "a")) ["x"])) 21 | (is (= (set (get-in params ["a" "x"])) #{"b" {"y" "c"}}))) 22 | (let [params (handler {:params {"a[][x]" "c", "a[][y]" "d"}})] 23 | (is (= (keys params) ["a"])) 24 | (is (= (set (params "a")) #{{"x" "c"} {"y" "d"}})))) 25 | (testing "duplicate parameters" 26 | (are [p r] (= (handler {:params p}) r) 27 | {"a" ["b" "c"]} {"a" ["b" "c"]} 28 | {"a[b]" ["c" "d"]} {"a" {"b" ["c" "d"]}})) 29 | (testing "parameters with newlines" 30 | (are [p r] (= (handler {:params p}) r) 31 | {"foo\nbar" "baz"} {"foo\nbar" "baz"})) 32 | (testing "empty string" 33 | (is (= {"foo" {"bar" "baz"}} 34 | (handler {:params (sorted-map "foo" "" 35 | "foo[bar]" "baz")})))) 36 | (testing "double nested empty string" 37 | (is (= {"foo" {"bar" {"baz" "bam"}}} 38 | (handler {:params (sorted-map "foo[bar]" "" 39 | "foo[bar][baz]" "bam")})))) 40 | (testing "parameters are already nested" 41 | (is (= {"foo" [["bar" "baz"] ["asdf" "zxcv"]]} 42 | (handler {:params {"foo" [["bar" "baz"] ["asdf" "zxcv"]]}})))) 43 | (testing "pre-nested vector of maps" 44 | (is (= {"foo" [{"bar" "baz"} {"asdf" "zxcv"}]} 45 | (handler {:params {"foo" [{"bar" "baz"} {"asdf" "zxcv"}]}})))) 46 | (testing "pre-nested map" 47 | (is (= {"foo" [{"bar" "baz" "asdf" "zxcv"}]} 48 | (handler {:params {"foo" [{"bar" "baz" "asdf" "zxcv"}]}})))) 49 | (testing "double-nested map" 50 | (is (= {"foo" {"key" {"bar" "baz" "asdf" "zxcv"}}} 51 | (handler {:params {"foo" {"key" {"bar" "baz" "asdf" "zxcv"}}}})))))) 52 | 53 | (deftest nested-params-test-with-options 54 | (let [handler (wrap-nested-params :params 55 | {:key-parser #(string/split % #"\.")})] 56 | (testing ":key-parser option" 57 | (are [p r] (= (handler {:params p}) r) 58 | {"foo" "bar"} {"foo" "bar"} 59 | {"x.y" "z"} {"x" {"y" "z"}} 60 | {"a.b.c" "d"} {"a" {"b" {"c" "d"}}} 61 | {"a" "b", "c" "d"} {"a" "b", "c" "d"})))) 62 | 63 | (deftest wrap-nested-params-cps-test 64 | (let [handler (wrap-nested-params (fn [req respond _] (respond (:params req)))) 65 | request {:params {"x[y]" "z"}} 66 | response (promise) 67 | exception (promise)] 68 | (handler request response exception) 69 | (is (= {"x" {"y" "z"}} @response)) 70 | (is (not (realized? exception))))) 71 | 72 | (deftest nested-params-request-test 73 | (is (fn? nested-params-request))) 74 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/test/file.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.test.file 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.file :refer :all]) 4 | (:import [java.io File])) 5 | 6 | (deftest wrap-file-no-directory 7 | (is (thrown-with-msg? Exception #".*Directory does not exist.*" 8 | (wrap-file (constantly :response) "not_here")))) 9 | 10 | (def public-dir "test/ring/assets") 11 | (def index-html (File. ^String public-dir "index.html")) 12 | (def foo-html (File. ^String public-dir "foo.html")) 13 | 14 | (def app (wrap-file (constantly :response) public-dir)) 15 | 16 | (deftest test-wrap-file-unsafe-method 17 | (is (= :response (app {:request-method :post :uri "/foo"})))) 18 | 19 | (deftest test-wrap-file-forbidden-url 20 | (is (= :response (app {:request-method :get :uri "/../foo"})))) 21 | 22 | (deftest test-wrap-file-no-file 23 | (is (= :response (app {:request-method :get :uri "/dynamic"})))) 24 | 25 | (deftest test-wrap-file-directory 26 | (let [{:keys [status headers body]} (app {:request-method :get :uri "/"})] 27 | (is (= 200 status)) 28 | (is (= (into #{} (keys headers)) #{"Content-Length" "Last-Modified"})) 29 | (is (= index-html body)))) 30 | 31 | (deftest test-wrap-file-with-extension 32 | (let [{:keys [status headers body]} (app {:request-method :get :uri "/foo.html"})] 33 | (is (= 200 status)) 34 | (is (= (into #{} (keys headers)) #{"Content-Length" "Last-Modified"})) 35 | (is (= foo-html body)))) 36 | 37 | (deftest test-wrap-file-no-index 38 | (let [app (wrap-file (constantly :response) public-dir {:index-files? false}) 39 | resp (app {:request-method :get :uri "/"})] 40 | (is (= :response resp)))) 41 | 42 | (deftest test-wrap-file-path-info 43 | (let [request {:request-method :get 44 | :uri "/bar/foo.html" 45 | :context "/bar" 46 | :path-info "/foo.html"} 47 | {:keys [status headers body]} (app request)] 48 | (is (= 200 status)) 49 | (is (= (into #{} (keys headers)) #{"Content-Length" "Last-Modified"})) 50 | (is (= foo-html body)))) 51 | 52 | (deftest wrap-file-cps-test 53 | (let [dynamic-response {:status 200, :headers {}, :body "foo"} 54 | handler (-> (fn [_ respond _] (respond dynamic-response)) 55 | (wrap-file public-dir))] 56 | (testing "file response" 57 | (let [request {:request-method :get :uri "/foo.html"} 58 | response (promise) 59 | exception (promise)] 60 | (handler request response exception) 61 | (is (= 200 (:status @response))) 62 | (is (= #{"Content-Length" "Last-Modified"} (-> @response :headers keys set))) 63 | (is (= foo-html (:body @response))) 64 | (is (not (realized? exception))))) 65 | 66 | (testing "non-file response" 67 | (let [request {:request-method :get :uri "/dynamic"} 68 | response (promise) 69 | exception (promise)] 70 | (handler request response exception) 71 | (is (= dynamic-response @response)) 72 | (is (not (realized? exception))))))) 73 | 74 | (deftest file-request-test 75 | (is (fn? file-request))) 76 | 77 | (deftest test-head-request 78 | (let [{:keys [status headers body]} (app {:request-method :head :uri "/foo.html"})] 79 | (is (= 200 status)) 80 | (is (= (into #{} (keys headers)) #{"Content-Length" "Last-Modified"})) 81 | (is (nil? body)))) 82 | 83 | (deftest test-wrap-file-with-java-io-file 84 | (let [app (wrap-file (constantly :response) (File. public-dir))] 85 | (let [{:keys [status headers body]} (app {:request-method :get :uri "/"})] 86 | (is (= 200 status)) 87 | (is (= (into #{} (keys headers)) #{"Content-Length" "Last-Modified"})) 88 | (is (= index-html body))) 89 | (let [{:keys [status headers body]} (app {:request-method :get :uri "/foo.html"})] 90 | (is (= 200 status)) 91 | (is (= (into #{} (keys headers)) #{"Content-Length" "Last-Modified"})) 92 | (is (= foo-html body))))) 93 | -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/session/cookie.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.session.cookie 2 | "A session storage engine that stores session data in encrypted cookies." 3 | (:require [ring.middleware.session.store :refer [SessionStore]] 4 | [ring.util.codec :as codec] 5 | [clojure.edn :as edn] 6 | [crypto.random :as random] 7 | [crypto.equality :as crypto]) 8 | (:import [java.security SecureRandom] 9 | [javax.crypto Cipher Mac] 10 | [javax.crypto.spec SecretKeySpec IvParameterSpec])) 11 | 12 | (def ^{:private true 13 | :doc "Algorithm to generate a HMAC."} 14 | hmac-algorithm 15 | "HmacSHA256") 16 | 17 | (def ^{:private true 18 | :doc "Type of encryption to use."} 19 | crypt-type 20 | "AES") 21 | 22 | (def ^{:private true 23 | :doc "Full algorithm to encrypt data with."} 24 | crypt-algorithm 25 | "AES/CBC/PKCS5Padding") 26 | 27 | (defn- hmac 28 | "Generates a Base64 HMAC with the supplied key on a string of data." 29 | [key data] 30 | (let [mac (Mac/getInstance hmac-algorithm)] 31 | (.init mac (SecretKeySpec. key hmac-algorithm)) 32 | (codec/base64-encode (.doFinal mac data)))) 33 | 34 | (defn- encrypt 35 | "Encrypt a string with a key." 36 | [key data] 37 | (let [cipher (Cipher/getInstance crypt-algorithm) 38 | secret-key (SecretKeySpec. key crypt-type) 39 | iv (random/bytes (.getBlockSize cipher))] 40 | (.init cipher Cipher/ENCRYPT_MODE secret-key (IvParameterSpec. iv)) 41 | (->> (.doFinal cipher data) 42 | (concat iv) 43 | (byte-array)))) 44 | 45 | (defn- decrypt 46 | "Decrypt an array of bytes with a key." 47 | [key data] 48 | (let [cipher (Cipher/getInstance crypt-algorithm) 49 | secret-key (SecretKeySpec. key crypt-type) 50 | [iv data] (split-at (.getBlockSize cipher) data) 51 | iv-spec (IvParameterSpec. (byte-array iv))] 52 | (.init cipher Cipher/DECRYPT_MODE secret-key iv-spec) 53 | (String. (.doFinal cipher (byte-array data))))) 54 | 55 | (defn- get-secret-key 56 | "Get a valid secret key from a map of options, or create a random one from 57 | scratch." 58 | [options] 59 | (if-let [secret-key (:key options)] 60 | (if (string? secret-key) 61 | (.getBytes ^String secret-key) 62 | secret-key) 63 | (random/bytes 16))) 64 | 65 | (defn- ^String serialize [x] 66 | {:post [(= x (edn/read-string %))]} 67 | (pr-str x)) 68 | 69 | (defn- seal 70 | "Seal a Clojure data structure into an encrypted and HMACed string." 71 | [key data] 72 | (let [data (encrypt key (.getBytes (serialize data)))] 73 | (str (codec/base64-encode data) "--" (hmac key data)))) 74 | 75 | (defn- unseal 76 | "Retrieve a sealed Clojure data structure from a string" 77 | [key ^String string] 78 | (let [[data mac] (.split string "--") 79 | data (codec/base64-decode data)] 80 | (if (crypto/eq? mac (hmac key data)) 81 | (edn/read-string (decrypt key data))))) 82 | 83 | (deftype CookieStore [secret-key] 84 | SessionStore 85 | (read-session [_ data] 86 | (if data (unseal secret-key data))) 87 | (write-session [_ _ data] 88 | (seal secret-key data)) 89 | (delete-session [_ _] 90 | (seal secret-key {}))) 91 | 92 | (ns-unmap *ns* '->CookieStore) 93 | 94 | (defn- valid-secret-key? [key] 95 | (and (= (type (byte-array 0)) (type key)) 96 | (= (count key) 16))) 97 | 98 | (defn cookie-store 99 | "Creates an encrypted cookie storage engine. Accepts the following options: 100 | 101 | :key - The secret key to encrypt the session cookie. Must be exactly 16 bytes 102 | If no key is provided then a random key will be generated. Note that in 103 | that case a server restart will invalidate all existing session 104 | cookies." 105 | ([] (cookie-store {})) 106 | ([options] 107 | (let [key (get-secret-key options)] 108 | (assert (valid-secret-key? key) "the secret key must be exactly 16 bytes") 109 | (CookieStore. (get-secret-key options))))) 110 | -------------------------------------------------------------------------------- /ring-core/src/ring/util/mime_type.clj: -------------------------------------------------------------------------------- 1 | (ns ring.util.mime-type 2 | "Utility functions for determining the mime-types files." 3 | (:require [clojure.string :as str])) 4 | 5 | (def ^{:doc "A map of file extensions to mime-types."} 6 | default-mime-types 7 | {"7z" "application/x-7z-compressed" 8 | "aac" "audio/aac" 9 | "ai" "application/postscript" 10 | "appcache" "text/cache-manifest" 11 | "asc" "text/plain" 12 | "atom" "application/atom+xml" 13 | "avi" "video/x-msvideo" 14 | "bin" "application/octet-stream" 15 | "bmp" "image/bmp" 16 | "bz2" "application/x-bzip" 17 | "class" "application/octet-stream" 18 | "cer" "application/pkix-cert" 19 | "crl" "application/pkix-crl" 20 | "crt" "application/x-x509-ca-cert" 21 | "css" "text/css" 22 | "csv" "text/csv" 23 | "deb" "application/x-deb" 24 | "dart" "application/dart" 25 | "dll" "application/octet-stream" 26 | "dmg" "application/octet-stream" 27 | "dms" "application/octet-stream" 28 | "doc" "application/msword" 29 | "dvi" "application/x-dvi" 30 | "edn" "application/edn" 31 | "eot" "application/vnd.ms-fontobject" 32 | "eps" "application/postscript" 33 | "etx" "text/x-setext" 34 | "exe" "application/octet-stream" 35 | "flv" "video/x-flv" 36 | "flac" "audio/flac" 37 | "gif" "image/gif" 38 | "gz" "application/gzip" 39 | "htm" "text/html" 40 | "html" "text/html" 41 | "ico" "image/x-icon" 42 | "iso" "application/x-iso9660-image" 43 | "jar" "application/java-archive" 44 | "jpe" "image/jpeg" 45 | "jpeg" "image/jpeg" 46 | "jpg" "image/jpeg" 47 | "js" "text/javascript" 48 | "json" "application/json" 49 | "lha" "application/octet-stream" 50 | "lzh" "application/octet-stream" 51 | "mov" "video/quicktime" 52 | "m4v" "video/mp4" 53 | "mp3" "audio/mpeg" 54 | "mp4" "video/mp4" 55 | "mpe" "video/mpeg" 56 | "mpeg" "video/mpeg" 57 | "mpg" "video/mpeg" 58 | "oga" "audio/ogg" 59 | "ogg" "audio/ogg" 60 | "ogv" "video/ogg" 61 | "pbm" "image/x-portable-bitmap" 62 | "pdf" "application/pdf" 63 | "pgm" "image/x-portable-graymap" 64 | "png" "image/png" 65 | "pnm" "image/x-portable-anymap" 66 | "ppm" "image/x-portable-pixmap" 67 | "ppt" "application/vnd.ms-powerpoint" 68 | "ps" "application/postscript" 69 | "qt" "video/quicktime" 70 | "rar" "application/x-rar-compressed" 71 | "ras" "image/x-cmu-raster" 72 | "rb" "text/plain" 73 | "rd" "text/plain" 74 | "rss" "application/rss+xml" 75 | "rtf" "application/rtf" 76 | "sgm" "text/sgml" 77 | "sgml" "text/sgml" 78 | "svg" "image/svg+xml" 79 | "swf" "application/x-shockwave-flash" 80 | "tar" "application/x-tar" 81 | "tif" "image/tiff" 82 | "tiff" "image/tiff" 83 | "ttf" "application/x-font-ttf" 84 | "txt" "text/plain" 85 | "webm" "video/webm" 86 | "wmv" "video/x-ms-wmv" 87 | "woff" "application/font-woff" 88 | "xbm" "image/x-xbitmap" 89 | "xls" "application/vnd.ms-excel" 90 | "xml" "text/xml" 91 | "xpm" "image/x-xpixmap" 92 | "xwd" "image/x-xwindowdump" 93 | "zip" "application/zip"}) 94 | 95 | (defn- filename-ext 96 | "Returns the file extension of a filename or filepath." 97 | [filename] 98 | (if-let [ext (second (re-find #"\.([^./\\]+)$" filename))] 99 | (str/lower-case ext))) 100 | 101 | (defn ext-mime-type 102 | "Get the mimetype from the filename extension. Takes an optional map of 103 | extensions to mimetypes that overrides values in the default-mime-types map." 104 | ([filename] 105 | (ext-mime-type filename {})) 106 | ([filename mime-types] 107 | (let [mime-types (merge default-mime-types mime-types)] 108 | (mime-types (filename-ext filename))))) 109 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/test/file_info.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.test.file-info 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.file-info :refer :all]) 4 | (:import [java.io File])) 5 | 6 | (def non-file-app (wrap-file-info (constantly {:headers {} :body "body"}))) 7 | 8 | (def known-file (File. "test/ring/assets/plain.txt")) 9 | (def known-file-app (wrap-file-info (constantly {:headers {} :body known-file}))) 10 | 11 | (def unknown-file (File. "test/ring/assets/random.xyz")) 12 | (def unknown-file-app (wrap-file-info (constantly {:headers {} :body unknown-file}))) 13 | 14 | (defmacro with-last-modified 15 | "Lets us use a known file modification time for tests, without permanently changing 16 | the file's modification time." 17 | [[file new-time] form] 18 | `(let [old-time# (.lastModified ~file)] 19 | (.setLastModified ~file ~(* new-time 1000)) 20 | (try ~form 21 | (finally (.setLastModified ~file old-time#))))) 22 | 23 | (def custom-type-app 24 | (wrap-file-info 25 | (constantly {:headers {} :body known-file}) 26 | {"txt" "custom/type"})) 27 | 28 | (deftest wrap-file-info-non-file-response 29 | (is (= {:headers {} :body "body"} (non-file-app {})))) 30 | 31 | (deftest wrap-file-info-known-file-response 32 | (with-last-modified [known-file 1263506400] 33 | (is (= {:headers {"Content-Type" "text/plain" 34 | "Content-Length" "6" 35 | "Last-Modified" "Thu, 14 Jan 2010 22:00:00 +0000"} 36 | :body known-file} 37 | (known-file-app {:headers {}}))))) 38 | 39 | (deftest wrap-file-info-unknown-file-response 40 | (with-last-modified [unknown-file 1263506400] 41 | (is (= {:headers {"Content-Type" "application/octet-stream" 42 | "Content-Length" "7" 43 | "Last-Modified" "Thu, 14 Jan 2010 22:00:00 +0000"} 44 | :body unknown-file} 45 | (unknown-file-app {:headers {}}))))) 46 | 47 | (deftest wrap-file-info-custom-mime-types 48 | (with-last-modified [known-file 0] 49 | (is (= {:headers {"Content-Type" "custom/type" 50 | "Content-Length" "6" 51 | "Last-Modified" "Thu, 01 Jan 1970 00:00:00 +0000"} 52 | :body known-file} 53 | (custom-type-app {:headers {}}))))) 54 | 55 | (deftest wrap-file-info-if-modified-since-hit 56 | (with-last-modified [known-file 1263506400] 57 | (is (= {:status 304 58 | :headers {"Content-Type" "text/plain" 59 | "Content-Length" "0" 60 | "Last-Modified" "Thu, 14 Jan 2010 22:00:00 +0000"} 61 | :body ""} 62 | (known-file-app 63 | {:headers {"if-modified-since" "Thu, 14 Jan 2010 22:00:00 +0000" }}))))) 64 | 65 | (deftest wrap-file-info-if-modified-miss 66 | (with-last-modified [known-file 1263506400] 67 | (is (= {:headers {"Content-Type" "text/plain" 68 | "Content-Length" "6" 69 | "Last-Modified" "Thu, 14 Jan 2010 22:00:00 +0000"} 70 | :body known-file} 71 | (known-file-app 72 | {:headers {"if-modified-since" "Wed, 13 Jan 2010 22:00:00 +0000"}}))))) 73 | 74 | (deftest wrap-file-info-cps-test 75 | (testing "file response" 76 | (with-last-modified [known-file 1263506400] 77 | (let [handler (wrap-file-info 78 | (fn [_ respond _] (respond {:headers {} :body known-file}))) 79 | response (promise) 80 | exception (promise)] 81 | (handler {:headers {}} response exception) 82 | (is (= {:headers {"Content-Type" "text/plain" 83 | "Content-Length" "6" 84 | "Last-Modified" "Thu, 14 Jan 2010 22:00:00 +0000"} 85 | :body known-file} 86 | @response)) 87 | (is (not (realized? exception)))))) 88 | 89 | (testing "non-file response" 90 | (let [handler (wrap-file-info 91 | (fn [_ respond _] (respond {:headers {} :body "body"}))) 92 | response (promise) 93 | exception (promise)] 94 | (handler {:headers {}} response exception) 95 | (is (= {:headers {} :body "body"} 96 | @response)) 97 | (is (not (realized? exception)))))) 98 | 99 | (deftest file-info-response-test 100 | (is (fn? file-info-response))) 101 | -------------------------------------------------------------------------------- /ring-devel/src/ring/middleware/stacktrace.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.stacktrace 2 | "Middleware that catches exceptions thrown by the handler, and reports the 3 | error and stacktrace via a webpage and STDERR log. 4 | 5 | This middleware is for debugging purposes, and should be limited to 6 | development environments." 7 | (:require [clojure.java.io :as io] 8 | [hiccup.core :refer [html h]] 9 | [hiccup.page :refer [html5]] 10 | [clj-stacktrace.core :refer :all] 11 | [clj-stacktrace.repl :refer :all] 12 | [ring.util.response :refer [content-type response status]])) 13 | 14 | (defn wrap-stacktrace-log 15 | "Wrap a handler such that exceptions are logged to *err* and then rethrown. 16 | Accepts the following options: 17 | 18 | :color? - if true, apply ANSI colors to stacktrace (default false)" 19 | ([handler] 20 | (wrap-stacktrace-log handler {})) 21 | ([handler options] 22 | (let [color? (:color? options)] 23 | (fn 24 | ([request] 25 | (try 26 | (handler request) 27 | (catch Throwable ex 28 | (pst-on *err* color? ex) 29 | (throw ex)))) 30 | ([request respond raise] 31 | (try 32 | (handler request respond (fn [ex] (pst-on *err* color? ex) (raise ex))) 33 | (catch Throwable ex 34 | (pst-on *err* color? ex) 35 | (throw ex)))))))) 36 | 37 | (defn- style-resource [path] 38 | (html [:style {:type "text/css"} (slurp (io/resource path))])) 39 | 40 | (defn- elem-partial [elem] 41 | (if (:clojure elem) 42 | [:tr.clojure 43 | [:td.source (h (source-str elem))] 44 | [:td.method (h (clojure-method-str elem))]] 45 | [:tr.java 46 | [:td.source (h (source-str elem))] 47 | [:td.method (h (java-method-str elem))]])) 48 | 49 | (defn- html-exception [ex] 50 | (let [[ex & causes] (iterate :cause (parse-exception ex))] 51 | (html5 52 | [:head 53 | [:title "Ring: Stacktrace"] 54 | (style-resource "ring/css/stacktrace.css")] 55 | [:body 56 | [:div#exception 57 | [:h1 (h (.getName ^Class (:class ex)))] 58 | [:div.message (h (:message ex))] 59 | [:div.trace 60 | [:table 61 | [:tbody (map elem-partial (:trace-elems ex))]]] 62 | (for [cause causes :while cause] 63 | [:div#causes 64 | [:h2 "Caused by " [:span.class (h (.getName ^Class (:class cause)))]] 65 | [:div.message (h (:message cause))] 66 | [:div.trace 67 | [:table 68 | [:tbody (map elem-partial (:trace-elems cause))]]]])]]))) 69 | 70 | (defn- js-ex-response [e] 71 | (-> (response (with-out-str (pst e))) 72 | (status 500) 73 | (content-type "text/javascript"))) 74 | 75 | (defn- html-ex-response [ex] 76 | (-> (response (html-exception ex)) 77 | (status 500) 78 | (content-type "text/html"))) 79 | 80 | (defn- ex-response 81 | "Returns a response showing debugging information about the exception. 82 | Currently supports delegation to either js or html exception views." 83 | [req ex] 84 | (let [accept (get-in req [:headers "accept"])] 85 | (if (and accept (re-find #"^text/javascript" accept)) 86 | (js-ex-response ex) 87 | (html-ex-response ex)))) 88 | 89 | (defn wrap-stacktrace-web 90 | "Wrap a handler such that exceptions are caught and a response containing 91 | a HTML representation of the exception and stacktrace is returned." 92 | [handler] 93 | (fn 94 | ([request] 95 | (try 96 | (handler request) 97 | (catch Throwable ex 98 | (ex-response request ex)))) 99 | ([request respond raise] 100 | (try 101 | (handler request respond (fn [ex] (respond (ex-response request ex)))) 102 | (catch Throwable ex 103 | (respond (ex-response request ex))))))) 104 | 105 | (defn wrap-stacktrace 106 | "Wrap a handler such that exceptions are caught, a corresponding stacktrace is 107 | logged to *err*, and a HTML representation of the stacktrace is returned as a 108 | response. 109 | 110 | Accepts the following option: 111 | 112 | :color? - if true, apply ANSI colors to terminal stacktrace (default false)" 113 | {:arglists '([handler] [handler options])} 114 | ([handler] 115 | (wrap-stacktrace handler {})) 116 | ([handler options] 117 | (-> handler 118 | (wrap-stacktrace-log options) 119 | (wrap-stacktrace-web)))) 120 | -------------------------------------------------------------------------------- /ring-devel/src/ring/middleware/lint.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.lint 2 | "Middleware that checks Ring requests and responses for correctness." 3 | (:require [clojure.set :as set] 4 | [clojure.string :as str]) 5 | (:import [java.io File InputStream] 6 | [java.security.cert X509Certificate])) 7 | 8 | (defn- lint 9 | "Asserts that spec applied to val returns logical truth, otherwise raises 10 | an exception with a message produced by applying format to the message-pattern 11 | argument and a printing of an invalid val." 12 | [val spec message] 13 | (try 14 | (if-not (spec val) 15 | (throw (Exception. (format "Ring lint error: specified %s, but %s was not" 16 | message (pr-str val))))) 17 | (catch Exception e 18 | (if-not (re-find #"^Ring lint error: " (.getMessage e)) 19 | (throw (Exception. (format 20 | "Ring lint error: exception occured when checking that %s on %s: %s" 21 | message (pr-str val) (.getMessage e)))) 22 | (throw e))))) 23 | 24 | (defn- check-req 25 | "Validates the request, throwing an exception on violations of the spec." 26 | [req] 27 | (lint req map? "Ring request must a Clojure map") 28 | 29 | (lint (:server-port req) integer? 30 | ":server-port must be an Integer") 31 | (lint (:server-name req) string? 32 | ":server-name must be a String") 33 | (lint (:remote-addr req) string? 34 | ":remote-addr must be a String") 35 | (lint (:uri req) #(and (string? %) (.startsWith ^String % "/")) 36 | ":uri must be a String starting with \"/\"") 37 | (lint (:query-string req) #(or (nil? %) (string? %)) 38 | ":query-string must be nil or a String") 39 | (lint (:scheme req) #{:http :https} 40 | ":scheme must be one of :http or :https") 41 | (lint (:request-method req) #(and (keyword? %) (= (name %) (str/lower-case (name %)))) 42 | ":request-method must be a lowercase keyword") 43 | (lint (:content-type req) #(or (nil? %) (string? %)) 44 | ":content-type must be nil or a String") 45 | (lint (:content-length req) #(or (nil? %) (integer? %)) 46 | ":content-length must be nil or an Integer") 47 | (lint (:character-encoding req) #(or (nil? %) (string? %)) 48 | ":character-encoding must be nil or a String") 49 | (lint (:ssl-client-cert req) #(or (nil? %) (instance? X509Certificate %)) 50 | ":ssl-client-cert must be nil or an X509Certificate") 51 | 52 | (let [headers (:headers req)] 53 | (lint headers map? 54 | ":headers must be a Clojure map") 55 | (doseq [[hname hval] headers] 56 | (lint hname string? 57 | "header names must be Strings") 58 | (lint hname #(= % (.toLowerCase ^String %)) 59 | "header names must be in lower case") 60 | (lint hval string? 61 | "header values must be strings"))) 62 | 63 | (lint (:body req) #(or (nil? %) (instance? InputStream %)) 64 | ":body must be nil or an InputStream")) 65 | 66 | (defn- check-resp 67 | "Validates the response, throwing an exception on violations of the spec" 68 | [resp] 69 | (lint resp map? 70 | "Ring response must be a Clojure map") 71 | 72 | (lint (:status resp) #(and (integer? %) (>= % 100)) 73 | ":status must be an Integer greater than or equal to 100") 74 | 75 | (let [headers (:headers resp)] 76 | (lint headers map? 77 | ":headers must be a Clojure map") 78 | (doseq [[hname hval] headers] 79 | (lint hname string? 80 | "header names must Strings") 81 | (lint hval #(or (string? %) (every? string? %)) 82 | "header values must be Strings or collections of Strings"))) 83 | 84 | (lint (:body resp) #(or (nil? %) 85 | (string? %) 86 | (seq? %) 87 | (instance? File %) 88 | (instance? InputStream %)) 89 | ":body must a String, ISeq, File, or InputStream")) 90 | 91 | (defn wrap-lint 92 | "Wrap a handler to validate incoming requests and outgoing responses 93 | according to the current Ring specification. An exception is raised if either 94 | the request or response is invalid." 95 | [handler] 96 | (fn 97 | ([request] 98 | (check-req request) 99 | (let [response (handler request)] 100 | (check-resp response) 101 | response)) 102 | ([request respond raise] 103 | (check-req request) 104 | (handler request 105 | (fn [response] 106 | (check-resp response) 107 | (respond response)) 108 | raise)))) 109 | -------------------------------------------------------------------------------- /ring-devel/test/ring/middleware/test/lint.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.test.lint 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.lint :refer :all] 4 | [ring.util.io :refer [string-input-stream]]) 5 | (:import [java.io File InputStream])) 6 | 7 | (def valid-request 8 | {:server-port 80 9 | :server-name "localhost" 10 | :remote-addr "192.0.2.235" 11 | :uri "/" 12 | :query-string nil 13 | :scheme :http 14 | :request-method :get 15 | :headers {} 16 | :content-type nil 17 | :content-length nil 18 | :character-encoding nil 19 | :body nil}) 20 | 21 | (def valid-response 22 | {:status 200 :headers {} :body "valid"}) 23 | 24 | (def valid-response-app 25 | (wrap-lint (fn [req] valid-response))) 26 | 27 | (defn constant-app 28 | [constant-response] 29 | (wrap-lint (fn [req] constant-response))) 30 | 31 | (defn is-lint-error [f] 32 | (is (thrown-with-msg? Exception #"Ring lint error:.*" (f)))) 33 | 34 | (defmacro lints-req 35 | [key goods bads] 36 | (let [good-name (symbol (str "request-" key "-valid-values")) 37 | bad-name (symbol (str "request-" key "-invalid-values"))] 38 | `(do 39 | (deftest ~good-name 40 | (doseq [good# ~goods] 41 | (is (= valid-response 42 | (valid-response-app (assoc valid-request ~key good#))) 43 | (format "%s is a valid value for request key %s" 44 | (pr-str good#) ~key)))) 45 | (deftest ~bad-name 46 | (doseq [bad# ~bads] 47 | (is-lint-error 48 | (fn [] (valid-response-app (assoc valid-request ~key bad#))))))))) 49 | 50 | (defmacro lints-resp 51 | [key goods bads] 52 | (let [good-name (symbol (str "response-" key "-valid-values")) 53 | bad-name (symbol (str "response-" key "-invalid-values"))] 54 | `(do 55 | (deftest ~good-name 56 | (doseq [good# ~goods] 57 | (let [response# (assoc valid-response ~key good#) 58 | app# (constant-app response#)] 59 | (is (= response# (app# valid-request)) 60 | (format "%s is a valid value for response key %s" 61 | (pr-str good#) ~key))))) 62 | (deftest ~bad-name 63 | (doseq [bad# ~bads] 64 | (let [response# (assoc valid-response ~key bad#) 65 | app# (constant-app response#)] 66 | (is-lint-error (fn [] (app# valid-request))))))))) 67 | 68 | (lints-req :server-port 69 | [80 8080] 70 | [nil "80"]) 71 | 72 | (lints-req :server-name 73 | ["localhost" "www.amazon.com" "192.0.2.235"] 74 | [nil 123]) 75 | 76 | (lints-req :remote-addr 77 | ["192.0.2.235" "0:0:0:0:0:0:0:1%0"] 78 | [nil 123]) 79 | 80 | (lints-req :uri 81 | ["/" "/foo" "/foo/bar"] 82 | [nil ""]) 83 | 84 | (lints-req :query-string 85 | [nil "" "foo=bar" "foo=bar&biz=bat"] 86 | [:foo]) 87 | 88 | (lints-req :scheme 89 | [:http :https] 90 | [nil "http"]) 91 | 92 | (lints-req :request-method 93 | [:get :head :options :put :patch :post :delete] 94 | [nil :FOOBAR "get" 'get]) 95 | 96 | (lints-req :content-type 97 | [nil "text/html"] 98 | [:text/html]) 99 | 100 | (lints-req :content-length 101 | [nil 123] 102 | ["123"]) 103 | 104 | (lints-req :character-encoding 105 | [nil "UTF-8"] 106 | [:utf-8]) 107 | 108 | (lints-req :ssl-client-cert 109 | [nil (proxy [java.security.cert.X509Certificate] [] (toString [] "X509Certificate"))] 110 | ["01234567890ABCDEF"]) 111 | 112 | (lints-req :headers 113 | [{"foo" "bar"} {"bat" "Biz"} {"whiz-bang" "high-low"}] 114 | [nil {:foo "bar"} {"bar" :foo} {"Bar" "foo"}]) 115 | 116 | (lints-req :body 117 | [nil (string-input-stream "thebody")] 118 | ["thebody" :thebody]) 119 | 120 | 121 | (lints-resp :status 122 | [100 301 500] 123 | [nil "100" 99]) 124 | 125 | (lints-resp :headers 126 | [{} {"foo" "bar"} {"Biz" "Bat"} {"vert" ["high" "low"]} {"horz" #{"left right"}}] 127 | [nil {:foo "bar"} {"foo" :bar} {"dir" 123}]) 128 | 129 | (lints-resp :body 130 | [nil "thebody" (list "foo" "bar") (string-input-stream "thebody") (File. "test/ring/assets/foo.html")] 131 | [123 :thebody]) 132 | 133 | (deftest wrap-lint-cps-test 134 | (testing "valid request and response" 135 | (let [handler (wrap-lint (fn [_ respond _] (respond valid-response))) 136 | response (promise) 137 | exception (promise)] 138 | (handler valid-request response exception) 139 | (is (= valid-response @response)) 140 | (is (not (realized? exception))))) 141 | 142 | (testing "invalid request" 143 | (let [handler (wrap-lint (fn [_ respond _] (respond valid-response)))] 144 | (is-lint-error #(handler {} (fn [_]) (fn [_]))))) 145 | 146 | (testing "invalid response" 147 | (let [handler (wrap-lint (fn [_ respond _] (respond {})))] 148 | (is-lint-error #(handler valid-request (fn [_]) (fn [_])))))) 149 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/test/not_modified.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.test.not-modified 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.not-modified :refer :all])) 4 | 5 | (defn- handler-etag [etag] 6 | (constantly 7 | {:status 200 8 | :headers {"etag" etag} 9 | :body ""})) 10 | 11 | (defn- handler-modified [modified] 12 | (constantly 13 | {:status 200 14 | :headers {"last-modified" modified} 15 | :body ""})) 16 | 17 | (defn- etag-request [etag] 18 | {:request-method :get, :headers {"if-none-match" etag}}) 19 | 20 | (defn- modified-request [modified-date] 21 | {:request-method :get, :headers {"if-modified-since" modified-date}}) 22 | 23 | (deftest test-wrap-not-modified 24 | (with-redefs [ring.middleware.not-modified/not-modified-response #(vector %1 %2)] 25 | (let [req (modified-request "Sun, 23 Sep 2012 11:00:00 GMT") 26 | handler (handler-modified "Jan, 23 Sep 2012 11:00:00 GMT")] 27 | (is (= [(handler req) req] ; Not modified since is called with expected args 28 | ((wrap-not-modified handler) req)))))) 29 | 30 | (deftest wrap-not-modified-cps-test 31 | (testing "etag match" 32 | (let [etag "known-etag" 33 | handler (wrap-not-modified 34 | (fn [req respond _] 35 | (respond {:status 200, :headers {"etag" etag}, :body ""}))) 36 | request {:request-method :get, :headers {"if-none-match" etag}} 37 | response (promise) 38 | exception (promise)] 39 | (handler request response exception) 40 | (is (= 304 (:status @response))) 41 | (is (not (realized? exception)))))) 42 | 43 | (deftest test-not-modified-response 44 | (testing "etag match" 45 | (let [known-etag "known-etag" 46 | request {:request-method :get, :headers {"if-none-match" known-etag}} 47 | handler-resp #(hash-map :status 200 :headers {"etag" %} :body "")] 48 | (is (= 304 (:status (not-modified-response (handler-resp known-etag) request)))) 49 | (is (= 200 (:status (not-modified-response (handler-resp "unknown-etag") request)))))) 50 | 51 | (testing "not modified" 52 | (let [req #(hash-map :request-method :get, :headers {"if-modified-since" %}) 53 | last-modified "Sun, 23 Sep 2012 11:00:00 GMT" 54 | h-resp {:status 200 :headers {"Last-Modified" last-modified} :body ""}] 55 | (is (= 304 (:status (not-modified-response h-resp (req last-modified))))) 56 | (is (= 304 (:status (not-modified-response h-resp (req "Sun, 23 Sep 2012 11:52:50 GMT"))))) 57 | (is (= 200 (:status (not-modified-response h-resp (req "Sun, 23 Sep 2012 10:00:50 GMT"))))))) 58 | 59 | (testing "not modified body and content-length" 60 | (let [req #(hash-map :request-method :get :headers {"if-modified-since" %}) 61 | last-modified "Sun, 23 Sep 2012 11:00:00 GMT" 62 | h-resp {:status 200 :headers {"Last-Modified" last-modified} :body ""} 63 | resp (not-modified-response h-resp (req last-modified))] 64 | (is (nil? (:body resp))) 65 | (is (= (get-in resp [:headers "Content-Length"]) "0")))) 66 | 67 | (testing "no modification info" 68 | (let [response {:status 200 :headers {} :body ""}] 69 | (is (= 200 (:status (not-modified-response response (etag-request "\"12345\""))))) 70 | (is (= 200 (:status (not-modified-response response (modified-request "Sun, 23 Sep 2012 10:00:00 GMT"))))))) 71 | 72 | (testing "header case insensitivity" 73 | (let [h-resp {:status 200 74 | :headers {"LasT-ModiFied" "Sun, 23 Sep 2012 11:00:00 GMT" 75 | "EtAg" "\"123456abcdef\""}}] 76 | (is (= 304 (:status (not-modified-response 77 | h-resp {:request-method :get 78 | :headers {"if-modified-since" 79 | "Sun, 23 Sep 2012 11:00:00 GMT"}})))) 80 | (is (= 304 (:status (not-modified-response 81 | h-resp {:request-method :get 82 | :headers {"if-none-match" "\"123456abcdef\""}})))))) 83 | 84 | (testing "doesn't affect POST, PUT, PATCH or DELETE" 85 | (let [date "Sat, 22 Sep 2012 11:00:00 GMT" 86 | req {:headers {"if-modified-since" date}} 87 | resp {:status 200 :headers {"Last-Modified" date} :body ""}] 88 | (are [m s] (= s (:status (not-modified-response resp (assoc req :request-method m)))) 89 | :head 304 90 | :get 304 91 | :post 200 92 | :put 200 93 | :patch 200 94 | :delete 200))) 95 | 96 | (testing "only affects 200 OK responses" 97 | (let [date "Sat, 22 Sep 2012 11:00:00 GMT" 98 | req {:request-method :get, :headers {"if-modified-since" date}} 99 | resp {:headers {"Last-Modified" date} :body ""}] 100 | (are [s1 s2] (= s2 (:status (not-modified-response (assoc resp :status s1) req))) 101 | 200 304 102 | 302 302 103 | 404 404 104 | 500 500)))) 105 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/test/multipart_params.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.test.multipart-params 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.multipart-params :refer :all] 4 | [ring.util.io :refer [string-input-stream]]) 5 | (:import [java.io InputStream])) 6 | 7 | (defn string-store [item] 8 | (-> (select-keys item [:filename :content-type]) 9 | (assoc :content (slurp (:stream item))))) 10 | 11 | (deftest test-wrap-multipart-params 12 | (let [form-body (str "--XXXX\r\n" 13 | "Content-Disposition: form-data;" 14 | "name=\"upload\"; filename=\"test.txt\"\r\n" 15 | "Content-Type: text/plain\r\n\r\n" 16 | "foo\r\n" 17 | "--XXXX\r\n" 18 | "Content-Disposition: form-data;" 19 | "name=\"baz\"\r\n\r\n" 20 | "qux\r\n" 21 | "--XXXX--") 22 | handler (wrap-multipart-params identity {:store string-store}) 23 | request {:headers {"content-type" "multipart/form-data; boundary=XXXX" 24 | "content-length" (str (count form-body))} 25 | :params {"foo" "bar"} 26 | :body (string-input-stream form-body)} 27 | response (handler request)] 28 | (is (= (get-in response [:params "foo"]) "bar")) 29 | (is (= (get-in response [:params "baz"]) "qux")) 30 | (let [upload (get-in response [:params "upload"])] 31 | (is (= (:filename upload) "test.txt")) 32 | (is (= (:content-type upload) "text/plain")) 33 | (is (= (:content upload) "foo"))))) 34 | 35 | (deftest test-multiple-params 36 | (let [form-body (str "--XXXX\r\n" 37 | "Content-Disposition: form-data;" 38 | "name=\"foo\"\r\n\r\n" 39 | "bar\r\n" 40 | "--XXXX\r\n" 41 | "Content-Disposition: form-data;" 42 | "name=\"foo\"\r\n\r\n" 43 | "baz\r\n" 44 | "--XXXX--") 45 | handler (wrap-multipart-params identity {:store string-store}) 46 | request {:headers {"content-type" "multipart/form-data; boundary=XXXX" 47 | "content-length" (str (count form-body))} 48 | :body (string-input-stream form-body)} 49 | response (handler request)] 50 | (is (= (get-in response [:params "foo"]) 51 | ["bar" "baz"])))) 52 | 53 | (defn all-threads [] 54 | (.keySet (Thread/getAllStackTraces))) 55 | 56 | (deftest test-multipart-threads 57 | (testing "no thread leakage when handler called" 58 | (let [handler (wrap-multipart-params identity)] 59 | (dotimes [_ 200] 60 | (handler {})) 61 | (is (< (count (all-threads)) 62 | 100)))) 63 | 64 | (testing "no thread leakage from default store" 65 | (let [form-body (str "--XXXX\r\n" 66 | "Content-Disposition: form-data;" 67 | "name=\"upload\"; filename=\"test.txt\"\r\n" 68 | "Content-Type: text/plain\r\n\r\n" 69 | "foo\r\n" 70 | "--XXXX--")] 71 | (dotimes [_ 200] 72 | (let [handler (wrap-multipart-params identity) 73 | request {:headers {"content-type" "multipart/form-data; boundary=XXXX" 74 | "content-length" (str (count form-body))} 75 | :body (string-input-stream form-body)}] 76 | (handler request)))) 77 | (is (< (count (all-threads)) 78 | 100)))) 79 | 80 | (deftest wrap-multipart-params-cps-test 81 | (let [handler (wrap-multipart-params (fn [req respond _] (respond req))) 82 | form-body (str "--XXXX\r\n" 83 | "Content-Disposition: form-data;" 84 | "name=\"foo\"\r\n\r\n" 85 | "bar\r\n" 86 | "--XXXX--") 87 | request {:headers {"content-type" "multipart/form-data; boundary=XXXX"} 88 | :body (string-input-stream form-body "UTF-8")} 89 | response (promise) 90 | exception (promise)] 91 | (handler request response exception) 92 | (is (= (get-in @response [:multipart-params "foo"]) "bar")) 93 | (is (not (realized? exception))))) 94 | 95 | (deftest multipart-params-request-test 96 | (is (fn? multipart-params-request))) 97 | 98 | (deftest test-utf8-encoding-support 99 | (let [form-body (str "--XXXX\r\n" 100 | "Content-Disposition: form-data;" 101 | "name=\"foo\"\r\n\r\n" 102 | "Øæß箣èé\r\n" 103 | "--XXXX--") 104 | request {:headers {"content-type" 105 | (str "multipart/form-data; boundary=XXXX")} 106 | :body (string-input-stream form-body "UTF-8")} 107 | request* (multipart-params-request request)] 108 | (is (= (get-in request* [:multipart-params "foo"]) "Øæß箣èé")))) 109 | -------------------------------------------------------------------------------- /SPEC: -------------------------------------------------------------------------------- 1 | === Ring Spec (1.4-SNAPSHOT) 2 | Ring is defined in terms of handlers, middleware, adapters, requests maps, and 3 | response maps, each of which are described below. 4 | 5 | 6 | == Handlers 7 | Ring handlers constitute the core logic of the web application. Handlers are 8 | implemented as Clojure functions. 9 | 10 | A synchronous handler takes 1 argument, a request map, and returns a response 11 | map. 12 | 13 | An asynchronous handler takes 3 arguments: a request map, a callback function 14 | for sending a response and a callback function for raising an exception. The 15 | response callback takes a response map as its argument. The exception callback 16 | takes an exception as its argument. 17 | 18 | A handler function may simultaneously support synchronous and asynchronous 19 | behavior by accepting both arities. 20 | 21 | 22 | == Middleware 23 | Ring middleware augments the functionality of handlers by invoking them in the 24 | process of generating responses. Typically middleware will be implemented as a 25 | higher-order function that takes one or more handlers and configuration options 26 | as arguments and returns a new handler with the desired compound behavior. 27 | 28 | 29 | == Adapters 30 | Handlers are run via Ring adapters, which are in turn responsible for 31 | implementing the HTTP protocol and abstracting the handlers that they run from 32 | the details of the protocol. 33 | 34 | Adapters are implemented as functions of two arguments: a handler and an options 35 | map. The options map provides any needed configuration to the adapter, such as 36 | the port on which to run. 37 | 38 | Once initialized, adapters receive HTTP requests, parse them to construct a 39 | request map, and then invoke their handler with this request map as an 40 | argument. Once the handler returns a response map, the adapter uses it to 41 | construct and send an HTTP response to the client. 42 | 43 | 44 | == Request Map 45 | A request map is a Clojure map containing at least the following keys and 46 | corresponding values: 47 | 48 | :server-port 49 | (Required, Integer) 50 | The port on which the request is being handled. 51 | 52 | :server-name 53 | (Required, String) 54 | The resolved server name, or the server IP address. 55 | 56 | :remote-addr 57 | (Required, String) 58 | The IP address of the client or the last proxy that sent the request. 59 | 60 | :uri 61 | (Required, String) 62 | The request URI, excluding the query string and the "?" separator. 63 | Must start with "/". 64 | 65 | :query-string 66 | (Optional, String) 67 | The query string, if present. 68 | 69 | :scheme 70 | (Required, clojure.lang.Keyword) 71 | The transport protocol, must be one of :http or :https. 72 | 73 | :request-method 74 | (Required, clojure.lang.Keyword) 75 | The HTTP request method, must be a lowercase keyword corresponding to a HTTP 76 | request method, such as :get or :post. 77 | 78 | :protocol 79 | (Required, String) 80 | The protocol the request was made with, e.g. "HTTP/1.1". 81 | 82 | :content-type [DEPRECATED] 83 | (Optional, String) 84 | The MIME type of the request body, if known. 85 | 86 | :content-length [DEPRECATED] 87 | (Optional, Integer) 88 | The number of bytes in the request body, if known. 89 | 90 | :character-encoding [DEPRECATED] 91 | (Optional, String) 92 | The name of the character encoding used in the request body, if known. 93 | 94 | :ssl-client-cert 95 | (Optional, java.security.cert.X509Certificate) 96 | The SSL client certificate, if supplied. 97 | 98 | :headers 99 | (Required, clojure.lang.IPersistentMap) 100 | A Clojure map of downcased header name Strings to corresponding header value 101 | Strings. 102 | 103 | :body 104 | (Optional, java.io.InputStream) 105 | An InputStream for the request body, if present. 106 | 107 | 108 | == Response Map 109 | A response map is a Clojure map containing at least the following keys and 110 | corresponding values: 111 | 112 | :status 113 | (Required, Integer) 114 | The HTTP status code, must be greater than or equal to 100. 115 | 116 | :headers 117 | (Required, clojure.lang.IPersistentMap) 118 | A Clojure map of HTTP header names to header values. These values may be 119 | either Strings, in which case one name/value header will be sent in the 120 | HTTP response, or a seq of Strings, in which case a name/value header will be 121 | sent for each such String value. 122 | 123 | :body 124 | (Optional, ring.core.protocols/StreamableResponseBody) 125 | A representation of the response body, if a response body is appropriate for 126 | the response's status code. The response body must satisfy the 127 | StreamableResponseBody protocol located in the ring.core.protocols namespace. 128 | 129 | By default the protocol is satisfied by the following types: 130 | 131 | String: 132 | Contents are sent to the client as-is. 133 | ISeq: 134 | Each element of the seq is sent to the client as a string. 135 | File: 136 | Contents at the specified location are sent to the client. The server may 137 | use an optimized method to send the file if such a method is available. 138 | InputStream: 139 | Contents are consumed from the stream and sent to the client. When the 140 | stream is exhausted, it is .close'd. 141 | -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/session.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.session 2 | "Middleware for maintaining browser sessions using cookies. 3 | 4 | Sessions are stored using types that adhere to the 5 | ring.middleware.session.store/SessionStore protocol. 6 | Ring comes with two stores included: 7 | 8 | ring.middleware.session.memory/memory-store 9 | ring.middleware.session.cookie/cookie-store" 10 | (:require [ring.middleware.cookies :as cookies] 11 | [ring.middleware.session.store :as store] 12 | [ring.middleware.session.memory :as mem])) 13 | 14 | (defn- session-options 15 | [options] 16 | {:store (options :store (mem/memory-store)) 17 | :cookie-name (options :cookie-name "ring-session") 18 | :cookie-attrs (merge {:path "/" 19 | :http-only true} 20 | (options :cookie-attrs) 21 | (if-let [root (options :root)] 22 | {:path root}))}) 23 | 24 | (defn- bare-session-request 25 | [request {:keys [store cookie-name]}] 26 | (let [req-key (get-in request [:cookies cookie-name :value]) 27 | session (store/read-session store req-key) 28 | session-key (if session req-key)] 29 | (merge request {:session (or session {}) 30 | :session/key session-key}))) 31 | 32 | (defn session-request 33 | "Reads current HTTP session map and adds it to :session key of the request. 34 | See: wrap-session." 35 | {:added "1.2"} 36 | ([request] 37 | (session-request request)) 38 | ([request options] 39 | (-> request 40 | cookies/cookies-request 41 | (bare-session-request options)))) 42 | 43 | (defn- bare-session-response 44 | [response {session-key :session/key} {:keys [store cookie-name cookie-attrs]}] 45 | (let [new-session-key (if (contains? response :session) 46 | (if-let [session (response :session)] 47 | (if (:recreate (meta session)) 48 | (do 49 | (store/delete-session store session-key) 50 | (->> (vary-meta session dissoc :recreate) 51 | (store/write-session store nil))) 52 | (store/write-session store session-key session)) 53 | (if session-key 54 | (store/delete-session store session-key)))) 55 | session-attrs (:session-cookie-attrs response) 56 | cookie {cookie-name 57 | (merge cookie-attrs 58 | session-attrs 59 | {:value (or new-session-key session-key)})} 60 | response (dissoc response :session :session-cookie-attrs)] 61 | (if (or (and new-session-key (not= session-key new-session-key)) 62 | (and session-attrs (or new-session-key session-key))) 63 | (assoc response :cookies (merge (response :cookies) cookie)) 64 | response))) 65 | 66 | (defn session-response 67 | "Updates session based on :session key in response. See: wrap-session." 68 | {:added "1.2"} 69 | ([response request] 70 | (session-response response request {})) 71 | ([response request options] 72 | (if response 73 | (-> response 74 | (bare-session-response request options) 75 | cookies/cookies-response)))) 76 | 77 | (defn wrap-session 78 | "Reads in the current HTTP session map, and adds it to the :session key on 79 | the request. If a :session key is added to the response by the handler, the 80 | session is updated with the new value. If the value is nil, the session is 81 | deleted. 82 | 83 | Accepts the following options: 84 | 85 | :store - An implementation of the SessionStore protocol in the 86 | ring.middleware.session.store namespace. This determines how 87 | the session is stored. Defaults to in-memory storage using 88 | ring.middleware.session.store/memory-store. 89 | 90 | :root - The root path of the session. Any path above this will not be 91 | able to see this session. Equivalent to setting the cookie's 92 | path attribute. Defaults to \"/\". 93 | 94 | :cookie-name - The name of the cookie that holds the session key. Defaults to 95 | \"ring-session\". 96 | 97 | :cookie-attrs - A map of attributes to associate with the session cookie. 98 | Defaults to {:http-only true}. This may be overridden on a 99 | per-response basis by adding :session-cookie-attrs to the 100 | response." 101 | ([handler] 102 | (wrap-session handler {})) 103 | ([handler options] 104 | (let [options (session-options options)] 105 | (fn 106 | ([request] 107 | (let [request (session-request request options)] 108 | (-> (handler request) 109 | (session-response request options)))) 110 | ([request respond raise] 111 | (let [request (session-request request options)] 112 | (handler request 113 | (fn [response] 114 | (respond (session-response response request options))) 115 | raise))))))) 116 | -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/multipart_params.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.multipart-params 2 | "Middleware that parses multipart request bodies into parameters. 3 | 4 | This middleware is necessary to handle file uploads from web browsers. 5 | 6 | Ring comes with two different multipart storage engines included: 7 | 8 | ring.middleware.multipart-params.byte-array/byte-array-store 9 | ring.middleware.multipart-params.temp-file/temp-file-store" 10 | (:require [ring.util.codec :refer [assoc-conj]] 11 | [ring.util.request :as req]) 12 | (:import [org.apache.commons.fileupload.util Streams] 13 | [org.apache.commons.fileupload UploadContext 14 | FileItemIterator 15 | FileItemStream 16 | FileUpload 17 | ProgressListener])) 18 | (defn- progress-listener 19 | "Create a progress listener that calls the supplied function." 20 | [request progress-fn] 21 | (reify ProgressListener 22 | (update [this bytes-read content-length item-count] 23 | (progress-fn request bytes-read content-length item-count)))) 24 | 25 | (defn- multipart-form? 26 | "Does a request have a multipart form?" 27 | [request] 28 | (= (req/content-type request) "multipart/form-data")) 29 | 30 | (defn- request-context 31 | "Create an UploadContext object from a request map." 32 | {:tag UploadContext} 33 | [request encoding] 34 | (reify UploadContext 35 | (getContentType [this] (get-in request [:headers "content-type"])) 36 | (getContentLength [this] (or (req/content-length request) -1)) 37 | (contentLength [this] (or (req/content-length request) -1)) 38 | (getCharacterEncoding [this] encoding) 39 | (getInputStream [this] (:body request)))) 40 | 41 | (defn- file-item-iterator-seq 42 | "Create a lazy seq from a FileItemIterator instance." 43 | [^FileItemIterator it] 44 | (lazy-seq 45 | (if (.hasNext it) 46 | (cons (.next it) (file-item-iterator-seq it))))) 47 | 48 | (defn- file-item-seq 49 | "Create a seq of FileItem instances from a request context." 50 | [request ^ProgressListener progress-fn context] 51 | (let [upload (if progress-fn 52 | (doto (FileUpload.) 53 | (.setProgressListener (progress-listener request progress-fn))) 54 | (FileUpload.))] 55 | (file-item-iterator-seq 56 | (.getItemIterator ^FileUpload upload context)))) 57 | 58 | (defn- parse-file-item 59 | "Parse a FileItemStream into a key-value pair. If the request is a file the 60 | supplied store function is used to save it." 61 | [^FileItemStream item store encoding] 62 | [(.getFieldName item) 63 | (if (.isFormField item) 64 | (Streams/asString (.openStream item) encoding) 65 | (store {:filename (.getName item) 66 | :content-type (.getContentType item) 67 | :stream (.openStream item)}))]) 68 | 69 | (defn- parse-multipart-params 70 | "Parse a map of multipart parameters from the request." 71 | [request encoding store progress-fn] 72 | (->> (request-context request encoding) 73 | (file-item-seq request progress-fn) 74 | (map #(parse-file-item % store encoding)) 75 | (reduce (fn [m [k v]] (assoc-conj m k v)) {}))) 76 | 77 | (defn- load-var 78 | "Returns the var named by the supplied symbol, or nil if not found. Attempts 79 | to load the var namespace on the fly if not already loaded." 80 | [sym] 81 | (require (symbol (namespace sym))) 82 | (find-var sym)) 83 | 84 | (def ^:private default-store 85 | (delay 86 | (let [store 'ring.middleware.multipart-params.temp-file/temp-file-store 87 | func (load-var store)] 88 | (func)))) 89 | 90 | (defn multipart-params-request 91 | "Adds :multipart-params and :params keys to request. 92 | See: wrap-multipart-params." 93 | {:added "1.2"} 94 | ([request] 95 | (multipart-params-request request {})) 96 | ([request options] 97 | (let [store (or (:store options) @default-store) 98 | encoding (or (:encoding options) 99 | (req/character-encoding request) 100 | "UTF-8") 101 | progress (:progress-fn options) 102 | params (if (multipart-form? request) 103 | (parse-multipart-params request encoding store progress) 104 | {})] 105 | (merge-with merge request 106 | {:multipart-params params} 107 | {:params params})))) 108 | 109 | (defn wrap-multipart-params 110 | "Middleware to parse multipart parameters from a request. Adds the 111 | following keys to the request map: 112 | 113 | :multipart-params - a map of multipart parameters 114 | :params - a merged map of all types of parameter 115 | 116 | The following options are accepted 117 | 118 | :encoding - character encoding to use for multipart parsing. If not 119 | specified, uses the request character encoding, or \"UTF-8\" 120 | if no request character encoding is set. 121 | 122 | :store - a function that stores a file upload. The function should 123 | expect a map with :filename, content-type and :stream keys, 124 | and its return value will be used as the value for the 125 | parameter in the multipart parameter map. The default storage 126 | function is the temp-file-store. 127 | 128 | :progress-fn - a function that gets called during uploads. The function 129 | should expect four parameters: request, bytes-read, 130 | content-length, and item-count." 131 | ([handler] 132 | (wrap-multipart-params handler {})) 133 | ([handler options] 134 | (fn 135 | ([request] 136 | (handler (multipart-params-request request options))) 137 | ([request respond raise] 138 | (handler (multipart-params-request request options) respond raise))))) 139 | -------------------------------------------------------------------------------- /ring-core/src/ring/middleware/cookies.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.cookies 2 | "Middleware for parsing and generating cookies." 3 | (:import [org.joda.time DateTime Interval]) 4 | (:require [ring.util.codec :as codec] 5 | [clojure.string :as str] 6 | [clj-time.core :refer [in-seconds]] 7 | [clj-time.format :refer [formatters unparse with-locale]] 8 | [ring.util.parsing :refer [re-token]])) 9 | 10 | (def ^{:private true, :doc "RFC6265 cookie-octet"} 11 | re-cookie-octet 12 | #"[!#$%&'()*+\-./0-9:<=>?@A-Z\[\]\^_`a-z\{\|\}~]") 13 | 14 | (def ^{:private true, :doc "RFC6265 cookie-value"} 15 | re-cookie-value 16 | (re-pattern (str "\"" re-cookie-octet "*\"|" re-cookie-octet "*"))) 17 | 18 | (def ^{:private true, :doc "RFC6265 set-cookie-string"} 19 | re-cookie 20 | (re-pattern (str "\\s*(" re-token ")=(" re-cookie-value ")\\s*[;,]?"))) 21 | 22 | (def ^{:private true 23 | :doc "Attributes defined by RFC6265 that apply to the Set-Cookie header."} 24 | set-cookie-attrs 25 | {:domain "Domain", :max-age "Max-Age", :path "Path" 26 | :secure "Secure", :expires "Expires", :http-only "HttpOnly"}) 27 | 28 | (def ^:private rfc822-formatter 29 | (with-locale (formatters :rfc822) java.util.Locale/US)) 30 | 31 | (defn- parse-cookie-header 32 | "Turn a HTTP Cookie header into a list of name/value pairs." 33 | [header] 34 | (for [[_ name value] (re-seq re-cookie header)] 35 | [name value])) 36 | 37 | (defn- strip-quotes 38 | "Strip quotes from a cookie value." 39 | [value] 40 | (str/replace value #"^\"|\"$" "")) 41 | 42 | (defn- decode-values [cookies decoder] 43 | (for [[name value] cookies] 44 | (if-let [value (decoder (strip-quotes value))] 45 | [name {:value value}]))) 46 | 47 | (defn- parse-cookies 48 | "Parse the cookies from a request map." 49 | [request encoder] 50 | (if-let [cookie (get-in request [:headers "cookie"])] 51 | (->> cookie 52 | parse-cookie-header 53 | ((fn [c] (decode-values c encoder))) 54 | (remove nil?) 55 | (into {})) 56 | {})) 57 | 58 | (defn- write-value 59 | "Write the main cookie value." 60 | [key value encoder] 61 | (encoder {key value})) 62 | 63 | (defn- valid-attr? 64 | "Is the attribute valid?" 65 | [[key value]] 66 | (and (contains? set-cookie-attrs key) 67 | (not (.contains (str value) ";")) 68 | (case key 69 | :max-age (or (instance? Interval value) (integer? value)) 70 | :expires (or (instance? DateTime value) (string? value)) 71 | true))) 72 | 73 | (defn- write-attr-map 74 | "Write a map of cookie attributes to a string." 75 | [attrs] 76 | {:pre [(every? valid-attr? attrs)]} 77 | (for [[key value] attrs] 78 | (let [attr-name (name (set-cookie-attrs key))] 79 | (cond 80 | (instance? Interval value) (str ";" attr-name "=" (in-seconds value)) 81 | (instance? DateTime value) (str ";" attr-name "=" (unparse rfc822-formatter value)) 82 | (true? value) (str ";" attr-name) 83 | (false? value) "" 84 | :else (str ";" attr-name "=" value))))) 85 | 86 | (defn- write-cookies 87 | "Turn a map of cookies into a seq of strings for a Set-Cookie header." 88 | [cookies encoder] 89 | (for [[key value] cookies] 90 | (if (map? value) 91 | (apply str (write-value key (:value value) encoder) 92 | (write-attr-map (dissoc value :value))) 93 | (write-value key value encoder)))) 94 | 95 | (defn- set-cookies 96 | "Add a Set-Cookie header to a response if there is a :cookies key." 97 | [response encoder] 98 | (if-let [cookies (:cookies response)] 99 | (update-in response 100 | [:headers "Set-Cookie"] 101 | concat 102 | (doall (write-cookies cookies encoder))) 103 | response)) 104 | 105 | (defn cookies-request 106 | "Parses cookies in the request map. See: wrap-cookies." 107 | {:added "1.2"} 108 | ([request] 109 | (cookies-request request {})) 110 | ([request options] 111 | (let [{:keys [decoder] :or {decoder codec/form-decode-str}} options] 112 | (if (request :cookies) 113 | request 114 | (assoc request :cookies (parse-cookies request decoder)))))) 115 | 116 | (defn cookies-response 117 | "For responses with :cookies, adds Set-Cookie header and returns response 118 | without :cookies. See: wrap-cookies." 119 | {:added "1.2"} 120 | ([response] 121 | (cookies-response response {})) 122 | ([response options] 123 | (let [{:keys [encoder] :or {encoder codec/form-encode}} options] 124 | (-> response 125 | (set-cookies encoder) 126 | (dissoc :cookies))))) 127 | 128 | (defn wrap-cookies 129 | "Parses the cookies in the request map, then assocs the resulting map 130 | to the :cookies key on the request. 131 | 132 | Accepts the following options: 133 | 134 | :decoder - a function to decode the cookie value. Expects a function that 135 | takes a string and returns a string. Defaults to URL-decoding. 136 | 137 | :encoder - a function to encode the cookie name and value. Expects a 138 | function that takes a name/value map and returns a string. 139 | Defaults to URL-encoding. 140 | 141 | Each cookie is represented as a map, with its value being held in the 142 | :value key. A cookie may optionally contain a :path, :domain or :port 143 | attribute. 144 | 145 | To set cookies, add a map to the :cookies key on the response. The values 146 | of the cookie map can either be strings, or maps containing the following 147 | keys: 148 | 149 | :value - the new value of the cookie 150 | :path - the subpath the cookie is valid for 151 | :domain - the domain the cookie is valid for 152 | :max-age - the maximum age in seconds of the cookie 153 | :expires - a date string at which the cookie will expire 154 | :secure - set to true if the cookie requires HTTPS, prevent HTTP access 155 | :http-only - set to true if the cookie is valid for HTTP and HTTPS only 156 | (ie. prevent JavaScript access)" 157 | ([handler] 158 | (wrap-cookies handler {})) 159 | ([handler options] 160 | (fn 161 | ([request] 162 | (-> request 163 | (cookies-request options) 164 | handler 165 | (cookies-response options))) 166 | ([request respond raise] 167 | (handler (cookies-request request options) 168 | (fn [response] (respond (cookies-response response options))) 169 | raise))))) 170 | -------------------------------------------------------------------------------- /ring-servlet/src/ring/util/servlet.clj: -------------------------------------------------------------------------------- 1 | (ns ring.util.servlet 2 | "Compatibility functions for turning a ring handler into a Java servlet." 3 | (:require [clojure.java.io :as io] 4 | [clojure.string :as string] 5 | [ring.core.protocols :as protocols]) 6 | (:import [java.io File InputStream FileInputStream] 7 | [java.util Locale] 8 | [javax.servlet AsyncContext] 9 | [javax.servlet.http HttpServlet 10 | HttpServletRequest 11 | HttpServletResponse])) 12 | 13 | (defn- get-headers 14 | "Creates a name/value map of all the request headers." 15 | [^HttpServletRequest request] 16 | (reduce 17 | (fn [headers, ^String name] 18 | (assoc headers 19 | (.toLowerCase name Locale/ENGLISH) 20 | (->> (.getHeaders request name) 21 | (enumeration-seq) 22 | (string/join ",")))) 23 | {} 24 | (enumeration-seq (.getHeaderNames request)))) 25 | 26 | (defn- get-content-length 27 | "Returns the content length, or nil if there is no content." 28 | [^HttpServletRequest request] 29 | (let [length (.getContentLength request)] 30 | (if (>= length 0) length))) 31 | 32 | (defn- get-client-cert 33 | "Returns the SSL client certificate of the request, if one exists." 34 | [^HttpServletRequest request] 35 | (first (.getAttribute request "javax.servlet.request.X509Certificate"))) 36 | 37 | (defn build-request-map 38 | "Create the request map from the HttpServletRequest object." 39 | [^HttpServletRequest request] 40 | {:server-port (.getServerPort request) 41 | :server-name (.getServerName request) 42 | :remote-addr (.getRemoteAddr request) 43 | :uri (.getRequestURI request) 44 | :query-string (.getQueryString request) 45 | :scheme (keyword (.getScheme request)) 46 | :request-method (keyword (.toLowerCase (.getMethod request) Locale/ENGLISH)) 47 | :protocol (.getProtocol request) 48 | :headers (get-headers request) 49 | :content-type (.getContentType request) 50 | :content-length (get-content-length request) 51 | :character-encoding (.getCharacterEncoding request) 52 | :ssl-client-cert (get-client-cert request) 53 | :body (.getInputStream request)}) 54 | 55 | (defn merge-servlet-keys 56 | "Associate servlet-specific keys with the request map for use with legacy 57 | systems." 58 | [request-map 59 | ^HttpServlet servlet 60 | ^HttpServletRequest request 61 | ^HttpServletResponse response] 62 | (merge request-map 63 | {:servlet servlet 64 | :servlet-request request 65 | :servlet-response response 66 | :servlet-context (.getServletContext servlet) 67 | :servlet-context-path (.getContextPath request)})) 68 | 69 | (defn- set-headers 70 | "Update a HttpServletResponse with a map of headers." 71 | [^HttpServletResponse response, headers] 72 | (doseq [[key val-or-vals] headers] 73 | (if (string? val-or-vals) 74 | (.setHeader response key val-or-vals) 75 | (doseq [val val-or-vals] 76 | (.addHeader response key val)))) 77 | ; Some headers must be set through specific methods 78 | (when-let [content-type (get headers "Content-Type")] 79 | (.setContentType response content-type))) 80 | 81 | (defn- make-output-stream 82 | [^HttpServletResponse response ^AsyncContext context] 83 | (if (nil? context) 84 | (.getOutputStream response) 85 | (proxy [java.io.FilterOutputStream] [(.getOutputStream response)] 86 | (close [] 87 | (.complete context) 88 | (proxy-super close))))) 89 | 90 | (defn update-servlet-response 91 | "Update the HttpServletResponse using a response map. Takes an optional 92 | AsyncContext." 93 | ([response response-map] 94 | (update-servlet-response response nil response-map)) 95 | ([^HttpServletResponse response context response-map] 96 | (let [{:keys [status headers body]} response-map] 97 | (when (nil? response) 98 | (throw (NullPointerException. "HttpServletResponse is nil"))) 99 | (when (nil? response-map) 100 | (throw (NullPointerException. "Response map is nil"))) 101 | (when status 102 | (.setStatus response status)) 103 | (set-headers response headers) 104 | (let [output-stream (make-output-stream response context)] 105 | (protocols/write-body-to-stream body response-map output-stream))))) 106 | 107 | (defn- make-blocking-service-method [handler] 108 | (fn [servlet request response] 109 | (-> request 110 | (build-request-map) 111 | (merge-servlet-keys servlet request response) 112 | (handler) 113 | (->> (update-servlet-response response))))) 114 | 115 | (defn- make-async-service-method [handler] 116 | (fn [servlet request ^HttpServletResponse response] 117 | (let [^AsyncContext context (.startAsync request)] 118 | (handler 119 | (-> request 120 | (build-request-map) 121 | (merge-servlet-keys servlet request response)) 122 | (fn [response-map] 123 | (update-servlet-response response context response-map)) 124 | (fn [^Throwable exception] 125 | (.sendError response 500 (.getMessage exception))))))) 126 | 127 | (defn make-service-method 128 | "Turns a handler into a function that takes the same arguments and has the 129 | same return value as the service method in the HttpServlet class." 130 | ([handler] 131 | (make-service-method {})) 132 | ([handler options] 133 | (if (:async? options) 134 | (make-async-service-method handler) 135 | (make-blocking-service-method handler)))) 136 | 137 | (defn servlet 138 | "Create a servlet from a Ring handler." 139 | ([handler] 140 | (servlet handler {})) 141 | ([handler options] 142 | (let [service-method (make-service-method handler options)] 143 | (proxy [HttpServlet] [] 144 | (service [request response] 145 | (service-method this request response)))))) 146 | 147 | (defmacro defservice 148 | "Defines a service method with an optional prefix suitable for being used by 149 | genclass to compile a HttpServlet class. 150 | 151 | For example: 152 | 153 | (defservice my-handler) 154 | (defservice \"my-prefix-\" my-handler)" 155 | ([handler] 156 | `(defservice "-" ~handler)) 157 | ([prefix handler] 158 | (if (map? handler) 159 | `(defservice ~prefix ~handler) 160 | `(defservice ~prefix ~handler {}))) 161 | ([prefix handler options] 162 | `(let [service-method# (make-service-method ~handler ~options)] 163 | (defn ~(symbol (str prefix "service")) 164 | [servlet# request# response#] 165 | (service-method# servlet# request# response#))))) 166 | -------------------------------------------------------------------------------- /ring-jetty-adapter/src/ring/adapter/jetty.clj: -------------------------------------------------------------------------------- 1 | (ns ring.adapter.jetty 2 | "A Ring adapter that uses the Jetty 9 embedded web server. 3 | 4 | Adapters are used to convert Ring handlers into running web servers." 5 | (:require [ring.util.servlet :as servlet]) 6 | (:import [org.eclipse.jetty.server 7 | Request 8 | Server 9 | ServerConnector 10 | ConnectionFactory 11 | HttpConfiguration 12 | HttpConnectionFactory 13 | SslConnectionFactory 14 | SecureRequestCustomizer] 15 | [org.eclipse.jetty.server.handler AbstractHandler] 16 | [org.eclipse.jetty.util.thread ThreadPool QueuedThreadPool] 17 | [org.eclipse.jetty.util.ssl SslContextFactory] 18 | [javax.servlet AsyncContext] 19 | [javax.servlet.http HttpServletRequest HttpServletResponse])) 20 | 21 | (defn- ^AbstractHandler proxy-handler [handler] 22 | (proxy [AbstractHandler] [] 23 | (handle [_ ^Request base-request request response] 24 | (let [request-map (servlet/build-request-map request) 25 | response-map (handler request-map)] 26 | (servlet/update-servlet-response response response-map) 27 | (.setHandled base-request true))))) 28 | 29 | (defn- ^AbstractHandler async-proxy-handler [handler] 30 | (proxy [AbstractHandler] [] 31 | (handle [_ ^Request base-request request ^HttpServletResponse response] 32 | (let [^AsyncContext context (.startAsync request)] 33 | (handler 34 | (servlet/build-request-map request) 35 | (fn [response-map] 36 | (servlet/update-servlet-response response context response-map) 37 | (.setHandled base-request true)) 38 | (fn [^Throwable exception] 39 | (.sendError response 500 (.getMessage exception)) 40 | (.setHandled base-request true))))))) 41 | 42 | (defn- ^ServerConnector server-connector [server & factories] 43 | (ServerConnector. server (into-array ConnectionFactory factories))) 44 | 45 | (defn- ^HttpConfiguration http-config [options] 46 | (doto (HttpConfiguration.) 47 | (.setSendDateHeader (:send-date-header? options true)) 48 | (.setOutputBufferSize (:output-buffer-size options 32768)) 49 | (.setRequestHeaderSize (:request-header-size options 8192)) 50 | (.setResponseHeaderSize (:response-header-size options 8192)) 51 | (.setSendServerVersion (:send-server-version? options true)))) 52 | 53 | (defn- ^ServerConnector http-connector [server options] 54 | (let [http-factory (HttpConnectionFactory. (http-config options))] 55 | (doto (server-connector server http-factory) 56 | (.setPort (options :port 80)) 57 | (.setHost (options :host)) 58 | (.setIdleTimeout (options :max-idle-time 200000))))) 59 | 60 | (defn- ^SslContextFactory ssl-context-factory [options] 61 | (let [context (SslContextFactory.)] 62 | (if (string? (options :keystore)) 63 | (.setKeyStorePath context (options :keystore)) 64 | (.setKeyStore context ^java.security.KeyStore (options :keystore))) 65 | (.setKeyStorePassword context (options :key-password)) 66 | (cond 67 | (string? (options :truststore)) 68 | (.setTrustStorePath context (options :truststore)) 69 | (instance? java.security.KeyStore (options :truststore)) 70 | (.setTrustStore context ^java.security.KeyStore (options :truststore))) 71 | (when (options :trust-password) 72 | (.setTrustStorePassword context (options :trust-password))) 73 | (case (options :client-auth) 74 | :need (.setNeedClientAuth context true) 75 | :want (.setWantClientAuth context true) 76 | nil) 77 | (if-let [exclude-ciphers (options :exclude-ciphers)] 78 | (.addExcludeCipherSuites context (into-array String exclude-ciphers))) 79 | (if-let [exclude-protocols (options :exclude-protocols)] 80 | (.addExcludeProtocols context (into-array String exclude-protocols))) 81 | context)) 82 | 83 | (defn- ^ServerConnector ssl-connector [server options] 84 | (let [ssl-port (options :ssl-port 443) 85 | http-factory (HttpConnectionFactory. 86 | (doto (http-config options) 87 | (.setSecureScheme "https") 88 | (.setSecurePort ssl-port) 89 | (.addCustomizer (SecureRequestCustomizer.)))) 90 | ssl-factory (SslConnectionFactory. 91 | (ssl-context-factory options) 92 | "http/1.1")] 93 | (doto (server-connector server ssl-factory http-factory) 94 | (.setPort ssl-port) 95 | (.setHost (options :host)) 96 | (.setIdleTimeout (options :max-idle-time 200000))))) 97 | 98 | (defn- ^ThreadPool create-threadpool [options] 99 | (let [pool (QueuedThreadPool. ^Integer (options :max-threads 50))] 100 | (.setMinThreads pool (options :min-threads 8)) 101 | (when (:daemon? options false) 102 | (.setDaemon pool true)) 103 | pool)) 104 | 105 | (defn- ^Server create-server [options] 106 | (let [server (Server. (create-threadpool options))] 107 | (when (:http? options true) 108 | (.addConnector server (http-connector server options))) 109 | (when (or (options :ssl?) (options :ssl-port)) 110 | (.addConnector server (ssl-connector server options))) 111 | server)) 112 | 113 | (defn ^Server run-jetty 114 | "Start a Jetty webserver to serve the given handler according to the 115 | supplied options: 116 | 117 | :configurator - a function called with the Jetty Server instance 118 | :async? - if true, treat the handler as asynchronous 119 | :port - the port to listen on (defaults to 80) 120 | :host - the hostname to listen on 121 | :join? - blocks the thread until server ends (defaults to true) 122 | :daemon? - use daemon threads (defaults to false) 123 | :http? - listen on :port for HTTP traffic (defaults to true) 124 | :ssl? - allow connections over HTTPS 125 | :ssl-port - the SSL port to listen on (defaults to 443, implies 126 | :ssl? is true) 127 | :exclude-ciphers - When :ssl? is true, exclude these cipher suites 128 | :exclude-protocols - When :ssl? is true, exclude these protocols 129 | :keystore - the keystore to use for SSL connections 130 | :key-password - the password to the keystore 131 | :truststore - a truststore to use for SSL connections 132 | :trust-password - the password to the truststore 133 | :max-threads - the maximum number of threads to use (default 50) 134 | :min-threads - the minimum number of threads to use (default 8) 135 | :max-idle-time - the maximum idle time in milliseconds for a connection 136 | (default 200000) 137 | :client-auth - SSL client certificate authenticate, may be set to 138 | :need,:want or :none (defaults to :none) 139 | :send-date-header? - add a date header to the response (default true) 140 | :output-buffer-size - the response body buffer size (default 32768) 141 | :request-header-size - the maximum size of a request header (default 8192) 142 | :response-header-size - the maximum size of a response header (default 8192) 143 | :send-server-version? - add Server header to HTTP response (default true)" 144 | [handler options] 145 | (let [server (create-server (dissoc options :configurator)) 146 | proxyf (if (:async? options) async-proxy-handler proxy-handler)] 147 | (.setHandler server (proxyf handler)) 148 | (when-let [configurator (:configurator options)] 149 | (configurator server)) 150 | (try 151 | (.start server) 152 | (when (:join? options true) 153 | (.join server)) 154 | server 155 | (catch Exception ex 156 | (.stop server) 157 | (throw ex))))) 158 | -------------------------------------------------------------------------------- /ring-servlet/test/ring/util/test/servlet.clj: -------------------------------------------------------------------------------- 1 | (ns ring.util.test.servlet 2 | (:require [clojure.test :refer :all] 3 | [ring.util.servlet :refer :all]) 4 | (:import [java.util Locale])) 5 | 6 | (defmacro ^:private with-locale [locale & body] 7 | `(let [old-locale# (Locale/getDefault)] 8 | (try (Locale/setDefault ~locale) 9 | (do ~@body) 10 | (finally (Locale/setDefault old-locale#))))) 11 | 12 | (defn- enumeration [coll] 13 | (let [e (atom coll)] 14 | (proxy [java.util.Enumeration] [] 15 | (hasMoreElements [] (not (empty? @e))) 16 | (nextElement [] (let [f (first @e)] (swap! e rest) f))))) 17 | 18 | (defn- async-context [completed] 19 | (proxy [javax.servlet.AsyncContext] [] 20 | (complete [] (reset! completed true)))) 21 | 22 | (defn- servlet-request [request] 23 | (let [attributes {"javax.servlet.request.X509Certificate" 24 | [(request :ssl-client-cert)]}] 25 | (proxy [javax.servlet.http.HttpServletRequest] [] 26 | (getServerPort [] (request :server-port)) 27 | (getServerName [] (request :server-name)) 28 | (getRemoteAddr [] (request :remote-addr)) 29 | (getRequestURI [] (request :uri)) 30 | (getQueryString [] (request :query-string)) 31 | (getContextPath [] (request :servlet-context-path)) 32 | (getScheme [] (name (request :scheme))) 33 | (getMethod [] (-> request :request-method name .toUpperCase)) 34 | (getProtocol [] (request :protocol)) 35 | (getHeaderNames [] (enumeration (keys (request :headers)))) 36 | (getHeaders [name] (enumeration (get-in request [:headers name]))) 37 | (getContentType [] (request :content-type)) 38 | (getContentLength [] (or (request :content-length) -1)) 39 | (getCharacterEncoding [] (request :character-encoding)) 40 | (getAttribute [k] (attributes k)) 41 | (getInputStream [] (request :body)) 42 | (startAsync [] (async-context (request :completed)))))) 43 | 44 | (defn- servlet-response [response] 45 | (let [output-stream (java.io.ByteArrayOutputStream.)] 46 | (swap! response assoc :body output-stream) 47 | (proxy [javax.servlet.http.HttpServletResponse] [] 48 | (getOutputStream [] 49 | (proxy [javax.servlet.ServletOutputStream] [] 50 | (write 51 | ([b] (.write output-stream b)) 52 | ([b off len] (.write output-stream b off len))))) 53 | (setStatus [status] 54 | (swap! response assoc :status status)) 55 | (setHeader [name value] 56 | (swap! response assoc-in [:headers name] value)) 57 | (setCharacterEncoding [value]) 58 | (setContentType [value] 59 | (swap! response assoc :content-type value))))) 60 | 61 | (defn- servlet-config [] 62 | (proxy [javax.servlet.ServletConfig] [] 63 | (getServletContext [] nil))) 64 | 65 | (defn- run-servlet 66 | ([handler request response] 67 | (run-servlet handler request response {})) 68 | ([handler request response options] 69 | (doto (servlet handler options) 70 | (.init (servlet-config)) 71 | (.service (servlet-request request) 72 | (servlet-response response))))) 73 | 74 | (deftest servlet-test 75 | (let [body (proxy [javax.servlet.ServletInputStream] []) 76 | cert (proxy [java.security.cert.X509Certificate] []) 77 | request {:server-port 8080 78 | :server-name "foobar" 79 | :remote-addr "127.0.0.1" 80 | :uri "/foo" 81 | :query-string "a=b" 82 | :scheme :http 83 | :request-method :get 84 | :protocol "HTTP/1.1" 85 | :headers {"X-Client" ["Foo", "Bar"] 86 | "X-Server" ["Baz"] 87 | "X-Capital-I" ["Qux"]} 88 | :content-type "text/plain" 89 | :content-length 10 90 | :character-encoding "UTF-8" 91 | :servlet-context-path "/foo" 92 | :ssl-client-cert cert 93 | :body body} 94 | response (atom {})] 95 | (letfn [(handler [r] 96 | (are [k v] (= (r k) v) 97 | :server-port 8080 98 | :server-name "foobar" 99 | :remote-addr "127.0.0.1" 100 | :uri "/foo" 101 | :query-string "a=b" 102 | :scheme :http 103 | :request-method :get 104 | :protocol "HTTP/1.1" 105 | :headers {"x-client" "Foo,Bar" 106 | "x-server" "Baz" 107 | "x-capital-i" "Qux"} 108 | :content-type "text/plain" 109 | :content-length 10 110 | :character-encoding "UTF-8" 111 | :servlet-context-path "/foo" 112 | :ssl-client-cert cert 113 | :body body) 114 | {:status 200, :headers {}})] 115 | (testing "request" 116 | (run-servlet handler request response)) 117 | (testing "mapping request header names to lower case" 118 | (with-locale (Locale. "tr") 119 | (run-servlet handler request response)))) 120 | (testing "response" 121 | (letfn [(handler [r] 122 | {:status 200 123 | :headers {"Content-Type" "text/plain" 124 | "X-Server" "Bar"} 125 | :body "Hello World"})] 126 | (run-servlet handler request response) 127 | (is (= (@response :status) 200)) 128 | (is (= (@response :content-type) "text/plain")) 129 | (is (= (get-in @response [:headers "X-Server"]) "Bar")) 130 | (is (= (.toString (@response :body)) "Hello World")))))) 131 | 132 | (deftest servlet-cps-test 133 | (let [handler (fn [_ respond _] 134 | (respond {:status 200 135 | :headers {"Content-Type" "text/plain"} 136 | :body "Hello World"})) 137 | request {:completed (atom false) 138 | :server-port 8080 139 | :server-name "foobar" 140 | :remote-addr "127.0.0.1" 141 | :uri "/foo" 142 | :scheme :http 143 | :request-method :get 144 | :protocol "HTTP/1.1" 145 | :headers {} 146 | :body nil} 147 | response (atom {})] 148 | (run-servlet handler request response {:async? true}) 149 | (is (= @(:completed request) true)) 150 | (is (= (@response :status) 200)) 151 | (is (= (@response :content-type) "text/plain")) 152 | (is (= (.toString (@response :body)) "Hello World")))) 153 | 154 | (defservice "foo-" 155 | (fn [_] 156 | {:status 200 157 | :headers {"Content-Type" "text/plain"} 158 | :body "Hello World"})) 159 | 160 | (deftest defservice-test 161 | (let [body (proxy [javax.servlet.ServletInputStream] []) 162 | servlet (doto (proxy [javax.servlet.http.HttpServlet] []) 163 | (.init (servlet-config))) 164 | request {:server-port 8080 165 | :server-name "foobar" 166 | :remote-addr "127.0.0.1" 167 | :uri "/foo" 168 | :query-string "" 169 | :scheme :http 170 | :request-method :get 171 | :headers {} 172 | :content-type "text/plain" 173 | :content-length 10 174 | :character-encoding "UTF-8" 175 | :body body} 176 | response (atom {})] 177 | (foo-service servlet 178 | (servlet-request request) 179 | (servlet-response response)) 180 | (is (= (@response :status) 200)) 181 | (is (= (get-in @response [:headers "Content-Type" ]) "text/plain")) 182 | (is (= (.toString (@response :body)) "Hello World")))) 183 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/test/cookies.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.test.cookies 2 | (:require [clojure.test :refer :all] 3 | [clojure.string :as str] 4 | [ring.middleware.cookies :refer :all] 5 | [clj-time.core :refer [date-time interval]])) 6 | 7 | (deftest wrap-cookies-basic-cookie 8 | (let [req {:headers {"cookie" "a=b"}} 9 | resp ((wrap-cookies :cookies) req)] 10 | (is (= {"a" {:value "b"}} resp)))) 11 | 12 | (deftest wrap-cookies-multiple-cookies 13 | (let [req {:headers {"cookie" "a=b; c=d,e=f"}} 14 | resp ((wrap-cookies :cookies) req)] 15 | (is (= {"a" {:value "b"}, "c" {:value "d"}, "e" {:value "f"}} 16 | resp)))) 17 | 18 | (deftest wrap-cookies-quoted-cookies 19 | (let [req {:headers {"cookie" "a=\"b\""}} 20 | resp ((wrap-cookies :cookies) req)] 21 | (is (= {"a" {:value "b"}} 22 | resp)))) 23 | 24 | (deftest wrap-cookies-quoted-cookies-no-urlencode 25 | (let [req {:headers {"cookie" "a=U3VwIFdpenpvcmxkCg%3D%3D"}} 26 | resp ((wrap-cookies :cookies {:decoder identity}) req)] 27 | (is (= {"a" {:value "U3VwIFdpenpvcmxkCg%3D%3D"}} 28 | resp)))) 29 | 30 | (deftest wrap-cookies-set-basic-cookie 31 | (let [handler (constantly {:cookies {"a" "b"}}) 32 | resp ((wrap-cookies handler) {})] 33 | (is (= {"Set-Cookie" (list "a=b")} 34 | (:headers resp))))) 35 | 36 | (deftest wrap-cookies-set-multiple-cookies 37 | (let [handler (constantly {:cookies {"a" "b", "c" "d"}}) 38 | resp ((wrap-cookies handler) {})] 39 | (is (= {"Set-Cookie" (list "a=b" "c=d")} 40 | (:headers resp))))) 41 | 42 | (deftest wrap-cookies-set-keyword-cookie 43 | (let [handler (constantly {:cookies {:a "b"}}) 44 | resp ((wrap-cookies handler) {})] 45 | (is (= {"Set-Cookie" (list "a=b")} 46 | (:headers resp))))) 47 | 48 | (defn- split-set-cookie [headers] 49 | (letfn [(split-header [v] (set (mapcat #(str/split % #";") v)))] 50 | (update-in headers ["Set-Cookie"] split-header))) 51 | 52 | (deftest wrap-cookies-set-extra-attrs 53 | (let [cookies {"a" {:value "b", :path "/", :secure true, :http-only true }} 54 | handler (constantly {:cookies cookies}) 55 | resp ((wrap-cookies handler) {})] 56 | (is (= {"Set-Cookie" #{"a=b" "Path=/" "Secure" "HttpOnly"}} 57 | (split-set-cookie (:headers resp)))))) 58 | 59 | (deftest wrap-cookies-always-assocs-map 60 | (let [req {:headers {}} 61 | resp ((wrap-cookies :cookies) req)] 62 | (is (= {} resp)))) 63 | 64 | (deftest wrap-cookies-read-urlencoded 65 | (let [req {:headers {"cookie" "a=hello+world"}} 66 | resp ((wrap-cookies :cookies) req)] 67 | (is (= {"a" {:value "hello world"}} resp)))) 68 | 69 | (deftest wrap-cookies-no-urlencode 70 | (let [req {:headers {"cookie" "a=hello+world"}} 71 | resp ((wrap-cookies :cookies {:decoder identity}) req)] 72 | (is (= {"a" {:value "hello+world"}} resp)))) 73 | 74 | (deftest wrap-cookies-no-urlencode-base64 75 | (let [req {:headers {"cookie" "a=U3VwIFdpenpvcmxkCg=="}} 76 | resp ((wrap-cookies :cookies {:decoder identity}) req)] 77 | (is (= {"a" {:value "U3VwIFdpenpvcmxkCg=="}} resp)))) 78 | 79 | (deftest wrap-cookies-set-urlencoded-cookie 80 | (let [handler (constantly {:cookies {"a" "hello world"}}) 81 | resp ((wrap-cookies handler) {})] 82 | (is (= {"Set-Cookie" (list "a=hello+world")} 83 | (:headers resp))))) 84 | 85 | (deftest wrap-cookies-set-no-urlencoded-cookie 86 | (let [handler (constantly {:cookies {"a" "hello world"}}) 87 | resp ((wrap-cookies handler {:encoder 88 | (fn [m] 89 | (apply format "%s=%s" (first (seq m))))}) {})] 90 | (is (= {"Set-Cookie" (list "a=hello world")} 91 | (:headers resp))))) 92 | 93 | (deftest wrap-cookies-invalid-url-encoded 94 | (let [req {:headers {"cookie" "a=%D"}} 95 | resp ((wrap-cookies :cookies) req)] 96 | (is (= {} resp)))) 97 | 98 | (deftest wrap-cookies-keep-set-cookies-intact 99 | (let [handler (constantly {:headers {"Set-Cookie" (list "a=b")} 100 | :cookies {:c "d"}}) 101 | resp ((wrap-cookies handler) {})] 102 | (is (= {"Set-Cookie" (list "a=b" "c=d")} 103 | (:headers resp))))) 104 | 105 | (deftest wrap-cookies-invalid-attrs 106 | (let [response {:cookies {"a" {:value "foo" :invalid true}}} 107 | handler (wrap-cookies (constantly response))] 108 | (is (thrown? AssertionError (handler {}))))) 109 | 110 | (deftest wrap-cookies-accepts-max-age 111 | (let [cookies {"a" {:value "b", :path "/", 112 | :secure true, :http-only true, 113 | :max-age 123}} 114 | handler (constantly {:cookies cookies}) 115 | resp ((wrap-cookies handler) {})] 116 | (is (= {"Set-Cookie" #{"a=b" "Path=/" "Secure" "HttpOnly" "Max-Age=123"}} 117 | (split-set-cookie (:headers resp)))))) 118 | 119 | (deftest wrap-cookies-accepts-expires 120 | (let [cookies {"a" {:value "b", :path "/", 121 | :secure true, :http-only true, 122 | :expires "123"}} 123 | handler (constantly {:cookies cookies}) 124 | resp ((wrap-cookies handler) {})] 125 | (is (= {"Set-Cookie" #{"a=b" "Path=/" "Secure" "HttpOnly" "Expires=123"}} 126 | (split-set-cookie (:headers resp)))))) 127 | 128 | (deftest wrap-cookies-accepts-max-age-from-clj-time 129 | (let [cookies {"a" {:value "b", :path "/", 130 | :secure true, :http-only true, 131 | :max-age (interval (date-time 2012) 132 | (date-time 2015))}} 133 | handler (constantly {:cookies cookies}) 134 | resp ((wrap-cookies handler) {}) 135 | max-age 94694400] 136 | (is (= {"Set-Cookie" #{"a=b" "Path=/" "Secure" "HttpOnly" (str "Max-Age=" max-age)}} 137 | (split-set-cookie (:headers resp)))))) 138 | 139 | (deftest wrap-cookies-accepts-expires-from-clj-time 140 | (let [cookies {"a" {:value "b", :path "/", 141 | :secure true, :http-only true, 142 | :expires (date-time 2015 12 31)}} 143 | handler (constantly {:cookies cookies}) 144 | resp ((wrap-cookies handler) {}) 145 | expires "Thu, 31 Dec 2015 00:00:00 +0000"] 146 | (is (= {"Set-Cookie" #{"a=b" "Path=/" "Secure" "HttpOnly" (str "Expires=" expires)}} 147 | (split-set-cookie (:headers resp)))))) 148 | 149 | (deftest wrap-cookies-accepts-expires-from-clj-time-in-non-us-locale 150 | (let [default-locale (java.util.Locale/getDefault)] 151 | (try 152 | (java.util.Locale/setDefault java.util.Locale/FRANCE) 153 | (let [cookies {"a" {:value "b", :path "/", 154 | :secure true, :http-only true, 155 | :expires (date-time 2015 12 31)}} 156 | handler (constantly {:cookies cookies}) 157 | resp ((wrap-cookies handler) {}) 158 | expires "Thu, 31 Dec 2015 00:00:00 +0000"] 159 | (is (= {"Set-Cookie" #{"a=b" "Path=/" "Secure" "HttpOnly" (str "Expires=" expires)}} 160 | (split-set-cookie (:headers resp))))) 161 | (finally 162 | (java.util.Locale/setDefault default-locale))))) 163 | 164 | (deftest wrap-cookies-throws-exception-when-not-using-intervals-correctly 165 | (let [cookies {"a" {:value "b", :path "/", 166 | :secure true, :http-only true, 167 | :expires (interval (date-time 2012) 168 | (date-time 2015))}} 169 | handler (constantly {:cookies cookies})] 170 | (is (thrown? AssertionError ((wrap-cookies handler) {}))))) 171 | 172 | (deftest wrap-cookies-throws-exception-when-not-using-datetime-correctly 173 | (let [cookies {"a" {:value "b", :path "/", 174 | :secure true, :http-only true, 175 | :max-age (date-time 2015 12 31)}} 176 | handler (constantly {:cookies cookies})] 177 | (is (thrown? AssertionError ((wrap-cookies handler) {}))))) 178 | 179 | (deftest parse-cookies-on-request-basic-cookie 180 | (let [req {:headers {"cookie" "a=b"}}] 181 | (is (= {"a" {:value "b"}} 182 | ((cookies-request req) :cookies))))) 183 | 184 | (deftest parse-cookies-on-request-multiple-cookies 185 | (let [req {:headers {"cookie" "a=b; c=d,e=f"}}] 186 | (is (= {"a" {:value "b"}, "c" {:value "d"}, "e" {:value "f"}} 187 | ((cookies-request req) :cookies))))) 188 | 189 | (deftest parse-cookies-url-encoding 190 | (let [req {:headers {"cookie" "a=%22/test%22"}}] 191 | (is (= {"a" {:value "\"/test\""}} ((cookies-request req) :cookies))))) 192 | 193 | (deftest wrap-cookies-cps-test 194 | (testing "read cookie" 195 | (let [handler (wrap-cookies (fn [req respond _] (respond (:cookies req)))) 196 | request {:headers {"cookie" "a=b"}} 197 | response (promise) 198 | exception (promise)] 199 | (handler request response exception) 200 | (is (= {"a" {:value "b"}} @response)) 201 | (is (not (realized? exception))))) 202 | 203 | (testing "write cookie" 204 | (let [handler (wrap-cookies (fn [_ respond _] (respond {:cookies {"a" "b"}}))) 205 | response (promise) 206 | exception (promise)] 207 | (handler {} response exception) 208 | (is (= {"Set-Cookie" (list "a=b")} (:headers @response))) 209 | (is (not (realized? exception)))))) 210 | 211 | (deftest cookies-response-test 212 | (is (fn? cookies-response))) 213 | 214 | (deftest cookies-request-test 215 | (is (fn? cookies-request))) 216 | -------------------------------------------------------------------------------- /ring-core/test/ring/middleware/test/session.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.test.session 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.session :refer :all] 4 | [ring.middleware.session.store :refer :all] 5 | [ring.middleware.session.memory :refer [memory-store]])) 6 | 7 | (defn- make-store [reader writer deleter] 8 | (reify SessionStore 9 | (read-session [_ k] (reader k)) 10 | (write-session [_ k s] (writer k s)) 11 | (delete-session [_ k] (deleter k)))) 12 | 13 | (defn trace-fn [f] 14 | (let [trace (atom [])] 15 | (with-meta 16 | (fn [& args] 17 | (swap! trace conj args) 18 | (apply f args)) 19 | {:trace trace}))) 20 | 21 | (defn trace [f] 22 | (-> f meta :trace deref)) 23 | 24 | (defn get-cookies [response] 25 | (get-in response [:headers "Set-Cookie"])) 26 | 27 | (defn is-session-cookie? [c] 28 | (.contains c "ring-session=")) 29 | 30 | (defn get-session-cookie [response] 31 | (first (filter is-session-cookie? (get-cookies response)))) 32 | 33 | (deftest session-is-read 34 | (let [reader (trace-fn (constantly {:bar "foo"})) 35 | writer (trace-fn (constantly nil)) 36 | deleter (trace-fn (constantly nil)) 37 | store (make-store reader writer deleter) 38 | handler (trace-fn (constantly {})) 39 | handler* (wrap-session handler {:store store})] 40 | (handler* {:cookies {"ring-session" {:value "test"}}}) 41 | (is (= (trace reader) [["test"]])) 42 | (is (= (trace writer) [])) 43 | (is (= (trace deleter) [])) 44 | (is (= (-> handler trace first first :session) 45 | {:bar "foo"})))) 46 | 47 | (deftest session-is-written 48 | (let [reader (trace-fn (constantly {})) 49 | writer (trace-fn (constantly nil)) 50 | deleter (trace-fn (constantly nil)) 51 | store (make-store reader writer deleter) 52 | handler (constantly {:session {:foo "bar"}}) 53 | handler (wrap-session handler {:store store})] 54 | (handler {:cookies {}}) 55 | (is (= (trace reader) [[nil]])) 56 | (is (= (trace writer) [[nil {:foo "bar"}]])) 57 | (is (= (trace deleter) [])))) 58 | 59 | (deftest session-is-deleted 60 | (let [reader (trace-fn (constantly {})) 61 | writer (trace-fn (constantly nil)) 62 | deleter (trace-fn (constantly nil)) 63 | store (make-store reader writer deleter) 64 | handler (constantly {:session nil}) 65 | handler (wrap-session handler {:store store})] 66 | (handler {:cookies {"ring-session" {:value "test"}}}) 67 | (is (= (trace reader) [["test"]])) 68 | (is (= (trace writer) [])) 69 | (is (= (trace deleter) [["test"]])))) 70 | 71 | (deftest session-write-outputs-cookie 72 | (let [store (make-store (constantly {}) 73 | (constantly "foo:bar") 74 | (constantly nil)) 75 | handler (constantly {:session {:foo "bar"}}) 76 | handler (wrap-session handler {:store store}) 77 | response (handler {:cookies {}})] 78 | (is (get-session-cookie response)))) 79 | 80 | (deftest session-delete-outputs-cookie 81 | (let [store (make-store (constantly {:foo "bar"}) 82 | (constantly nil) 83 | (constantly "deleted")) 84 | handler (constantly {:session nil}) 85 | handler (wrap-session handler {:store store}) 86 | response (handler {:cookies {"ring-session" {:value "foo:bar"}}})] 87 | (is (.contains (get-session-cookie response) 88 | "ring-session=deleted")))) 89 | 90 | (deftest session-cookie-has-attributes 91 | (let [store (make-store (constantly {}) 92 | (constantly "foo:bar") 93 | (constantly nil)) 94 | handler (constantly {:session {:foo "bar"}}) 95 | handler (wrap-session handler {:store store 96 | :cookie-attrs {:max-age 5 :path "/foo"}}) 97 | response (handler {:cookies {}}) 98 | session-cookie (get-session-cookie response)] 99 | (is (and (.contains session-cookie "ring-session=foo%3Abar") 100 | (.contains session-cookie "Max-Age=5") 101 | (.contains session-cookie "Path=/foo") 102 | (.contains session-cookie "HttpOnly"))))) 103 | 104 | (deftest session-does-not-clobber-response-cookies 105 | (let [store (make-store (constantly {}) 106 | (constantly "foo:bar") 107 | (constantly nil)) 108 | handler (constantly {:session {:foo "bar"} 109 | :cookies {"cookie2" "value2"}}) 110 | handler (wrap-session handler {:store store :cookie-attrs {:max-age 5}}) 111 | response (handler {:cookies {}})] 112 | (is (= (first (remove is-session-cookie? (get-cookies response))) 113 | "cookie2=value2")))) 114 | 115 | (deftest session-root-can-be-set 116 | (let [store (make-store (constantly {}) 117 | (constantly "foo:bar") 118 | (constantly nil)) 119 | handler (constantly {:session {:foo "bar"}}) 120 | handler (wrap-session handler {:store store, :root "/foo"}) 121 | response (handler {:cookies {}})] 122 | (is (.contains (get-session-cookie response) 123 | "Path=/foo")))) 124 | 125 | (deftest session-attrs-can-be-set-per-request 126 | (let [store (make-store (constantly {}) 127 | (constantly "foo:bar") 128 | (constantly nil)) 129 | handler (constantly {:session {:foo "bar"} 130 | :session-cookie-attrs {:max-age 5}}) 131 | handler (wrap-session handler {:store store}) 132 | response (handler {:cookies {}})] 133 | (is (.contains (get-session-cookie response) 134 | "Max-Age=5")))) 135 | 136 | (deftest cookie-attrs-override-is-respected 137 | (let [store (make-store (constantly {}) 138 | (constantly {}) 139 | (constantly nil)) 140 | handler (constantly {:session {}}) 141 | handler (wrap-session handler {:store store :cookie-attrs {:http-only false}}) 142 | response (handler {:cookies {}})] 143 | (is (not (.contains (get-session-cookie response) 144 | "HttpOnly"))))) 145 | 146 | (deftest session-response-is-nil 147 | (let [handler (wrap-session (constantly nil))] 148 | (is (nil? (handler {}))))) 149 | 150 | (deftest session-made-up-key 151 | (let [store-ref (atom {}) 152 | store (make-store 153 | #(@store-ref %) 154 | #(do (swap! store-ref assoc %1 %2) %1) 155 | #(do (swap! store-ref dissoc %) nil)) 156 | handler (wrap-session 157 | (constantly {:session {:foo "bar"}}) 158 | {:store store})] 159 | (handler {:cookies {"ring-session" {:value "faked-key"}}}) 160 | (is (not (contains? @store-ref "faked-key"))))) 161 | 162 | (deftest session-request-test 163 | (is (fn? session-request))) 164 | 165 | (deftest session-response-test 166 | (is (fn? session-response))) 167 | 168 | (deftest session-cookie-attrs-change 169 | (let [a-resp (atom {:session {:foo "bar"}}) 170 | handler (wrap-session (fn [req] @a-resp)) 171 | response (handler {}) 172 | sess-key (->> (get-in response [:headers "Set-Cookie"]) 173 | (first) 174 | (re-find #"(?<==)[^;]+"))] 175 | (is (not (nil? sess-key))) 176 | (reset! a-resp {:session-cookie-attrs {:max-age 3600}}) 177 | 178 | (testing "Session cookie attrs with no active session" 179 | (is (= (handler {}) {}))) 180 | 181 | (testing "Session cookie attrs with active session" 182 | (let [response (handler {:foo "bar" :cookies {"ring-session" {:value sess-key}}})] 183 | (is (get-session-cookie response)))))) 184 | 185 | (deftest session-is-recreated-when-recreate-key-present-in-metadata 186 | (let [reader (trace-fn (constantly {})) 187 | writer (trace-fn (constantly nil)) 188 | deleter (trace-fn (constantly nil)) 189 | store (make-store reader writer deleter) 190 | handler (constantly {:session ^:recreate {:foo "bar"}}) 191 | handler (wrap-session handler {:store store})] 192 | (handler {:cookies {"ring-session" {:value "test"}}}) 193 | (is (= (trace reader) [["test"]])) 194 | (is (= (trace writer) [[nil {:foo "bar"}]])) 195 | (is (= (trace deleter) [["test"]])) 196 | (testing "session was not written with :recreate metadata intact" 197 | (let [[[_ written]] (trace writer)] 198 | (is (not (:recreate (meta written)))))))) 199 | 200 | (deftest wrap-sesssion-cps-test 201 | (testing "reading session" 202 | (let [memory (atom {"test" {:foo "bar"}}) 203 | store (memory-store memory) 204 | handler (-> (fn [req respond _] (respond (:session req))) 205 | (wrap-session {:store store})) 206 | request {:cookies {"ring-session" {:value "test"}}} 207 | response (promise) 208 | exception (promise)] 209 | (handler request response exception) 210 | (is (= {:foo "bar"} @response)) 211 | (is (= {"test" {:foo "bar"}} @memory)) 212 | (is (not (realized? exception))))) 213 | 214 | (testing "writing session" 215 | (let [memory (atom {"test" {}}) 216 | store (memory-store memory) 217 | handler (-> (fn [_ respond _] (respond {:session {:foo "bar"}, :body "foo"})) 218 | (wrap-session {:store store})) 219 | request {:cookies {"ring-session" {:value "test"}}} 220 | response (promise) 221 | exception (promise)] 222 | (handler request response exception) 223 | (is (= "foo" (:body @response))) 224 | (is (= {"test" {:foo "bar"}} @memory)) 225 | (is (not (realized? exception)))))) 226 | -------------------------------------------------------------------------------- /ring-core/test/ring/util/test/response.clj: -------------------------------------------------------------------------------- 1 | (ns ring.util.test.response 2 | (:require [clojure.test :refer :all] 3 | [clojure.java.io :as io] 4 | [ring.util.response :refer :all]) 5 | (:import [java.io File InputStream] 6 | [org.apache.commons io.FileUtils])) 7 | 8 | (deftest test-redirect 9 | (is (= {:status 302 :headers {"Location" "http://google.com"} :body ""} 10 | (redirect "http://google.com"))) 11 | (are [x y] (= (->> x 12 | (redirect "/foo") 13 | :status) 14 | y) 15 | :moved-permanently 301 16 | :found 302 17 | :see-other 303 18 | :temporary-redirect 307 19 | :permanent-redirect 308 20 | 300 300)) 21 | 22 | (deftest test-redirect-after-post 23 | (is (= {:status 303 :headers {"Location" "http://example.com"} :body ""} 24 | (redirect-after-post "http://example.com")))) 25 | 26 | (deftest test-not-found 27 | (is (= {:status 404 :headers {} :body "Not found"} 28 | (not-found "Not found")))) 29 | 30 | (deftest test-created 31 | (testing "with location and without body" 32 | (is (= {:status 201 :headers {"Location" "foobar/location"} :body nil} 33 | (created "foobar/location")))) 34 | (testing "with body and with location" 35 | (is (= {:status 201 :headers {"Location" "foobar/location"} :body "foobar"} 36 | (created "foobar/location" "foobar"))))) 37 | 38 | (deftest test-response 39 | (is (= {:status 200 :headers {} :body "foobar"} 40 | (response "foobar")))) 41 | 42 | (deftest test-status 43 | (is (= {:status 200 :body ""} (status {:status nil :body ""} 200)))) 44 | 45 | (deftest test-content-type 46 | (is (= {:status 200 :headers {"Content-Type" "text/html" "Content-Length" "10"}} 47 | (content-type {:status 200 :headers {"Content-Length" "10"}} 48 | "text/html")))) 49 | 50 | (deftest test-charset 51 | (testing "add charset" 52 | (is (= (charset {:status 200 :headers {"Content-Type" "text/html"}} "UTF-8") 53 | {:status 200 :headers {"Content-Type" "text/html; charset=UTF-8"}}))) 54 | (testing "replace existing charset" 55 | (is (= (charset {:status 200 :headers {"Content-Type" "text/html; charset=UTF-16"}} 56 | "UTF-8") 57 | {:status 200 :headers {"Content-Type" "text/html; charset=UTF-8"}}))) 58 | (testing "default content-type" 59 | (is (= (charset {:status 200 :headers {}} "UTF-8") 60 | {:status 200 :headers {"Content-Type" "text/plain; charset=UTF-8"}}))) 61 | (testing "case insensitive" 62 | (is (= (charset {:status 200 :headers {"content-type" "text/html"}} "UTF-8") 63 | {:status 200 :headers {"content-type" "text/html; charset=UTF-8"}})))) 64 | 65 | (deftest test-get-charset 66 | (testing "simple charset" 67 | (is (= (get-charset {:headers {"Content-Type" "text/plain; charset=UTF-8"}}) 68 | "UTF-8"))) 69 | (testing "case insensitive" 70 | (is (= (get-charset {:headers {"content-type" "text/plain; charset=UTF-16"}}) 71 | "UTF-16"))) 72 | (testing "missing charset" 73 | (is (nil? (get-charset {:headers {"Content-Type" "text/plain"}})))) 74 | (testing "missing content-type" 75 | (is (nil? (get-charset {:headers {}}))))) 76 | 77 | (deftest test-header 78 | (is (= {:status 200 :headers {"X-Foo" "Bar"}} 79 | (header {:status 200 :headers {}} "X-Foo" "Bar")))) 80 | 81 | (deftest test-response? 82 | (is (response? {:status 200, :headers {}})) 83 | (is (response? {:status 200, :headers {}, :body "Foo"})) 84 | (is (not (response? {}))) 85 | (is (not (response? {:users []})))) 86 | 87 | (defmacro with-classloader 88 | "Temporarily replaces the current context classloader with one that 89 | includes everything in dir" 90 | [[dir] & forms] 91 | `(let [current-thread# (Thread/currentThread) 92 | original-loader# (.getContextClassLoader current-thread#) 93 | new-loader# (java.net.URLClassLoader. (into-array [(.toURL ~dir)]) 94 | original-loader#)] 95 | (try 96 | (.setContextClassLoader current-thread# new-loader#) 97 | ~@forms 98 | (finally 99 | (.setContextClassLoader current-thread# original-loader#))))) 100 | 101 | (defn- make-jar-url [jar-path res-path] 102 | (io/as-url (str "jar:file:" jar-path "!/" res-path))) 103 | 104 | (deftest test-url-response 105 | (testing "resources from a jar file" 106 | (let [base-path (.getPath (io/resource "ring/assets/test-resource.jar")) 107 | root-url (make-jar-url base-path "public/") 108 | empty-resource (make-jar-url base-path "public/empty-resource") 109 | non-empty-resource (make-jar-url base-path "public/hi-resource")] 110 | (is (nil? (url-response root-url))) 111 | (is (slurp (:body (url-response empty-resource))) "") 112 | (is (slurp (:body (url-response non-empty-resource))) "hi")))) 113 | 114 | (deftest test-resource-response 115 | (testing "response map" 116 | (let [resp (resource-response "/ring/assets/foo.html")] 117 | (is (= (resp :status) 200)) 118 | (is (= (into #{} (keys (resp :headers))) #{"Content-Length" "Last-Modified"})) 119 | (is (= (slurp (resp :body)) "foo")))) 120 | 121 | (testing "with root option" 122 | (let [resp (resource-response "/foo.html" {:root "/ring/assets"})] 123 | (is (= (slurp (resp :body)) "foo")))) 124 | 125 | (testing "with child class-loader" 126 | (let [resource (File/createTempFile "response_test" nil)] 127 | (FileUtils/writeStringToFile resource "just testing") 128 | (with-classloader [(.getParentFile resource)] 129 | (let [resp (resource-response (.getName resource))] 130 | (is (= (slurp (resp :body)) 131 | "just testing")))))) 132 | 133 | (testing "missing resource" 134 | (is (nil? (resource-response "/missing/resource.clj")))) 135 | 136 | (testing "response body type" 137 | (let [body (:body (resource-response "ring/util/response.clj"))] 138 | (is (instance? File body)) 139 | (is (.startsWith (slurp body) "(ns ring.util.response"))) 140 | (let [body (:body (resource-response "clojure/java/io.clj"))] 141 | (is (instance? InputStream body)) 142 | (is (.contains (slurp body) "clojure.java.io")))) 143 | 144 | (testing "resource is a directory" 145 | (is (nil? (resource-response "/ring/assets")))) 146 | 147 | (testing "resource is a directory in a jar file" 148 | (is (nil? (resource-response "/clojure/lang")))) 149 | 150 | (testing "resource is a directory in a jar file with a trailing slash" 151 | (is (nil? (resource-response "/clojure/lang/")))) 152 | 153 | (testing "resource is a file with spaces in path" 154 | (let [resp (resource-response "/ring/assets/hello world.txt")] 155 | (is (= (:body resp) 156 | (.getAbsoluteFile (File. "test/ring/assets/hello world.txt")))) 157 | (is (= (slurp (:body resp)) 158 | "Hello World\n")))) 159 | 160 | (testing "nil resource-data values" 161 | (defmethod resource-data :http [_] {}) 162 | (with-redefs [io/resource (constantly (java.net.URL. "http://foo"))] 163 | (is (= (response nil) (resource-response "whatever"))))) 164 | 165 | (comment 166 | ;; This test requires the ability to have file names in the source 167 | ;; tree with non-ASCII characters in them encoded as UTF-8. That 168 | ;; may be platform-specific. Comment out for now. 169 | 170 | ;; If this fails on Mac OS X, try again with the command line: 171 | ;; LC_CTYPE="UTF-8" lein test 172 | (testing "resource is a file with UTF-8 characters in path" 173 | (let [resp (resource-response "/ring/assets/abcíe.txt")] 174 | (is (= (:body resp) 175 | (.getAbsoluteFile (File. "test/ring/assets/abcíe.txt")))) 176 | (is (.contains (slurp (:body resp)) "UTF-8")))))) 177 | 178 | (deftest test-file-response 179 | (testing "response map" 180 | (let [resp (file-response "foo.html" {:root "test/ring/assets"})] 181 | (is (= (resp :status) 200)) 182 | (is (= (into #{} (keys (resp :headers))) #{"Content-Length" "Last-Modified"})) 183 | (is (= (get-in resp [:headers "Content-Length"]) "3")) 184 | (is (= (slurp (resp :body)) "foo")))) 185 | 186 | (testing "file path cannot contain '..' " 187 | (is (nil? (file-response "../../../project.clj" {:root "test/ring/assets"}))) 188 | (is (nil? (file-response "../../../project.clj" {:root "test/ring/assets/bars" :allow-symlinks? true})))) 189 | 190 | (testing "file response optionally follows symlinks" 191 | (let [resp (file-response "backlink/foo.html" {:root "test/ring/assets/bars" :allow-symlinks? true})] 192 | (is (= (resp :status) 200)) 193 | (is (= (into #{} (keys (resp :headers))) #{"Content-Length" "Last-Modified"})) 194 | (is (= (get-in resp [:headers "Content-Length"]) "3")) 195 | (is (= (slurp (resp :body)) "foo"))) 196 | 197 | (is (nil? (file-response "backlink/foo.html" {:root "test/ring/assets/bars"}))))) 198 | 199 | (deftest test-set-cookie 200 | (is (= {:status 200 :headers {} :cookies {"Foo" {:value "Bar"}}} 201 | (set-cookie {:status 200 :headers {}} 202 | "Foo" "Bar"))) 203 | (is (= {:status 200 :headers {} :cookies {"Foo" {:value "Bar" :http-only true}}} 204 | (set-cookie {:status 200 :headers {}} 205 | "Foo" "Bar" {:http-only true})))) 206 | 207 | (deftest test-find-header 208 | (is (= (find-header {:headers {"Content-Type" "text/plain"}} "Content-Type") 209 | ["Content-Type" "text/plain"])) 210 | (is (= (find-header {:headers {"content-type" "text/plain"}} "Content-Type") 211 | ["content-type" "text/plain"])) 212 | (is (= (find-header {:headers {"Content-typE" "text/plain"}} "content-type") 213 | ["Content-typE" "text/plain"])) 214 | (is (nil? (find-header {:headers {"Content-Type" "text/plain"}} "content-length")))) 215 | 216 | (deftest test-get-header 217 | (is (= (get-header {:headers {"Content-Type" "text/plain"}} "Content-Type") 218 | "text/plain")) 219 | (is (= (get-header {:headers {"content-type" "text/plain"}} "Content-Type") 220 | "text/plain")) 221 | (is (= (get-header {:headers {"Content-typE" "text/plain"}} "content-type") 222 | "text/plain")) 223 | (is (nil? (get-header {:headers {"Content-Type" "text/plain"}} "content-length")))) 224 | 225 | (deftest test-update-header 226 | (is (= (update-header {:headers {"Content-Type" "text/plain"}} 227 | "content-type" 228 | str "; charset=UTF-8") 229 | {:headers {"Content-Type" "text/plain; charset=UTF-8"}})) 230 | (is (= (update-header {:headers {}} 231 | "content-type" 232 | str "; charset=UTF-8") 233 | {:headers {"content-type" "; charset=UTF-8"}}))) 234 | -------------------------------------------------------------------------------- /ring-core/src/ring/util/response.clj: -------------------------------------------------------------------------------- 1 | (ns ring.util.response 2 | "Functions for generating and augmenting response maps." 3 | (:require [clojure.java.io :as io] 4 | [clojure.string :as str] 5 | [ring.util.io :refer [last-modified-date]] 6 | [ring.util.parsing :refer [re-charset]] 7 | [ring.util.time :refer [format-date]]) 8 | (:import [java.io File] 9 | [java.util Date] 10 | [java.net URL URLDecoder URLEncoder])) 11 | 12 | (def ^{:added "1.4"} redirect-status-codes 13 | "Map a keyword to a redirect status code." 14 | {:moved-permanently 301 15 | :found 302 16 | :see-other 303 17 | :temporary-redirect 307 18 | :permanent-redirect 308}) 19 | 20 | (defn redirect 21 | "Returns a Ring response for an HTTP 302 redirect. Status may be 22 | a key in redirect-status-codes or a numeric code. Defaults to 302" 23 | ([url] (redirect url :found)) 24 | ([url status] 25 | {:status (redirect-status-codes status status) 26 | :headers {"Location" url} 27 | :body ""})) 28 | 29 | (defn redirect-after-post 30 | "Returns a Ring response for an HTTP 303 redirect. Deprecated in favor 31 | of using redirect with a :see-other status." 32 | {:deprecated "1.4"} 33 | [url] 34 | {:status 303 35 | :headers {"Location" url} 36 | :body ""}) 37 | 38 | (defn created 39 | "Returns a Ring response for a HTTP 201 created response." 40 | {:added "1.2"} 41 | ([url] (created url nil)) 42 | ([url body] 43 | {:status 201 44 | :headers {"Location" url} 45 | :body body})) 46 | 47 | (defn not-found 48 | "Returns a 404 'not found' response." 49 | {:added "1.1"} 50 | [body] 51 | {:status 404 52 | :headers {} 53 | :body body}) 54 | 55 | (defn response 56 | "Returns a skeletal Ring response with the given body, status of 200, and no 57 | headers." 58 | [body] 59 | {:status 200 60 | :headers {} 61 | :body body}) 62 | 63 | (defn status 64 | "Returns an updated Ring response with the given status." 65 | [resp status] 66 | (assoc resp :status status)) 67 | 68 | (defn header 69 | "Returns an updated Ring response with the specified header added." 70 | [resp name value] 71 | (assoc-in resp [:headers name] (str value))) 72 | 73 | (defn- safe-path? 74 | "Is a filepath safe for a particular root?" 75 | [^String root ^String path] 76 | (.startsWith (.getCanonicalPath (File. root path)) 77 | (.getCanonicalPath (File. root)))) 78 | 79 | (defn- directory-transversal? 80 | "Check if a path contains '..'." 81 | [^String path] 82 | (-> (str/split path #"/|\\") 83 | (set) 84 | (contains? ".."))) 85 | 86 | (defn- find-file-named [^File dir ^String filename] 87 | (let [path (File. dir filename)] 88 | (if (.isFile path) 89 | path))) 90 | 91 | (defn- find-file-starting-with [^File dir ^String prefix] 92 | (first 93 | (filter 94 | #(.startsWith (.toLowerCase (.getName ^File %)) prefix) 95 | (.listFiles dir)))) 96 | 97 | (defn- find-index-file 98 | "Search the directory for an index file." 99 | [^File dir] 100 | (or (find-file-named dir "index.html") 101 | (find-file-named dir "index.htm") 102 | (find-file-starting-with dir "index."))) 103 | 104 | (defn- safely-find-file [^String path opts] 105 | (if-let [^String root (:root opts)] 106 | (if (or (safe-path? root path) 107 | (and (:allow-symlinks? opts) (not (directory-transversal? path)))) 108 | (File. root path)) 109 | (File. path))) 110 | 111 | (defn- find-file [^String path opts] 112 | (if-let [^File file (safely-find-file path opts)] 113 | (cond 114 | (.isDirectory file) 115 | (and (:index-files? opts true) (find-index-file file)) 116 | (.exists file) 117 | file))) 118 | 119 | (defn- file-data [^File file] 120 | {:content file 121 | :content-length (.length file) 122 | :last-modified (last-modified-date file)}) 123 | 124 | (defn- content-length [resp len] 125 | (if len 126 | (header resp "Content-Length" len) 127 | resp)) 128 | 129 | (defn- last-modified [resp last-mod] 130 | (if last-mod 131 | (header resp "Last-Modified" (format-date last-mod)) 132 | resp)) 133 | 134 | (defn file-response 135 | "Returns a Ring response to serve a static file, or nil if an appropriate 136 | file does not exist. 137 | Options: 138 | :root - take the filepath relative to this root path 139 | :index-files? - look for index.* files in directories, defaults to true 140 | :allow-symlinks? - serve files through symbolic links, defaults to false" 141 | [filepath & [opts]] 142 | (if-let [file (find-file filepath opts)] 143 | (let [data (file-data file)] 144 | (-> (response (:content data)) 145 | (content-length (:content-length data)) 146 | (last-modified (:last-modified data)))))) 147 | 148 | ;; In Clojure 1.5.1, the as-file function does not correctly decode 149 | ;; UTF-8 byte sequences. 150 | ;; 151 | ;; See: http://dev.clojure.org/jira/browse/CLJ-1177 152 | ;; 153 | ;; As a work-around, we'll backport the fix from CLJ-1177 into 154 | ;; url-as-file. 155 | 156 | (defn- ^File url-as-file [^java.net.URL u] 157 | (-> (.getFile u) 158 | (str/replace \/ File/separatorChar) 159 | (str/replace "+" (URLEncoder/encode "+" "UTF-8")) 160 | (URLDecoder/decode "UTF-8") 161 | io/as-file)) 162 | 163 | (defn content-type 164 | "Returns an updated Ring response with the a Content-Type header corresponding 165 | to the given content-type." 166 | [resp content-type] 167 | (header resp "Content-Type" content-type)) 168 | 169 | (defn find-header 170 | "Looks up a header in a Ring response (or request) case insensitively, 171 | returning the header map entry, or nil if not present." 172 | {:added "1.4"} 173 | [resp ^String header-name] 174 | (->> (:headers resp) 175 | (filter #(.equalsIgnoreCase header-name (key %))) 176 | (first))) 177 | 178 | (defn get-header 179 | "Looks up a header in a Ring response (or request) case insensitively, 180 | returning the value of the header, or nil if not present." 181 | {:added "1.2"} 182 | [resp header-name] 183 | (some-> resp (find-header header-name) val)) 184 | 185 | (defn update-header 186 | "Looks up a header in a Ring response (or request) case insensitively, 187 | then updates the header with the supplied function and arguments in the 188 | manner of update-in." 189 | {:added "1.4"} 190 | [resp header-name f & args] 191 | (let [header-key (or (some-> resp (find-header header-name) key) header-name)] 192 | (update-in resp [:headers header-key] #(apply f % args)))) 193 | 194 | (defn charset 195 | "Returns an updated Ring response with the supplied charset added to the 196 | Content-Type header." 197 | {:added "1.1"} 198 | [resp charset] 199 | (update-header resp "Content-Type" 200 | (fn [content-type] 201 | (-> (or content-type "text/plain") 202 | (str/replace #";\s*charset=[^;]*" "") 203 | (str "; charset=" charset))))) 204 | 205 | (defn get-charset 206 | "Gets the character encoding of a Ring response." 207 | {:added "1.6"} 208 | [resp] 209 | (if-let [content-type (get-header resp "Content-Type")] 210 | (second (re-find re-charset content-type)))) 211 | 212 | (defn set-cookie 213 | "Sets a cookie on the response. Requires the handler to be wrapped in the 214 | wrap-cookies middleware." 215 | {:added "1.1"} 216 | [resp name value & [opts]] 217 | (assoc-in resp [:cookies name] (merge {:value value} opts))) 218 | 219 | (defn response? 220 | "True if the supplied value is a valid response map." 221 | {:added "1.1"} 222 | [resp] 223 | (and (map? resp) 224 | (integer? (:status resp)) 225 | (map? (:headers resp)))) 226 | 227 | (defmulti resource-data 228 | "Returns data about the resource specified by url, or nil if an 229 | appropriate resource does not exist. 230 | 231 | The return value is a map with optional values for: 232 | :content - the content of the URL, suitable for use as the :body 233 | of a ring response 234 | :content-length - the length of the :content, nil if not available 235 | :last-modified - the Date the :content was last modified, nil if not 236 | available 237 | 238 | This dispatches on the protocol of the URL as a keyword, and 239 | implementations are provided for :file and :jar. If you are on a 240 | platform where (Class/getResource) returns URLs with a different 241 | protocol, you will need to provide an implementation for that 242 | protocol. 243 | 244 | This function is used internally by url-response." 245 | {:arglists '([url]), :added "1.4"} 246 | (fn [^java.net.URL url] 247 | (keyword (.getProtocol url)))) 248 | 249 | (defmethod resource-data :file 250 | [url] 251 | (if-let [file (url-as-file url)] 252 | (if-not (.isDirectory file) 253 | (file-data file)))) 254 | 255 | (defn- add-ending-slash [^String path] 256 | (if (.endsWith path "/") 257 | path 258 | (str path "/"))) 259 | 260 | (defn- jar-directory? [^java.net.JarURLConnection conn] 261 | (let [jar-file (.getJarFile conn) 262 | entry-name (.getEntryName conn) 263 | dir-entry (.getEntry jar-file (add-ending-slash entry-name))] 264 | (and dir-entry (.isDirectory dir-entry)))) 265 | 266 | (defn- connection-content-length [^java.net.URLConnection conn] 267 | (let [len (.getContentLength conn)] 268 | (if (<= 0 len) len))) 269 | 270 | (defn- connection-last-modified [^java.net.URLConnection conn] 271 | (let [last-mod (.getLastModified conn)] 272 | (if-not (zero? last-mod) 273 | (Date. last-mod)))) 274 | 275 | (defmethod resource-data :jar 276 | [^java.net.URL url] 277 | (let [conn (.openConnection url)] 278 | (if-not (jar-directory? conn) 279 | {:content (.getInputStream conn) 280 | :content-length (connection-content-length conn) 281 | :last-modified (connection-last-modified conn)}))) 282 | 283 | (defn url-response 284 | "Return a response for the supplied URL." 285 | {:added "1.2"} 286 | [^URL url] 287 | (if-let [data (resource-data url)] 288 | (-> (response (:content data)) 289 | (content-length (:content-length data)) 290 | (last-modified (:last-modified data))))) 291 | 292 | (defn resource-response 293 | "Returns a Ring response to serve a packaged resource, or nil if the 294 | resource does not exist. 295 | Options: 296 | :root - take the resource relative to this root 297 | :loader - resolve the resource in this class loader" 298 | [path & [{:keys [root loader] :as opts}]] 299 | (let [path (-> (str (or root "") "/" path) 300 | (.replace "//" "/") 301 | (.replaceAll "^/" ""))] 302 | (if-let [resource (if loader 303 | (io/resource path loader) 304 | (io/resource path))] 305 | (url-response resource)))) 306 | --------------------------------------------------------------------------------