Recently when implementing a Docker Registry I came across
the need to hash the incoming request body in a way that
matches the clients hash. Since the Docker engine is an
existing client that speaks a couple of different
Content-Type
s (such as
application/vnd.docker.distribution.manifest.v2+json
, we
cannot use Servant's alternate content types to get a
ByteString to hash. This leads us to one possible
implementation, which is to create a new content type that
uses application/vnd.docker.distribution.manifest.v2+json
as the Accept
header, but also hashes the content body
with SHA256
. Our content type will behave very similarly
to application/json
.
We will use cryptonite for hashing and Aeson for JSON decoding. The full code follows.
{-# LANGUAGE FlexibleInstances #-}{-# LANGUAGE MultiParamTypeClasses #-}{-# LANGUAGE OverloadedStrings #-}module SR.HashedJSONContentType whereimport Crypto.Hash (Digest, SHA256)import Data.Aesonimport qualified Data.ByteString.Lazy.Char8 as BSCimport Network.HTTP.Media hiding (Accept)import Servant.API.ContentTypesimport Utils (mkDigest)data HashedJSON = HashedJSON Stringinstance Accept HashedJSON wherecontentType _ = "application" // "vnd.docker.distribution.manifest.v2+json"-- | We don't need MimeRender for this since we are only using-- the content-type for receiving data.-- instance Show a => MimeRender HashedJSON a where-- mimeRender _ val = pack ("This is MINE! " ++ show val)instance FromJSON a => MimeUnrender HashedJSON (Digest SHA256, a) wheremimeUnrender _ bs = case eitherDecodeLenient bs ofLeft err -> Left errRight val -> Right (mkDigest $ BSC.toStrict bs, val)
The most interesting part is the MimeUnrender
instance for
our custom content type HashedJSON
. Servant provides a
more lenient version of Aeson's decode
called
eitherDecodeLenient
. We use this for compatibility with
the normal 'JSON
content type in Servant.
The instance uses a tuple (Digest SHA256, a)
so any route
types we write in the future will look similar to the
following code which uses a fake type OurType
.
ReqBody '[HashedJSON] (Digest SHA256, OurType)
An example handler using the above ReqBody
declaration
follows.
putManifest :: (CH.Digest CH.SHA256, Manifest) -> App NoContent)putManifest (digest, manifest) = do-- do stuff with digests and manifestsreturn NoContent
One can envision how changing the type of the hash in the
type might allow us to specify which algorithm we want to
use in the same way that we specify Manifest
.