% Dhall: A programmable configuration language % Gabriella Gonzalez
<style> code { font-family: "Fira Code"; } </style>This talk introduces the Dhall configuration language to functional programmers.
In this talk, I will cover:
- Using Dhall as an ordinary configuration file format
- Using Dhall as a programming language
- Dhall's tooling
- Dhall's language security guarantees
Dhall is a programmable configuration language that you can think of as: JSON + functions + types + imports
Dhall originated as an academic exercise to settle the following question:
Would people tolerate programmable configuration files if they were not Turing-complete?
The project then grew beyond my expectations, so I went with the flow and learned a lot along the way.
When I say Dhall is like JSON I mean that:
- Dhall can model scalars, records, and lists (and other things)
- You can marshal a Dhall configuration file directly into a program as data
Here is an example Dhall configuration file:
{ home = "/home/bill"
, privateKey = "/home/bill/.ssh/id_ed25519"
, publicKey = "/home/bill/.ssh/id_ed25519.pub"
}
… which corresponds to this JSON:
$ dhall-to-json --file ./config.dhall | tee ./config.json
{
"home": "/home/bill",
"privateKey": "/home/bill/.ssh/id_ed25519",
"publicKey": "/home/bill/.ssh/id_ed25519.pub"
}
You can migrate an existing JSON file to Dhall.For example, this JSON:
[
{
"discontinued": false,
"description": "Google Pixel XL 128GB Unlocked"
},
{
"description": "Apple iPhone SE (64GB, Black)"
}
]
… corresponds to this Dhall configuration:
$ json-to-dhall --file ./inventory.json
[ { description = "Google Pixel XL 128GB Unlocked"
, discontinued = Some False
}
, { description = "Apple iPhone SE (64GB, Black)"
, discontinued = None Bool
}
]
[ { name = "John Doe"
, age = 14
}
, { name = "Mary Jane"
, age = 13
}
]
… corresponds to this YAML:
$ dhall-to-yaml --file ./students.dhall
- age: 14
name: John Doe
- age: 13
name: Mary Jane
You can also convert YAML to Dhall. For example, this YAML:
docker:
- image: ubuntu:14.04
- image: mongo:2.6.8
command: [mongod, --smallfiles]
- image: postgres:9.4.1
… corresponds to this Dhall configuration:
$ yaml-to-dhall --file ./docker.yaml
{ docker =
[ { command = None (List Text)
, image = "ubuntu:14.04"
}
, { command = Some [ "mongod", "--smallfiles" ]
, image = "mongo:2.6.8"
}
, { command = None (List Text)
, image = "postgres:9.4.1"
}
]
}
Here is a more "real world" example of a Dhall configuration for Kubernetes:
let kubernetes = https://raw.githubusercontent.com/dhall-lang/dhall-kubernetes/a4126b7f8f0c0935e4d86f0f596176c41efbe6fe/package.dhall sha256:ef3845f617b91eaea1b7abb5bd62aeebffd04bcc592d82b7bd6b39dda5e5d545
in kubernetes.Deployment::{
, metadata = kubernetes.ObjectMeta::{ name = Some "nginx" }
, spec = Some kubernetes.DeploymentSpec::{
, selector = kubernetes.LabelSelector::{
, matchLabels = Some (toMap { name = "nginx" })
}
, template = kubernetes.PodTemplateSpec::{
, metadata = kubernetes.ObjectMeta::{ name = Some "nginx" }
, spec = Some kubernetes.PodSpec::{
, containers =
[ kubernetes.Container::{
, name = "nginx"
, image = Some "nginx:1.15.3"
, ports = Some
[ kubernetes.ContainerPort::{ containerPort = 80 } ]
}
]
}
}
}
}
… which corresponds to this YAML Kubernetes configuration:
$ dhall-to-yaml --file ./deployment.dhall
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
name: nginx
template:
metadata:
name: nginx
spec:
containers:
- image: nginx:1.15.3
name: nginx
ports:
- containerPort: 80
The reverse direction works, too, if we provide a bit more information:
$ yaml-to-dhall '(https://raw.githubusercontent.com/dhall-lang/dhall-kubernetes/a4126b7f8f0c0935e4d86f0f596176c41efbe6fe/types.dhall).Deployment' --file ./deployment.yaml \
| dhall rewrite-with-schemas --schemas 'https://raw.githubusercontent.com/dhall-lang/dhall-kubernetes/a4126b7f8f0c0935e4d86f0f596176c41efbe6fe/schemas.dhall'
let schemas = https://raw.githubusercontent.com/dhall-lang/dhall-kubernetes/a4126b7f8f0c0935e4d86f0f596176c41efbe6fe/schemas.dhall
in schemas.Deployment::{
, metadata = schemas.ObjectMeta::{ name = Some "nginx" }
, spec = Some schemas.DeploymentSpec::{
, selector = schemas.LabelSelector::{
, matchLabels = Some [ { mapKey = "name", mapValue = "nginx" } ]
}
, template = schemas.PodTemplateSpec::{
, metadata = schemas.ObjectMeta::{ name = Some "nginx" }
, spec = Some schemas.PodSpec::{
, containers =
[ schemas.Container::{
, image = Some "nginx:1.15.3"
, name = "nginx"
, ports = Some [ schemas.ContainerPort::{ containerPort = 80 } ]
}
]
}
}
}
}
You can convert between Dhall and the following file formats:
- JSON -
dhall-to-json
/json-to-dhall
- YAML -
dhall-to-yaml
/yaml-to-dhall
- XML -
dhall-to-xml
/xml-to-dhall
- Bash -
dhall-to-bash
- Nix -
dhall-to-nix
Dhall is not just a preprocessor for JSON/YAML
Some languages (like Haskell) can read Dhall files without going through JSON:
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE OverloadedStrings #-}
module Example where
import Dhall (Generic, FromDhall, Text)
import qualified Dhall
data Configuration = Configuration
{ home :: Text
, privateKey :: Text
, publicKey :: Text
} deriving (Generic, FromDhall, Show)
main :: IO ()
main = do
config <- Dhall.input Dhall.auto "./config.dhall"
print (config :: Configuration)
The currently official language bindings are:
- Clojure
- Haskell
- Go
- Ruby
- Rust
… and there is one unofficial language binding that is worth mentioning:
- Java - Fairly comprehensive
The most wished-for language binding is Python
The latest list can be found at:
Many people generate JSON/YAML when they first start trying out Dhall, because:
-
You can "try before you buy" (doesn't require immediate buy-in from your peers)
-
Generating JSON/YAML is a more incremental migration path
-
You can download prebuilt statically-linked executables for Windows/OS X/Linux
dhall
dhall-to-{json,yaml}
/{json,yaml}-to-dhall
dhall-lsp-server
There are some benefits of reading a Dhall configuration file directly when you commit more fully to the language:
-
Simplicity
-
Marshalling unions (a.k.a. sum types)
JSON/YAML don't natively support sum types
-
Marshalling functions
For example, this works:
f :: (Natural -> [Natural]) <- Dhall.input Dhall.auto "λ(x : Natural) → [ x, x ]" print (f 2) -- [ 2, 2 ]
This is limited to monomorphic non-higher-order functions
This means that you can factor out shared logic into (immutable) variables:
let user = "bill"
in { home = "/home/${user}"
, privateKey = "/home/${user}/.ssh/id_ed25519"
, publicKey = "/home/${user}/.ssh/id_ed25519.pub"
}
… and use programming language features like string interpolation.
DEMO: dhall-lang.org
Dhall supports the following simple ("scalar") types:
Bool
:True
,False
Text
:"Hello, world!"
Natural
:0
,1
,2
, …Integer
: …,-2
,-1
,+0
,+1
,+2
, …Double
:3.14159265
,6.0221409e+23
Dhall also supports the following complex ("composite") types:
Optional
:Some 1
,None Natural
List
:[ 2, 3, 5 ]
,[] : List Natural
- Records:
{ x = 1.2, y = -2.5 }
- Unions:
let Example = < Left : Natural | Right : Bool > in Example.Left 0
- Enums:
let DNA = < A | T | G | C > in [ DNA.A, DNA.T, DNA.C ]
- Enums:
Complex types can be nested arbitrarily (like JSON/YAML, unlike INI/TOML)
Dhall's Text
support is best-in-class among configuration languages
For example, Dhall supports string interpolation:
λ(name : Text) → "Hello, ${name}!"
… and multi-line strings with automatic dedentation:
let header = "Header"
let content = "Content"
in ''
<!DOCTYPE html>
<html>
<body>
<h1>${header}</h1>
<p>${content}</p>
</body>
</html>
''
Dhall supports Nix-style dotted-field syntax. For example, this:
{ a.b.c = 1, a.b.d = True }
… is the same thing as:
{ a = { b = { c = 1, d = True } } }
This comes in handy when working with deeply-nested records.
Dhall is more flexible than Nix. For example, this works in Dhall:
{ a.b.c = 1, a = { b.d = 2 } }
… but does not work in Nix.
You can avoid repeating yourself (DRY) using functions:
let makeUser = \(user : Text) ->
let home = "/home/${user}"
let privateKey = "${home}/.ssh/id_ed25519"
let publicKey = "${privateKey}.pub"
in { home, privateKey, publicKey }
in [ makeUser "bill"
, makeUser "jane"
]
… which is equivalent to this JSON:
[
{
"home": "/home/bill",
"privateKey": "/home/bill/.ssh/id_ed25519",
"publicKey": "/home/bill/.ssh/id_ed25519.pub"
},
{
"home": "/home/jane",
"privateKey": "/home/jane/.ssh/id_ed25519",
"publicKey": "/home/jane/.ssh/id_ed25519.pub"
}
]
You can validate configuration files ahead of time using types:
let Config : Type =
{ home : Text
, privateKey : Text
, publicKey : Text
}
let makeUser : Text -> Config = \(user : Text) ->
let home : Text = "/home/${user}"
let privateKey : Text = "${home}/.ssh/id_ed25519"
let publicKey : Text = "${privateKey}.pub"
let config : Config = { home, privateKey, publicKey }
in config
let configs : List Config =
[ makeUser "bill"
, makeUser "jane"
]
in configs
You can embed other expressions by path:
-- ./number.dhall
2
-- This is valid Dhall code
./number.dhall + ./number.dhall
-- More common idiom, equivalent to the previous code:
let number = ./number.dhall
in number + number
… or by environment variable:
"Hello, my name is ${env:USER as Text}"
You can also embed other expressions by URL!
let generate = https://prelude.dhall-lang.org/List/generate.dhall
let makeUser = \(user : Text) ->
let home = "/home/${user}"
let privateKey = "${home}/.ssh/id_ed25519"
let publicKey = "${privateKey}.pub"
in { home, privateKey, publicKey }
let buildUser = \(index : Natural) ->
makeUser "build${Natural/show index}"
let Config =
{ home : Text
, privateKey : Text
, publicKey : Text
}
in generate 10 Config buildUser
The language design ensures that URL imports are secure
Dhall provides language support for tests!
--| `lessThanEqual` checks if one Natural is less than or equal to another.
let lessThanEqual
: Natural → Natural → Bool
= λ(x : Natural) → λ(y : Natural) → Natural/isZero (Natural/subtract y x)
let example0 = assert : lessThanEqual 5 6 ≡ True
let example1 = assert : lessThanEqual 5 5 ≡ True
let example2 = assert : lessThanEqual 5 4 ≡ False
…
in lessThanEqual
You can implement derived features by mixing these simpler features
For example, a package is just a (potentially nested) record that you import:
let Prelude = https://prelude.dhall-lang.org/v20.0.0/package.dhall
in Prelude.Bool.not True
In Dhall, a template is just a function that interpolates Text
:
\(args : { year : Natural, copyrightHolder : Text }) ->
''
Copyright ${Natural/show args.year} ${args.copyrightHolder}
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
''
Here is an example of how you would apply a template using dhall text
:
$ dhall text <<< './template.dhall { year = 2020, copyrightHolder = "Gabriella Gonzalez" }'
Copyright 2020 Gabriella Gonzalez
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
A schema is just a type annotation that you import:
-- ./schema.dhall
{ home : Text, privateKey : Text, publicKey : Text }
-- ./config.dhall
{ home = "/home/bill"
, privateKey = "/home/bill/.ssh/id_ed25519"
, publicKey = "/home/bill/.ssh/id_ed25519.pub"
} : ./schema.dhall
A property test is just a unit test where both sides have the same normal form:
--| `lessThanEqual` checks if one Natural is less than or equal to another.
let lessThanEqual
: Natural → Natural → Bool
= λ(x : Natural) → λ(y : Natural) → Natural/isZero (Natural/subtract y x)
…
let property0 = λ(n : Natural) → assert : lessThanEqual 0 n ≡ True
let property1 = λ(n : Natural) → assert : lessThanEqual n n ≡ True
in lessThanEqual
This works because the interpreter can normalize abstract expressions
Dhall has a very restricted set of primitive operations ("builtins")
Main things that may surprise new users:
- No support for
Text
introspection - No support for
Double
arithmetic ==
/!=
only work onBool
s
The following page explains some of these design decisions:
tl;dr: The available built-ins discourage weakly-typed programming idioms
Dhall is a total functional programming language
This means Dhall configuration files never hang, crash, or throw exceptions
One benefit of being total is strong normalization:
$ dhall repl
⊢ :let generate = https://prelude.dhall-lang.org/List/generate.dhall
generate : ∀(n : Natural) → ∀(a : Type) → ∀(f : Natural → a) → List a
⊢ generate 10
λ(a : Type) →
λ(f : Natural → a) →
[ f 0, f 1, f 2, f 3, f 4, f 5, f 6, f 7, f 8, f 9 ]
Dhall does not provide language support for recursion
Recursion is not impossible; it's just not ergonomic.
For example, this is how Dhall models JSON:
let JSON/Type
: Type
= ∀(JSON : Type) →
∀ ( json
: { array : List JSON → JSON
, bool : Bool → JSON
, double : Double → JSON
, integer : Integer → JSON
, null : JSON
, object : List { mapKey : Text, mapValue : JSON } → JSON
, string : Text → JSON
}
) →
JSON
in JSON/Type
See: docs.dhall-lang.org
- How to translate recursive code to Dhall
Dhall does not (yet) support bidirectional type inference
This means that the language requires types or type annotations in a few places, like:
-
An empty list:
[] : List Natural
-
An empty
Optional
value:None Natural
-
A function's input type:
\(x : Natural) -> x + 1
-
Specializing a polymorphic function:
Prelude.List.map Natural Natural (\(x : Natural) -> x + 1)
$ dhall repl
⊢ :let Prelude = https://prelude.dhall-lang.org/package.dhall
…
⊢ Prelude.<TAB>
Prelude.Bool Prelude.JSON Prelude.Monoid Prelude.Text
Prelude.Double Prelude.List Prelude.Natural Prelude.XML
Prelude.Function Prelude.Location Prelude.Operator
Prelude.Integer Prelude.Map Prelude.Optional
⊢ Prelude.Natural.<TAB>
Prelude.Natural.build Prelude.Natural.listMin
Prelude.Natural.enumerate Prelude.Natural.max
Prelude.Natural.equal Prelude.Natural.min
Prelude.Natural.even Prelude.Natural.odd
Prelude.Natural.fold Prelude.Natural.product
Prelude.Natural.greaterThan Prelude.Natural.show
Prelude.Natural.greaterThanEqual Prelude.Natural.sort
Prelude.Natural.isZero Prelude.Natural.subtract
Prelude.Natural.lessThan Prelude.Natural.sum
Prelude.Natural.lessThanEqual Prelude.Natural.toDouble
Prelude.Natural.listMax Prelude.Natural.toInteger
⊢ Prelude.Natural.product [ 2, 3, 5 ]
30
The interpreter supports rich "diff"s of arbitrary expressions. For example:
$ dhall diff '{ x = [ 1, 2, 3 ], y = True }' '{ x = [ 2, 3, 4 ], z = "ABC" }'
{ - y = …
, + z = …
, x = [ - 1
, …
, + 4
]
}
Only the difference is displayed. Fields/elements/values in common are omitted
This feature is used in two places to improve the user experience
The interpreter uses rich diffs to highlight what went wrong for type errors:
⊢ :let Person = { name : Text, age : Natural }
Person : Type
⊢ { name = "John Doe", agee = 24 } : Person
Error: Expression doesn't match annotation
{ - age : …
, + agee : …
, …
}
1│ { name = "John Doe", agee = 24 } : Person
(input):1:1
This makes it easy to drill down to the offending typo, even for large records.
Also, test failures will display a rich diff:
Error: Assertion failed
"[▮true, 1▮]"
1│ assert : JSON.renderCompact (JSON.array [ JSON.bool True, JSON.natural 1 ]) === "[true, 1]"
(input):1:1
The language also supports hashing arbitrary expressions
Dhall hashes normal forms, so two αβ-equivalent expressions have the same hash:
$ dhall hash <<< '\(x : Bool) -> [ x && True, x || False ]'
sha256:486b561c6e38adf2c2853b6395358c16c3ed1befc35c33067996dcfb51a74e62
$ dhall hash <<< '\(y : Bool) -> [ y, y ]'
sha256:486b561c6e38adf2c2853b6395358c16c3ed1befc35c33067996dcfb51a74e62
You can verify that a refactor is behavior-preserving using these hashes
If the hash doesn't change, then it is a behavior-preserving refactor
These hashes are insensitive to:
- Comments / coding style
- Variable names
- Module organization
You can protect a remote import with a hash of the import, like this:
let Prelude =
https://prelude.dhall-lang.org/v20.0.0/package.dhall
sha256:21754b84b493b98682e73f64d9d57b18e1ca36a118b81b33d0a243de8455814b
…
The hash serves two purposes here:
-
The import is always verified against the hash to protect against tampering
-
The import is cached locally in a content-addressable store
… where the hash is the lookup key
Dhall provides a language server (dhall-lsp-server
):
This means that you can get IDE support for VSCode, Sublime, Emacs, and Vim
… and any other editor that supports the language server protocol
Dhall also includes a documentation generator (dhall-docs
)
This plays a role analogous to Haskell's haddock
tool
Examples:
Note: there is not yet a repository for hosting packages and their documentation
The most widely used Dhall package is the Prelude:
let Prelude = https://prelude.dhall-lang.org/v20.0.0/package.dhall
This package includes:
- Widely-used general-purpose utilities (analogous to Haskell's Prelude)
- Support for commonly used formats (e.g. JSON / XML)
- Types related to the core language (e.g.
Map
/Location
)
Right now the main way to discover other Dhall packages is
awesome-dhall
If you can combine rich diffs with imports you get semantic change logs:
$ dhall diff \
'https://prelude.dhall-lang.org/v18.0.0/package.dhall' \
'https://prelude.dhall-lang.org/v19.0.0/package.dhall'
{ + Operator = …
, `Optional` = { + concatMap = …
, …
}
, …
}
$ dhall diff \
'https://prelude.dhall-lang.org/v19.0.0/package.dhall' \
'https://prelude.dhall-lang.org/v20.0.0/package.dhall'
{ JSON = { + renderCompact = …
, …
}
, `Text` = { + replace = …
, …
}
, XML = { render = λ(… : …
→ …
→ …)
→ …
…
{ element = λ(… : { …
})
→ "<"
++ …
. …
++ ""
++ …
{ …
}
…
. …
…
( λ(… : { …
})
→ λ(… : …)
→ " "
++ …
. …
++ "=""
++ - ….…
+ … …
++ """
++ …
++ ""
)
""
++ ""
++ if …
… then "/>"
else ">"
++ …
…
…
. …
…
…
""
++ "</"
++ …
. …
++ ">"
++ ""
, text = λ(… : …)
→ - _
+ … …
}
, …
}
, …
}
You can validation configuration values using the language support for tests:
let Prelude = https://prelude.dhall-lang.org/v20.0.0/package.dhall
let Config = { opacity : Natural, minReplicas : Natural, maxReplicas : Natural }
let validateConfig =
\(config : Config) ->
let validOpacity = Prelude.Natural.lessThanEqual config.opacity 100
let validReplicas = Prelude.Natural.lessThanEqual config.minReplicas config.maxReplicas
in { validOpacity = True, validReplicas = True }
=== { validOpacity, validReplicas }
let config
: Config
= { opacity = 101, minReplicas = 4, maxReplicas = 4 }
let validate = assert : validateConfig config
in config
Validation failures only highlight the tests that failed:
Error: Assertion failed
{ validOpacity = - True
+ False
, …
}
18│ assert : validateConfig config
19│
This makes use of the tooling support for rich diffs
Dhall's is best-in-class when it comes to language security (LangSec)
This was the raison d'être for Dhall, to balance programming with safety
Most of what I'll cover can be found here:
dhall-lang.org
explains our guiding principle:
The language aims to support safely importing and evaluating untrusted Dhall code, even code authored by malicious users. We treat the inability to do so as a specification bug.
Pedantically, being Turing-complete or not does not mean much per se
However, preventing Turing-completeness means getting other things right, like:
-
Having a type system (or some other form of static analysis)
-
Eliminating escape hatches or back doors
-
Forbidding general recursion
Note: There are benefits to avoiding Turing-completeness for theorem provers, but Dhall is not a theorem prover
When people say they don't want Turing-complete config files, they really mean:
-
They want to forbid arbitrary side effects
-
They want configuration files to be easier to statically analyze
-
They don't want configuration files to throw exceptions
Dhall gets these right, too, but there isn't a convenient short-hand for the above features.
The language's purity is the single greatest safeguard against malicious code
Specifically:
-
The only permitted side-effect is importing Dhall code
In other words, Dhall can not compromise the host machine in any way
-
The import system protects against data exfiltration
… so that Dhall configuration files cannot harvest/leak secrets/keys/passwords
-
You can also completely disable all imports or just HTTP(S) imports
… if you are extra paranoid and you don't need them
-
Other than that, the language is completely pure
The only other effect Dhall can have is via the program being configured
The language's static analysis tools are the second most important safeguard
Specifically:
-
The type system has no escape hatch
This means that you can make very strong assertions about any expression that type-checks
-
Integrity checks (hashes) provide even stronger guarantees about an expression
I expand upon this in:
Semantic integrity checks are the next generation of semantic versioning
-
Strong normalization improves program comprehension
You can reduce any Dhall config to an canonical normal form free of obfuscation
Imports have additional safeguards:
-
Integrity checks protect against man-in-the-middle attacks
-
Caching protected imports mitigates usage tracking
-
The referential sanity check protects against the Dhall analog of cross-site scripting (XSS)
-
The Dhall analog of the same origin policy protects against data exfiltration
-
Dhall also uses CORS to stop server-side request forging
The following web server securely interprets any Dhall code it receives:
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module Main where
import Control.Exception (Exception, SomeException)
import Control.DeepSeq (($!!))
import Control.Monad.IO.Class (liftIO)
import Data.ByteString.Lazy (ByteString)
import Dhall.Import (Status(..))
import Dhall.Pretty (CharacterSet(..))
import Network.Wai (Application)
import Dhall.Core
( Directory(..)
, File(..)
, FilePrefix(..)
, Import(..)
, ImportHashed(..)
, ImportMode(..)
, ImportType(..)
, Scheme(..)
, URL(..)
)
import qualified Control.Exception as Exception
import qualified Control.Monad.Except as Except
import qualified Control.Monad.State.Strict as State
import qualified Data.ByteString.Lazy as ByteString
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Encoding
import qualified Dhall.Core as Core
import qualified Dhall.Import as Import
import qualified Dhall.Parser as Parser
import qualified Dhall.Pretty
import qualified Dhall.TypeCheck as TypeCheck
import qualified Network.HTTP.Types as Types
import qualified Network.Wai as Wai
import qualified Network.Wai.Handler.Warp as Warp
import qualified Prettyprinter.Render.Text as Pretty
import qualified System.Timeout as Timeout
toBytes :: Exception e => e -> ByteString
toBytes exception = ByteString.fromStrict (Encoding.encodeUtf8 text)
where
text = Text.pack (show exception) <> "\n"
rootImport :: Import
rootImport =
Import
{ importHashed = ImportHashed
{ hash = Nothing
, importType = Remote (URL HTTPS "0.0.0.0" path_ Nothing Nothing)
}
, importMode = Code
}
where
path_ = File (Directory []) ""
getStatus :: IO Status
getStatus = flip State.evalStateT (Import.emptyStatus ".") do
let here = Import.chainedFromLocalHere Here (File (Directory []) "") Code
-- This is the key bit, which sets the starting import to a URL, which
-- turns on Dhall's various safeguards against exploits
chainedImport <- Import.chainImport here rootImport
return (Import.emptyStatus "."){ _stack = pure chainedImport }
application :: Status -> Application
application status request respond = do
let die message =
Except.throwError (Wai.responseLBS Types.status400 [] message)
result <- (Timeout.timeout 7_000_000 . Except.runExceptT) do
inputBytes <- liftIO (Wai.strictRequestBody request)
inputText <- case Encoding.decodeUtf8' (ByteString.toStrict inputBytes) of
Left _ -> die "The request body was not valid UTF-8 text"
Right inputText -> return inputText
parsedExpression <- case Parser.exprFromText "(request)" inputText of
Left exception -> die (toBytes exception)
Right parsedExpression -> return parsedExpression
resolvedExpression <- do
let load =
State.evalStateT (Import.loadWith parsedExpression) status
result <- liftIO (Exception.try @SomeException load)
case result of
Left exception -> die (toBytes exception)
Right resolvedExpression -> return resolvedExpression
case TypeCheck.typeOf resolvedExpression of
Left exception -> die (toBytes exception)
Right _ -> return ()
let normalizedExpression = Core.normalize resolvedExpression
let doc = Dhall.Pretty.prettyCharacterSet Unicode normalizedExpression
let outputText = Pretty.renderStrict (Dhall.Pretty.layout doc) <> "\n"
let outputBytes = ByteString.fromStrict (Encoding.encodeUtf8 outputText)
evaluatedBytes <- liftIO (Exception.evaluate $!! outputBytes)
Except.throwError (Wai.responseLBS Types.status200 [] evaluatedBytes)
respond case result of
Nothing ->
Wai.responseLBS Types.status413 [] "Timeout"
Just (Left response) ->
response
Just (Right response) ->
response
main :: IO ()
main = do
status <- getStatus
Warp.run 3000 (application status)
Dhall is a programmable configuration language with an emphasis on language security
Dhall's tooling and documentation is good
You can use the JSON/YAML support to introduce Dhall incrementally within your organization
You can visit dhall-lang.org to learn more
You can find these slides at: