Clojure Pedestal root serving as application/octet-stream - clojure

I am trying to host static assets along with services in Pedestal 0.5.1. I am using the ::file-path to point to a directory to host the files. This works fine if I navigate directly to the file http://localhost:8888/index.html but if I go to the root of the site http://localhost:8888 it serves the files as application/octet-stream rather than text/html. I adapted the Hello World Sample and it has the same behavior.
src/hello_world/server.clj
(ns hello-world.server
(:require [io.pedestal.http :as http]
[io.pedestal.http.route :as route])
(:gen-class))
(def routes
(route/expand-routes [[]]))
(def service
{:env :prod
::http/join? false
::http/routes routes
::http/file-path "/tmp/www"
::http/type :jetty
::http/allowed-origins {:creds true :allowed-origins (constantly true)}
::http/port 8888})
(defonce runnable-service (http/create-server service))
(defn -main
"The entry-point for 'lein run'"
[& args]
(println "\nCreating your server...")
(http/start runnable-service))
Start lein run
$ curl -i localhost:8888
HTTP/1.1 200 OK
Date: Fri, 18 Nov 2016 16:02:56 GMT
Last-Modified: Fri, 18 Nov 2016 15:10:22 GMT
Content-Type: application/octet-stream
Content-Length: 12
Server: Jetty(9.3.8.v20160314)
hello world
$ curl -i localhost:8888/index.html
HTTP/1.1 200 OK
Date: Fri, 18 Nov 2016 16:03:02 GMT
Last-Modified: Fri, 18 Nov 2016 15:10:22 GMT
Content-Type: text/html
Content-Length: 12
Server: Jetty(9.3.8.v20160314)
hello world
Is there some way to fix the "/" route to serve the correct content-type?

To get correct content-types for files served as directory indexes,
add the interceptor io.pedestal.http.ring-middlewares/file-info to
your Pedestal configuration.
This requires that you override the default interceptor chain with
your own, so you will have to include all of the default interceptors
that your app needs.
For example, your service might look something like this:
(ns hello-world.service
(:require
[io.pedestal.http :as http]
[io.pedestal.http.ring-middlewares :as middlewares]
[io.pedestal.http.route :as route]
[io.pedestal.http.route.definition :refer [defroutes]]))
(defroutes routes
[[]])
(def service
{::http/type :jetty
::http/port 8080
::http/interceptors [http/log-request
http/not-found
middlewares/session
route/query-params
(middlewares/file-info) ; HERE
(middlewares/file "/tmp/www")
;; ... insert other interceptors ...
(route/router #(deref #'routes) :map-tree)]})
For examples of other default interceptors you might want to include,
see default-interceptors.
Explanation
This probably does not come up very often in practice because many web
applications use a handler function to generate the home page instead
of returning a static file.
For an alternate solution, you could write a route handler for the /
route which returns the contents of index.html with the appropriate
content-type.
Pedestal's default interceptor stack includes
io.pedestal.http.ring-middlewares/file
and io.pedestal.http.ring-middlewares/content-type.
These interceptors just wrap the Ring middleware functions
file-request
and content-type-response, respectively.
file-request returns a java.io.File object as the HTTP response.
content-type-response examines the request URI to determine the
value of the Content-Type header. Since the URI is just / it
defaults to application/octet-stream.
By contrast ring.middleware.file-info (which is deprecated) examines
path of the actual File object in the response.
See file-info-response.
io.pedestal.http.ring-middlewares/file-info is the interceptor
wrapper around ring.middleware.file-info/file-info-response.

Related

How to use optional query parameters in Yada?

I'm making a toy API using the Yada library in Clojure. It searches a database for city names starting with the given characters and returns some info about it.
I want a URI of the form: /cities/:name?count=:count so for example /cities/ber?count=4 will return the top 4 matches. But I also want /cities/ber without the ?count= parameter to return a default number of results (say just the first).
I've defined my route and yada handler like this:
(defn city-search-fn
[ctx]
(let [name (get-in ctx [:parameters :path :name])
count (get-in ctx [:parameters :query :count] 1)]
(city->geoposition name count)))
(def cities (yada/handler (yada/resource
{:methods
{:get
{:parameters {:path {:name String}
:query {:count Long}}
:produces ["application/json"
"application/edn"]
:response city-search-fn}}})))
(def routes
[["/cities/" :name] cities])
(def server
(yada/listener routes {:port 30000}))
This works fine if I supply the ?count= query parameter:
$ curl -i 'http://localhost:30000/cities/ber?count=2'
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Length: 259
Content-Type: application/json
Vary: accept
Server: Aleph/0.4.4
Connection: Keep-Alive
Date: Mon, 09 Sep 2019 16:01:45 GMT
[{"name":"Berlin","state":"Berlin","countrycode":"DE","timezone":"Europe/Berlin","latitude":52.52437,"longitude":13.41053},{"name":"Berbera","state":"Woqooyi Galbeed","countrycode":"SO","timezone":"Africa/Mogadishu","latitude":10.43959,"longitude":45.01432}]
But I get status 400 ({:status 400, :errors ([:query {:error {:count missing-required-key}}])}) if I don't supply it:
$ curl -i 'http://localhost:30000/cities/ber'
HTTP/1.1 400 Bad Request
Content-Length: 77
Content-Type: text/plain;charset=utf-8
Server: Aleph/0.4.4
Connection: Keep-Alive
Date: Mon, 09 Sep 2019 16:06:56 GMT
{:status 400, :errors ([:query {:error {:count missing-required-key}}])}
The documentation of yada says it supports optional query parameters using the "schema" library. So I found in schema's documentation that there exists a schema.core/maybe function. I tried to modify my yada resource as follows:
:parameters {:path.....
:query (schema/maybe {:count Long})}
this doesn't work (same 400 error).
Then I tried:
:parameters {:path.....
:query {:count (schema/maybe Long)}}
this also didn't work.
So my question is: what is the correct way to have an optional query parameter in yada?
To answer my own question, digging more into Schema documentation, here is the correct way:
:parameters {:path.....
:query {(schema/optional-key :count) Long}}
The key itself needs to be marked as optional.

How to get the content of `HttpInputOverHTTP` in Clojure Compojure/Ring project?

How to get the content of an incoming POST http request's :body #object[org.eclipse.jetty.server.HttpInputOverHTTP 0x42c3599b "HttpInputOverHTTP#42c3599b"] in a Compojure/Ring project?
I know that this :body is composed of a part named data whose MIME-type is text-plain and another part named excel whose MIME-type is application/excel.
I slurped the content of :body and it shows:
Parsing a binary stream manually would be difficult. Wrap your handler as follows:
(wrap-multipart-params handler options)
This middleware parses the body and populates :params parameters with parsed data as well.
See ring.middleware.multipart-params documentation for more details.
I was seeing it in Reitit, what fixed for me was to change the order of the middlewares so the exception-middleware is after the multipart/multipart-middleware.
:middleware [;; multipart
multipart/multipart-middleware
;; exception handling
exception-middleware]
You can find a basic example in the Clojure Cookbook (O'Reilly), which I highly recommend:
(ns ringtest
(:require
[ring.adapter.jetty :as jetty]
clojure.pprint))
;; Echo (with pretty-print) the request received
(defn handler [request]
{:status 200
:headers {"content-type" "text/clojure"}
:body (with-out-str (clojure.pprint/pprint request))})
(defn -main []
;; Run the server on port 3000
(jetty/run-jetty handler {:port 3000}))

How can I explicitly set content type on compojure response?

I am playing with compojure-api and am blocked at trying to manage Content-Type for my simple webapp. What I want is to emit an HTTP response that is just plain/text, but somehow Compojure-API keeps setting it to "application/json".
(POST "/echo" []
:new-relic-name "/v1/echo"
:summary "info log the input message and echo it back"
:description nil
:return String
:form-params [message :- String]
(log/infof "/v1/echo message: %s" message)
(let [resp (-> (resp/response message)
(resp/status 200)
(resp/header "Content-Type" "text/plain"))]
(log/infof "response is %s" resp)
resp))
but curl shows the server responded Content-Type:application/json.
$ curl -X POST -i --header 'Content-Type: application/x-www-form-urlencoded' -d 'message=frickin compojure-api' 'http://localhost:8080/v1/echo'
HTTP/1.1 200 OK
Date: Fri, 13 Jan 2017 02:04:47 GMT
Content-Type: application/json; charset=utf-8
x-http-request-id: 669dee08-0c92-4fb4-867f-67ff08d7b72f
x-http-caller-id: UNKNOWN_CALLER
Content-Length: 23
Server: Jetty(9.2.10.v20150310)
My logging shows that the function requested "plain/text", but somehow the framework trumped it.
2017-01-12 18:04:47,581 INFO [qtp789647098-46]kthxbye.v1.api [669dee08-0c92-4fb4-867f-67ff08d7b72f] - response is {:status 200, :headers {"Content-Type" "text/plain"}, :body "frickin compojure-api"}
How do I gain control over Content-Type in a Compojure-API Ring application?
compojure-api serves response in format requested by HTTP client which is indicated using HTTP Accept header.
With curl you need to add:
-H "Accept: text/plain"
You can also provide a list of acceptable formats and the server will serve the response in the first supported format from that list:
-H "Accept: text/plain, text/html, application/xml, application/json, */*"
I never tried compojure so here goes nothing:
1.) your local val reps has the same name as the aliased namespace - kind of confusing
2.) to get access to the params - it seems - you have to apply ring.middleware.params/wrap-params to your routes
3.) ah yes the Content-Type: since you required :form-params, which didn't get delivered due to missing wrap-params you ended up in some sort of default route - hence not text/plain. Thats what I think happend, at least.
with
lein try compojure ring-server
demo/paste into repl:
(require '[compojure.core :refer :all])
(require '[ring.util.response :as resp])
(require '[ring.server.standalone :as server])
(require '[ring.middleware.params :refer [wrap-params]])
(def x
(POST "/echo" [message]
:summary "info log the input message and echo it back"
:description nil
:return String
:form-params [message :- String]
(let [resp (-> (resp/response (str "message: " message))
(resp/status 200)
(resp/header "Content-Type" "text/plain"))]
resp)))
(defroutes app (wrap-params x))
(server/serve app {:port 4042})
test:
curl -X POST -i --header 'Content-Type: application/x-www-form-urlencoded' -d 'message=frickin' 'http://localhost:4042/echo'
HTTP/1.1 200 OK
Date: Fri, 13 Jan 2017 17:32:03 GMT
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 14
Server: Jetty(7.6.13.v20130916)
message: frickin

Compojure app not sending CSRF by default

I'm using reagent and compojure to make a toy webapp and I can't figure out why my server isn't sending out a CSRF cookie. Other answers and several blog posts seem to imply that the default settings for compojure now send the CSRF token and that manually resending it is actually a bug. When I try to hit the POST /art endpoint I get back a 403 Forbidden response. None of the pages get the cookie with the CSRF token in it so I can't send it with the POST request. Any advice?
;;server.clj
(ns my-app.server
(:require [my-app.handler :refer [app]]
[environ.core :refer [env]]
[ring.adapter.jetty :refer [run-jetty]])
(:gen-class))
(defn -main [& args]
(let [port (Integer/parseInt (or (env :port) "3000"))]
(run-jetty app {:port port :join? false})))
;; handler.clj
(ns my-app.handler
(:require [compojure.core :refer [GET POST defroutes]]
[compojure.route :refer [not-found resources]]
[hiccup.page :refer [include-js include-css html5]]
[my-app.middleware :refer [wrap-middleware]]
[environ.core :refer [env]]))
(defroutes routes
(GET "/" [] loading-page)
(GET "/about" [] loading-page)
(GET "/art" [] loading-page)
(POST "/art" request {:sent (:body request) :hello "world"})
(resources "/")
(not-found "Not Found"))
(def app (wrap-middleware #'routes))
;;middleware.clj
(ns stagistry.middleware
(:require [ring.middleware.defaults :refer [site-defaults wrap-defaults]]
[prone.middleware :refer [wrap-exceptions]]
[ring.middleware.reload :refer [wrap-reload]]))
(defn wrap-middleware [handler]
(-> handler
(wrap-defaults site-defaults)
wrap-exceptions
wrap-reload))
I threw the code itself on github here since I still can't see what's wrong.
Other answers and several blog posts seem to imply that the default settings for compojure now send the CSRF token and that manually resending it is actually a bug.
(wrap-defaults site-defaults) applies the ring-anti-forgery middleware. This will only add a CSRF token to each ring browser session and look for the token on POST requests. If the token is missing the middleware will return a 403 for the request. Adding the token to your form or ajax/whatever post requests is up to you, the price of freedom. :)
From the ring-anti-forgery docs:
By default, the token is expected to be in a form field named '__anti-forgery-token', or in the 'X-CSRF-Token' or 'X-XSRF-Token' headers.
For example try adding this route:
(GET "/someform" request (html5 (ring.util.anti-forgery/anti-forgery-field)))
The anti-forgery-field helper will add a hidden input field with the CSRF token as its value, which is picked up by the middleware if the form is posted. To access the token directly you can either use ring.middleware.anti-forgery/*anti-forgery-token* or look it up in the session of the current request map:
(-> request :session :ring.middleware.anti-forgery/anti-forgery-token)
The global var (and by extension the helper) is bound to the handler context though, you can't access it from outside or from another thread in the same context.
Simple curl header example:
Get the CSRF token:
$ curl -v -c /tmp/cookiestore.txt http://localhost:3000/someform
Set the token via header and post some stuff:
$ curl -v -b /tmp/cookiestore.txt --header "X-CSRF-Token: ->token from prev. req<-" -X POST -d '{:foo "bar"}' localhost:3000/art
So I found an answer on a related question that's good enough for me at the moment. I switched the middleware.clj site-defaults to api-defaults which doesn't use CSRF. Still curious how to make CSRF work here, but it's not critical for what I'm doing. If anyone latter suggests a fix that works I'll mark that as correct.

Serve static favicon file in Compojure

According to Compojure:
Compojure does not serve static files by default, nor does it
automatically deal out 404s when no route matches.
So to deal with that, we have to set these Public Vars
files
not-found
resources
And this is how we currently set it:
(defroutes app-routes
(route/files "/" {:root "path/to/public"})
(route/resources "/")
(route/not-found "Not Found"))
It worked as expected when most of the static files are accessed through the web browser.
e.g.
http://localhost:3000/img/icon.png
But the problem is, it doesn't work on favicon files.
e.g.
http://localhost:3000/img/favicon.ico
It treats this as a different call which it should be serve as a static file.
Response to to the CURL I run:
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /img/favicon.ico HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:3000
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Tue, 28 Jul 2015 19:05:24 GMT
< Last-Modified: Thu, 28 May 2015 14:51:16 +0000
< Content-Length: 1106
< Content-Type: image/x-icon
* Server Jetty(7.6.13.v20130916) is not blacklisted
< Server: Jetty(7.6.13.v20130916)
files, resources, and not-found are functions, not variables that you set. When you call (route/files "/" {:root "path/to/public"}), a route for resolving URLs under "/" as static files under "path/to/public" is returned.
defroutes defines a collection of routes. These routes are tried in the order they are listed, until the first one that returns a response.
If you add a route (GET "/:slug" [* :as req slug] (search req slug)) before the others, then any URL other than "/" will be handled by this new route — including the favicon request. On the other hand, if you add it just before the not-found route, then it should work.
Also, if there isn't a static file that matches the request, then the files route will fail and the next one will be tried. So you should also check that favicon.ico actually exists and is in the img sub-directory.
umm...
This works for me, (wherever you define/declare your routes):
In the namespace section:
[ring.util.response :refer [resource-response]]
Down below (at least above (def app ):
(defn wrap-return-favicon [handler]
(fn [req]
(if (= [:get "/favicon.ico"] [(:request-method req) (:uri req)])
(resource-response "favicon.ico" {:root "public/img"})
(handler req))))
then in:
(def app
(-> routes
...
wrap-return-favicon
wrap-stacktrace))