Introduction
Catacumba is an asynchronous or non-blocking web toolkit for Clojure built on top of ratpack and netty drawing inspiration from ring, pedestal and ratpack.
Note
|
Project Maturity: Rationale: |
Quick Start
This section intends to explain how to get catacumba up and running.
Install
The simplest way to use catacumba in a clojure project is by including it in the dependency vector on your project.clj file:
[funcool/catacumba "2.2.1"]
Note
|
Catacumba will only run with JDK8 and Clojure >= 1.7. |
Handlers
The handler consists of a function that accepts a "context" as it’s first parameter and returns something renderable. Let’s see an example:
(defn example-handler
[context]
"Hello World")
It looks very similar to ring handler with two main differences:
-
instead of request it receives a context that works like request but with more responsibilities (explained in other sections).
-
returns a string instead of a hash-map (it is also allowed but is not mandatory, also explained later).
Routing
Now knowing how we can define handlers, the next step is define a route (http
endpoint) for our handler. That can be done with routes
function:
(require '[catacumba.core :as ct])
(def app
(ct/routes [[:all "" example-handler]
[:get "foobar" example-handler]]))
The routes
function receives a vector of ordered entry points for our handlers.
In this example we have defined two routes for the same handler (just for
demonstration purposes):
The first entry defines a /
route for all kind of requests and the second entry
defines a /foobar
route only for GET
requests for the same handler.
You can read a complete documentation about catacumba’s routing here.
Run the server
For run the previously defined handler, just use the run-server
function:
(ct/run-server app {:port 3030})
Tip
|
The run-server function doesn’t block so you can execute it in a repl without
problems.
|
You can read more about all available options that you can pass to run-server
function here.
Put all together
This is what the complete source code of the example looks like:
(ns exampleapp.core
(:require [catacumba.core :as ct])
(:gen-class))
(defn example-handler
[context]
"Hello World")
(def app
(ct/routes [[:all "" example-handler]
[:get "foobar" example-handler]]))
(defn -main
[& args]
(ct/run-server app {:port 3030}))
Catacumba also comes with a little collection of Examples that may help you setup your first project.
User Guide
This section intends explain all the different parts of catacumba and how they work together.
Handlers
Is the fundamental building block in the catacumba toolkit and has two main types:
-
Ending: Handlers that process a request and return a response (usually named controller in other web frameworks or toolkits).
-
Middle: Handlers that does some logic but does not return any response ( delegating that task to other handler) (usually named as middleware or decorator).
The both handler types are defined in term of functions and looks identically, the principal change is the responsibility. Let see an example:
(defn sample-ending-handler
[context]
"Hello World")
(defn sample-middle-handler
[context]
(println "hello world")
(ct/delegate))
The ending handlers
As you have seen, the examples until now are always returning a simple string that in fact is not very useful in real-world use cases. The great news here is that return values are handled using open polymorphic abstractions such are protocols.
This means that you can return anything that catacumbla already has implementation for it or anything that yourself have implemented. Let see some examples:
(defn some-handler
[context]
{:status 200
:headers {}
:body "Hello World"})
(require '[catacumba.http :as http])
(defn some-handler
[context]
(http/ok "Hello World"))
But this is not the end, if you want to know all the different kind of handlers and its return types, please take a look on Handler types section.
The middle handlers
The catacumba toolkit in request/response handling perspective behaves like a an asynchronous pipeline of handlers (when only one handler is attached it will be a pipeline of one unique element).
As you have observed in the previous example, the middle handler instead of returning a response, returns a result an opaque type that indicates to catacumba that this handler is not of ending type and forces to catacumba take the next handler from the pipeline and execute it. This step is done asynchronously. And so until ending handler is found and response is returned.
There are nothing especial, the opaque object that delegate
function returns
just implement appropriate protocol and if you don’t like the default behavior,
you are free to implement your own.
This is a little introduction to the delegation process and how the middle handlers participates on it. For in depth understanding and how you can use it in your application, please read the handler delegation section.
Context
The second thing most important in the catacumba is the context. It can be considered a central part of both: IO and the control flow (will be explained in advanced section).
In other words, it can be considered as a combination of request and response, but
in most cases you will use it like a request. It exposes the already familiat set of
attributes: :method
, :query
, :path
, :headers
and :cookies
. And all them
are accessible with keyword lookup:
(:method context)
;; => :get
This is a reference table of request attributes are availeble under context:
Key | Type | Description | Example |
---|---|---|---|
|
A object that represents a request body (not always awailable, see below). |
||
|
|
A request method. |
|
|
|
A raw string representation of the uri querystring. |
|
|
|
A raw string representation of the uri path. |
|
|
|
A optionally multi value hash map of the request headers. |
|
|
|
A optionally multivalue hash map of request cookies (explained in details in its own section). |
|
|
|
A optionally multivalue hash map of the parsed |
|
The only exception to the rule is the :body
attribute, that by default does not
comes. This is because this operation is delayed until is really needed and is done
asynchronously, using get-body!
function.
Nevertheless, in most cases you will prefer use a more high level and extensible abstraction that parses the body using it’s content type, see body parsing section for more information.
Note
|
In previous versions, body was always available as :body attribute in the
context, although is not very efficient approach, you can return the previous
behavior just using the catacumba.handlers.parse/read-body auxiliar handler on top
of your route pipeline or as decorator.
|
Routing
In contrast to ring, catacumba is a toolkit for web development and offers builtin support for advanced routing that allows handlers chaining, partitioning, error handling, among other features.
Note
|
Catacumba has a polymorphic and extensible way to setup handlers, and routing is one of multiple possible implementations. Is completely optional and you can use any other routing library if you want. |
Basic syntax
The routes in catacumba are defined using clojure data structures: vectors and keywords. Let’s see a little example of the aspect in a complete example:
(def routes
(ct/routes [[:prefix "api"
[:get "users" users-handler]]]))
The order of statements is very important because the routing in catacumba is a simple chain or pipeline. Each handler has the ability to delegate the request handling to the next handler in the pipeline.
This is a complete list of route directives that you can use a part of :get
:
:any
(matches all routes, often used for add chain handlers), :post
, :put
,
:patch
and :delete
.
Dispatch by method
In some circumstances you may want have different handlers depending on the HTTP method used for one concrete endpoint. You can do it in the following way:
(ct/routes [[:prefix "api/users"
[:get list-users-handler]
[:post create-users-handler]]])
This also can be done in this an other way:
(ct/routes [[:get "api/users" list-users-handler]
[:post "api/users" create-users-handler]])
But is considered not idiomatic and the first example should be considered the right way to do it.
Note
|
Before, there was an other way to setup by method using the |
Routing params
catacumba's routing also allows to capture URL values encoded in the URL or as URL
parameters using special symbols. For example, the path string "foo/:val" will
match paths such as "foo/bar", "foo/123". The matched parameters are automatically
populated to the context under the :route-params
key:
(def article-detail
[context]
(let [id (get-in context [:route-params :id])]
(http/ok (str "You have requested article with id=" id))))
(def app
(ct/routes [[:get "articles/:id" article-detail]]))
Additionally to the basic token for representing URL parameters, catacumba also allows the use of regular expressions for delimiting the input or marking a URL token optional.
See the following table for all supported URL tokens:
Path Type | Syntax | Route example | Matching url example |
---|---|---|---|
Literal |
|
|
|
Mandatory |
|
|
|
Optional |
|
|
|
Mandatory & Regex |
|
|
|
Optional & Regex |
|
|
|
Routing chain
The chaining of handlers can be done in two different ways:
-
inline: providing more that one handler for concrete http method.
-
multiple routes: providing a "match all" handler at the start of prefix.
Chaining handlers inline follows this pattern:
(ct/routes [[:get "users" permission-check-handler get-users-handler]])
Additionally, you can setup "match all" handlers at the start of a routing definition and use them as interceptors:
(def routes
(ct/routes [[:prefix "api"
[:any authentication-handler]
[:get "users" users-handler]]]))
For a better understanding of how the handler delegation chain works, see the Handlers delegation section in advanced guide chapter.
Error handling
The catacumba router chain allows to setup user defined error handling functions.
This requires a very simple setup, you only have to add another route entry with
using :error
route directive:
(def routes
(ct/routes [[:error my-error-handler]
[:get "users" users-handler]]))
With the previous code we have set up a global error handler, applying to all routes in the chain. But there is also the possibility to set different error handlers for different prefixes:
(def routes
(ct/routes [[:prefix "api"
[:error my-error-handler-for-this-prefix]
[:any authentication-handler]
[:get "users" users-handler]
[:put "users" check-permissions-handler update-users-hander]]
[:prefix "admin"
[:error my-error-handler-for-this-other-prefix]
[:get "dashboard" my-dashboard-handler]]]))
The error handler signature is very similar to standard HTTP handler signature, with the difference being that it receives the throwable instance as an additional parameter:
(defn my-error-handler
[context error]
(http/internal-server-error (.getMessage error)))
Serving static files
Catacumba also comes with the ability to serve static files. This is can be done
using :assets
routing directive. Here an example:
(ct/routes [[:assets "assets" {:dir "public/assets"}]])
Additionally, it has support for specify a index file, that will be returned if no file is requested. This is very useful for SPA (single page applications):
(ct/routes [[:assets "assets" {:dir "public/assets"
:indexes ["index.html"]}]])
So, if you make a http request to /assets/
the index.html
will be automatically
returned.
Note
|
the assets are resolved using the :basedir parameter of the server
constructor; for more details see the Launching the server
section.
|
Cookies
You can access to the request cookies through direct keyword lookup on context object:
(get-in context [:cookies :somecookie])
;; => {:value "foo" :path "/" ...}
The cookies map is almost identical to the one that you can find in ring, and has the following possible properties:
-
:domain
- restrict the cookie to a specific domain -
:path
- restrict the cookie to a specific path -
:secure
- restrict the cookie to HTTPS URLs if true -
:http-only
- restrict the cookie to HTTP if true (not accessible via e.g. JavaScript) -
:max-age
- the number of seconds until the cookie expires
For set cookies, you should use the set-cookies!
function as you can see in the
following example:
(ct/set-cookies! context {:cookiename {:value "foobar" :max-age 3600}})
Advanced topics
Handler delegation
A part of the obvious (and previously explained) responsibility of the context
object in catacubla, it has some others responsibilities. Here just a summary of
them:
Here a small summary of the context responsibilities besides the obvious one explained in previous sections (IO handling):
-
Provide direct access to the request and response objects.
-
Access to the contextual objects (called registry).
-
Flow control in handler chaining.
-
Convenience helpers for common handlers operation.
In a catacumba design (inherited from ratpack), a handler is a unit of work in an asynchronous handler pipeline and the context is a execution controller and local storage for the current request state.
In other words it can be explained as "flow control" in the chain of handlers.
The request process is an asynchronous pipeline of handlers that can be composed in different ways (as we previously seen in other parts of the documentation). So the each handler in the pipeline has the ability to do some work and delegate the rest of processing to next handler in the chain.
This approach allows you build different kind of modular and completely decoupled handlers and compose them into a pipeline to work together.
The delegation response can be done with delegate
function. Let see a simple
example:
(defn handler1
[context]
(do-something context)
(ct/delegate)
(defn handler2
[context]
(http/ok "hello world"))
(def router
(ct/routes [[:get "foo" handler1 handler2]]))
In this example, when the request arrives at handler1
, it delegates the execution
to the next handler in the chain. It do not need to know about next handler, it just
delegates to the routing chain to find a next handler or raise a corresponding
error.
In addition to the simple handler delegation, catacumba offers a simple way to
pass context data to the next handler in the chain. It can be done by passing an
additional parameter to the delegate
function:
(defn handler1
[context]
(do-something context)
(ct/delegate {:message "foobar"}))
(defn handler2
[context]
(let [message (:message context)]
(http/ok message)))
In the example above, the second handler prints the message found in the context.
Launching the server
Getting Started
As you can see in the quick start section, the main entry point for start the server
is the run-server
function that receives a handler chain and a map with options.
(require '[catacumba.core :as ct])
;; handler definition goes here
(ct/run-server #'my-handler {:port 4040 :debug true})
If you want to stop the server, you just need to call the .close
method on
the object returned by the run-server
function.
Configuration Options
Here a complete reference of the currently supported options that can be passed to
the run-server
function:
Keyword | Default | Description |
---|---|---|
|
|
The port used to bind the socket. |
|
|
The host used to bind the socket. |
|
(num of cores * 2) |
The number of threads for handler requests. |
|
|
Start in development mode. |
|
|
A callback for configuration step (low level ratpack access). |
|
|
The application base directory, used mainly for resolving relative paths and assets. |
|
|
A relative path in the classpath to the ssl keystore. |
|
|
A secret for the ssl key store |
|
|
A vector of handlers to attach at the start of the pipeline |
|
|
A file name that will be used to find the base directory in the class path. |
|
1048576 bytes (1mb) |
A maximum length of body. |
All supported options of this function, can be overwritten at JVM startup, using environment variables or system properties. This allows to customize the server without modifying source code and exists for convenience to make easy customizations in deployments.
For example, you can change the default port on JVM startup using the
CATACUMBA_PORT
environment variable or catacumba.port
system property:
export CATACUMBA_PORT=8000
export CATACUMBA_BASEDIR=`pwd`
java -jar yourjarhere.jar
java -Dcatacumba.port=8000 -Dcatacumba.debug=true -jar yourjarhere.jar
Note
|
If no |
Warning
|
If you are deploying your application as uberjar and you want serve static files
from the classpath, you should set the |
SSL Configuration
Catacumba server can be configured to use TLS (commonly known as SSL). The process is pretty simple but it requires to have a proper key and certificate.
The first thing that you should care about is that catacumba is built on jvm that
the default ssl certificate/key format used by nginx/apache it isn’t compatible but
is very easy create a compatible file using the openssl
command line.
Having a key and the certificate, just execute this command:
openssl pkcs12 -export -in cert.pem -inkey key.pem -out store.p12
This process will ask you for a password that you must memorize and later provide it to catacumba. Now, having the properly formated trusted store, just pass some additional parameters on starting the server:
(ct/run-server #'my-handler {:port 4040
:keystore-secret "yoursecrethere"
:keystore-path "path/to/store.p12"})
Note
|
catacumba at this moment does not has the "upgrade" approach so if you setup ssl, only ssl connections will be accepted. So the most recommended way to use ssl on your application is put catacumba behind nginx or haproxy and make them handle the ssl. |
Handler types
This section intends to explain the different kind of built in handler types and the response types that comes out of the box with catacumba. This section is organized on handler types as first level and possible supported return values as second level.
Asynchronous
Asynchronous handlers are handlers that return a value in an asynchronous way using one of the supported abstractions, such as core.async, reactive-streams and many others (explained below).
Channel (core.async)
The core.async
channel is one of the supported abstractions that comes with
catacumba out of the box. It consists of a handler that returns a body as a
channel or response as a channel.
This is the aspect of async handler returning a core.async channel as a body:
(defn my-async-handler
[context]
(let [ch (chan)]
(go
(dotimes [i 10]
(<! (timeout 500))
(>! ch (str i "\n")))
(close! ch))
(http/ok ch)))
Do not worry about how much data you can send to the client, if you are using channels in a right way (in a go block), you will send data to the client as fast as the client can consume it. This technique is also called back pressure, and is fully supported for chunked responses.
Additionally, you also can return a channel as the handler response. The main difference is that in this case you should put a complete response into the channel:
(defn my-async-handler
[context]
(go
(let [result (<! (do-some-async-task))]
(http/ok (:data result)))))
CompletableFuture
Sometimes, you do not need send a chunked stream to the client, but your "business logic" is defined in an asynchronous friendly API using promises (or something similar). In this case, with catacumba you can return a promise as a body or as a response and the data will be sent to the client when the promise has been resolved successfully.
The CompletableFuture
is an other asynchronous primitive supported out of the box
by catacumba; so you can return it as body or as response out of the box.
For more pleasant usage of CompletableFuture
in clojure, the
promesa library is used. That library
provides a more clojure friendly api on top of JDK8 CompletableFuture
and a great
sugar syntax for composing them.
CompletableFuture
instance and return it as body.(require '[promesa.core :as p])
(defn my-async-handler
[context]
(let [prm (p/resolved "hello world")]
(http/ok prm {:content-type "text/plain"})))
Like as usual, you can return an instance of CompletableFuture
as response:
(require '[promesa.core :as p])
(defn my-async-handler
[context]
(p/promise (fn [resolve]
(future
(Thread/sleep 100)
(resolve (http/ok "hello world"))))))
One of the advantages of using CompletableFuture
abstraction with promesa
library is because it exposes additional sugar syntax that help workin with
asynchronous flows in more pleasant way.
Let see an example:
(require '[promesa.core :as p])
(defn my-async-handler
[context]
(p/alet [a (p/await (some-async-op1))
b (p/await (some-async-op@))
result (str a b)]
(http/ok result)))
The result of alet
macro expression will be an instance of CompletableFuture
that eventually will be completed with the http response.
Manifold Deferred
The manifold library also offers a promise like abstraction. The main advantage of using it is that is build for clojure and is not restricted to JDK8.
(require '[manifold.deferred :as d])
(defn my-async-handler
[context]
(let [result (d/future
(Thread/sleep 1000)
"hello world")]
(http/ok result {"content-type" "text/plain"})))
Like the previously explained abstractions, you also can return manifold deferreds as handler response.
Reactive-Streams
The reactive-streams support is inherited from ratpack and like manifold streams it is only can be used for send the response body.
Here there isn’t anything new to explain, just build and/or compose your streams and return them as http response body:
(require '[catacumba.stream :as stream])
(require '[cuerdas.core :as str])
(defn my-async-handler
[context]
(let [pub (->> (stream/publisher ["hello" " " "world"])
(stream/transform (map str/upper)))]
(http/ok pub)))
;; It will return a chunked response to the client with "HELLO WORLD" string.
One of the best parts of the reactive-strams is that them comes with back pressure support out of the box and it native support in ratpack makes them a great glue abstraction for similar async primitives. In fact, the support for all stream like primitives explained until now are implemented in terms of reactive-streams publisher.
WebSockets
One of the main goals of catacumba is come with builtin, full featured and back pressure aware websockets support.
You can start a websocket connection in any catacumba handler or route handler
using websocket
function. It does not require any special handlers for dealing
with websockets. Let see an example:
(defn my-websocket-echo-handler
[{:keys [in out]}]
(go-loop []
(if-let [received (<! in)]
(do
(>! out received)
(recur))
(close! out))))
(defn my-handler
[context]
(ct/websocket context my-websocket-echo-handler))
(def route
(ct/routes [[:prefix "events"
[:any my-handler]]]))
Additionally, catacumba offers a way to set up a websocket handler directly, without an additional step:
(defn echo-handler
"This is my echo handler that serves as
a websocket handler example."
{:handler-type :catacumba/websocket}
[{:keys [in out]}]
(go-loop []
(if-let [received (<! in)]
(do
(>! out received)
(recur))
(close! out))))
(def route
(ct/routes [[:prefix "events"
[:any #'echo-handler]]]))
As you can observe, the var metadata is used for properly choice the right adapter.
Note
|
Is very important pass a var reference to the router instead of the function directly, because the metadata defined in the function is bound to the var and not to the function. |
Also, you can attach metadata inline, using the with-meta
Clojure built-in
function:
(ct/routes [[:prefix "events"
[:any (with-meta echo-handler
{:handler-type :catacumba/websocket})]]])
Clojure offers a lot of flexibility for working with metadata so you can set the handler type in the way that you prefer.
SSE (Server-Sent Events)
WebSockets are cool because they allow bi-directional communication, but in some circumstances we only need something unidirectional, for notifying the client about some changes or any other events. For this purpose exists Server-Sent Events (SSE) and catacumba also has support for it.
The handler for SSE does not differs much from websockets (that we have seen in the previous section). The main difference is that server-sent events are unidirectional and they only can send data in the server to client direction.
(defn time-notification
"Handler that notifies each second
the current server time to the client."
{:handler-type :catacumba/sse}
[{:keys [out ctrl] :as context}]
(go-loop []
(when-let [_ (>! out (str (java.time.Instant/now)))]
(<! (timeout 1000))
(recur))))
(def route
(ct/routes [[:prefix "events"
[:any #'time-notification]]]))
In a similar way to websockets, you can start SSE in any place, such as a standard catacumba handler:
(defn time-notification
"Handler that notifies each second
the current server time to the client."
[context]
(ct/sse context
(fn [{:keys [out ctrl]}]
(go-loop []
(when-let [_ (>! out (str (java.time.Instant/now)))]
(<! (timeout 1000))
(recur))))))
(def route
(ct/routes [[:prefix "events"
[:any time-notification]]]))
Let see some examples how you can send other parameters than simple data:
;; Send data
(>! out "data as string")
(>! out {:data "data as string"})
;; Send data with event name
(>! out {:data "data as string" :event "foobar"})
;; Set id
(>! out {:id "2"})
Note
|
The catacumba's SSE support uses core.async channels, but if you are not happy with core.async and want use something different (such as manifold streams or beicon), you may want know that everything in catacumba is implemented using abstractions and to implement your own SSE type of handler that uses manifold streams is very easy. |
CPS (Continuation-passing style)
Is a low level handler type that works in a cps style (in other words, they works with callbacks). This is not general purpose handler type but you maybe found it useful for integrate catacumba with other scenarios that it is not initially designed to work.
This is the aspect ot the cps style handler:
(defn my-cps-handler
"Some usefull docstring."
{:handler-type :catacumba/cps}
[context callback]
(future
(Thread/sleep 1000)
(callback "hello world")))
Built-in Handlers
This section will cover different kind of built-in additional handlers to make the experience of using catacumba more pleasant.
Body parsing
Catacumba comes with builtin support for conditional body parsing depending on
the incoming content type. It consists of a routing chain handler that adds the
:data
entry in the context with the parsed data or nil
in case of an
incoming content type does not have an attached parser implementation.
In order to use it you should prepending the body-params
handler to your route
chain:
(require '[catacumba.handlers.parse :as parse])
(defn example-handler
[context]
(let [body (:data context)]
(println "Received data:" body)
(http/no-content)))
(def app
(ct/routes [[:any (parse/body-params)]
[:any example-handler]]))
;; ...
By default, the application/x-www-form-urlencoded
, multipart/form-data
,
application/json
, application/transit+json
and application/transit+msgpack
parsers come out of the box. The
cheshire json parser is used for
parsing the body with the application/json
content type.
The body parsing is a open system, implemented using clojure’s polymorphism facilities such as multimethods. If you want add additional parser, just add an additional implementation to the parse multimethod with your content-type as dispatch tag.
(require '[catacumba.handlers.parse :as parse])
(import 'ratpack.http.TypedData
'ratpack.handling.Context)
(defmethod parse/parse-body :application/xml
[^Context ctx ^TypedData body]
;; your parsing logic here
)
Autoreload
The autoreload handler consist in a very simple concept: reload all modified namespaces on each request. If you are familiar with the ring reload middleware, this one works in almost identical way.
For use it, just attach it to your routing chain:
(require '[catacumba.handlers.misc :as misc])
(def app
(ct/routes [[:any (misc/autoreloader)]
[:get "foo" #'somens/your-handler]
[:get "bar" #'somens/other-handler]
[:post ...]]))
You can see a working example in the Website example code.
Tip
|
The auto-reloading can only work if you pass var references to the router definition instead resolved values. In same way as the previous example. |
Sessions
Getting Started
The HTTP sessions in catacumba are also implemented as chain handler. So you can add session handling support to you application just by adding the handler to your routing chain:
(require '[catacumba.handlers.session :as session])
(def app
(ct/routes [[:any (session/session {:storage :inmemory})]
[:get your-handler]]))
All handlers in the route pipeline that are going after the session handler will
come with :session
key in the context with a "atom" like object. You just
treat it as atom, so for attaching some data to the session you should use the
well known swap!
function:
(defn my-handler
[context]
(let [session (:session context)]
(swap! session assoc :userid 1)
"my response"))
You can clean the session just reseting to the empty map:
(reset! session {})
One of the big advantages of using the routing chain for session set up, is that you can restrict session handling to a concrete subset of urls/resources avoiding unnecessary code execution for handlers that do not need sessions:
(def app
(ct/routes [[:prefix "admin"
[:any (session/session {:storage :inmemory})]
[:get your-handler]]
[:prefix "api"
[:get "users" other-handler]
[:get ...]]]))
Session storages
Currently catacumba comes with one basic session storage, the :inmemory
. But
the session storage system is pluggable and is defined in terms of the following
protocol:
(defprotocol ISessionStorage
(read-session [_ key])
(write-session [_ key data])
(delete-session [_ key]))
If you are familiar with the ring based session storages, you can observe that the catacumba session storage abstraction is almost identical to the ring session abstraction, so migrating from or adapting the ring session storages is really easy. The unique difference is that functions should return a promise (from promesa library).
To use a concrete session storage, just pass a instance of it as value of the
:storage
key in a session handler constructor:
(session/session {:storage (my-storage-constructor)})
If you want implement own session storage, take a look to the :inmemory
builtin one.
Authentication
Catacumba also comes with authentication facilities heavily inspired by buddy-auth.
We do not have used directly buddy-auth because it is designed for ring based applications, therefore the buddy-auth abstractions are blocking, and blocking api is not well suited for async based applications.
So, catacumba defines own abstractions for handle authentication, that are very very similar to the buddy-auth, with the exception that them expose asynchronous api, so adapt existing buddy-auth backends should be very easy.
Like buddy-auth, catacumba comes with a little set of builtin backends that can be used directly: session, jws (token) and jwe (encrypted token).
Session
Let start with session authentication backend. This backend is mainly used for web based applications and consists in verify some value on the session. So this is the easiest authentication scheme and fits perfectly for the first contact.
Start importing some needed namespaces and create an instance of the authentication backend:
(require '[catacumba.http :as http])
(require '[catacumba.handlers.auth :as cauth])
(def auth-backend
(cauth/session-backend))
Now, continue defining a handler for the login action. It consists in receive
credentials from the user input and verify them. In case of success
verification, we just need setup the :identity
key in the session.
Let see a partially implemented example:
(defn login-handler
[context]
(let [data (:body context)
user (find-user (:username data) ;; (implementation omitted)
(:password data))]
(swap! (:session context) assoc :identity user)
(http/ok "ok")))
In order to start using auth facilities in your application, you should add the authentication handler to the routing chain:
;; The application routes definition with session, auth and body
;; parsing chain handlers
(def app
(ct/routes [[:any (session/session {:storage :inmemory})] ;; Http Session
[:any (cauth/auth auth-backend)] ;; Auth backend
[:any (parse/body-params)] ;; Body parsing
[:get "login" login-handler]
[:get some-handler]])) ;; (implementation omitted)
You can see a working example using auth facilities here.
JWS Token
This authentication backend consists in use self contained tokens for authenticate the user. It behaves very similar to the session one but instead of strong the user information in a server storage, it stores it directly in a token, enabling so, completely stateless authentication.
Note
|
The security and the implementation of cryptographic primitives for that token is relied to the buddy-sign library (an other module of buddy) that implements the JWS specification. That library should be used for generate JWS tokens. |
Let start creating a backend instance:
(def secret "mysecret")
(def auth-backend
(cauth/jws-backend {:secret secret}))
Following of our new login handler:
(require '[buddy.sign.jwt :as jwt])
(require '[cheshire.core :as json])
(defn login-handler
[context]
(let [data (:body context)
user (find-user (:username data) ;; (implementation omitted)
(:password data))]
(-> (json/encode {:token (jwt/sign {:user (:id user)} secret)})
(http/ok {:content-type "application/json"}))))
And finally, put the new backend into the routing chain:
(def app
(ct/routes [[:any (cauth/auth auth-backend)] ;; Auth backend
[:any (parse/body-params)] ;; Body parsing
[:get "login" login-handler]
[:get some-handler]])) ;; (implementation omitted)
Warning
|
Take care that using jws for create tokens, the data is serialized using json + base64 and signed using strong cryptography signatures. That method ensure that the data can not be manipulated by third party but it not protect it from privacy. If you need store private data in the token, consider using JWE. |
JWE Token
This authentication backend consists in using self contained tokens for authenticate the user. It works identically to the JWS (explained previously) with the exception that instead of only signing data, it also encrypts the data, so ensuring the data privacy.
You can create the backend instance so:
(require '[buddy.sign.jwt :as jwt])
(require '[buddy.core.keys :as keys])
(def pubkey (keys/public-key "pubkey.pem"))
(def privkey (keys/private-key "privkey.pem" "thekeysecret"))
(def auth-backend
(auth/jwe-backend privkey))
Note
|
In this example we use asymmetric encryption scheme, if you want use an other encryption scheme, please check buddy-sign documentation for the complete list of supported encryption algorithms. |
The login handler is almost identical:
(require '[buddy.sign.jwt :as jwt])
(require '[cheshire.core :as json])
(defn login-handler
[context]
(let [data (:body context)
user (find-user (:username data) ;; (implementation ommited)
(:password data))]
(-> (json/encode {:token (jwt/encrypt {:user (:id user)} pubkey)})
(http/ok {:content-type "application/json"}))))
Instead of signing the content, we encrypt it using the public key. The routing chain is completely identical from the JWE Token examples.
Other
If you not happy with the builtin auth facilities, the catacumba's handler system is very flexible and you really don’t need to use buddy. You can write your own auth facilities and attach them to catacumba using the routing chain.
Security
Cross-Origin Resource Sharing
Cross-Origin Resource Sharing (CORS) is a mechanism that allows restricted resources (e.g. fonts, JavaScript, etc.) on a web page to be requested from another domain outside the domain from which the resource originated.
Is often used for allowing API resources to be accessed in a web browser, out of the domain of your web applications.
Catacumba has builtin support for CORS, and this is how you can use it:
(require '[catacumba.handlers.misc :as misc])
(def cors-conf {:origin #{"http://website.com"} ;; mandatory
:max-age 3600 ;; optional
:allow-methods #{:post :put :get :delete} ;; optional
:allow-headers #{:x-requested-with :content-type}}) ;; optional
(def app
(ct/routes [[:prefix "api/v1"
[:any (misc/cors cors-conf)]
[:get "foo" some-handler]
[:post "foo" some-save-handler]]]))
The :origin
key can be a set of possible origins or simply "*"
to allow all
origins.
Content Security Policy
Is a security related chain handler that appropriately sets the
Content-Security-Policy
headers.
Content Security Policy (CSP) is an added layer of security that helps to detect and mitigate certain types of attacks, including Cross Site Scripting (XSS) and data injection attacks. These attacks are used for everything from data theft to site defacement or distribution of malware.
Here a simple example on how to use it:
(def cspconf {:default-src "'self' *.trusted.com"
:img-src "*"
:frame-ancestors "'none'"
:reflected-xss "filter"})
(def app
(ct/routes [[:prefix "web"
[:any (csp-headers cspconf)]
[:get your-handler]]])
You can read more about that here: https://developer.mozilla.org/en-US/docs/Web/Security/CSP. The complete list of directives can be found here: https://developer.mozilla.org/en-US/docs/Web/Security/CSP/CSP_policy_directives
This handler supports the following directives: :default-src
, :frame-ancestors
,
:frame-src
, :child-src
, :connect-src
, :font-src
, :form-action
, :img-src
,
:media-src
, :object-src
, and :reflected-xss
.
Frame Options
This is a security related chain handler that adds X-Frame-Options
header to
the response.
The X-Frame-Options HTTP response header can be used to indicate whether or not
a browser should be allowed to render a page in a <frame>
, <iframe>
or
<object>
. Sites can use this to avoid clickjacking attacks, by ensuring that
their content is not embedded into other sites.
Example:
(require '[catacumba.handlers.security :as sec])
(def app
(ct/routes [[:prefix "web"
[:any (sec/frame-options-headers {:policy :deny})]
[:get your-handler]]]))
The possible values for the :policy
key are: :deny
and :sameorigin
.
Warning
|
The frame-ancestors directive from the CSP Level 2 specification officially replaces this non-standard header. |
Strict Transport Security
This is a security related chain handler that adds the
Strict-Transport-Security
header to the response.
HTTP Strict Transport Security (often abbreviated as HSTS) is a security feature that lets a web site tell browsers that it should only be communicated with using HTTPS, instead of using HTTP.
Usage example:
(require '[catacumba.handlers.security :as sec])
(def app
(ct/routes [[:prefix "web"
[:any (sec/hsts-headers {:max-age 31536000 :subdomains true })]
[:get your-handler]]]))
You can read more about that header here: https://developer.mozilla.org/en-US/docs/Web/Security/HTTP_strict_transport_security
Content Type Options
This is a security related chain handler that adds the X-Content-Type-Options
header to the response. It prevents resources with invalid media types being
loaded as stylesheets or scripts.
This chain handler does not have any additional parameters. Let see an example on how you can use it:
(require '[catacumba.handlers.security :as sec])
(def app
(ct/routes [[:prefix "web"
[:any sec/content-type-options-headers]
[:get your-handler]]]))
More information:
CSRF (Cross-Site Request Forgery)
This is a security related chain handler that protects the following handlers from one-click attack.
For use it, just add it to your routing pipeline:
(require '[catacumba.handlers.security :as sec])
(def app
(ct/routes [[:prefix "web"
[:any (sec/csrf-protect)]
[:get your-handler]]]))
The response will be populated automatically with csrftoken
cookie that should
be read by the client side javascript and put the same value under the
x-csrftoken
header or under csrftoken
form encoded field.
If you want access to the current value of the csrftoken inside catacumba
handler, you can do it using :catacumba.handlers.security
keyword lookup on
the context.
More information:
Request logging
catacumba by default does not logs almost anything in console, and the request logging is not an exception. This is a good default and is very recommended use reverse proxy logging facilities.
But, if you want request logging in catacumba, you can easy activate it just attaching additional handler to your routing chain:
(require '[catacumba.handlers.misc :as misc])
(def app
(ct/routes [[:any (misc/log)]
;; here your handlers
]))
The default implementation in most cases is more than enough, but if you don’t happy with it you can provide your own function for logging:
(defn my-logging-handler
[context, outcome]
(println context outcome))
(def app
(ct/routes [[:any (misc/log my-logging-handler)]
;; here your handlers
]))
The context
parameter is just a context that you have already used previously,
and outcome
is hash map that contains additional data such as: response
headers, respose status and request duration time.
Instrumentation
TODO
Plugins
This section will explain useful modules that are not part of the core of catacumba but are fully supported.
catacumba-prone
Prone is a exception reporting middleware for ring based applications that show a beautiful, navigable and human readable stacktraces when an exception is throwed in your application.
Full documentation: https://github.com/funcool/catacumba-prone
Note
|
You can found a complete example using prone in the examples section. |
Examples
Website and Auth
This example tries to show the way to use catacumba in a website like projects, with authentication and sessions.
Just run the following commands:
$ git clone git@github.com:funcool/catacumba.git
$ cd catacumba/
$ lein with-profile website-example run
[main] INFO ratpack.server.RatpackServer - Ratpack started for http://localhost:5050
You can found the source code of this example here.
WebSocket Echo
This example application tries to show a very simple application using the websockets capabilities of catacumba
Get it up and running following this commands:
$ git clone git@github.com:funcool/catacumba.git
$ cd catacumba/
$ lein with-profile websocket-echo-example run
[main] INFO ratpack.server.RatpackServer - Ratpack started for http://localhost:5050
You can found the source code of this example here.
Multiuser chat with SSE
This example tries to demonstrate how can you build a simple chat using "Server-Sent Events" for communicating events to the client:
For make this example application run, follow this commands:
$ git clone git@github.com:funcool/catacumba.git
$ cd catacumba/
$ lein with-profile sse-chat-example run
[main] INFO ratpack.server.RatpackServer - Ratpack started for http://localhost:5050
Now, open http://localhost:5050 in two different browsers and try send messages between them.
You can found the source code of this example here.
Debugging with prone
Prone is really awesome middleware for ring that shows a beautiful and human readable stack traces when a exception is raised in your application.
Just follow the following commands for get it up and running:
$ git clone git@github.com:funcool/catacumba.git
$ cd catacumba/
$ lein with-profile debugging-example run
[main] INFO ratpack.server.RatpackServer - Ratpack started for http://localhost:5050
You can found the source code of this example here.
Note
|
Obviously, if you are using the ring type of handler, you can use Prone as is, without any additional adaptation. This example shows how it can be used with catacumba's default handler type. |
Instrumentation
Catacumba comes with the ability to instrument your application for taking different kinds of diagnosis, such as performance, latency, etc. This example shows how it can be done.
In case of this concrete example application, it uses the instrumentation facilities of catacumba for monitoring the time of execution of request handlers.
Follow this steps for get this example up and running:
$ git clone git@github.com:funcool/catacumba.git
$ cd catacumba/
$ lein with-profile interceptor-example run
[main] INFO ratpack.server.RatpackServer - Ratpack started for http://localhost:5050
And go to http://localhost:5050/
After some requests, you will see the similar output in the console:
Computation :compute elapsed in: 0.025150461 (sec)
Computation :compute elapsed in: 0.001690894 (sec)
Computation :compute elapsed in: 0.001541675 (sec)
Computation :compute elapsed in: 0.001554894 (sec)
Computation :compute elapsed in: 0.00175033 (sec)
You can found the source code of this example here.
Single file web app
This example application requires that you should have boot-clj properly installed on your system.
This example tries to show how you can use catacumba for building small web applications that fits in one file and execute them like a shell script or an executable.
You should execute the following commands for get it up and running:
$ git clone git@github.com:funcool/catacumba.git
$ cd catacumba/examples/single-file
$ export BOOT_CLOJURE_VERSION=1.7.0
$ ./main.clj
[main] INFO ratpack.server.RatpackServer - Ratpack started for http://localhost:5050
You can found the source code of this example here.
FAQ
What is the difference between catacumba and Aleph?
First of all, Aleph is not a real alternative to catacumba, because its approach is so much low level and its web server support is a little bit constrained by ring spec. Furthermore, aleph is already used in catacumba as http client in tests code and manifold (async abstractions behind aleph) is a first class abstractions for handle async values.
So, I’m happy to tell you that you can use the both libraries together because they are very complementary.
What is the rationale behind this project?
I started writing this library as a research project to provide a simple, non obstructive (a la ring) without the constraints of the existing ring spec. The aim is to create a web toolkit for building asynchronous web services.
Here is an incomplete list of things that catacumba aims to achieve:
-
Allow different types of handlers by being flexible and extensible
-
Provide a simple and lightweight approach for defining asynchronous web services with support for different abstractions such as promises, futures, core.async, manifold, reactive-streams, etc…
-
Build upon abstractions with simplicity and extensibility in mind.
-
Provide built in declarative style routing.
-
Remain unopinionated and versatile.
-
Come with back pressure support out of the box.
catacumba is not designed:
-
To be a fully integrated full stack solution like Immutant or Pedestal.
-
To provide an opinionated way to structure your "business logic"
-
To provide all possible features that you might need.
-
To be a low level, ring based library.
The result of this research project is a powerful, lightweight, and fully extensible asynchronous web toolkit built on top of existing and well designed components such as Ratpack and Netty.
Developers Guide
Philosophy
Five most important rules:
-
Beautiful is better than ugly.
-
Explicit is better than implicit.
-
Simple is better than complex.
-
Complex is better than complicated.
-
Readability counts.
All contributions to catacumba should keep these important rules in mind.
Contributing
Unlike Clojure and other Clojure contributed libraries catacumba does not have many restrictions for contributions. Just open an issue or pull request.
Source Code
catacumba is open source and can be found on github.
You can clone the public repository with this command:
git clone https://github.com/funcool/catacumba
License
catacumba is licensed under BSD (2-Clause) license:
Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz> All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.