Can not execute middleware if using a exception handler? - clojure

wrap-cors does not return access control headers when there is a bad request against my api endpoint. I believe this is because I am using a exception handler which might be blocking the middleware from running. I want to know how I can still execute the middleware for this route and append the cors headers to the response of bad requests.
exception handler
(defn- bad-request-handler
"Handles bad requests."
[f]
(f
(ring/response {:status "bad request"})))
app
(def app
(api
{:exceptions {:handlers
{::ex/request-validation (bad-request-handler response/bad-request)}}}
(POST "/" [] :body [item {(schema/required-key :item) schema/Bool}]
:middleware [#(wrap-cors % :access-control-allow-origin [#".*"]
:access-control-allow-methods [:post])]
:responses {200 {:description "ok"}
400 {:description "bad request"}} item)))

I've decided to append my own headers rather than using r0man/ring-cors.
I can determine the contents of the Access-Control-Allow-Methods by retrieving the :request-method value from the request.
However, this makes the assumption that the bad request handler will only ever be called by valid routes.
(defn- append-cors
"Allow requests from all origins"
[methods]
{"Access-Control-Allow-Origin" "*"
"Access-Control-Allow-Methods" methods})
(defn- bad-request-handler
"Handles bad requests."
[f]
(fn [^Exception e data request]
(->
(f request)
(update-in [:headers] merge (->
(request :request-method)
(name)
(clojure.string/upper-case)
(append-cors)))
(assoc :body {:status "bad request"}))))
I'm still not really sure why the cors headers are only added when the request is allowed.

Related

Custom middleware that bypasses chain in Compojure

I'm trying to write a custom middleware that checks if user is authenticated by checking existence of :user key in request.
(defn wrap-authenticated [handler]
(fn [{user :user :as req}]
(if (nil? user)
(do
(println "unauthorized")
{:status 401 :body "Unauthorized." :headers {:content-type "text/text"}})
(handler req))))
(def app
(wrap-authenticated (wrap-defaults app-routes (assoc site-defaults :security false))))
But when I try to return response hashmap with 401 status, I get the following exception:
WARN:oejs.AbstractHttpConnection:/main
java.lang.ClassCastException: clojure.lang.Keyword cannot be cast to java.lang.String
Perhaps I don't understand the logic that needed to be implemented inside Compojure middleware.
How do I write my middleware that breaks the middleware chain and just returns custom response or redirects to handler?
I believe in your case the mistake is in the :headers map, since the keys are expected to be strings and you're using :content-type, which is a keyword. Try this instead:
{"Content-Type" "text/html"}

Getting the POST body data from a POST request to Pedestal

I have POSTed data to a Pedestal endpoint "/my-post. I have routed that end point as such:
[[["/" {:get landing} ^:interceptors [(body-params/body-params) ...]
["/my-post {:post mypost-handler}
....
So to my mind this means that the body-params interceptor will fire for /my-post too.
In mypost-handler I have:
(defn mypost-handler
[request]
****HOW TO ACCESS THEN FORM DATA HERE ****
)
How do I now access the form data here? I can see from printing the request that I have a #object[org.eclipse.jetty.sever.HttpInputOverHTTP..] which will clearly need further processing before it is useful to me.
(I must say, the documentation for Pedestal is pretty sketchy at best...)
Something like this should work. Note the body-params interceptor on the mypost-handler route
(defn mypost-handler
[{:keys [headers params json-params path-params] :as request}]
;; json-params is the posted json, so
;; (:name json-params) will be the value (i.e. John) of name property of the posted json {"name": "John"}
;; handle request
{:status 200
:body "ok"})
(defroutes routes
[[["/mypost-handler" {:post mypost-handler}
^:interceptors [(body-params/body-params)]
]
]])
The mypost-handler is acting as a Ring handler, i. e. it should accept a Ring request map and return a Ring response map. Thus, you can expect a typical Ring request structure:
(defn mypost-handler
[{:keys [headers params json-params path-params] :as request}]
;; handle request
{:status 200
:body "ok"})
Here's more relevant info on defining such handlers in your route tables.

Invalid anti-forgery error during lein test

Test code:
(testing "adding a record"
(let [response (app (mock/request :post "/api/df"
"{\"id\":123}"))]
(prn response)
(is (= (:status response) 200))))
Testing error when prn the response:
{:status 403, :headers {"Content-Type" "text/html; charset=utf-8", "X-XSS-Protection" "1; mode=block", "X-Frame-Options" "SAMEORIGIN", "X-Content-Type-Options" "nosniff"}, :body "<h1>Invalid anti-forgery token</h1>"}
Cross Site Request Forgery, is an attack where an evildooer put's a link on their site that tricks the browser of someone on that site into making a request to your site.
If that someone happens to be logged into your site at the time, then that request will cause things to happen as if they had asked for it (because their browser asked for it to happen). This can be a very serious problem and it affects both GET and POST requests.
The general solution is to make it so no serious actions can happen on the first connection to your site, but rather the first connection sets a token header, that the server expects to see on the next request. This allows the server to verify the chain of requests, and thus prevents CSRF (or XSRF).
It sounds like, if you want your test to make requests to this service, you need your request to first acquire a proper CSRF token, and then make the request it want's to test.
As it stands, your test is testing that this call is not vulnerable to CSRF, so it's a perfectly useful test, you should keep it, and write another that get's a proper token before making the request.
We can disable csrf in tests using (assoc site-defaults :security false). The complete code is something like this:
; Create a copy of testing app in utilities.testing
; by wrapping handler with testing middlewares
(ns utilities.testing
(:require [your-web-app.handler :refer [path-handler]]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]))
; Disabling CSRF for testing
(def app
(-> path-handler
(wrap-defaults (assoc site-defaults :security false))))
Now you can use this app in tests
(ns users.views-test
(:require [utilities.testing :refer [app]]
;...
))
;...
(testing "adding a record"
(let [response (app (mock/request :post "/api/df"
"{\"id\":123}"))]
(prn response)
(is (= (:status response) 200))))

using a custom :store with wrap-multipart-params in ring middle ware

I'm trying to use a custom :store option for wrap-multipart-params and instead I'm clearly getting the default store. My custom function isn't even being called.
(mp/wrap-multipart-params
(POST "/upload-x" request (upload/upload-file request))
{:store upload/logging-store})
My logging store function looks like this (it's just a dummy for now - eventually I want to handle the stream in a custom way) None of that IO happens.
(defn logging-store [{filename :filename
content-type :content-type
stream :stream
:as params}]
(println "in logging store")
(pprint filename)
(pprint params)
filename)
upload-file looks like this:
(defn upload-file [{params :params
session :session :as request}]
(let [user-id (:user-id session)
files (get params "files")]
(pprint request)
(pprint params)
(response/response
{:status :success})))
The printing for the request and the params clearly show the multipart params in there and that they are being handled by the temp file store:
:multipart-params
{"files"
{:size 1674,
:tempfile
#<File /var/folders/rx/9ntjyyvs35qbmcbp6rhfmj200000gn/T/ring-multipart-3853352501927893381.tmp>,
:content-type "application/octet-stream",
:filename "blog-test.clj"}},
EDIT: app definition (as requested)
(defroutes file-list-routes
(GET "/simple-upload" request (upload/simple-upload-file request))
(mp/wrap-multipart-params
(POST "/upload-x" request (upload/upload-file request))
{:store upload/logging-store})
)
(defroutes scratch-app
(context "/files" request file-list-routes)
(route/resources "/")
(route/not-found "Page not found"))
(def my-app
(handler/site
(ring.middleware.json/wrap-json-response
(ring.middleware.json/wrap-json-params
(ring.middleware.stacktrace/wrap-stacktrace
(ring.middleware.session/wrap-session
scratch-app
{:store (ring.middleware.session.memory/memory-store all-the-sessions)
:cookie-attrs {:max-age 3600 ;in s 3600s = 1h
}}))))))
(ring.util.servlet/defservice my-app)
The compojure.handler/site function contains the wrap-multipart-params middleware, so you're unknowingly applying the multipart middleware twice: first with the defaults, then with your custom options. Because the multipart middleware with the default options is applied first, that take priority.

compojure defroutes - route sometimes not recognized

I have a clojure / compojure webapp with the following routes
(defroutes my-routes
(GET "/app/preview" request (my-preview-function request))
(ANY "*" request (str "ANY page <br>" (request :params))))
The preview GET request is made with a couple of parameters. I find this works most of the time but sometimes the /ebook/preview is not found and processing drops to the ANY route, in which case the output is similar to this,
ANY page
{:* "/app/preview", :section "50", :id "48"}
Can anyone suggest what might cause the /ebook/preview request to be skipped? It is definitely a GET request being made; the HTML does not have a POST for the /app/preview URL and to be doubly sure I added a POST route for /app/preview and that was not being hit.
JAR versions:
Clojure 1.2
compojure-0.6.2
ring-core-0.3.7
jetty-6.1.14
ring-jetty-adapter-0.3.1
ring-servlet-0.3.1jar
servlet-api-2.5-6.1.14
Routes are wrapped as follows
(require '[compojure.handler :as handler])
(defn wrap-charset [handler charset]
(fn [request]
(if-let [response (handler request)]
(if-let [content-type (get-in response [:headers "Content-Type"])]
(if (.contains content-type "charset")
response
(assoc-in response
[:headers "Content-Type"]
(str content-type "; charset=" charset)))
response))))
(def app (-> my-routes
handler/site
wrap-stateful-session
(wrap-charset "utf-8")
(wrap-file "public")))
(defn run []
(run-jetty (var app) {:join? false :port 8080}))
If you're trying to figure out what request is causing the problems, stop throwing away the request map with (request :params) and just have a look at request. That will give you a map with all the information Compojure has; you can inspect it, and pass it back into your routes later to observe what happens (after you make some changes, say).
If
(my-preview-function request)
returns nil, then the routing will try the next route. Take a look at (source GET) and see how it matches (or doesn't) your route.