Compare commits
314 commits
Author | SHA1 | Date | |
---|---|---|---|
|
a2e003367d | ||
|
b3214eac38 | ||
|
f71953e63d | ||
|
c382a1f34e | ||
|
2daae80ea8 | ||
|
a22600979a | ||
|
b8675c0924 | ||
|
751350ba9e | ||
|
a4194dc490 | ||
|
6392dce4bf | ||
|
8f081bd9ad | ||
|
ad25e98e19 | ||
|
0fc6e395cb | ||
|
58aa0d1c0c | ||
|
18bc2cf314 | ||
|
d5b9cbf634 | ||
|
ff135e868b | ||
|
86c61c6dbd | ||
|
3f6886ad2d | ||
|
53c132173c | ||
|
a445fbafd6 | ||
|
52f76ea722 | ||
|
4627683a64 | ||
|
e4650de303 | ||
|
2323906080 | ||
|
f0e2316895 | ||
|
43c57332dd | ||
|
1833ef0d6e | ||
|
489cbd59f4 | ||
|
1fba9dc604 | ||
|
8ef5021a5f | ||
|
036102af58 | ||
|
59b5fe67cd | ||
|
ae8e1e6003 | ||
|
cb310b8294 | ||
|
5e1569e9e2 | ||
|
4e8fb045e2 | ||
|
4cc714d654 | ||
|
3006e90126 | ||
|
c48a6702b7 | ||
|
9c81b4927a | ||
|
117a2cc5e1 | ||
|
78280dc267 | ||
|
c19ed0fb92 | ||
|
658585a7cd | ||
|
65de6f701c | ||
|
a19cb84a0e | ||
|
9d66e16706 | ||
|
77b92d0d7d | ||
|
f5a91d20e1 | ||
|
dd29f25f77 | ||
|
04f59c012b | ||
|
256cec566f | ||
|
276ca2ed01 | ||
|
c1c631eaff | ||
|
0e051ccfdf | ||
|
658217b021 | ||
|
af3dde1b1d | ||
|
ced5f1a655 | ||
|
626e1c3a7c | ||
|
0c80bc8f8e | ||
|
d52c5d08a0 | ||
|
89b66a3634 | ||
|
3370b75622 | ||
|
9a99ef9a0b | ||
|
408352320e | ||
|
010e6a72af | ||
|
39898676a8 | ||
|
bbd82a736f | ||
|
17e3eb1041 | ||
|
de923fc887 | ||
|
222ccf107c | ||
|
d05da71f09 | ||
|
cedab6572d | ||
|
15b364ae93 | ||
|
8fccfccae0 | ||
|
181e51db8a | ||
|
0e4d02ae75 | ||
|
b4c4131778 | ||
|
6d5c3023ce | ||
|
7ef9730f77 | ||
|
6da8488f9b | ||
|
f4cd56446b | ||
|
50355d0125 | ||
|
34aed1d289 | ||
|
5c80214351 | ||
|
009dc06e76 | ||
|
e2a9165229 | ||
|
d35b3e9b70 | ||
|
002fa2107a | ||
|
bd9151b9de | ||
|
17b55634b3 | ||
|
3158809631 | ||
|
cdd7c34add | ||
|
67322d8ab8 | ||
|
67da8514a0 | ||
|
61d0d14b5c | ||
|
a8f1a7603f | ||
|
78034cd2b3 | ||
|
6f12e38698 | ||
|
9a3fd77a3a | ||
|
e14f445e2a | ||
|
4caa1f563b | ||
|
e1b59dbb31 | ||
|
b17d018d3f | ||
|
e98ae8adba | ||
|
e4945740aa | ||
|
7a770b5a1e | ||
|
22d5790e73 | ||
|
75db4a5327 | ||
|
75cb9ac246 | ||
|
aab7e0d5dd | ||
|
3493d135f0 | ||
|
e8c301afc9 | ||
|
b56d681fde | ||
|
b33442423e | ||
|
c388c5e82c | ||
|
73c87bc2bc | ||
|
29d2553e74 | ||
|
cb294aa2b3 | ||
|
a975cfc361 | ||
|
9a3979926d | ||
|
05ef0dd1d3 | ||
|
62033db535 | ||
|
d9d8fa7525 | ||
|
42ceb3916d | ||
|
bcb484774e | ||
|
39fb875951 | ||
|
efffc70919 | ||
|
8af80d35a0 | ||
|
e01188aaad | ||
|
3ed24fdd90 | ||
|
0e41e37c93 | ||
|
f2bd982eaf | ||
|
1bb0282abc | ||
|
575aa70eca | ||
|
d81c8d9911 | ||
|
6718752b4a | ||
|
5f8aaec146 | ||
|
fca59556dd | ||
|
b033871dfc | ||
|
861cd4f997 | ||
|
5ead291f8d | ||
|
b0b02f1948 | ||
|
65e3070cac | ||
|
67a37dc3f6 | ||
|
04e4de5260 | ||
|
42b7d0eb9b | ||
|
f3d25bfdb3 | ||
|
4e4ad495ef | ||
|
043d5a0e90 | ||
|
70f6c49524 | ||
|
70b3721537 | ||
|
fea40bd0fc | ||
|
ca6774d797 | ||
|
e2e9ce0596 | ||
|
53b1d9d2b6 | ||
|
551d4936af | ||
|
bd9e4b1090 | ||
|
e05826a799 | ||
|
95033be30f | ||
|
7c012d70d3 | ||
|
48d22a35b8 | ||
|
8e7a775cdd | ||
|
05674e4870 | ||
|
119e54a800 | ||
|
26b01f03f2 | ||
|
abc53b54e3 | ||
|
b0f8c89472 | ||
|
f92d2c7ad6 | ||
|
43760caf97 | ||
|
9df5195710 | ||
|
b7c6a95929 | ||
|
51c8edb74d | ||
|
e9ae1eeed8 | ||
|
910a3ae7ec | ||
|
d5e439e56b | ||
|
9666f1956b | ||
|
48bc24768e | ||
|
c011f12d24 | ||
|
5115c41617 | ||
|
9be55b3ba3 | ||
|
61d097db44 | ||
|
2ea6664124 | ||
|
0b706aa6d1 | ||
|
a4aacc9475 | ||
|
e5f1604a9d | ||
|
e56f0092d7 | ||
|
6e5dffbb91 | ||
|
1fa1878180 | ||
|
af7d281ef0 | ||
|
b1a9876dc9 | ||
|
8da966f057 | ||
|
8b93af3d12 | ||
|
29aa10176d | ||
|
bf160cc1ad | ||
|
993277e8f4 | ||
|
3af3129f75 | ||
|
799537f82d | ||
|
47bd25266f | ||
|
19ec395e66 | ||
|
21682f6b72 | ||
|
e2b897d3c0 | ||
|
3e29b5194e | ||
|
f527f09ac3 | ||
|
2eba8866b7 | ||
|
6cf2da8b64 | ||
|
4c05338876 | ||
|
61111178f0 | ||
|
cc67b9ec6e | ||
|
0c961f6ebb | ||
|
ba30dd1700 | ||
|
0f9cc7eeec | ||
|
0cb2d603c4 | ||
|
da8e64b534 | ||
|
26f0f93874 | ||
|
4016aafe66 | ||
|
507f0a4671 | ||
|
4a79cea3ff | ||
|
448c444db6 | ||
|
3c520683ce | ||
|
ad76c47c2f | ||
|
97967d87d1 | ||
|
4fe6997659 | ||
|
bbd016df09 | ||
|
486f89da04 | ||
|
4a7a1080a0 | ||
|
bc6144716b | ||
|
f30b72cc90 | ||
|
81a73dfcda | ||
|
d06b65c4e6 | ||
|
e4865644c1 | ||
|
07f7954cc6 | ||
|
a0265097e8 | ||
|
c3a517cb4f | ||
|
d4f7b0397d | ||
|
ba379287c8 | ||
|
0743ca724d | ||
|
a28856a11a | ||
|
d6fb3826c8 | ||
|
53e943b5bb | ||
|
269e546a6a | ||
|
dd1ab6dd36 | ||
|
a74d9d911e | ||
|
507990cafe | ||
|
6452942a69 | ||
|
95d4f5030f | ||
|
579a372eb9 | ||
|
f9dd1f691f | ||
|
9357583459 | ||
|
08b5e86536 | ||
|
86eb25018e | ||
|
133ed94442 | ||
|
613dcf9ed5 | ||
|
f1b5a64466 | ||
|
27173c9223 | ||
|
1f701aa97d | ||
|
7412ac3472 | ||
|
7675e725d2 | ||
|
6ebb9e419e | ||
|
505e6d346b | ||
|
fe849b27bf | ||
|
2f20c32704 | ||
|
a8f584f80b | ||
|
08579ca003 | ||
|
0bda65e315 | ||
|
f7dc40ca8d | ||
|
57badc7c74 | ||
|
0ad2bd221a | ||
|
ce638027a8 | ||
|
aa4f54e92e | ||
|
1d0b34df50 | ||
|
da0c83d318 | ||
|
339eec6a90 | ||
|
0ea692bb64 | ||
|
8e2a654e0e | ||
|
4c72c08830 | ||
|
c95faa53fe | ||
|
bd698cad3b | ||
|
9e4a97eb78 | ||
|
c1105899f4 | ||
|
64f3543034 | ||
|
81ce30302c | ||
|
6a66ca6d65 | ||
|
0c0fe5b9d3 | ||
|
83bbc6d520 | ||
|
264846a61f | ||
|
e364470dd9 | ||
|
e3a29addf4 | ||
|
1760cc8527 | ||
|
27f9662830 | ||
|
2906f0137c | ||
|
0d97d76c3b | ||
|
b4b649c8f4 | ||
|
1e4872c8b6 | ||
|
9f8127ed54 | ||
|
858fb6cce5 | ||
|
6dcb29bada | ||
|
be679589bd | ||
|
e93376939c | ||
|
067ab350ef | ||
|
d740c18992 | ||
|
43cf589e0e | ||
|
c5717a61a3 | ||
|
4a6db6e5ff | ||
|
55f5a78b1b | ||
|
cb0224d063 | ||
|
d94ad9df9b | ||
|
bd2a813c1a | ||
|
7218c66fd0 | ||
|
1a09b1d3a4 | ||
|
cb80fa6263 | ||
|
57f0b0b390 | ||
|
1f1f7f309a |
239 changed files with 10543 additions and 1322 deletions
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
@ -1,6 +1,6 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: [arianvp]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
|
|
14
.github/run-ghcjs-tests.sh
vendored
Executable file
14
.github/run-ghcjs-tests.sh
vendored
Executable file
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# cabal v2-test does not work with GHCJS
|
||||
# See: https://github.com/haskell/cabal/issues/6175
|
||||
#
|
||||
# This invokes cabal-plan to figure out test binaries, and invokes them with node.
|
||||
|
||||
cabal-plan list-bins '*:test:*' | while read -r line
|
||||
do
|
||||
testpkg=$(echo "$line" | perl -pe 's/:.*//')
|
||||
testexe=$(echo "$line" | awk '{ print $2 }')
|
||||
echo "testing $textexe in package $textpkg"
|
||||
(cd "$testpkg" && node "$testexe".jsexe/all.js)
|
||||
done
|
148
.github/workflows/master.yml
vendored
Normal file
148
.github/workflows/master.yml
vendored
Normal file
|
@ -0,0 +1,148 @@
|
|||
name: CI
|
||||
|
||||
# Trigger the workflow on push or pull request, but only for the master branch
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
cabal:
|
||||
name: ${{ matrix.os }} / ghc ${{ matrix.ghc }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
cabal: ["3.6"]
|
||||
ghc:
|
||||
- "8.6.5"
|
||||
- "8.8.4"
|
||||
- "8.10.7"
|
||||
- "9.0.2"
|
||||
- "9.2.2"
|
||||
- "9.4.2"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: haskell/actions/setup@v1
|
||||
id: setup-haskell-cabal
|
||||
name: Setup Haskell
|
||||
with:
|
||||
ghc-version: ${{ matrix.ghc }}
|
||||
cabal-version: ${{ matrix.cabal }}
|
||||
|
||||
- name: Freeze
|
||||
run: |
|
||||
cabal configure --enable-tests --enable-benchmarks --test-show-details=direct
|
||||
cabal freeze
|
||||
|
||||
- uses: actions/cache@v2.1.3
|
||||
name: Cache ~/.cabal/store and dist-newstyle
|
||||
with:
|
||||
path: |
|
||||
${{ steps.setup-haskell-cabal.outputs.cabal-store }}
|
||||
dist-newstyle
|
||||
key: ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.ghc }}-
|
||||
|
||||
- name: Configure
|
||||
run: |
|
||||
cabal install --ignore-project -j2 doctest --constraint='doctest ^>=0.20'
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cabal build all
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
cabal test all
|
||||
|
||||
- name: Run doctests
|
||||
run: |
|
||||
# Necessary for doctest to be found in $PATH
|
||||
export PATH="$HOME/.cabal/bin:$PATH"
|
||||
|
||||
DOCTEST="cabal repl --with-ghc=doctest --ghc-options=-w"
|
||||
(cd servant && eval $DOCTEST)
|
||||
(cd servant-client && eval $DOCTEST)
|
||||
(cd servant-client-core && eval $DOCTEST)
|
||||
(cd servant-http-streams && eval $DOCTEST)
|
||||
(cd servant-docs && eval $DOCTEST)
|
||||
(cd servant-foreign && eval $DOCTEST)
|
||||
(cd servant-server && eval $DOCTEST)
|
||||
(cd servant-machines && eval $DOCTEST)
|
||||
(cd servant-conduit && eval $DOCTEST)
|
||||
(cd servant-pipes && eval $DOCTEST)
|
||||
|
||||
# stack:
|
||||
# name: stack / ghc ${{ matrix.ghc }}
|
||||
# runs-on: ubuntu-latest
|
||||
# strategy:
|
||||
# matrix:
|
||||
# stack: ["2.7.5"]
|
||||
# ghc: ["8.10.7"]
|
||||
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
|
||||
# - uses: haskell/actions/setup@v1
|
||||
# name: Setup Haskell Stack
|
||||
# with:
|
||||
# ghc-version: ${{ matrix.ghc }}
|
||||
# stack-version: ${{ matrix.stack }}
|
||||
|
||||
# - uses: actions/cache@v2.1.3
|
||||
# name: Cache ~/.stack
|
||||
# with:
|
||||
# path: ~/.stack
|
||||
# key: ${{ runner.os }}-${{ matrix.ghc }}-stack
|
||||
|
||||
# - name: Install dependencies
|
||||
# run: |
|
||||
# stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --only-dependencies
|
||||
|
||||
# - name: Build
|
||||
# run: |
|
||||
# stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks
|
||||
|
||||
# - name: Test
|
||||
# run: |
|
||||
# stack test --system-ghc
|
||||
|
||||
ghcjs:
|
||||
name: ubuntu-latest / ghcjs 8.6
|
||||
runs-on: "ubuntu-latest"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: cachix/install-nix-action@v13
|
||||
with:
|
||||
extra_nix_config: |
|
||||
trusted-public-keys = ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=1aba6f367982bd6dd78ec2fda75ab246a62d32c5 cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
|
||||
substituters = https://nixcache.reflex-frp.org https://cache.nixos.org/
|
||||
- name: Setup
|
||||
run: |
|
||||
# Override cabal.project with the lightweight GHCJS one
|
||||
cp cabal.ghcjs.project cabal.project
|
||||
cat cabal.project
|
||||
nix-shell ghcjs.nix --run "cabal v2-update && cabal v2-freeze"
|
||||
|
||||
- uses: actions/cache@v2.1.3
|
||||
name: Cache ~/.cabal/store and dist-newstyle
|
||||
with:
|
||||
path: |
|
||||
~/.cabal/store
|
||||
dist-newstyle
|
||||
key: ${{ runner.os }}-ghcjs8.6-${{ hashFiles('cabal.project.freeze') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-ghcjs8.6-
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
nix-shell ghcjs.nix --run "cabal v2-build --ghcjs --enable-tests --enable-benchmarks all"
|
||||
|
||||
- name: Tests
|
||||
run: |
|
||||
nix-shell ghcjs.nix --run ".github/run-ghcjs-tests.sh"
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -30,6 +30,8 @@ doc/_build
|
|||
doc/venv
|
||||
doc/tutorial/static/api.js
|
||||
doc/tutorial/static/jq.js
|
||||
shell.nix
|
||||
.hspec-failures
|
||||
|
||||
# nix
|
||||
result*
|
||||
|
|
307
.travis.yml
307
.travis.yml
|
@ -1,307 +0,0 @@
|
|||
# This Travis job script has been generated by a script via
|
||||
#
|
||||
# haskell-ci '--config=cabal.haskell-ci' '--output=.travis.yml' 'cabal.project'
|
||||
#
|
||||
# For more information, see https://github.com/haskell-CI/haskell-ci
|
||||
#
|
||||
# version: 0.9.20200121
|
||||
#
|
||||
version: ~> 1.0
|
||||
language: c
|
||||
os: linux
|
||||
dist: bionic
|
||||
git:
|
||||
# whether to recursively clone submodules
|
||||
submodules: false
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
addons:
|
||||
google: stable
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cabal/packages
|
||||
- $HOME/.cabal/store
|
||||
- $HOME/.hlint
|
||||
before_cache:
|
||||
- rm -fv $CABALHOME/packages/hackage.haskell.org/build-reports.log
|
||||
# remove files that are regenerated by 'cabal update'
|
||||
- rm -fv $CABALHOME/packages/hackage.haskell.org/00-index.*
|
||||
- rm -fv $CABALHOME/packages/hackage.haskell.org/*.json
|
||||
- rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.cache
|
||||
- rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar
|
||||
- rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar.idx
|
||||
- rm -rfv $CABALHOME/packages/head.hackage
|
||||
jobs:
|
||||
include:
|
||||
- compiler: ghcjs-8.4
|
||||
addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu bionic main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"},{"sourceline":"deb http://ppa.launchpad.net/hvr/ghcjs/ubuntu bionic main"},{"sourceline":"deb https://deb.nodesource.com/node_10.x bionic main","key_url":"https://deb.nodesource.com/gpgkey/nodesource.gpg.key"}],"packages":["ghcjs-8.4","cabal-install-3.0","ghc-8.4.4","nodejs"]}}
|
||||
os: linux
|
||||
- compiler: ghc-8.10.1
|
||||
addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu bionic main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.10.1","cabal-install-3.2"]}}
|
||||
os: linux
|
||||
- compiler: ghc-8.8.2
|
||||
addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu bionic main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.8.2","cabal-install-3.0"]}}
|
||||
os: linux
|
||||
- compiler: ghc-8.6.5
|
||||
addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu bionic main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.6.5","cabal-install-3.0"]}}
|
||||
os: linux
|
||||
- compiler: ghc-8.4.4
|
||||
addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu bionic main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.4.4","cabal-install-3.0"]}}
|
||||
os: linux
|
||||
- compiler: ghc-8.2.2
|
||||
addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu bionic main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.2.2","cabal-install-3.0"]}}
|
||||
os: linux
|
||||
- compiler: ghc-8.0.2
|
||||
addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu bionic main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.0.2","cabal-install-3.0"]}}
|
||||
os: linux
|
||||
before_install:
|
||||
- |
|
||||
if echo $CC | grep -q ghcjs; then
|
||||
GHCJS=true;
|
||||
else
|
||||
GHCJS=false;
|
||||
fi
|
||||
- HC=$(echo "/opt/$CC/bin/ghc" | sed 's/-/\//')
|
||||
- WITHCOMPILER="-w $HC"
|
||||
- if $GHCJS ; then HC=${HC}js ; fi
|
||||
- if $GHCJS ; then WITHCOMPILER="--ghcjs ${WITHCOMPILER}js" ; fi
|
||||
- HADDOCK=$(echo "/opt/$CC/bin/haddock" | sed 's/-/\//')
|
||||
- if $GHCJS ; then PATH="/opt/ghc/8.4.4/bin:$PATH" ; fi
|
||||
- HCPKG="$HC-pkg"
|
||||
- unset CC
|
||||
- CABAL=/opt/ghc/bin/cabal
|
||||
- CABALHOME=$HOME/.cabal
|
||||
- export PATH="$CABALHOME/bin:$PATH"
|
||||
- TOP=$(pwd)
|
||||
- "HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\\d+)\\.(\\d+)\\.(\\d+)(\\.(\\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))')"
|
||||
- echo $HCNUMVER
|
||||
- CABAL="$CABAL -vnormal+nowrap"
|
||||
- set -o pipefail
|
||||
- TEST=--enable-tests
|
||||
- BENCH=--enable-benchmarks
|
||||
- HEADHACKAGE=false
|
||||
- rm -f $CABALHOME/config
|
||||
- |
|
||||
echo "verbose: normal +nowrap +markoutput" >> $CABALHOME/config
|
||||
echo "remote-build-reporting: anonymous" >> $CABALHOME/config
|
||||
echo "write-ghc-environment-files: always" >> $CABALHOME/config
|
||||
echo "remote-repo-cache: $CABALHOME/packages" >> $CABALHOME/config
|
||||
echo "logs-dir: $CABALHOME/logs" >> $CABALHOME/config
|
||||
echo "world-file: $CABALHOME/world" >> $CABALHOME/config
|
||||
echo "extra-prog-path: $CABALHOME/bin" >> $CABALHOME/config
|
||||
echo "symlink-bindir: $CABALHOME/bin" >> $CABALHOME/config
|
||||
echo "installdir: $CABALHOME/bin" >> $CABALHOME/config
|
||||
echo "build-summary: $CABALHOME/logs/build.log" >> $CABALHOME/config
|
||||
echo "store-dir: $CABALHOME/store" >> $CABALHOME/config
|
||||
echo "install-dirs user" >> $CABALHOME/config
|
||||
echo " prefix: $CABALHOME" >> $CABALHOME/config
|
||||
echo "repository hackage.haskell.org" >> $CABALHOME/config
|
||||
echo " url: http://hackage.haskell.org/" >> $CABALHOME/config
|
||||
install:
|
||||
- ${CABAL} --version
|
||||
- echo "$(${HC} --version) [$(${HC} --print-project-git-commit-id 2> /dev/null || echo '?')]"
|
||||
- node --version
|
||||
- echo $GHCJS
|
||||
- GHCJOBS=-j2
|
||||
- |
|
||||
echo "program-default-options" >> $CABALHOME/config
|
||||
echo " ghc-options: $GHCJOBS +RTS -M6G -RTS" >> $CABALHOME/config
|
||||
- cat $CABALHOME/config
|
||||
- rm -fv cabal.project cabal.project.local cabal.project.freeze
|
||||
- travis_retry ${CABAL} v2-update -v
|
||||
- if ! $GHCJS ; then (cd /tmp && ${CABAL} v2-install $WITHCOMPILER -j2 doctest --constraint='doctest ==0.16.3.*') ; fi
|
||||
- if $GHCJS ; then (cd /tmp && ${CABAL} v2-install -w ghc-8.4.4 cabal-plan --constraint='cabal-plan ^>=0.6.0.0' --constraint='cabal-plan +exe') ; fi
|
||||
- if $GHCJS ; then (cd /tmp && ${CABAL} v2-install -w ghc-8.4.4 hspec-discover) ; fi
|
||||
# Generate cabal.project
|
||||
- rm -rf cabal.project cabal.project.local cabal.project.freeze
|
||||
- touch cabal.project
|
||||
- |
|
||||
echo "packages: servant" >> cabal.project
|
||||
if ! $GHCJS ; then echo "packages: servant-client" >> cabal.project ; fi
|
||||
echo "packages: servant-client-core" >> cabal.project
|
||||
if ! $GHCJS ; then echo "packages: servant-http-streams" >> cabal.project ; fi
|
||||
if ! $GHCJS ; then echo "packages: servant-docs" >> cabal.project ; fi
|
||||
if ! $GHCJS ; then echo "packages: servant-foreign" >> cabal.project ; fi
|
||||
if ! $GHCJS ; then echo "packages: servant-server" >> cabal.project ; fi
|
||||
if ! $GHCJS ; then echo "packages: doc/tutorial" >> cabal.project ; fi
|
||||
if ! $GHCJS ; then echo "packages: servant-machines" >> cabal.project ; fi
|
||||
if ! $GHCJS ; then echo "packages: servant-conduit" >> cabal.project ; fi
|
||||
if ! $GHCJS ; then echo "packages: servant-pipes" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: doc/cookbook/basic-auth" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: doc/cookbook/curl-mock" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: doc/cookbook/basic-streaming" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: doc/cookbook/db-postgres-pool" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: doc/cookbook/file-upload" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: doc/cookbook/generic" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: doc/cookbook/pagination" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: doc/cookbook/testing" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: doc/cookbook/structuring-apis" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: doc/cookbook/using-custom-monad" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: doc/cookbook/using-free-client" >> cabal.project ; fi
|
||||
- |
|
||||
echo "constraints: foundation >=0.0.14" >> cabal.project
|
||||
echo "constraints: memory <0.14.12 || >0.14.12" >> cabal.project
|
||||
echo "constraints: base-compat ^>=0.11" >> cabal.project
|
||||
echo "constraints: semigroups ^>=0.19" >> cabal.project
|
||||
echo "constraints: sqlite-simple < 0" >> cabal.project
|
||||
echo "allow-newer: servant-pagination-2.2.2:servant" >> cabal.project
|
||||
echo "allow-newer: servant-pagination-2.2.2:servant-server" >> cabal.project
|
||||
echo "optimization: False" >> cabal.project
|
||||
- "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(cookbook-basic-auth|cookbook-basic-streaming|cookbook-curl-mock|cookbook-db-postgres-pool|cookbook-file-upload|cookbook-generic|cookbook-pagination|cookbook-structuring-apis|cookbook-testing|cookbook-using-custom-monad|cookbook-using-free-client|servant|servant-client|servant-client-core|servant-conduit|servant-docs|servant-foreign|servant-http-streams|servant-machines|servant-pipes|servant-server|tutorial)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
|
||||
- cat cabal.project || true
|
||||
- cat cabal.project.local || true
|
||||
- if [ -f "servant/configure.ac" ]; then (cd "servant" && autoreconf -i); fi
|
||||
- if [ -f "servant-client/configure.ac" ]; then (cd "servant-client" && autoreconf -i); fi
|
||||
- if [ -f "servant-client-core/configure.ac" ]; then (cd "servant-client-core" && autoreconf -i); fi
|
||||
- if [ -f "servant-http-streams/configure.ac" ]; then (cd "servant-http-streams" && autoreconf -i); fi
|
||||
- if [ -f "servant-docs/configure.ac" ]; then (cd "servant-docs" && autoreconf -i); fi
|
||||
- if [ -f "servant-foreign/configure.ac" ]; then (cd "servant-foreign" && autoreconf -i); fi
|
||||
- if [ -f "servant-server/configure.ac" ]; then (cd "servant-server" && autoreconf -i); fi
|
||||
- if [ -f "doc/tutorial/configure.ac" ]; then (cd "doc/tutorial" && autoreconf -i); fi
|
||||
- if [ -f "servant-machines/configure.ac" ]; then (cd "servant-machines" && autoreconf -i); fi
|
||||
- if [ -f "servant-conduit/configure.ac" ]; then (cd "servant-conduit" && autoreconf -i); fi
|
||||
- if [ -f "servant-pipes/configure.ac" ]; then (cd "servant-pipes" && autoreconf -i); fi
|
||||
- if [ -f "doc/cookbook/basic-auth/configure.ac" ]; then (cd "doc/cookbook/basic-auth" && autoreconf -i); fi
|
||||
- if [ -f "doc/cookbook/curl-mock/configure.ac" ]; then (cd "doc/cookbook/curl-mock" && autoreconf -i); fi
|
||||
- if [ -f "doc/cookbook/basic-streaming/configure.ac" ]; then (cd "doc/cookbook/basic-streaming" && autoreconf -i); fi
|
||||
- if [ -f "doc/cookbook/db-postgres-pool/configure.ac" ]; then (cd "doc/cookbook/db-postgres-pool" && autoreconf -i); fi
|
||||
- if [ -f "doc/cookbook/file-upload/configure.ac" ]; then (cd "doc/cookbook/file-upload" && autoreconf -i); fi
|
||||
- if [ -f "doc/cookbook/generic/configure.ac" ]; then (cd "doc/cookbook/generic" && autoreconf -i); fi
|
||||
- if [ -f "doc/cookbook/pagination/configure.ac" ]; then (cd "doc/cookbook/pagination" && autoreconf -i); fi
|
||||
- if [ -f "doc/cookbook/testing/configure.ac" ]; then (cd "doc/cookbook/testing" && autoreconf -i); fi
|
||||
- if [ -f "doc/cookbook/structuring-apis/configure.ac" ]; then (cd "doc/cookbook/structuring-apis" && autoreconf -i); fi
|
||||
- if [ -f "doc/cookbook/using-custom-monad/configure.ac" ]; then (cd "doc/cookbook/using-custom-monad" && autoreconf -i); fi
|
||||
- if [ -f "doc/cookbook/using-free-client/configure.ac" ]; then (cd "doc/cookbook/using-free-client" && autoreconf -i); fi
|
||||
- ${CABAL} v2-freeze $WITHCOMPILER ${TEST} ${BENCH}
|
||||
- "cat cabal.project.freeze | sed -E 's/^(constraints: *| *)//' | sed 's/any.//'"
|
||||
- rm cabal.project.freeze
|
||||
script:
|
||||
- DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
|
||||
# Packaging...
|
||||
- echo 'Packaging...' && echo -en 'travis_fold:start:sdist\\r'
|
||||
- ${CABAL} v2-sdist all
|
||||
- echo -en 'travis_fold:end:sdist\\r'
|
||||
# Unpacking...
|
||||
- echo 'Unpacking...' && echo -en 'travis_fold:start:unpack\\r'
|
||||
- mv dist-newstyle/sdist/*.tar.gz ${DISTDIR}/
|
||||
- cd ${DISTDIR} || false
|
||||
- find . -maxdepth 1 -type f -name '*.tar.gz' -exec tar -xvf '{}' \;
|
||||
- find . -maxdepth 1 -type f -name '*.tar.gz' -exec rm '{}' \;
|
||||
- PKGDIR_servant="$(find . -maxdepth 1 -type d -regex '.*/servant-[0-9.]*')"
|
||||
- PKGDIR_servant_client="$(find . -maxdepth 1 -type d -regex '.*/servant-client-[0-9.]*')"
|
||||
- PKGDIR_servant_client_core="$(find . -maxdepth 1 -type d -regex '.*/servant-client-core-[0-9.]*')"
|
||||
- PKGDIR_servant_http_streams="$(find . -maxdepth 1 -type d -regex '.*/servant-http-streams-[0-9.]*')"
|
||||
- PKGDIR_servant_docs="$(find . -maxdepth 1 -type d -regex '.*/servant-docs-[0-9.]*')"
|
||||
- PKGDIR_servant_foreign="$(find . -maxdepth 1 -type d -regex '.*/servant-foreign-[0-9.]*')"
|
||||
- PKGDIR_servant_server="$(find . -maxdepth 1 -type d -regex '.*/servant-server-[0-9.]*')"
|
||||
- PKGDIR_tutorial="$(find . -maxdepth 1 -type d -regex '.*/tutorial-[0-9.]*')"
|
||||
- PKGDIR_servant_machines="$(find . -maxdepth 1 -type d -regex '.*/servant-machines-[0-9.]*')"
|
||||
- PKGDIR_servant_conduit="$(find . -maxdepth 1 -type d -regex '.*/servant-conduit-[0-9.]*')"
|
||||
- PKGDIR_servant_pipes="$(find . -maxdepth 1 -type d -regex '.*/servant-pipes-[0-9.]*')"
|
||||
- PKGDIR_cookbook_basic_auth="$(find . -maxdepth 1 -type d -regex '.*/cookbook-basic-auth-[0-9.]*')"
|
||||
- PKGDIR_cookbook_curl_mock="$(find . -maxdepth 1 -type d -regex '.*/cookbook-curl-mock-[0-9.]*')"
|
||||
- PKGDIR_cookbook_basic_streaming="$(find . -maxdepth 1 -type d -regex '.*/cookbook-basic-streaming-[0-9.]*')"
|
||||
- PKGDIR_cookbook_db_postgres_pool="$(find . -maxdepth 1 -type d -regex '.*/cookbook-db-postgres-pool-[0-9.]*')"
|
||||
- PKGDIR_cookbook_file_upload="$(find . -maxdepth 1 -type d -regex '.*/cookbook-file-upload-[0-9.]*')"
|
||||
- PKGDIR_cookbook_generic="$(find . -maxdepth 1 -type d -regex '.*/cookbook-generic-[0-9.]*')"
|
||||
- PKGDIR_cookbook_pagination="$(find . -maxdepth 1 -type d -regex '.*/cookbook-pagination-[0-9.]*')"
|
||||
- PKGDIR_cookbook_testing="$(find . -maxdepth 1 -type d -regex '.*/cookbook-testing-[0-9.]*')"
|
||||
- PKGDIR_cookbook_structuring_apis="$(find . -maxdepth 1 -type d -regex '.*/cookbook-structuring-apis-[0-9.]*')"
|
||||
- PKGDIR_cookbook_using_custom_monad="$(find . -maxdepth 1 -type d -regex '.*/cookbook-using-custom-monad-[0-9.]*')"
|
||||
- PKGDIR_cookbook_using_free_client="$(find . -maxdepth 1 -type d -regex '.*/cookbook-using-free-client-[0-9.]*')"
|
||||
# Generate cabal.project
|
||||
- rm -rf cabal.project cabal.project.local cabal.project.freeze
|
||||
- touch cabal.project
|
||||
- |
|
||||
echo "packages: ${PKGDIR_servant}" >> cabal.project
|
||||
if ! $GHCJS ; then echo "packages: ${PKGDIR_servant_client}" >> cabal.project ; fi
|
||||
echo "packages: ${PKGDIR_servant_client_core}" >> cabal.project
|
||||
if ! $GHCJS ; then echo "packages: ${PKGDIR_servant_http_streams}" >> cabal.project ; fi
|
||||
if ! $GHCJS ; then echo "packages: ${PKGDIR_servant_docs}" >> cabal.project ; fi
|
||||
if ! $GHCJS ; then echo "packages: ${PKGDIR_servant_foreign}" >> cabal.project ; fi
|
||||
if ! $GHCJS ; then echo "packages: ${PKGDIR_servant_server}" >> cabal.project ; fi
|
||||
if ! $GHCJS ; then echo "packages: ${PKGDIR_tutorial}" >> cabal.project ; fi
|
||||
if ! $GHCJS ; then echo "packages: ${PKGDIR_servant_machines}" >> cabal.project ; fi
|
||||
if ! $GHCJS ; then echo "packages: ${PKGDIR_servant_conduit}" >> cabal.project ; fi
|
||||
if ! $GHCJS ; then echo "packages: ${PKGDIR_servant_pipes}" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: ${PKGDIR_cookbook_basic_auth}" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: ${PKGDIR_cookbook_curl_mock}" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: ${PKGDIR_cookbook_basic_streaming}" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: ${PKGDIR_cookbook_db_postgres_pool}" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: ${PKGDIR_cookbook_file_upload}" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: ${PKGDIR_cookbook_generic}" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: ${PKGDIR_cookbook_pagination}" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: ${PKGDIR_cookbook_testing}" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: ${PKGDIR_cookbook_structuring_apis}" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: ${PKGDIR_cookbook_using_custom_monad}" >> cabal.project ; fi
|
||||
if ! $GHCJS && [ $HCNUMVER -ge 80400 ] ; then echo "packages: ${PKGDIR_cookbook_using_free_client}" >> cabal.project ; fi
|
||||
- |
|
||||
echo "constraints: foundation >=0.0.14" >> cabal.project
|
||||
echo "constraints: memory <0.14.12 || >0.14.12" >> cabal.project
|
||||
echo "constraints: base-compat ^>=0.11" >> cabal.project
|
||||
echo "constraints: semigroups ^>=0.19" >> cabal.project
|
||||
echo "constraints: sqlite-simple < 0" >> cabal.project
|
||||
echo "allow-newer: servant-pagination-2.2.2:servant" >> cabal.project
|
||||
echo "allow-newer: servant-pagination-2.2.2:servant-server" >> cabal.project
|
||||
echo "optimization: False" >> cabal.project
|
||||
- "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(cookbook-basic-auth|cookbook-basic-streaming|cookbook-curl-mock|cookbook-db-postgres-pool|cookbook-file-upload|cookbook-generic|cookbook-pagination|cookbook-structuring-apis|cookbook-testing|cookbook-using-custom-monad|cookbook-using-free-client|servant|servant-client|servant-client-core|servant-conduit|servant-docs|servant-foreign|servant-http-streams|servant-machines|servant-pipes|servant-server|tutorial)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
|
||||
- cat cabal.project || true
|
||||
- cat cabal.project.local || true
|
||||
- |
|
||||
pkgdir() {
|
||||
case $1 in
|
||||
servant) echo ${PKGDIR_servant} ;;
|
||||
servant-client) echo ${PKGDIR_servant_client} ;;
|
||||
servant-client-core) echo ${PKGDIR_servant_client_core} ;;
|
||||
servant-http-streams) echo ${PKGDIR_servant_http_streams} ;;
|
||||
servant-docs) echo ${PKGDIR_servant_docs} ;;
|
||||
servant-foreign) echo ${PKGDIR_servant_foreign} ;;
|
||||
servant-server) echo ${PKGDIR_servant_server} ;;
|
||||
tutorial) echo ${PKGDIR_tutorial} ;;
|
||||
servant-machines) echo ${PKGDIR_servant_machines} ;;
|
||||
servant-conduit) echo ${PKGDIR_servant_conduit} ;;
|
||||
servant-pipes) echo ${PKGDIR_servant_pipes} ;;
|
||||
cookbook-basic-auth) echo ${PKGDIR_cookbook_basic_auth} ;;
|
||||
cookbook-curl-mock) echo ${PKGDIR_cookbook_curl_mock} ;;
|
||||
cookbook-basic-streaming) echo ${PKGDIR_cookbook_basic_streaming} ;;
|
||||
cookbook-db-postgres-pool) echo ${PKGDIR_cookbook_db_postgres_pool} ;;
|
||||
cookbook-file-upload) echo ${PKGDIR_cookbook_file_upload} ;;
|
||||
cookbook-generic) echo ${PKGDIR_cookbook_generic} ;;
|
||||
cookbook-pagination) echo ${PKGDIR_cookbook_pagination} ;;
|
||||
cookbook-testing) echo ${PKGDIR_cookbook_testing} ;;
|
||||
cookbook-structuring-apis) echo ${PKGDIR_cookbook_structuring_apis} ;;
|
||||
cookbook-using-custom-monad) echo ${PKGDIR_cookbook_using_custom_monad} ;;
|
||||
cookbook-using-free-client) echo ${PKGDIR_cookbook_using_free_client} ;;
|
||||
esac
|
||||
}
|
||||
- echo -en 'travis_fold:end:unpack\\r'
|
||||
# Building with tests and benchmarks...
|
||||
- echo 'Building with tests and benchmarks...' && echo -en 'travis_fold:start:build-everything\\r'
|
||||
# build & run tests, build benchmarks
|
||||
- ${CABAL} v2-build $WITHCOMPILER ${TEST} ${BENCH} all
|
||||
- echo -en 'travis_fold:end:build-everything\\r'
|
||||
# Testing...
|
||||
- if ! $GHCJS ; then ${CABAL} v2-test $WITHCOMPILER ${TEST} ${BENCH} all ; fi
|
||||
- if $GHCJS ; then cabal-plan list-bins '*:test:*' | while read -r line; do testpkg=$(echo "$line" | perl -pe 's/:.*//'); testexe=$(echo "$line" | awk '{ print $2 }'); echo "testing $textexe in package $textpkg"; (cd "$(pkgdir $testpkg)" && nodejs "$testexe".jsexe/all.js); done ; fi
|
||||
# Doctest...
|
||||
- echo 'Doctest...' && echo -en 'travis_fold:start:doctest\\r'
|
||||
- perl -i -e 'while (<ARGV>) { print unless /package-id\s+(base-compat-batteries)-\d+(\.\d+)*/; }' .ghc.environment.*
|
||||
- if ! $GHCJS ; then (cd ${PKGDIR_servant} && doctest src) ; fi
|
||||
- if ! $GHCJS ; then (cd ${PKGDIR_servant_client} && doctest src) ; fi
|
||||
- if ! $GHCJS ; then (cd ${PKGDIR_servant_client_core} && doctest src) ; fi
|
||||
- if ! $GHCJS ; then (cd ${PKGDIR_servant_http_streams} && doctest src) ; fi
|
||||
- if ! $GHCJS ; then (cd ${PKGDIR_servant_docs} && doctest src) ; fi
|
||||
- if ! $GHCJS ; then (cd ${PKGDIR_servant_foreign} && doctest src) ; fi
|
||||
- if ! $GHCJS ; then (cd ${PKGDIR_servant_server} && doctest src) ; fi
|
||||
- if ! $GHCJS ; then (cd ${PKGDIR_servant_machines} && doctest src) ; fi
|
||||
- if ! $GHCJS ; then (cd ${PKGDIR_servant_conduit} && doctest src) ; fi
|
||||
- if ! $GHCJS ; then (cd ${PKGDIR_servant_pipes} && doctest src) ; fi
|
||||
- echo -en 'travis_fold:end:doctest\\r'
|
||||
# haddock...
|
||||
- echo 'haddock...' && echo -en 'travis_fold:start:haddock\\r'
|
||||
- if ! $GHCJS ; then ${CABAL} v2-haddock $WITHCOMPILER --with-haddock $HADDOCK ${TEST} ${BENCH} all ; fi
|
||||
- echo -en 'travis_fold:end:haddock\\r'
|
||||
|
||||
# REGENDATA ("0.9.20200121",["--config=cabal.haskell-ci","--output=.travis.yml","cabal.project"])
|
||||
# EOF
|
|
@ -79,8 +79,10 @@ not been a timely response to a PR, you can ping the Maintainers group (with
|
|||
We encourage people to experiment with new combinators and instances - it is
|
||||
one of the most powerful ways of using `servant`, and a wonderful way of
|
||||
getting to know it better. If you do write a new combinator, we would love to
|
||||
know about it! Either hop on #servant on freenode and let us know, or open an
|
||||
issue with the `news` tag (which we will close when we read it).
|
||||
know about it! Either hop on
|
||||
[#haskell-servant on libera.chat](https://web.libera.chat/#haskell-servant) and
|
||||
let us know, or open an issue with the `news` tag (which we will close when we
|
||||
read it).
|
||||
|
||||
As for adding them to the main repo: maintaining combinators can be expensive,
|
||||
since official combinators must have instances for all classes (and new classes
|
||||
|
|
32
Makefile
32
Makefile
|
@ -1,32 +0,0 @@
|
|||
# With common maintenance tasks
|
||||
|
||||
HC ?= ghc-8.4.4
|
||||
|
||||
all :
|
||||
@echo "Don't try to make all at once!"
|
||||
|
||||
really-all :
|
||||
$(MAKE) build-ghc
|
||||
$(MAKE) build-ghc HC=ghc-8.0.2
|
||||
$(MAKE) build-ghc HC=ghc-8.2.2
|
||||
$(MAKE) build-ghc HC=ghc-8.6.5
|
||||
$(MAKE) build-ghcjs
|
||||
|
||||
build-ghc :
|
||||
cabal v2-build -w $(HC) all
|
||||
|
||||
build-ghcjs :
|
||||
cabal v2-build --builddir=dist-newstyle-ghcjs --project-file=cabal.ghcjs.project all
|
||||
|
||||
packdeps :
|
||||
packdeps */*.cabal
|
||||
|
||||
doctest : doctest-servant doctest-servant-server
|
||||
perl -i -e 'while (<ARGV>) { print unless /package-id\s+base-compat-\d+(\.\d+)*/; }' .ghc.environment.*
|
||||
|
||||
doctest-servant :
|
||||
(cd servant && doctest src)
|
||||
(cd servant && doctest test/Servant/LinksSpec.hs)
|
||||
|
||||
doctest-servant-server :
|
||||
(cd servant-server && doctest src)
|
28
README.md
28
README.md
|
@ -13,7 +13,7 @@ The core documentation can be found [here](http://docs.servant.dev/).
|
|||
Other blog posts, videos and slides can be found on the
|
||||
[website](http://www.servant.dev/).
|
||||
|
||||
If you need help, drop by the IRC channel (#servant on freenode) or [mailing
|
||||
If you need help, drop by the IRC channel (#haskell-servant on libera.chat) or [mailing
|
||||
list](https://groups.google.com/forum/#!forum/haskell-servant).
|
||||
|
||||
## Contributing
|
||||
|
@ -40,26 +40,6 @@ See `CONTRIBUTING.md`
|
|||
- `git push --tags`
|
||||
- `cabal sdist` and `cabal upload`
|
||||
|
||||
## travis
|
||||
|
||||
`.travis.yml` is generated using `make-travis-yml` tool, in
|
||||
[multi-ghc-travis](https://github.com/haskell-hvr/multi-ghc-travis) repository.
|
||||
|
||||
To regenerate the script use (*note:* atm you need to comment `doc/cookbook/` packages).
|
||||
|
||||
```sh
|
||||
runghc ~/Documents/other-haskell/multi-ghc-travis/make_travis_yml_2.hs regenerate
|
||||
```
|
||||
|
||||
In case Travis jobs fail due to a dependency failing to build, you can temporarily
|
||||
add `constraints` to the `cabal.project` file, and regenerate the `.travis.yml`.
|
||||
For example, the following will disallow a single `troublemaker-13.37` package version:
|
||||
|
||||
```
|
||||
constraints:
|
||||
troublemaker <13.37 && > 13.37
|
||||
```
|
||||
|
||||
## TechEmpower framework benchmarks
|
||||
|
||||
We develop and maintain the servant TFB entry in https://github.com/haskell-servant/FrameworkBenchmarks/
|
||||
|
@ -83,3 +63,9 @@ To compare with `reitit` (Clojure framework)
|
|||
```
|
||||
|
||||
You can see the visualised results at https://www.techempower.com/benchmarks/#section=test
|
||||
|
||||
## Nix
|
||||
|
||||
A developer shell.nix file is provided in the `nix` directory
|
||||
|
||||
See [nix/README.md](nix/README.md)
|
||||
|
|
|
@ -2,9 +2,13 @@
|
|||
|
||||
packages:
|
||||
servant/
|
||||
servant-client/
|
||||
servant-client-core/
|
||||
servant-jsaddle/
|
||||
|
||||
-- we need to tell cabal we are using GHCJS
|
||||
compiler: ghcjs
|
||||
tests: True
|
||||
|
||||
-- Constraints so that reflex-platform provided packages are selected.
|
||||
constraints: attoparsec == 0.13.2.2
|
||||
constraints: hashable == 1.3.0.0
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
distribution: bionic
|
||||
folds: all-but-test
|
||||
branches: master
|
||||
jobs-selection: any
|
||||
google-chrome: True
|
||||
ghcjs-tests: True
|
||||
doctest: True
|
||||
doctest-filter-packages: base-compat-batteries
|
||||
doctest-skip: tutorial
|
||||
|
||||
-- https://github.com/haskell/cabal/issues/6176
|
||||
ghcjs-tools: hspec-discover
|
||||
|
||||
-- We have inplace packages (servant-js) so we skip installing dependencies in a separate step
|
||||
install-dependencies: False
|
||||
|
||||
-- this speed-ups the build a little, but we have to check these for release
|
||||
no-tests-no-benchmarks: False
|
||||
unconstrained: False
|
||||
|
||||
-- Don't run cabal check, as cookbook examples won't pass it
|
||||
cabal-check: False
|
||||
|
||||
-- ghc-options: -j2
|
||||
jobs: :2
|
|
@ -1,11 +1,18 @@
|
|||
packages:
|
||||
servant/
|
||||
servant-auth/servant-auth
|
||||
servant-auth/servant-auth-client
|
||||
servant-auth/servant-auth-docs
|
||||
servant-auth/servant-auth-server
|
||||
servant-auth/servant-auth-swagger
|
||||
|
||||
servant-client/
|
||||
servant-client-core/
|
||||
servant-http-streams/
|
||||
servant-docs/
|
||||
servant-foreign/
|
||||
servant-server/
|
||||
servant-swagger/
|
||||
doc/tutorial/
|
||||
|
||||
-- servant streaming
|
||||
|
@ -22,43 +29,26 @@ packages:
|
|||
packages:
|
||||
doc/cookbook/basic-auth
|
||||
doc/cookbook/curl-mock
|
||||
doc/cookbook/custom-errors
|
||||
doc/cookbook/basic-streaming
|
||||
doc/cookbook/db-postgres-pool
|
||||
-- doc/cookbook/db-sqlite-simple
|
||||
doc/cookbook/db-sqlite-simple
|
||||
doc/cookbook/file-upload
|
||||
doc/cookbook/generic
|
||||
-- doc/cookbook/hoist-server-with-context
|
||||
-- doc/cookbook/https
|
||||
-- doc/cookbook/jwt-and-basic-auth/
|
||||
doc/cookbook/hoist-server-with-context
|
||||
doc/cookbook/https
|
||||
doc/cookbook/jwt-and-basic-auth
|
||||
doc/cookbook/pagination
|
||||
-- doc/cookbook/sentry
|
||||
doc/cookbook/testing
|
||||
-- Commented out because servant-quickcheck currently doesn't build.
|
||||
-- doc/cookbook/testing
|
||||
doc/cookbook/uverb
|
||||
doc/cookbook/structuring-apis
|
||||
doc/cookbook/using-custom-monad
|
||||
doc/cookbook/using-free-client
|
||||
-- doc/cookbook/open-id-connect
|
||||
doc/cookbook/managed-resource
|
||||
|
||||
tests: True
|
||||
optimization: False
|
||||
-- reorder-goals: True
|
||||
|
||||
constraints:
|
||||
-- see https://github.com/haskell-infra/hackage-trustees/issues/119
|
||||
foundation >=0.0.14,
|
||||
memory <0.14.12 || >0.14.12
|
||||
|
||||
constraints: base-compat ^>=0.11
|
||||
constraints: semigroups ^>=0.19
|
||||
|
||||
-- MonadFail
|
||||
-- https://github.com/nurpax/sqlite-simple/issues/74
|
||||
constraints: sqlite-simple < 0
|
||||
-- allow-newer: sqlite-simple-0.4.16.0:semigroups
|
||||
-- allow-newer: direct-sqlite-2.3.24:semigroups
|
||||
|
||||
-- needed for doctests
|
||||
write-ghc-environment-files: always
|
||||
|
||||
-- https://github.com/chordify/haskell-servant-pagination/pull/12
|
||||
allow-newer: servant-pagination-2.2.2:servant
|
||||
allow-newer: servant-pagination-2.2.2:servant-server
|
||||
|
|
9
changelog.d/1432
Normal file
9
changelog.d/1432
Normal file
|
@ -0,0 +1,9 @@
|
|||
synopsis: Fixes encoding of URL parameters in servant-client
|
||||
prs: #1432
|
||||
issues: #1418
|
||||
description: {
|
||||
Some applications use query parameters to pass arbitrary (non-unicode) binary
|
||||
data. This change modifies how servant-client handles query parameters, so
|
||||
that application developers can use `ToHttpApiData` to marshal binary data into
|
||||
query parameters.
|
||||
}
|
11
changelog.d/1469
Normal file
11
changelog.d/1469
Normal file
|
@ -0,0 +1,11 @@
|
|||
synopsis: Derive HasClient good response status from Verb status
|
||||
prs: #1469
|
||||
description: {
|
||||
`HasClient` instances for the `Verb` datatype use `runRequest` in
|
||||
`clientWithRoute` definitions.
|
||||
This means that a request performed with `runClientM` will be successful if and
|
||||
only if the endpoint specify a response status code >=200 and <300.
|
||||
This change replaces `runRequest` with `runRequestAcceptStatus` in `Verb`
|
||||
instances for the `HasClient` class, deriving the good response status from
|
||||
the `Verb` status.
|
||||
}
|
9
changelog.d/1477
Normal file
9
changelog.d/1477
Normal file
|
@ -0,0 +1,9 @@
|
|||
synopsis: Enable FlexibleContexts in Servant.API.ContentTypes
|
||||
prs: #1477
|
||||
|
||||
description: {
|
||||
|
||||
Starting with GHC 9.2, UndecidableInstances no longer implies FlexibleContexts.
|
||||
Add this extension where it's needed to make compilation succeed.
|
||||
|
||||
}
|
10
changelog.d/1529
Normal file
10
changelog.d/1529
Normal file
|
@ -0,0 +1,10 @@
|
|||
synopsis: Fix performRequest in servant-client-ghcjs
|
||||
prs: #1529
|
||||
|
||||
description: {
|
||||
|
||||
performRequest function in servant-client-ghcjs was not compatible with the
|
||||
latest RunClient typeclass. Added the acceptStatus parameter and fixed the
|
||||
functionality to match what servant-client provides.
|
||||
|
||||
}
|
81
changelog.d/1556
Normal file
81
changelog.d/1556
Normal file
|
@ -0,0 +1,81 @@
|
|||
synopsis: Display capture hints in router layout
|
||||
prs: #1556
|
||||
|
||||
description: {
|
||||
|
||||
This PR enhances the `Servant.Server.layout` function, which produces a textual description of the routing layout of an API. More precisely, it changes `<capture>` blocks, so that they display the name and type of the variable being captured instead.
|
||||
|
||||
Example:
|
||||
|
||||
For the following API
|
||||
```haskell
|
||||
type API =
|
||||
"a" :> "d" :> Get '[JSON] NoContent
|
||||
:<|> "b" :> Capture "x" Int :> Get '[JSON] Bool
|
||||
:<|> "a" :> "e" :> Get '[JSON] Int
|
||||
```
|
||||
|
||||
we previously got the following output:
|
||||
|
||||
```
|
||||
/
|
||||
├─ a/
|
||||
│ ├─ d/
|
||||
│ │ └─•
|
||||
│ └─ e/
|
||||
│ └─•
|
||||
└─ b/
|
||||
└─ <capture>/
|
||||
├─•
|
||||
┆
|
||||
└─•
|
||||
```
|
||||
|
||||
now we get:
|
||||
|
||||
```
|
||||
/
|
||||
├─ a/
|
||||
│ ├─ d/
|
||||
│ │ └─•
|
||||
│ └─ e/
|
||||
│ └─•
|
||||
└─ b/
|
||||
└─ <x::Int>/
|
||||
├─•
|
||||
┆
|
||||
└─•
|
||||
```
|
||||
|
||||
This change is achieved by the introduction of a CaptureHint type, which is passed as an extra argument to the CaptureRouter and CaptureAllRouter constructors for the Router' type.
|
||||
CaptureHint values are then used in routerLayout, to display the name and type of captured values, instead of just `<capture>` previously.
|
||||
|
||||
N.B.:
|
||||
Because the choice smart constructor for routers can aggregate Capture combinators with different capture hints, the Capture*Router constructors actually take a list of CaptureHint, instead of a single one.
|
||||
|
||||
This PR also introduces Spec tests for the routerLayout function.
|
||||
|
||||
Warning:
|
||||
This change is potentially breaking, because it adds the constraint `Typeable a` to all types that are to be captured. Because all types are typeable since GHC 7.10, this is not as bad as it sounds ; it only break expressions where `a` is quantified in an expression with `Capture a`.
|
||||
In those cases, the fix is easy: it suffices to add `Typeable a` to the left-hand side of the quantification constraint.
|
||||
|
||||
For instance, the following code will no longer compile:
|
||||
```haskell
|
||||
type MyAPI a = Capture "foo" a :> Get '[JSON] ()
|
||||
|
||||
myServer :: forall a. Server (MyAPI a)
|
||||
myServer = const $ return ()
|
||||
|
||||
myApi :: forall a. Proxy (MyAPI a)
|
||||
myApi = Proxy
|
||||
|
||||
app :: forall a. (FromHttpApiData a) => Application
|
||||
app = serve (myApi @a) (myServer @a)
|
||||
```
|
||||
|
||||
Indeed, `app` should be replaced with:
|
||||
```haskell
|
||||
app :: forall a. (FromHttpApiData a, Typeable a) => Application
|
||||
app = serve (myApi @a) (myServer @a)
|
||||
```
|
||||
}
|
13
changelog.d/1569
Normal file
13
changelog.d/1569
Normal file
|
@ -0,0 +1,13 @@
|
|||
synopsis: Encode captures using toEncodedUrlPiece
|
||||
prs: #1569
|
||||
issues: #1511
|
||||
|
||||
description: {
|
||||
The `servant-client` library now makes direct use of `toEncodedUrlPiece` from `ToHttpApiData`
|
||||
to encode captured values when building the request path. It gives user freedom to implement
|
||||
URL-encoding however they need.
|
||||
|
||||
Previous behavior was to use `toUrlPiece` and URL-encode its output using `toEncodedUrlPiece`
|
||||
from the `Text` instance of `ToHttpApiData`. The issue with this approach is that
|
||||
`ToHttpApiData Text` is overly zealous and also encodes characters, such as `*`, which are perfectly valid in a URL.
|
||||
}
|
2
changelog.d/1573
Normal file
2
changelog.d/1573
Normal file
|
@ -0,0 +1,2 @@
|
|||
synopsis: Add API docs for ServerT
|
||||
prs: #1573
|
12
changelog.d/1580
Normal file
12
changelog.d/1580
Normal file
|
@ -0,0 +1,12 @@
|
|||
synopsis: Allow IO in validationKeys
|
||||
prs: #1580
|
||||
issues: #1579
|
||||
|
||||
description: {
|
||||
|
||||
Currently validationKeys are a fixed JWKSet. This does not work with OIDC
|
||||
providers such as AWS Cognito or Okta, which regularly fetching jwks_uri to
|
||||
discover new and expired keys.
|
||||
|
||||
This change alters the type of validationKeys from JWKSet to IO JWKSet.
|
||||
}
|
2
changelog.d/1589
Normal file
2
changelog.d/1589
Normal file
|
@ -0,0 +1,2 @@
|
|||
synopsis: Only include question mark for nonempty query strings
|
||||
prs: 1589
|
2
changelog.d/1595
Normal file
2
changelog.d/1595
Normal file
|
@ -0,0 +1,2 @@
|
|||
synopsis: Run ClientEnv's makeClientRequest in IO.
|
||||
prs: #1595
|
10
changelog.d/1606
Normal file
10
changelog.d/1606
Normal file
|
@ -0,0 +1,10 @@
|
|||
synopsis: Handle Cookies correctly for RunStreamingClient
|
||||
prs: #1606
|
||||
issues: #1605
|
||||
|
||||
description: {
|
||||
|
||||
Makes performWithStreamingRequest take into consideration the
|
||||
CookieJar, which it previously didn't.
|
||||
|
||||
}
|
2
changelog.d/1638
Normal file
2
changelog.d/1638
Normal file
|
@ -0,0 +1,2 @@
|
|||
synopsis: Add Functor instance to AuthHandler.
|
||||
prs: #1638
|
8
changelog.d/1649
Normal file
8
changelog.d/1649
Normal file
|
@ -0,0 +1,8 @@
|
|||
synopsis: Add HasStatus instance for Headers (that defers StatusOf to underlying value)
|
||||
prs: #1649
|
||||
|
||||
description: {
|
||||
|
||||
Adds a new HasStatus (Headers hs a) instance (StatusOf (Headers hs a) = StatusOf a)
|
||||
|
||||
}
|
16
changelog.d/servant-docs-curl
Normal file
16
changelog.d/servant-docs-curl
Normal file
|
@ -0,0 +1,16 @@
|
|||
synopsis: Add sample cURL requests to generated documentation
|
||||
prs: #1401
|
||||
|
||||
description: {
|
||||
|
||||
Add sample cURL requests to generated documentation.
|
||||
|
||||
Those supplying changes to the Request `header` field manually using
|
||||
lenses will need to add a sample bytestring value.
|
||||
|
||||
`headers <>~ ["unicorn"]`
|
||||
|
||||
becomes
|
||||
|
||||
`headers <>~ [("unicorn", "sample value")]`
|
||||
}
|
|
@ -10,7 +10,7 @@ BUILDDIR = _build
|
|||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@if [ ! -d venv ]; then echo "WARNING: There is no venv directory, did you forget to 'virtualenv venv'. Check building-the-docs file."; fi
|
||||
@if [ ! -d venv ]; then echo "WARNING: There is no venv directory, did you forget to 'virtualenv venv'. Check README.md."; fi
|
||||
@if [ ! "z$$(which $(SPHINXBUILD))" = "z$$(pwd)/venv/bin/sphinx-build" ]; then echo "WARNING: Did you forgot to 'source venv/bin/activate'"; fi
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ master_doc = 'index'
|
|||
|
||||
# General information about the project.
|
||||
project = u'Servant'
|
||||
copyright = u'2018, Servant Contributors'
|
||||
copyright = u'2022, Servant Contributors'
|
||||
author = u'Servant Contributors'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
|
@ -169,4 +169,3 @@ texinfo_documents = [
|
|||
source_parsers = {
|
||||
'.lhs': CommonMarkParser,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-basic-auth
|
||||
version: 0.1
|
||||
synopsis: Basic Authentication cookbook example
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.2
|
||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||
|
||||
executable cookbook-basic-auth
|
||||
main-is: BasicAuth.lhs
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-basic-streaming
|
||||
version: 2.1
|
||||
synopsis: Streaming in servant without streaming libs
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.2
|
||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||
|
||||
executable cookbook-basic-streaming
|
||||
main-is: Streaming.lhs
|
||||
|
|
|
@ -24,7 +24,6 @@ Language extensions and imports:
|
|||
import Control.Lens ((^.))
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Text
|
||||
import Data.Monoid ((<>))
|
||||
import Data.Proxy (Proxy (Proxy))
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeUtf8)
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-curl-mock
|
||||
version: 0.1
|
||||
synopsis: Generate curl mock requests cookbook example
|
||||
homepage: http://docs.servant.dev
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.2
|
||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||
|
||||
executable cookbock-curl-mock
|
||||
if impl(ghc >= 9.2)
|
||||
-- generic-arbitrary is incompatible
|
||||
buildable: False
|
||||
main-is: CurlMock.lhs
|
||||
build-depends: base == 4.*
|
||||
, aeson
|
||||
|
|
189
doc/cookbook/custom-errors/CustomErrors.lhs
Normal file
189
doc/cookbook/custom-errors/CustomErrors.lhs
Normal file
|
@ -0,0 +1,189 @@
|
|||
# Customizing errors from Servant
|
||||
|
||||
Servant handles a lot of parsing and validation of the input request. When it can't parse something: query
|
||||
parameters, URL parts or request body, it will return appropriate HTTP codes like 400 Bad Request.
|
||||
|
||||
These responses will contain the error message in their body without any formatting. However, it is often
|
||||
desirable to be able to provide custom formatting for these error messages, for example, to wrap them in JSON.
|
||||
|
||||
Recently Servant got a way to add such formatting. This Cookbook chapter demonstrates how to use it.
|
||||
|
||||
Extensions and imports:
|
||||
```haskell
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PolyKinds #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
|
||||
import Data.Aeson
|
||||
import Data.Proxy
|
||||
import Data.Text
|
||||
import GHC.Generics
|
||||
import Network.Wai
|
||||
import Network.Wai.Handler.Warp
|
||||
|
||||
import Servant
|
||||
|
||||
import Data.String.Conversions
|
||||
(cs)
|
||||
import Servant.API.ContentTypes
|
||||
```
|
||||
|
||||
The API (from `greet.hs` example in Servant sources):
|
||||
|
||||
```haskell
|
||||
-- | A greet message data type
|
||||
newtype Greet = Greet { _msg :: Text }
|
||||
deriving (Generic, Show)
|
||||
|
||||
instance FromJSON Greet
|
||||
instance ToJSON Greet
|
||||
|
||||
-- API specification
|
||||
type TestApi =
|
||||
-- GET /hello/:name?capital={true, false} returns a Greet as JSON
|
||||
"hello" :> Capture "name" Text :> QueryParam "capital" Bool :> Get '[JSON] Greet
|
||||
|
||||
-- POST /greet with a Greet as JSON in the request body,
|
||||
-- returns a Greet as JSON
|
||||
:<|> "greet" :> ReqBody '[JSON] Greet :> Post '[JSON] Greet
|
||||
|
||||
-- DELETE /greet/:greetid
|
||||
:<|> "greet" :> Capture "greetid" Text :> Delete '[JSON] NoContent
|
||||
|
||||
testApi :: Proxy TestApi
|
||||
testApi = Proxy
|
||||
|
||||
-- Server-side handlers.
|
||||
--
|
||||
-- There's one handler per endpoint, which, just like in the type
|
||||
-- that represents the API, are glued together using :<|>.
|
||||
--
|
||||
-- Each handler runs in the 'Handler' monad.
|
||||
server :: Server TestApi
|
||||
server = helloH :<|> postGreetH :<|> deleteGreetH
|
||||
|
||||
where helloH name Nothing = helloH name (Just False)
|
||||
helloH name (Just False) = return . Greet $ "Hello, " <> name
|
||||
helloH name (Just True) = return . Greet . toUpper $ "Hello, " <> name
|
||||
|
||||
postGreetH greet = return greet
|
||||
|
||||
deleteGreetH _ = return NoContent
|
||||
```
|
||||
|
||||
## Error formatters
|
||||
|
||||
`servant-server` provides an `ErrorFormatter` type to specify how the error message will be
|
||||
formatted. A formatter is just a function accepting three parameters:
|
||||
|
||||
- `TypeRep` from `Data.Typeable`: this is a runtime representation of the type of the combinator
|
||||
(like `Capture` or `ReqBody`) that generated the error. It can be used to display its name (with
|
||||
`show`) or even dynamically dispatch on the combinator type. See the docs for `Data.Typeable` and
|
||||
`Type.Reflection` modules.
|
||||
- `Request`: full information for the request that led to the error.
|
||||
- `String`: specific error message from the combinator.
|
||||
|
||||
The formatter is expected to produce a `ServerError` which will be returned from the handler.
|
||||
|
||||
Additionally, there is `NotFoundErrorFormatter`, which accepts only `Request` and can customize the
|
||||
error in case when no route can be matched (HTTP 404).
|
||||
|
||||
Let's make two formatters. First one will wrap our error in a JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "ERROR MESSAGE",
|
||||
"combinator": "NAME OF THE COMBINATOR"
|
||||
}
|
||||
```
|
||||
|
||||
Additionally, this formatter will examine the `Accept` header of the request and generate JSON
|
||||
message only if client can accept it.
|
||||
|
||||
```haskell
|
||||
customFormatter :: ErrorFormatter
|
||||
customFormatter tr req err =
|
||||
let
|
||||
-- aeson Value which will be sent to the client
|
||||
value = object ["combinator" .= show tr, "error" .= err]
|
||||
-- Accept header of the request
|
||||
accH = getAcceptHeader req
|
||||
in
|
||||
-- handleAcceptH is Servant's function that checks whether the client can accept a
|
||||
-- certain message type.
|
||||
-- In this case we call it with "Proxy '[JSON]" argument, meaning that we want to return a JSON.
|
||||
case handleAcceptH (Proxy :: Proxy '[JSON]) accH value of
|
||||
-- If client can't handle JSON, we just return the body the old way
|
||||
Nothing -> err400 { errBody = cs err }
|
||||
-- Otherwise, we return the JSON formatted body and set the "Content-Type" header.
|
||||
Just (ctypeH, body) -> err400
|
||||
{ errBody = body
|
||||
, errHeaders = [("Content-Type", cs ctypeH)]
|
||||
}
|
||||
|
||||
notFoundFormatter :: NotFoundErrorFormatter
|
||||
notFoundFormatter req =
|
||||
err404 { errBody = cs $ "Not found path: " <> rawPathInfo req }
|
||||
```
|
||||
|
||||
If you don't need to react to the `Accept` header, you can just unconditionally return the JSON like
|
||||
this (with `encode` from `Data.Aeson`):
|
||||
|
||||
```
|
||||
err400
|
||||
{ errBody = encode body
|
||||
, errHeaders = [("Content-Type", "application/json")]
|
||||
}
|
||||
```
|
||||
|
||||
## Passing formatters to Servant
|
||||
|
||||
Servant uses the Context to configure formatters. You only need to add a value of type
|
||||
`ErrorFormatters` to your context. This is a record with the following fields:
|
||||
|
||||
- `bodyParserErrorFormatter :: ErrorFormatter`
|
||||
- `urlParseErrorFormatter :: ErrorFormatter`
|
||||
- `headerParseErrorFormatter :: ErrorFormatter`
|
||||
- `notFoundErrorFormatter :: NotFoundErrorFormatter`
|
||||
|
||||
Default formatters are exported as `defaultErrorFormatters`, so you can use record update syntax to
|
||||
set the only ones you need:
|
||||
|
||||
```haskell
|
||||
customFormatters :: ErrorFormatters
|
||||
customFormatters = defaultErrorFormatters
|
||||
{ bodyParserErrorFormatter = customFormatter
|
||||
, notFoundErrorFormatter = notFoundFormatter
|
||||
}
|
||||
```
|
||||
|
||||
And at last, use `serveWithContext` to run your server as usual:
|
||||
|
||||
```haskell
|
||||
app :: Application
|
||||
app = serveWithContext testApi (customFormatters :. EmptyContext) server
|
||||
|
||||
main :: IO ()
|
||||
main = run 8000 app
|
||||
```
|
||||
|
||||
Now if we try to request something with a wrong body, we will get a nice error:
|
||||
|
||||
```
|
||||
$ http -j POST localhost:8000/greet 'foo=bar'
|
||||
HTTP/1.1 400 Bad Request
|
||||
Content-Type: application/json;charset=utf-8
|
||||
Date: Fri, 17 Jul 2020 13:34:18 GMT
|
||||
Server: Warp/3.3.12
|
||||
Transfer-Encoding: chunked
|
||||
|
||||
{
|
||||
"combinator": "ReqBody'",
|
||||
"error": "Error in $: parsing Main.Greet(Greet) failed, key \"_msg\" not found"
|
||||
}
|
||||
```
|
||||
|
||||
Notice the `Content-Type` header set by our combinator.
|
25
doc/cookbook/custom-errors/custom-errors.cabal
Normal file
25
doc/cookbook/custom-errors/custom-errors.cabal
Normal file
|
@ -0,0 +1,25 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-custom-errors
|
||||
version: 0.1
|
||||
synopsis: Return custom error messages from combinators
|
||||
homepage: http://docs.servant.dev
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||
|
||||
executable cookbook-custom-errors
|
||||
main-is: CustomErrors.lhs
|
||||
build-depends: base == 4.*
|
||||
, aeson
|
||||
, servant
|
||||
, servant-server
|
||||
, string-conversions
|
||||
, text
|
||||
, wai
|
||||
, warp
|
||||
default-language: Haskell2010
|
||||
ghc-options: -Wall -pgmL markdown-unlit
|
||||
build-tool-depends: markdown-unlit:markdown-unlit
|
|
@ -1,13 +1,13 @@
|
|||
cabal-version: 2.2
|
||||
name: mysql-basics
|
||||
version: 0.1.0.0
|
||||
synopsis: Simple MySQL API cookbook example
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
|
||||
executable run
|
||||
hs-source-dirs: .
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-db-postgres-pool
|
||||
version: 0.1
|
||||
synopsis: Simple PostgreSQL connection pool cookbook example
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.2
|
||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||
|
||||
executable cookbook-db-postgres-pool
|
||||
main-is: PostgresPool.lhs
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-db-sqlite-simple
|
||||
version: 0.1
|
||||
synopsis: Simple SQLite DB cookbook example
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.2
|
||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||
|
||||
executable cookbook-db-sqlite-simple
|
||||
main-is: DBConnection.lhs
|
||||
|
@ -23,7 +23,7 @@ executable cookbook-db-sqlite-simple
|
|||
, http-types >= 0.12
|
||||
, markdown-unlit >= 0.4
|
||||
, http-client >= 0.5
|
||||
, sqlite-simple >= 0.4
|
||||
, sqlite-simple >= 0.4.5.0
|
||||
, transformers
|
||||
default-language: Haskell2010
|
||||
ghc-options: -Wall -pgmL markdown-unlit
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-file-upload
|
||||
version: 0.1
|
||||
synopsis: File upload cookbook example
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.2
|
||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||
|
||||
executable cookbook-file-upload
|
||||
main-is: FileUpload.lhs
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-generic
|
||||
version: 0.1
|
||||
synopsis: Using custom monad to pass a state between handlers
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.2
|
||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||
|
||||
executable cookbook-using-custom-monad
|
||||
main-is: Generic.lhs
|
||||
|
|
|
@ -287,7 +287,7 @@ mkApp cfg cs jwts ctx =
|
|||
(flip runReaderT ctx) (adminServer cs jwts)
|
||||
```
|
||||
|
||||
One footenote: because we'd like our logs to be in JSON form, we'll also create a `Middleware` object
|
||||
One footnote: because we'd like our logs to be in JSON form, we'll also create a `Middleware` object
|
||||
so that `Warp` *also* will emit logs as JSON. This will ensure *all* logs are emitted as JSON:
|
||||
|
||||
```haskell
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-hoist-server-with-context
|
||||
version: 0.0.1
|
||||
synopsis: JWT and basic access authentication with a Custom Monad cookbook example
|
||||
description: Using servant-auth to support both JWT-based and basic
|
||||
authentication.
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
category: Servant
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.2
|
||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||
|
||||
executable cookbook-hoist-server-with-context
|
||||
main-is: HoistServerWithContext.lhs
|
||||
|
@ -24,7 +24,7 @@ executable cookbook-hoist-server-with-context
|
|||
, servant
|
||||
, servant-server
|
||||
, servant-auth >= 0.3.2
|
||||
, servant-auth-server
|
||||
, servant-auth-server >= 0.4.4.0
|
||||
, time
|
||||
, warp >= 3.2
|
||||
, wai >= 3.2
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-https
|
||||
version: 0.1
|
||||
synopsis: HTTPS cookbook example
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.2
|
||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||
|
||||
executable cookbook-https
|
||||
main-is: Https.lhs
|
||||
|
@ -17,7 +17,7 @@ executable cookbook-https
|
|||
, servant-server
|
||||
, wai >= 3.2
|
||||
, warp >= 3.2
|
||||
, warp-tls >= 3.2
|
||||
, warp-tls >= 3.2.9
|
||||
, markdown-unlit >= 0.4
|
||||
default-language: Haskell2010
|
||||
ghc-options: -Wall -pgmL markdown-unlit
|
||||
|
|
|
@ -6,8 +6,8 @@ how to solve many common problems with servant. If you're
|
|||
interested in contributing examples of your own, feel free
|
||||
to open an issue or a pull request on
|
||||
`our github repository <https://github.com/haskell-servant/servant>`_
|
||||
or even to just get in touch with us on the **#servant** IRC channel
|
||||
on freenode or on
|
||||
or even to just get in touch with us on the `**#haskell-servant** IRC channel
|
||||
on libera.chat <https://web.libera.chat/#haskell-servant>_ or on
|
||||
`the mailing list <https://groups.google.com/forum/#!forum/haskell-servant>`_.
|
||||
|
||||
The scope is very wide. Simple and fancy authentication schemes,
|
||||
|
@ -25,6 +25,8 @@ you name it!
|
|||
db-postgres-pool/PostgresPool.lhs
|
||||
using-custom-monad/UsingCustomMonad.lhs
|
||||
using-free-client/UsingFreeClient.lhs
|
||||
custom-errors/CustomErrors.lhs
|
||||
uverb/UVerb.lhs
|
||||
basic-auth/BasicAuth.lhs
|
||||
basic-streaming/Streaming.lhs
|
||||
jwt-and-basic-auth/JWTAndBasicAuth.lhs
|
||||
|
@ -35,3 +37,4 @@ you name it!
|
|||
sentry/Sentry.lhs
|
||||
testing/Testing.lhs
|
||||
open-id-connect/OpenIdConnect.lhs
|
||||
managed-resource/ManagedResource.lhs
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-jwt-and-basic-auth
|
||||
version: 0.0.1
|
||||
synopsis: JWT and basic access authentication cookbook example
|
||||
description: Using servant-auth to support both JWT-based and basic
|
||||
authentication.
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
category: Servant
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.2
|
||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||
|
||||
executable cookbook-jwt-and-basic-auth
|
||||
main-is: JWTAndBasicAuth.lhs
|
||||
|
@ -22,7 +22,7 @@ executable cookbook-jwt-and-basic-auth
|
|||
, servant
|
||||
, servant-client
|
||||
, servant-server
|
||||
, servant-auth ==0.3.*
|
||||
, servant-auth == 0.4.*
|
||||
, servant-auth-server >= 0.3.1.0
|
||||
, warp >= 3.2
|
||||
, wai >= 3.2
|
||||
|
|
114
doc/cookbook/managed-resource/ManagedResource.lhs
Normal file
114
doc/cookbook/managed-resource/ManagedResource.lhs
Normal file
|
@ -0,0 +1,114 @@
|
|||
# Request-lifetime Managed Resources
|
||||
|
||||
Let's see how we can write a handle that uses a resource managed by Servant. The resource is created automatically by Servant when the server recieves a request, and the resource is automatically destroyed when the server is finished handling a request.
|
||||
|
||||
As usual, we start with a little bit of throat clearing.
|
||||
|
||||
|
||||
``` haskell
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
import Control.Concurrent
|
||||
import Control.Exception (bracket, throwIO)
|
||||
import Control.Monad.IO.Class
|
||||
import Control.Monad.Trans.Resource
|
||||
import Data.Acquire
|
||||
import Network.HTTP.Client (newManager, defaultManagerSettings)
|
||||
import Network.Wai.Handler.Warp
|
||||
import Servant
|
||||
import Servant.Client
|
||||
import System.IO
|
||||
```
|
||||
|
||||
Here we define an API type that uses the `WithResource` combinator. The server handler for an endpoint with a `WithResource res` component will receive a value of that type as an argument.
|
||||
|
||||
``` haskell
|
||||
type API = WithResource Handle :> ReqBody '[PlainText] String :> Post '[JSON] NoContent
|
||||
|
||||
api :: Proxy API
|
||||
api = Proxy
|
||||
```
|
||||
|
||||
But this resource value has to come from somewhere. Servant obtains the value using an Acquire provided in the context. The Acquire knows how to both create and destroy resources of a particular type.
|
||||
|
||||
``` haskell
|
||||
appContext :: Context '[Acquire Handle]
|
||||
appContext = acquireHandle :. EmptyContext
|
||||
|
||||
acquireHandle :: Acquire Handle
|
||||
acquireHandle = mkAcquire newHandle closeHandle
|
||||
|
||||
newHandle :: IO Handle
|
||||
newHandle = do
|
||||
putStrLn "opening file"
|
||||
h <- openFile "test.txt" AppendMode
|
||||
putStrLn "opened file"
|
||||
return h
|
||||
|
||||
closeHandle :: Handle -> IO ()
|
||||
closeHandle h = do
|
||||
putStrLn "closing file"
|
||||
hClose h
|
||||
putStrLn "closed file"
|
||||
```
|
||||
|
||||
Now we create the handler which will use this resource. This handler will write the request message to the System.IO.Handle which was provided to us. In some situations the handler will succeed, but in some in will fail. In either case, Servant will clean up the resource for us.
|
||||
|
||||
``` haskell
|
||||
server :: Server API
|
||||
server = writeToFile
|
||||
|
||||
where writeToFile :: (ReleaseKey, Handle) -> String -> Handler NoContent
|
||||
writeToFile (_, h) msg = case msg of
|
||||
"illegal" -> error "wait, that's illegal!"
|
||||
legalMsg -> liftIO $ do
|
||||
putStrLn "writing file"
|
||||
hPutStrLn h legalMsg
|
||||
putStrLn "wrote file"
|
||||
return NoContent
|
||||
```
|
||||
|
||||
Finally we run the server in the background while we post messages to it.
|
||||
|
||||
``` haskell
|
||||
runApp :: IO ()
|
||||
runApp = run 8080 (serveWithContext api appContext $ server)
|
||||
|
||||
postMsg :: String -> ClientM NoContent
|
||||
postMsg = client api
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
mgr <- newManager defaultManagerSettings
|
||||
bracket (forkIO $ runApp) killThread $ \_ -> do
|
||||
ms <- flip runClientM (mkClientEnv mgr (BaseUrl Http "localhost" 8080 "")) $ do
|
||||
liftIO $ putStrLn "sending hello message"
|
||||
_ <- postMsg "hello"
|
||||
liftIO $ putStrLn "sending illegal message"
|
||||
_ <- postMsg "illegal"
|
||||
liftIO $ putStrLn "done"
|
||||
print ms
|
||||
```
|
||||
|
||||
This program prints
|
||||
|
||||
```
|
||||
sending hello message
|
||||
opening file
|
||||
opened file
|
||||
writing file
|
||||
wrote file
|
||||
closing file
|
||||
closed file
|
||||
sending illegal message
|
||||
opening file
|
||||
opened file
|
||||
closing file
|
||||
closed file
|
||||
wait, that's illegal!
|
||||
CallStack (from HasCallStack):
|
||||
error, called at ManagedResource.lhs:63:24 in main:Main
|
||||
Left (FailureResponse (Request {requestPath = (BaseUrl {baseUrlScheme = Http, baseUrlHost = "localhost", baseUrlPort = 8080, baseUrlPath = ""},""), requestQueryString = fromList [], requestBody = Just ((),text/plain;charset=utf-8), requestAccept = fromList [], requestHeaders = fromList [], requestHttpVersion = HTTP/1.1, requestMethod = "POST"}) (Response {responseStatusCode = Status {statusCode = 500, statusMessage = "Internal Server Error"}, responseHeaders = fromList [("Transfer-Encoding","chunked"),("Date","Thu, 24 Nov 2022 21:04:47 GMT"),("Server","Warp/3.3.23"),("Content-Type","text/plain; charset=utf-8")], responseHttpVersion = HTTP/1.1, responseBody = "Something went wrong"}))
|
||||
```
|
||||
|
||||
and appends to a file called `test.txt`. We can see from the output that when a legal message is sent, the file is opened, written to, and closed. We can also see that when an illegal message is sent, the file is opened but not written to. Crucially, it is still closed even though the handler threw an exception.
|
30
doc/cookbook/managed-resource/managed-resource.cabal
Normal file
30
doc/cookbook/managed-resource/managed-resource.cabal
Normal file
|
@ -0,0 +1,30 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-managed-resource
|
||||
version: 0.1
|
||||
synopsis: Simple managed resource cookbook example
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
tested-with: GHC==9.4.2
|
||||
|
||||
executable cookbook-managed-resource
|
||||
main-is: ManagedResource.lhs
|
||||
build-depends: base == 4.*
|
||||
, text >= 1.2
|
||||
, aeson >= 1.2
|
||||
, servant
|
||||
, servant-client
|
||||
, servant-server
|
||||
, warp >= 3.2
|
||||
, wai >= 3.2
|
||||
, http-types >= 0.12
|
||||
, markdown-unlit >= 0.4
|
||||
, http-client >= 0.5
|
||||
, transformers
|
||||
, resourcet
|
||||
default-language: Haskell2010
|
||||
ghc-options: -Wall -pgmL markdown-unlit
|
||||
build-tool-depends: markdown-unlit:markdown-unlit
|
|
@ -1,14 +1,14 @@
|
|||
cabal-version: 2.2
|
||||
name: open-id-connect
|
||||
version: 0.1
|
||||
synopsis: OpenId Connect with Servant example
|
||||
homepage: http://haskell-servant.readthedocs.org/
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
cabal-version: >= 1.10
|
||||
tested-with: GHC==8.4.4, GHC==8.6.5
|
||||
tested-with: GHC==8.6.5
|
||||
|
||||
executable cookbook-openidconnect
|
||||
main-is: OpenIdConnect.lhs
|
||||
|
|
|
@ -330,7 +330,7 @@ data Customer = Customer {
|
|||
```
|
||||
|
||||
Here is the code that displays the homepage.
|
||||
It should contain a link to the the `/login` URL.
|
||||
It should contain a link to the `/login` URL.
|
||||
When the user clicks on this link it will be redirected to Google login page
|
||||
with some generated information.
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-pagination
|
||||
version: 2.1
|
||||
synopsis: Pagination with Servant example
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.2
|
||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||
|
||||
executable cookbook-pagination
|
||||
main-is: Pagination.lhs
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-sentry
|
||||
version: 0.1
|
||||
synopsis: Collecting runtime exceptions using Sentry
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.2
|
||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||
|
||||
executable cookbook-sentry
|
||||
main-is: Sentry.lhs
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-structuring-apis
|
||||
version: 0.1
|
||||
synopsis: Example that shows how APIs can be structured
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.2
|
||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||
|
||||
executable cookbook-structuring-apis
|
||||
main-is: StructuringApis.lhs
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-testing
|
||||
version: 0.0.1
|
||||
synopsis: Common testing patterns in Servant apps
|
||||
description: This recipe includes various strategies for writing tests for Servant.
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
category: Servant
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.2
|
||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||
|
||||
executable cookbook-testing
|
||||
main-is: Testing.lhs
|
||||
|
@ -23,7 +23,7 @@ executable cookbook-testing
|
|||
, servant
|
||||
, servant-client
|
||||
, servant-server
|
||||
, servant-quickcheck
|
||||
, servant-quickcheck >= 0.0.10
|
||||
, http-client
|
||||
, http-types >= 0.12
|
||||
, hspec
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-using-custom-monad
|
||||
version: 0.1
|
||||
synopsis: Using custom monad to pass a state between handlers
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.2
|
||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||
|
||||
executable cookbook-using-custom-monad
|
||||
main-is: UsingCustomMonad.lhs
|
||||
|
|
|
@ -119,7 +119,7 @@ Now we can use `servant-client`'s internals to convert servant's `Request`
|
|||
to http-client's `Request`, and we can inspect it:
|
||||
|
||||
```haskell
|
||||
let req' = I.defaultMakeClientRequest burl req
|
||||
req' <- I.defaultMakeClientRequest burl req
|
||||
putStrLn $ "Making request: " ++ show req'
|
||||
```
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-using-free-client
|
||||
version: 0.1
|
||||
synopsis: Using Free client
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.2
|
||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||
|
||||
executable cookbook-using-free-client
|
||||
main-is: UsingFreeClient.lhs
|
||||
|
|
223
doc/cookbook/uverb/UVerb.lhs
Normal file
223
doc/cookbook/uverb/UVerb.lhs
Normal file
|
@ -0,0 +1,223 @@
|
|||
# Listing alternative responses and exceptions in your API types
|
||||
|
||||
Servant allows you to talk about the exceptions you throw in your API
|
||||
types. This is not limited to actual exceptions, you can write
|
||||
handlers that respond with arbitrary open unions of types.
|
||||
|
||||
## Compatibility
|
||||
|
||||
:warning: This cookbook is compatible with GHC 8.6.1 or higher :warning:
|
||||
|
||||
## Preliminaries
|
||||
|
||||
```haskell
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DerivingStrategies #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DerivingVia #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE InstanceSigs #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
{-# OPTIONS_GHC -Wall -Wno-orphans #-}
|
||||
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Concurrent.Async (async)
|
||||
import Control.Monad (when)
|
||||
import Control.Monad.Except (ExceptT (..), MonadError (..), MonadTrans (..), runExceptT)
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import Data.Aeson.Encode.Pretty (encodePretty)
|
||||
import Data.String.Conversions (cs)
|
||||
import Data.Swagger (ToSchema)
|
||||
import Data.Typeable (Proxy (Proxy))
|
||||
import qualified GHC.Generics as GHC
|
||||
import qualified Network.HTTP.Client as Client
|
||||
import qualified Network.Wai.Handler.Warp as Warp
|
||||
import Servant.API
|
||||
import Servant.Client
|
||||
import Servant.Server
|
||||
import Servant.Swagger
|
||||
```
|
||||
|
||||
## The API
|
||||
|
||||
This looks like a `Verb`-based routing table, except that `UVerb` has
|
||||
no status, and carries a list of response types rather than a single
|
||||
one. Each entry in the list carries its own response code.
|
||||
|
||||
```haskell
|
||||
type API =
|
||||
"fisx" :> Capture "bool" Bool
|
||||
:> UVerb 'GET '[JSON] '[FisxUser, WithStatus 303 String]
|
||||
:<|> "arian"
|
||||
:> UVerb 'GET '[JSON] '[WithStatus 201 ArianUser]
|
||||
```
|
||||
|
||||
Here are the details:
|
||||
|
||||
```haskell
|
||||
data FisxUser = FisxUser {name :: String}
|
||||
deriving (Eq, Show, GHC.Generic)
|
||||
|
||||
instance ToJSON FisxUser
|
||||
instance FromJSON FisxUser
|
||||
instance ToSchema FisxUser
|
||||
|
||||
-- | 'HasStatus' allows us to can get around 'WithStatus' if we want
|
||||
-- to, and associate the status code with our resource types directly.
|
||||
--
|
||||
-- (To avoid orphan instances and make it more explicit what's in the
|
||||
-- API and what isn't, we could even introduce a newtype 'Resource'
|
||||
-- that wraps all the types we're using in our routing table, and then
|
||||
-- define lots of 'HasStatus' instances for @Resource This@ and
|
||||
-- @Resource That@.)
|
||||
instance HasStatus FisxUser where
|
||||
type StatusOf FisxUser = 203
|
||||
|
||||
data ArianUser = ArianUser
|
||||
deriving (Eq, Show, GHC.Generic)
|
||||
|
||||
instance ToJSON ArianUser
|
||||
instance FromJSON ArianUser
|
||||
instance ToSchema ArianUser
|
||||
```
|
||||
|
||||
## Server, Client, Swagger
|
||||
|
||||
You can just respond with any of the elements of the union in handlers.
|
||||
|
||||
```haskell
|
||||
fisx :: Bool -> Handler (Union '[FisxUser, WithStatus 303 String])
|
||||
fisx True = respond (FisxUser "fisx")
|
||||
fisx False = respond (WithStatus @303 ("still fisx" :: String))
|
||||
|
||||
arian :: Handler (Union '[WithStatus 201 ArianUser])
|
||||
arian = respond (WithStatus @201 ArianUser)
|
||||
```
|
||||
|
||||
You can create client functions like you're used to:
|
||||
|
||||
```
|
||||
fisxClient :: Bool -> ClientM (Union '[FisxUser, WithStatus 303 String])
|
||||
arianClient :: ClientM (Union '[WithStatus 201 ArianUser])
|
||||
(fisxClient :<|> arianClient) = client (Proxy @API)
|
||||
```
|
||||
|
||||
... and that's basically it! Here are a few sample commands that
|
||||
show you how the swagger docs look like and how you can handle the
|
||||
result unions in clients:
|
||||
|
||||
```
|
||||
main :: IO ()
|
||||
main = do
|
||||
putStrLn . cs . encodePretty $ toSwagger (Proxy @API)
|
||||
_ <- async . Warp.run 8080 $ serve (Proxy @API) (fisx :<|> arian)
|
||||
threadDelay 50000
|
||||
mgr <- Client.newManager Client.defaultManagerSettings
|
||||
let cenv = mkClientEnv mgr (BaseUrl Http "localhost" 8080 "")
|
||||
result <- runClientM (fisxClient True) cenv
|
||||
print $ foldMapUnion (Proxy @Show) show <$> result
|
||||
print $ matchUnion @FisxUser <$> result
|
||||
print $ matchUnion @(WithStatus 303 String) <$> result
|
||||
pure ()
|
||||
```
|
||||
|
||||
## Idiomatic exceptions
|
||||
|
||||
Since `UVerb` (probably) will mostly be used for error-like responses, it may be desirable to be able to early abort handler, like with current servant one would use `throwError` with `ServerError`.
|
||||
|
||||
```haskell
|
||||
newtype UVerbT xs m a = UVerbT { unUVerbT :: ExceptT (Union xs) m a }
|
||||
deriving (Functor, Applicative, Monad, MonadTrans)
|
||||
|
||||
-- | Deliberately hide 'ExceptT's 'MonadError' instance to be able to use
|
||||
-- underlying monad's instance.
|
||||
instance MonadError e m => MonadError e (UVerbT xs m) where
|
||||
throwError = lift . throwError
|
||||
catchError (UVerbT act) h = UVerbT $ ExceptT $
|
||||
runExceptT act `catchError` (runExceptT . unUVerbT . h)
|
||||
|
||||
-- | This combinator runs 'UVerbT'. It applies 'respond' internally, so the handler
|
||||
-- may use the usual 'return'.
|
||||
runUVerbT :: (Monad m, HasStatus x, IsMember x xs) => UVerbT xs m x -> m (Union xs)
|
||||
runUVerbT (UVerbT act) = either id id <$> runExceptT (act >>= respond)
|
||||
|
||||
-- | Short-circuit 'UVerbT' computation returning one of the response types.
|
||||
throwUVerb :: (Monad m, HasStatus x, IsMember x xs) => x -> UVerbT xs m a
|
||||
throwUVerb = UVerbT . ExceptT . fmap Left . respond
|
||||
```
|
||||
|
||||
Example usage:
|
||||
|
||||
```haskell
|
||||
data Foo = Foo Int Int Int
|
||||
deriving (Show, Eq, GHC.Generic, ToJSON)
|
||||
deriving HasStatus via WithStatus 200 Foo
|
||||
|
||||
data Bar = Bar
|
||||
deriving (Show, Eq, GHC.Generic)
|
||||
|
||||
instance ToJSON Bar
|
||||
|
||||
h :: Handler (Union '[Foo, WithStatus 400 Bar])
|
||||
h = runUVerbT $ do
|
||||
when ({- something bad -} True) $
|
||||
throwUVerb $ WithStatus @400 Bar
|
||||
|
||||
when ({- really bad -} False) $
|
||||
throwError $ err500
|
||||
|
||||
-- a lot of code here...
|
||||
|
||||
return $ Foo 1 2 3
|
||||
```
|
||||
|
||||
## Related Work
|
||||
|
||||
There is the [issue from
|
||||
2017](https://github.com/haskell-servant/servant/issues/841) that was
|
||||
resolved by the introduction of `UVerb`, with a long discussion on
|
||||
alternative designs.
|
||||
|
||||
[servant-checked-exceptions](https://hackage.haskell.org/package/servant-checked-exceptions)
|
||||
is a good solution to the problem, but it restricts the user to JSON
|
||||
and a very specific envelop encoding for the union type, which is
|
||||
often not acceptable. (One good reason for this design choice is that
|
||||
it makes writing clients easier, where you need to get to the union
|
||||
type from one representative, and you don't want to run several
|
||||
parsers in the hope that the ones that should will always error out so
|
||||
you can try until the right one returns a value.)
|
||||
|
||||
[servant-exceptions](https://github.com/ch1bo/servant-exceptions) is
|
||||
another shot at the problem. It is inspired by
|
||||
servant-checked-exceptions, so it may be worth taking a closer look.
|
||||
The README claims that
|
||||
[cardano-sl](https://github.com/input-output-hk/cardano-sl) also has
|
||||
some code for generalized error handling.
|
||||
|
||||
In an earier version of the `UVerb` implementation, we have used some
|
||||
code from
|
||||
[world-peace](https://hackage.haskell.org/package/world-peace), but
|
||||
that package itself wasn't flexible enough, and we had to use
|
||||
[sop-core](https://hackage.haskell.org/package/sop-core) to implement
|
||||
the `HasServer` instance.
|
||||
|
||||
Here is a blog post we found on the subject:
|
||||
https://lukwagoallan.com/posts/unifying-servant-server-error-responses
|
||||
|
||||
(If you have anything else, please add it here or let us know.)
|
||||
|
||||
```haskell
|
||||
main :: IO ()
|
||||
main = return ()
|
||||
```
|
35
doc/cookbook/uverb/uverb.cabal
Normal file
35
doc/cookbook/uverb/uverb.cabal
Normal file
|
@ -0,0 +1,35 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-uverb
|
||||
version: 0.0.1
|
||||
synopsis: How to use the 'UVerb' type.
|
||||
description: Listing alternative responses and exceptions in your API types.
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD-3-Clause
|
||||
license-file: ../../../servant/LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
category: Servant
|
||||
build-type: Simple
|
||||
tested-with: GHC==8.6.5, GHC==8.8.4, GHC==8.10.7
|
||||
|
||||
executable cookbook-uverb
|
||||
main-is: UVerb.lhs
|
||||
build-depends: base == 4.*
|
||||
, aeson >= 1.2
|
||||
, aeson-pretty >= 0.8.8
|
||||
, async
|
||||
, http-client
|
||||
, mtl
|
||||
, servant
|
||||
, servant-client
|
||||
, servant-server
|
||||
, servant-swagger
|
||||
, string-conversions
|
||||
, swagger2
|
||||
, wai
|
||||
, warp
|
||||
if impl(ghc >= 9)
|
||||
buildable: False
|
||||
default-language: Haskell2010
|
||||
ghc-options: -Wall -pgmL markdown-unlit
|
||||
build-tool-depends: markdown-unlit:markdown-unlit
|
|
@ -12,7 +12,7 @@ Helpful Links
|
|||
`https://github.com/haskell-servant/servant/issues <https://github.com/haskell-servant/servant/issues>`_
|
||||
|
||||
- the irc channel:
|
||||
``#servant`` on freenode
|
||||
`#haskell-servant on libera.chat <https://web.libera.chat/#haskell-servant>`_
|
||||
|
||||
- the mailing list:
|
||||
`groups.google.com/forum/#!forum/haskell-servant <https://groups.google.com/forum/#!forum/haskell-servant>`_
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
recommonmark==0.5.0
|
||||
Sphinx==1.8.4
|
||||
sphinx_rtd_theme>=0.4.2
|
||||
jinja2<3.1.0
|
||||
|
|
|
@ -389,3 +389,30 @@ One example for this is if you want to serve a directory of static files along
|
|||
with the rest of your API. But you can plug in everything that is an
|
||||
`Application`, e.g. a whole web application written in any of the web
|
||||
frameworks that support `wai`.
|
||||
|
||||
Be mindful! The `servant-server`'s router works by pattern-matching the
|
||||
different routes that are composed using `:<|>`. `Raw`, as an escape hatch,
|
||||
matches any route that hasn't been matched by previous patterns. Therefore,
|
||||
any subsequent route will be silently ignored.
|
||||
|
||||
``` haskell
|
||||
type UserAPI14 = Raw
|
||||
:<|> "users" :> Get '[JSON] [User]
|
||||
-- In this situation, the /users endpoint
|
||||
-- will not be reachable because the Raw
|
||||
-- endpoint matches requests before
|
||||
```
|
||||
A simple way to avoid this pitfall is to either use `Raw` as the last
|
||||
definition, or to always have it under a static path.
|
||||
|
||||
``` haskell
|
||||
type UserAPI15 = "files" :> Raw
|
||||
-- The raw endpoint is under the /files
|
||||
-- static path, so it won't match /users.
|
||||
:<|> "users" :> Get '[JSON] [User]
|
||||
|
||||
type UserAPI16 = "users" :> Get '[JSON] [User]
|
||||
:<|> Raw
|
||||
-- The Raw endpoint is matched last, so
|
||||
-- it won't overlap another endpoint.
|
||||
```
|
||||
|
|
|
@ -47,7 +47,6 @@ module Authentication where
|
|||
import Data.Aeson (ToJSON)
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Map (Map, fromList)
|
||||
import Data.Monoid ((<>))
|
||||
import qualified Data.Map as Map
|
||||
import Data.Proxy (Proxy (Proxy))
|
||||
import Data.Text (Text)
|
||||
|
@ -133,7 +132,7 @@ combinator. Using `Context`, we can supply a function of type
|
|||
handler. This will allow the handler to check authentication and return a `User`
|
||||
to downstream handlers if successful.
|
||||
|
||||
In practice we wrap `BasicAuthData -> Handler` into a slightly
|
||||
In practice we wrap `BasicAuthData -> Handler User` into a slightly
|
||||
different function to better capture the semantics of basic authentication:
|
||||
|
||||
``` haskell ignore
|
||||
|
|
|
@ -313,8 +313,8 @@ For reference, here's a list of some combinators from **servant**:
|
|||
## The `FromHttpApiData`/`ToHttpApiData` classes
|
||||
|
||||
Wait... How does **servant** know how to decode the `Int`s from the URL? Or how
|
||||
to decode a `ClientInfo` value from the request body? This is what this and the
|
||||
following two sections address.
|
||||
to decode a `ClientInfo` value from the request body? The following three sections will
|
||||
help us answer these questions.
|
||||
|
||||
`Capture`s and `QueryParam`s are represented by some textual value in URLs.
|
||||
`Header`s are similarly represented by a pair of a header name and a
|
||||
|
|
|
@ -6,46 +6,10 @@ This is an introductory tutorial to **servant**. Whilst browsing is fine, it mak
|
|||
Any comments, issues or feedback about the tutorial can be submitted
|
||||
to `servant's issue tracker <http://github.com/haskell-servant/servant/issues>`_.
|
||||
|
||||
cabal-install
|
||||
--------
|
||||
|
||||
The whole tutorial is a `cabal <https://cabal.readthedocs.io/en/latest/>`_
|
||||
project and can be built locally as follows:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ git clone https://github.com/haskell-servant/servant.git
|
||||
$ cd servant
|
||||
# build
|
||||
$ cabal new-build tutorial
|
||||
# load in ghci to play with it
|
||||
$ cabal new-repl tutorial
|
||||
|
||||
stack
|
||||
--------
|
||||
|
||||
The servant `stack <https://docs.haskellstack.org/en/stable/README/>`_ template includes the working tutorial. To initialize this template, run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ stack new myproj servant
|
||||
$ cd myproj
|
||||
# build
|
||||
$ stack build
|
||||
# start server
|
||||
$ stack exec myproj-exe
|
||||
|
||||
The code can be found in the `*.lhs` files under `doc/tutorial/` in the
|
||||
repository. Feel free to edit it while you're reading this documentation and
|
||||
see the effect of your changes.
|
||||
|
||||
`Nix <https://nixos.org/nix/>`_ users should feel free to take a look at
|
||||
the `nix/shell.nix` file in the repository and use it to provision a suitable
|
||||
environment to build and run the examples.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
install.rst
|
||||
ApiType.lhs
|
||||
Server.lhs
|
||||
Client.lhs
|
||||
|
|
68
doc/tutorial/install.rst
Normal file
68
doc/tutorial/install.rst
Normal file
|
@ -0,0 +1,68 @@
|
|||
Install
|
||||
========
|
||||
|
||||
cabal-install
|
||||
--------
|
||||
|
||||
The whole tutorial is a `cabal <https://cabal.readthedocs.io/en/latest/>`_
|
||||
project and can be built locally as follows:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ git clone https://github.com/haskell-servant/servant.git
|
||||
$ cd servant
|
||||
# build
|
||||
$ cabal new-build tutorial
|
||||
# load in ghci to play with it
|
||||
$ cabal new-repl tutorial
|
||||
|
||||
stack
|
||||
--------
|
||||
|
||||
The servant `stack <https://docs.haskellstack.org/en/stable/README/>`_ template includes the working tutorial. To initialize this template, run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ stack new myproj servant
|
||||
$ cd myproj
|
||||
# build
|
||||
$ stack build
|
||||
# start server
|
||||
$ stack exec myproj-exe
|
||||
|
||||
The code can be found in the `*.lhs` files under `doc/tutorial/` in the
|
||||
repository. Feel free to edit it while you're reading this documentation and
|
||||
see the effect of your changes.
|
||||
|
||||
nix
|
||||
--------
|
||||
|
||||
`Nix <https://nixos.org/nix/>`_ users should feel free to take a look at
|
||||
the `nix/shell.nix` file in the repository and use it to provision a suitable
|
||||
environment to build and run the examples.
|
||||
|
||||
Note for Ubuntu users
|
||||
--------
|
||||
|
||||
Ubuntu's packages for `ghc`, `cabal`, and `stack` are years out of date.
|
||||
If the instructions above fail for you,
|
||||
try replacing the Ubuntu packages with up-to-date versions.
|
||||
First remove the installed versions:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# remove the obsolete versions
|
||||
$ sudo apt remove ghc haskell-stack cabal-install
|
||||
|
||||
Then install fresh versions of the Haskell toolchain
|
||||
using the `ghcup <https://www.haskell.org/ghcup/install/>`_ installer.
|
||||
|
||||
As of February 2022, one easy way to do this is by running a bootstrap script:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
|
||||
|
||||
The script is interactive and will prompt you for details about what
|
||||
you want installed and where. To install manually,
|
||||
see `the detailed instructions <https://www.haskell.org/ghcup/install/#manual-install>`_.
|
|
@ -1,3 +1,4 @@
|
|||
cabal-version: 2.2
|
||||
name: tutorial
|
||||
version: 0.10
|
||||
synopsis: The servant tutorial
|
||||
|
@ -6,18 +7,14 @@ description:
|
|||
<http://docs.servant.dev/>
|
||||
homepage: http://docs.servant.dev/
|
||||
category: Servant, Documentation
|
||||
license: BSD3
|
||||
license: BSD-3-Clause
|
||||
license-file: LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with:
|
||||
GHC==8.0.2
|
||||
GHC==8.2.2
|
||||
GHC==8.4.4
|
||||
GHC==8.6.5
|
||||
GHC==8.8.2
|
||||
GHC==8.8.3, GHC ==8.10.7
|
||||
extra-source-files:
|
||||
static/index.html
|
||||
static/ui.js
|
||||
|
@ -67,10 +64,10 @@ library
|
|||
, blaze-markup >= 0.8.0.0 && < 0.9
|
||||
, cookie >= 0.4.3 && < 0.5
|
||||
, js-jquery >= 3.3.1 && < 3.4
|
||||
, lucid >= 2.9.11 && < 2.10
|
||||
, random >= 1.1 && < 1.2
|
||||
, lucid >= 2.9.11 && < 2.12
|
||||
, random >= 1.1 && < 1.3
|
||||
, servant-js >= 0.9 && < 0.10
|
||||
, time >= 1.6.0.1 && < 1.10
|
||||
, time >= 1.6.0.1 && < 1.13
|
||||
|
||||
-- For legacy tools, we need to specify build-depends too
|
||||
build-depends: markdown-unlit >= 0.5.0 && <0.6
|
||||
|
|
22
ghcjs.nix
Normal file
22
ghcjs.nix
Normal file
|
@ -0,0 +1,22 @@
|
|||
let reflex-platform = import (builtins.fetchTarball
|
||||
{ name = "reflex-platform";
|
||||
url = "https://github.com/reflex-frp/reflex-platform/archive/1aba6f367982bd6dd78ec2fda75ab246a62d32c5.tar.gz";
|
||||
}) {};
|
||||
pkgs = import ./nix/nixpkgs.nix; in
|
||||
|
||||
pkgs.stdenv.mkDerivation {
|
||||
name = "ghcjs-shell";
|
||||
buildInputs =
|
||||
[ (reflex-platform.ghcjs.ghcWithPackages (p: with p; [
|
||||
attoparsec
|
||||
hashable
|
||||
]))
|
||||
pkgs.cabal-install
|
||||
pkgs.gmp
|
||||
pkgs.haskellPackages.cabal-plan
|
||||
pkgs.haskellPackages.hspec-discover
|
||||
pkgs.nodejs
|
||||
pkgs.perl
|
||||
pkgs.zlib
|
||||
];
|
||||
}
|
|
@ -21,3 +21,21 @@ a particular ghc version, e.g:
|
|||
``` sh
|
||||
$ nix-shell nix/shell.nix --argstr compiler ghcHEAD
|
||||
```
|
||||
|
||||
**Possible GHC versions**
|
||||
- `ghc865Binary`
|
||||
- `ghc884`
|
||||
- `ghc8104` - default
|
||||
- `ghc901`
|
||||
|
||||
### Cabal users
|
||||
|
||||
GHC version can be chosen via the nix-shell parameter
|
||||
|
||||
`cabal build all`
|
||||
|
||||
### Stack version
|
||||
|
||||
Since the ghc version is set by the LTS version, it is preferable to use the `ghc8104` version parameter for the nix-shell.
|
||||
|
||||
`stack --no-nix --system-ghc <command>`
|
4
nix/nixpkgs.nix
Normal file
4
nix/nixpkgs.nix
Normal file
|
@ -0,0 +1,4 @@
|
|||
import (builtins.fetchTarball {
|
||||
url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/21.05.tar.gz";
|
||||
sha256 = "sha256:1ckzhh24mgz6jd1xhfgx0i9mijk6xjqxwsshnvq789xsavrmsc36";
|
||||
}) {}
|
|
@ -1,21 +1,20 @@
|
|||
{ pkgs ? import <nixpkgs> {}
|
||||
, compiler ? "ghc822"
|
||||
{ compiler ? "ghc8104"
|
||||
, tutorial ? false
|
||||
, pkgs ? import ./nixpkgs.nix
|
||||
}:
|
||||
|
||||
with pkgs;
|
||||
with pkgs;
|
||||
|
||||
let
|
||||
ghc = haskell.packages.${compiler}.ghcWithPackages (_: []);
|
||||
docstuffs = python3.withPackages (ps: with ps; [ recommonmark sphinx sphinx_rtd_theme ]);
|
||||
in
|
||||
|
||||
stdenv.mkDerivation {
|
||||
name = "servant-dev";
|
||||
buildInputs = [ ghc zlib python3 wget ]
|
||||
++ (if tutorial then [docstuffs postgresql] else []);
|
||||
shellHook = ''
|
||||
eval $(grep export ${ghc}/bin/ghc)
|
||||
export LD_LIBRARY_PATH="${zlib}/lib";
|
||||
'';
|
||||
}
|
||||
let
|
||||
ghc = haskell.packages.${compiler}.ghcWithPackages (_: []);
|
||||
docstuffs = python3.withPackages (ps: with ps; [ recommonmark sphinx sphinx_rtd_theme ]);
|
||||
in
|
||||
stdenv.mkDerivation {
|
||||
name = "servant-dev";
|
||||
buildInputs = [ ghc zlib python3 wget cabal-install postgresql openssl stack haskellPackages.hspec-discover ]
|
||||
++ (if tutorial then [docstuffs postgresql] else []);
|
||||
shellHook = ''
|
||||
eval $(grep export ${ghc}/bin/ghc)
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"${zlib}/lib";
|
||||
'';
|
||||
}
|
||||
|
|
1
servant-auth/README.md
Symbolic link
1
servant-auth/README.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
servant-auth-server/README.lhs
|
1
servant-auth/servant-auth-client/.ghci
Normal file
1
servant-auth/servant-auth-client/.ghci
Normal file
|
@ -0,0 +1 @@
|
|||
:set -isrc -itest -idoctest/ghci-wrapper/src
|
26
servant-auth/servant-auth-client/CHANGELOG.md
Normal file
26
servant-auth/servant-auth-client/CHANGELOG.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [PVP Versioning](https://pvp.haskell.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.4.1.0] - 2020-10-06
|
||||
|
||||
- Support generic Bearer token auth
|
||||
|
||||
## [0.4.0.0] - 2019-03-08
|
||||
|
||||
## Changed
|
||||
|
||||
- #145 Support servant-0.16 in tests @domenkozar
|
||||
- #145 Drop GHC 7.10 support @domenkozar
|
||||
|
||||
## [0.3.3.0] - 2018-06-18
|
||||
|
||||
### Added
|
||||
- Support for GHC 8.4 by @phadej
|
||||
- Support for servant-0.14 by @phadej
|
||||
- Changelog by @domenkozar
|
31
servant-auth/servant-auth-client/LICENSE
Normal file
31
servant-auth/servant-auth-client/LICENSE
Normal file
|
@ -0,0 +1,31 @@
|
|||
Copyright Julian K. Arni (c) 2015
|
||||
|
||||
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.
|
||||
|
||||
* Neither the name of Julian K. Arni nor the names of other
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
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
|
||||
OWNER 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.
|
||||
|
2
servant-auth/servant-auth-client/Setup.hs
Normal file
2
servant-auth/servant-auth-client/Setup.hs
Normal file
|
@ -0,0 +1,2 @@
|
|||
import Distribution.Simple
|
||||
main = defaultMain
|
80
servant-auth/servant-auth-client/servant-auth-client.cabal
Normal file
80
servant-auth/servant-auth-client/servant-auth-client.cabal
Normal file
|
@ -0,0 +1,80 @@
|
|||
cabal-version: 2.2
|
||||
name: servant-auth-client
|
||||
version: 0.4.1.0
|
||||
synopsis: servant-client/servant-auth compatibility
|
||||
description: This package provides instances that allow generating clients from
|
||||
<https://hackage.haskell.org/package/servant servant>
|
||||
APIs that use
|
||||
<https://hackage.haskell.org/package/servant-auth servant-auth's> @Auth@ combinator.
|
||||
.
|
||||
For a quick overview of the usage, see the <https://github.com/haskell-servant/servant/tree/master/servant-auth#readme README>.
|
||||
category: Web, Servant, Authentication
|
||||
homepage: https://github.com/haskell-servant/servant/tree/master/servant-auth#readme
|
||||
bug-reports: https://github.com/haskell-servant/servant/issues
|
||||
author: Julian K. Arni
|
||||
maintainer: jkarni@gmail.com
|
||||
copyright: (c) Julian K. Arni
|
||||
license: BSD-3-Clause
|
||||
license-file: LICENSE
|
||||
tested-with: GHC ==8.6.5 || ==8.8.4 || ==8.10.4 || ==9.0.1
|
||||
build-type: Simple
|
||||
extra-source-files:
|
||||
CHANGELOG.md
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: https://github.com/haskell-servant/servant
|
||||
|
||||
library
|
||||
hs-source-dirs:
|
||||
src
|
||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
||||
ghc-options: -Wall
|
||||
build-depends:
|
||||
base >= 4.10 && < 4.18
|
||||
, bytestring >= 0.10.6.0 && < 0.12
|
||||
, containers >= 0.5.6.2 && < 0.7
|
||||
, servant-auth == 0.4.*
|
||||
, servant >= 0.13 && < 0.20
|
||||
, servant-client-core >= 0.13 && < 0.20
|
||||
|
||||
exposed-modules:
|
||||
Servant.Auth.Client
|
||||
Servant.Auth.Client.Internal
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite spec
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: Spec.hs
|
||||
hs-source-dirs:
|
||||
test
|
||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
||||
ghc-options: -Wall
|
||||
build-tool-depends: hspec-discover:hspec-discover >=2.5.5 && <2.10
|
||||
|
||||
-- dependencies with bounds inherited from the library stanza
|
||||
build-depends:
|
||||
base
|
||||
, servant-client
|
||||
, servant-auth
|
||||
, servant
|
||||
, servant-auth-client
|
||||
|
||||
-- test dependencies
|
||||
build-depends:
|
||||
hspec >= 2.5.5 && < 2.10
|
||||
, QuickCheck >= 2.11.3 && < 2.15
|
||||
, aeson >= 1.3.1.1 && < 3
|
||||
, bytestring >= 0.10.6.0 && < 0.12
|
||||
, http-client >= 0.5.13.1 && < 0.8
|
||||
, http-types >= 0.12.2 && < 0.13
|
||||
, servant-auth-server >= 0.4.2.0 && < 0.5
|
||||
, servant-server >= 0.13 && < 0.20
|
||||
, time >= 1.5.0.1 && < 1.13
|
||||
, transformers >= 0.4.2.0 && < 0.6
|
||||
, wai >= 3.2.1.2 && < 3.3
|
||||
, warp >= 3.2.25 && < 3.4
|
||||
, jose >= 0.10 && < 0.11
|
||||
other-modules:
|
||||
Servant.Auth.ClientSpec
|
||||
default-language: Haskell2010
|
|
@ -0,0 +1,3 @@
|
|||
module Servant.Auth.Client (Token(..), Bearer) where
|
||||
|
||||
import Servant.Auth.Client.Internal (Bearer, Token(..))
|
|
@ -0,0 +1,64 @@
|
|||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
#if __GLASGOW_HASKELL__ == 800
|
||||
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
|
||||
#endif
|
||||
module Servant.Auth.Client.Internal where
|
||||
|
||||
import qualified Data.ByteString as BS
|
||||
import Data.Monoid
|
||||
import Data.Proxy (Proxy (..))
|
||||
import Data.String (IsString)
|
||||
import GHC.Exts (Constraint)
|
||||
import GHC.Generics (Generic)
|
||||
import Servant.API ((:>))
|
||||
import Servant.Auth
|
||||
|
||||
import Servant.Client.Core
|
||||
import Data.Sequence ((<|))
|
||||
|
||||
-- | A simple bearer token.
|
||||
newtype Token = Token { getToken :: BS.ByteString }
|
||||
deriving (Eq, Show, Read, Generic, IsString)
|
||||
|
||||
type family HasBearer xs :: Constraint where
|
||||
HasBearer (Bearer ': xs) = ()
|
||||
HasBearer (JWT ': xs) = ()
|
||||
HasBearer (x ': xs) = HasBearer xs
|
||||
HasBearer '[] = BearerAuthNotEnabled
|
||||
|
||||
class BearerAuthNotEnabled
|
||||
|
||||
-- | @'HasBearer' auths@ is nominally a redundant constraint, but ensures we're not
|
||||
-- trying to send a token to an API that doesn't accept them.
|
||||
instance (HasBearer auths, HasClient m api) => HasClient m (Auth auths a :> api) where
|
||||
type Client m (Auth auths a :> api) = Token -> Client m api
|
||||
|
||||
clientWithRoute m _ req (Token token)
|
||||
= clientWithRoute m (Proxy :: Proxy api)
|
||||
$ req { requestHeaders = ("Authorization", headerVal) <| requestHeaders req }
|
||||
where
|
||||
headerVal = "Bearer " <> token
|
||||
|
||||
#if MIN_VERSION_servant_client_core(0,14,0)
|
||||
hoistClientMonad pm _ nt cl = hoistClientMonad pm (Proxy :: Proxy api) nt . cl
|
||||
#endif
|
||||
|
||||
|
||||
-- * Authentication combinators
|
||||
|
||||
-- | A Bearer token in the Authorization header:
|
||||
--
|
||||
-- @Authorization: Bearer <token>@
|
||||
--
|
||||
-- This can be any token recognized by the server, for example,
|
||||
-- a JSON Web Token (JWT).
|
||||
--
|
||||
-- Note that, since the exact way the token is validated is not specified,
|
||||
-- this combinator can only be used in the client. The server would not know
|
||||
-- how to validate it, while the client does not care.
|
||||
-- If you want to implement Bearer authentication in your server, you have to
|
||||
-- choose a specific combinator, such as 'JWT'.
|
||||
data Bearer
|
161
servant-auth/servant-auth-client/test/Servant/Auth/ClientSpec.hs
Normal file
161
servant-auth/servant-auth-client/test/Servant/Auth/ClientSpec.hs
Normal file
|
@ -0,0 +1,161 @@
|
|||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
module Servant.Auth.ClientSpec (spec) where
|
||||
|
||||
import Crypto.JOSE (JWK,
|
||||
KeyMaterialGenParam (OctGenParam),
|
||||
genJWK)
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import qualified Data.ByteString.Lazy as BSL
|
||||
import Data.Time (UTCTime, defaultTimeLocale,
|
||||
parseTimeOrError)
|
||||
import GHC.Generics (Generic)
|
||||
import Network.HTTP.Client (Manager, defaultManagerSettings,
|
||||
newManager)
|
||||
import Network.HTTP.Types (status401)
|
||||
import Network.Wai.Handler.Warp (testWithApplication)
|
||||
import Servant
|
||||
import Servant.Client (BaseUrl (..), Scheme (Http),
|
||||
ClientError (FailureResponse),
|
||||
#if MIN_VERSION_servant_client(0,16,0)
|
||||
ResponseF(..),
|
||||
#elif MIN_VERSION_servant_client(0,13,0)
|
||||
GenResponse(..),
|
||||
#elif MIN_VERSION_servant_client(0,12,0)
|
||||
Response(..),
|
||||
#endif
|
||||
client)
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
import Test.Hspec
|
||||
import Test.QuickCheck
|
||||
|
||||
#if MIN_VERSION_servant_client(0,13,0)
|
||||
import Servant.Client (mkClientEnv, runClientM)
|
||||
#elif MIN_VERSION_servant_client(0,9,0)
|
||||
import Servant.Client (ClientEnv (..), runClientM)
|
||||
#else
|
||||
import Control.Monad.Trans.Except (runExceptT)
|
||||
#endif
|
||||
#if !MIN_VERSION_servant_server(0,16,0)
|
||||
#define ClientError ServantError
|
||||
#endif
|
||||
|
||||
import Servant.Auth.Client
|
||||
import Servant.Auth.Server
|
||||
import Servant.Auth.Server.SetCookieOrphan ()
|
||||
|
||||
spec :: Spec
|
||||
spec = describe "The JWT combinator" $ do
|
||||
hasClientSpec
|
||||
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- * HasClient {{{
|
||||
|
||||
hasClientSpec :: Spec
|
||||
hasClientSpec = describe "HasClient" $ around (testWithApplication $ return app) $ do
|
||||
|
||||
let mkTok :: User -> Maybe UTCTime -> IO Token
|
||||
mkTok user mexp = do
|
||||
Right tok <- makeJWT user jwtCfg mexp
|
||||
return $ Token $ BSL.toStrict tok
|
||||
|
||||
it "succeeds when the token does not have expiry" $ \port -> property $ \user -> do
|
||||
tok <- mkTok user Nothing
|
||||
v <- getIntClient tok mgr (BaseUrl Http "localhost" port "")
|
||||
v `shouldBe` Right (length $ name user)
|
||||
|
||||
it "succeeds when the token is not expired" $ \port -> property $ \user -> do
|
||||
tok <- mkTok user (Just future)
|
||||
v <- getIntClient tok mgr (BaseUrl Http "localhost" port "")
|
||||
v `shouldBe` Right (length $ name user)
|
||||
|
||||
it "fails when token is expired" $ \port -> property $ \user -> do
|
||||
tok <- mkTok user (Just past)
|
||||
#if MIN_VERSION_servant_client(0,16,0)
|
||||
Left (FailureResponse _ (Response stat _ _ _))
|
||||
#elif MIN_VERSION_servant_client(0,12,0)
|
||||
Left (FailureResponse (Response stat _ _ _))
|
||||
#elif MIN_VERSION_servant_client(0,11,0)
|
||||
Left (FailureResponse _ stat _ _)
|
||||
#else
|
||||
Left (FailureResponse stat _ _)
|
||||
#endif
|
||||
<- getIntClient tok mgr (BaseUrl Http "localhost" port "")
|
||||
stat `shouldBe` status401
|
||||
|
||||
|
||||
getIntClient :: Token -> Manager -> BaseUrl -> IO (Either ClientError Int)
|
||||
#if MIN_VERSION_servant(0,13,0)
|
||||
getIntClient tok m burl = runClientM (client api tok) (mkClientEnv m burl)
|
||||
#elif MIN_VERSION_servant(0,9,0)
|
||||
getIntClient tok m burl = runClientM (client api tok) (ClientEnv m burl)
|
||||
#else
|
||||
getIntClient tok m burl = runExceptT $ client api tok m burl
|
||||
#endif
|
||||
-- }}}
|
||||
------------------------------------------------------------------------------
|
||||
-- * API and Server {{{
|
||||
|
||||
type API = Auth '[JWT] User :> Get '[JSON] Int
|
||||
|
||||
api :: Proxy API
|
||||
api = Proxy
|
||||
|
||||
theKey :: JWK
|
||||
theKey = unsafePerformIO . genJWK $ OctGenParam 256
|
||||
{-# NOINLINE theKey #-}
|
||||
|
||||
mgr :: Manager
|
||||
mgr = unsafePerformIO $ newManager defaultManagerSettings
|
||||
{-# NOINLINE mgr #-}
|
||||
|
||||
app :: Application
|
||||
app = serveWithContext api ctx server
|
||||
where
|
||||
ctx = cookieCfg :. jwtCfg :. EmptyContext
|
||||
|
||||
jwtCfg :: JWTSettings
|
||||
jwtCfg = defaultJWTSettings theKey
|
||||
|
||||
cookieCfg :: CookieSettings
|
||||
cookieCfg = defaultCookieSettings
|
||||
|
||||
|
||||
server :: Server API
|
||||
server = getInt
|
||||
where
|
||||
getInt :: AuthResult User -> Handler Int
|
||||
getInt (Authenticated u) = return . length $ name u
|
||||
getInt _ = throwAll err401
|
||||
|
||||
|
||||
-- }}}
|
||||
------------------------------------------------------------------------------
|
||||
-- * Utils {{{
|
||||
|
||||
past :: UTCTime
|
||||
past = parseTimeOrError True defaultTimeLocale "%Y-%m-%d" "1970-01-01"
|
||||
|
||||
future :: UTCTime
|
||||
future = parseTimeOrError True defaultTimeLocale "%Y-%m-%d" "2070-01-01"
|
||||
|
||||
|
||||
-- }}}
|
||||
------------------------------------------------------------------------------
|
||||
-- * Types {{{
|
||||
|
||||
data User = User
|
||||
{ name :: String
|
||||
, _id :: String
|
||||
} deriving (Eq, Show, Read, Generic)
|
||||
|
||||
instance FromJWT User
|
||||
instance ToJWT User
|
||||
instance FromJSON User
|
||||
instance ToJSON User
|
||||
|
||||
instance Arbitrary User where
|
||||
arbitrary = User <$> arbitrary <*> arbitrary
|
||||
|
||||
-- }}}
|
1
servant-auth/servant-auth-client/test/Spec.hs
Normal file
1
servant-auth/servant-auth-client/test/Spec.hs
Normal file
|
@ -0,0 +1 @@
|
|||
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
|
1
servant-auth/servant-auth-docs/.ghci
Normal file
1
servant-auth/servant-auth-docs/.ghci
Normal file
|
@ -0,0 +1 @@
|
|||
:set -isrc -itest -idoctest/ghci-wrapper/src
|
14
servant-auth/servant-auth-docs/CHANGELOG.md
Normal file
14
servant-auth/servant-auth-docs/CHANGELOG.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [PVP Versioning](https://pvp.haskell.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.10.0] - 2018-06-18
|
||||
|
||||
### Added
|
||||
- Support for GHC 8.4 by @phadej
|
||||
- Changelog by @domenkozar
|
31
servant-auth/servant-auth-docs/LICENSE
Normal file
31
servant-auth/servant-auth-docs/LICENSE
Normal file
|
@ -0,0 +1,31 @@
|
|||
Copyright Julian K. Arni (c) 2015
|
||||
|
||||
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.
|
||||
|
||||
* Neither the name of Julian K. Arni nor the names of other
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
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
|
||||
OWNER 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.
|
||||
|
33
servant-auth/servant-auth-docs/Setup.hs
Normal file
33
servant-auth/servant-auth-docs/Setup.hs
Normal file
|
@ -0,0 +1,33 @@
|
|||
{-# LANGUAGE CPP #-}
|
||||
{-# OPTIONS_GHC -Wall #-}
|
||||
module Main (main) where
|
||||
|
||||
#ifndef MIN_VERSION_cabal_doctest
|
||||
#define MIN_VERSION_cabal_doctest(x,y,z) 0
|
||||
#endif
|
||||
|
||||
#if MIN_VERSION_cabal_doctest(1,0,0)
|
||||
|
||||
import Distribution.Extra.Doctest ( defaultMainWithDoctests )
|
||||
main :: IO ()
|
||||
main = defaultMainWithDoctests "doctests"
|
||||
|
||||
#else
|
||||
|
||||
#ifdef MIN_VERSION_Cabal
|
||||
-- If the macro is defined, we have new cabal-install,
|
||||
-- but for some reason we don't have cabal-doctest in package-db
|
||||
--
|
||||
-- Probably we are running cabal sdist, when otherwise using new-build
|
||||
-- workflow
|
||||
#warning You are configuring this package without cabal-doctest installed. \
|
||||
The doctests test-suite will not work as a result. \
|
||||
To fix this, install cabal-doctest before configuring.
|
||||
#endif
|
||||
|
||||
import Distribution.Simple
|
||||
|
||||
main :: IO ()
|
||||
main = defaultMain
|
||||
|
||||
#endif
|
84
servant-auth/servant-auth-docs/servant-auth-docs.cabal
Normal file
84
servant-auth/servant-auth-docs/servant-auth-docs.cabal
Normal file
|
@ -0,0 +1,84 @@
|
|||
cabal-version: 2.2
|
||||
name: servant-auth-docs
|
||||
version: 0.2.10.0
|
||||
synopsis: servant-docs/servant-auth compatibility
|
||||
description: This package provides instances that allow generating docs from
|
||||
<https://hackage.haskell.org/package/servant servant>
|
||||
APIs that use
|
||||
<https://hackage.haskell.org/package/servant-auth servant-auth's> @Auth@ combinator.
|
||||
.
|
||||
For a quick overview of the usage, see the <https://github.com/haskell-servant/servant/tree/master/servant-auth#readme README>.
|
||||
category: Web, Servant, Authentication
|
||||
homepage: https://github.com/haskell-servant/servant/tree/master/servant-auth#readme
|
||||
bug-reports: https://github.com/haskell-servant/servant/issues
|
||||
author: Julian K. Arni
|
||||
maintainer: jkarni@gmail.com
|
||||
copyright: (c) Julian K. Arni
|
||||
license: BSD-3-Clause
|
||||
license-file: LICENSE
|
||||
tested-with: GHC ==8.6.5 || ==8.8.4 || ==8.10.4 || ==9.0.1
|
||||
build-type: Custom
|
||||
extra-source-files:
|
||||
CHANGELOG.md
|
||||
|
||||
custom-setup
|
||||
setup-depends:
|
||||
base, Cabal, cabal-doctest >=1.0.6 && <1.1
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: https://github.com/haskell-servant/servant
|
||||
|
||||
library
|
||||
hs-source-dirs:
|
||||
src
|
||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
||||
ghc-options: -Wall
|
||||
build-depends:
|
||||
base >= 4.10 && < 4.18
|
||||
, servant-docs >= 0.11.2 && < 0.13
|
||||
, servant >= 0.13 && < 0.20
|
||||
, servant-auth == 0.4.*
|
||||
, lens >= 4.16.1 && <5.3
|
||||
exposed-modules:
|
||||
Servant.Auth.Docs
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite doctests
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: doctests.hs
|
||||
build-depends:
|
||||
base,
|
||||
servant-auth-docs,
|
||||
doctest >= 0.16 && < 0.21,
|
||||
QuickCheck >= 2.11.3 && < 2.15,
|
||||
template-haskell
|
||||
ghc-options: -Wall -threaded
|
||||
hs-source-dirs: test
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite spec
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: Spec.hs
|
||||
hs-source-dirs:
|
||||
test
|
||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
||||
ghc-options: -Wall
|
||||
build-tool-depends: hspec-discover:hspec-discover >=2.5.5 && <2.10
|
||||
|
||||
-- dependencies with bounds inherited from the library stanza
|
||||
build-depends:
|
||||
base
|
||||
, text
|
||||
, servant-docs
|
||||
, servant
|
||||
, servant-auth
|
||||
, lens
|
||||
|
||||
-- test dependencies
|
||||
build-depends:
|
||||
servant-auth-docs
|
||||
, hspec >= 2.5.5 && < 2.10
|
||||
, QuickCheck >= 2.11.3 && < 2.15
|
||||
|
||||
default-language: Haskell2010
|
96
servant-auth/servant-auth-docs/src/Servant/Auth/Docs.hs
Normal file
96
servant-auth/servant-auth-docs/src/Servant/Auth/Docs.hs
Normal file
|
@ -0,0 +1,96 @@
|
|||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
module Servant.Auth.Docs
|
||||
(
|
||||
-- | The purpose of this package is provide the instance for 'servant-auth'
|
||||
-- combinators needed for 'servant-docs' documentation generation.
|
||||
--
|
||||
-- >>> type API = Auth '[JWT, Cookie, BasicAuth] Int :> Get '[JSON] Int
|
||||
-- >>> putStr $ markdown $ docs (Proxy :: Proxy API)
|
||||
-- ## GET /
|
||||
-- ...
|
||||
-- ... Authentication
|
||||
-- ...
|
||||
-- This part of the API is protected by the following authentication mechanisms:
|
||||
-- ...
|
||||
-- * JSON Web Tokens ([JWTs](https://en.wikipedia.org/wiki/JSON_Web_Token))
|
||||
-- * [Cookies](https://en.wikipedia.org/wiki/HTTP_cookie)
|
||||
-- * [Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication)
|
||||
-- ...
|
||||
-- Clients must supply the following data
|
||||
-- ...
|
||||
-- One of the following:
|
||||
-- ...
|
||||
-- * A JWT Token signed with this server's key
|
||||
-- * Cookies automatically set by browsers, plus a header
|
||||
-- * Cookies automatically set by browsers, plus a header
|
||||
-- ...
|
||||
|
||||
-- * Re-export
|
||||
JWT
|
||||
, BasicAuth
|
||||
, Cookie
|
||||
, Auth
|
||||
) where
|
||||
|
||||
import Control.Lens ((%~), (&), (|>))
|
||||
import Data.List (intercalate)
|
||||
import Data.Monoid
|
||||
import Data.Proxy (Proxy (Proxy))
|
||||
import Servant.API hiding (BasicAuth)
|
||||
import Servant.Auth
|
||||
import Servant.Docs hiding (pretty)
|
||||
import Servant.Docs.Internal (DocAuthentication (..), authInfo)
|
||||
|
||||
instance (AllDocs auths, HasDocs api) => HasDocs (Auth auths r :> api) where
|
||||
docsFor _ (endpoint, action) =
|
||||
docsFor (Proxy :: Proxy api) (endpoint, action & authInfo %~ (|> info))
|
||||
where
|
||||
(intro, reqData) = pretty $ allDocs (Proxy :: Proxy auths)
|
||||
info = DocAuthentication intro reqData
|
||||
|
||||
|
||||
pretty :: [(String, String)] -> (String, String)
|
||||
pretty [] = error "shouldn't happen"
|
||||
pretty [(i, d)] =
|
||||
( "This part of the API is protected by " <> i
|
||||
, d
|
||||
)
|
||||
pretty rs =
|
||||
( "This part of the API is protected by the following authentication mechanisms:\n\n"
|
||||
++ " * " <> intercalate "\n * " (fst <$> rs)
|
||||
, "\nOne of the following:\n\n"
|
||||
++ " * " <> intercalate "\n * " (snd <$> rs)
|
||||
)
|
||||
|
||||
|
||||
class AllDocs (x :: [*]) where
|
||||
allDocs :: proxy x
|
||||
-- intro, req
|
||||
-> [(String, String)]
|
||||
|
||||
instance (OneDoc a, AllDocs as) => AllDocs (a ': as) where
|
||||
allDocs _ = oneDoc (Proxy :: Proxy a) : allDocs (Proxy :: Proxy as)
|
||||
|
||||
instance AllDocs '[] where
|
||||
allDocs _ = []
|
||||
|
||||
class OneDoc a where
|
||||
oneDoc :: proxy a -> (String, String)
|
||||
|
||||
instance OneDoc JWT where
|
||||
oneDoc _ =
|
||||
("JSON Web Tokens ([JWTs](https://en.wikipedia.org/wiki/JSON_Web_Token))"
|
||||
, "A JWT Token signed with this server's key")
|
||||
|
||||
instance OneDoc Cookie where
|
||||
oneDoc _ =
|
||||
("[Cookies](https://en.wikipedia.org/wiki/HTTP_cookie)"
|
||||
, "Cookies automatically set by browsers, plus a header")
|
||||
|
||||
instance OneDoc BasicAuth where
|
||||
oneDoc _ =
|
||||
( "[Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication)"
|
||||
, "Cookies automatically set by browsers, plus a header")
|
||||
|
||||
-- $setup
|
||||
-- >>> instance ToSample Int where toSamples _ = singleSample 1729
|
1
servant-auth/servant-auth-docs/test/Spec.hs
Normal file
1
servant-auth/servant-auth-docs/test/Spec.hs
Normal file
|
@ -0,0 +1 @@
|
|||
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
|
12
servant-auth/servant-auth-docs/test/doctests.hs
Normal file
12
servant-auth/servant-auth-docs/test/doctests.hs
Normal file
|
@ -0,0 +1,12 @@
|
|||
module Main where
|
||||
|
||||
import Build_doctests (flags, pkgs, module_sources)
|
||||
import Data.Foldable (traverse_)
|
||||
import Test.DocTest
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
traverse_ putStrLn args
|
||||
doctest args
|
||||
where
|
||||
args = flags ++ pkgs ++ module_sources
|
1
servant-auth/servant-auth-server/.ghci
Normal file
1
servant-auth/servant-auth-server/.ghci
Normal file
|
@ -0,0 +1 @@
|
|||
:set -isrc -itest -idoctest/ghci-wrapper/src
|
130
servant-auth/servant-auth-server/CHANGELOG.md
Normal file
130
servant-auth/servant-auth-server/CHANGELOG.md
Normal file
|
@ -0,0 +1,130 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [PVP Versioning](https://pvp.haskell.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.4.6.0] - 2020-10-06
|
||||
|
||||
## Changed
|
||||
|
||||
- expose verifyJWT and use it in two places [@domenkozar]
|
||||
- support GHC 8.10 [@domenkozar]
|
||||
- move ToJWT/FromJWT to servant-auth [@erewok]
|
||||
- #165 fix AnySite with Cookie 3.5.0 [@odr]
|
||||
|
||||
## [0.4.5.1] - 2020-02-06
|
||||
|
||||
## Changed
|
||||
|
||||
- #158 servant 0.17 support [@phadej]
|
||||
|
||||
## [0.4.5.0] - 2019-12-28
|
||||
|
||||
## Changed
|
||||
- #144 servant 0.16 support and drop GHC 7.10 support [@domenkozar]
|
||||
- #148 removed unused constaint in HasServer instance for Auth
|
||||
- #154 GHC 8.8 support [@phadej]
|
||||
|
||||
### Added
|
||||
- #141 Support Stream combinator [@domenkozar]
|
||||
- #143 Allow servant-0.16 [@phadej]
|
||||
|
||||
## [0.4.4.0] - 2019-03-02
|
||||
|
||||
### Added
|
||||
- #141 Support Stream combinator [@domenkozar]
|
||||
- #143 Allow servant-0.16 [@phadej]
|
||||
|
||||
## [0.4.3.0] - 2019-01-17
|
||||
|
||||
## Changed
|
||||
- #117 Avoid running auth checks unnecessarily [@sopvop]
|
||||
- #110 Get rid of crypto-api dependency [@domenkozar]
|
||||
- #130 clearSession: improve cross-browser compatibility [@domenkozar]
|
||||
- #136 weed out bytestring-conversion [@stephenirl]
|
||||
|
||||
## [0.4.2.0] - 2018-11-05
|
||||
|
||||
### Added
|
||||
- `Headers hs a` instance for AddSetCookieApi [@domenkozar]
|
||||
- GHC 8.6.x support [@domenkozar]
|
||||
|
||||
## [0.4.1.0] - 2018-10-05
|
||||
|
||||
### Added
|
||||
- #125 Allow setting domain name for a cookie [@domenkozar]
|
||||
|
||||
## Changed
|
||||
- bump http-api-data to 0.3.10 that includes Cookie orphan instances previously located in servant-auth-server [@phadej]
|
||||
- #114 Export `HasSecurity` typeclass [@rockbmb]
|
||||
|
||||
## [0.4.0.1] - 2018-09-23
|
||||
|
||||
### Security
|
||||
- #123 Session cookie did not apply SameSite attribute [@domenkozar]
|
||||
|
||||
### Added
|
||||
- #112 HasLink instance for Auth combinator [@adetokunbo]
|
||||
- #111 Documentation for using hoistServer [@mschristiansen]
|
||||
- #107 Add utility functions for reading and writing a key to a file [@mschristiansen]
|
||||
|
||||
## [0.4.0.0] - 2018-06-17
|
||||
|
||||
### Added
|
||||
- Support GHC 8.4 by @phadej and @domenkozar
|
||||
- Support for servant-0.14 by @phadej
|
||||
- #96 Support for jose-0.7 by @xaviershay
|
||||
- #92 add `clearSession` for logout by @plredmond and @3noch
|
||||
- #95 makeJWT: allow setting Alg via defaultJWTSettings by @domenkozar
|
||||
- #89 Validate JWT against a JWKSet instead of JWK by @sopvop
|
||||
|
||||
### Changed
|
||||
- #92 Rename CSRF to XSRF by @plredmond and @3noch
|
||||
- #92 extract 'XsrfCookieSettings' from 'CookieSettings' and make XSRF checking optional
|
||||
by @plredmond and @3noch
|
||||
- #69 export SameSite by @domenkozar
|
||||
- #102 Reuse Servant.Api.IsSecure instead of duplicating ADT by @domenkozar
|
||||
|
||||
### Deprecated
|
||||
- #92 Renamed 'makeCsrfCookie' to 'makeXsrfCookie' and marked the former as deprecated
|
||||
by @plredmond and @3noc
|
||||
- #92 Made several changes to the structure of 'CookieSettings' which will require
|
||||
attention by users who have modified the XSRF settings by @plredmond and @3noch
|
||||
|
||||
### Security
|
||||
- #94 Force cookie expiration on serverside by @karshan
|
||||
|
||||
## [0.3.2.0] - 2018-02-21
|
||||
|
||||
### Added
|
||||
- #76 Export wwwAuthenticatedErr and elaborate its annotation by @defanor
|
||||
- Support for servant-0.14 by @phadej
|
||||
|
||||
### Changed
|
||||
- Disable the readme executable for ghcjs builds by @hamishmack
|
||||
- #84 Make AddSetCookieApi type family open by @qnikst
|
||||
- #79 Make CSRF checks optional for GET requests by @harendra-kumar
|
||||
|
||||
## [0.3.1.0] - 2017-11-08
|
||||
|
||||
### Added
|
||||
- Support for servant-0.12 by @phadej
|
||||
|
||||
## [0.3.0.0] - 2017-11-07
|
||||
|
||||
### Changed
|
||||
- #47 'cookiePath' and 'xsrfCookiePath' added to 'CookieSettings' by @mchaver
|
||||
|
||||
## [0.2.8.0] - 2017-05-26
|
||||
|
||||
### Added
|
||||
- #45 Support for servant-0.11 by @phadej
|
||||
|
||||
## [0.2.7.0] - 2017-02-11
|
||||
|
||||
### Changed
|
||||
- #27 #41 'acceptLogin' and 'makeCsrfCookie' functions by @bts
|
31
servant-auth/servant-auth-server/LICENSE
Normal file
31
servant-auth/servant-auth-server/LICENSE
Normal file
|
@ -0,0 +1,31 @@
|
|||
Copyright Julian K. Arni (c) 2015
|
||||
|
||||
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.
|
||||
|
||||
* Neither the name of Julian K. Arni nor the names of other
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
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
|
||||
OWNER 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.
|
||||
|
291
servant-auth/servant-auth-server/README.lhs
Normal file
291
servant-auth/servant-auth-server/README.lhs
Normal file
|
@ -0,0 +1,291 @@
|
|||
# servant-auth
|
||||
|
||||
These packages provides safe and easy-to-use authentication options for
|
||||
`servant`. The same API can be protected via:
|
||||
- basicauth
|
||||
- cookies
|
||||
- JWT tokens
|
||||
|
||||
|
||||
| Package | Hackage |
|
||||
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| servant-auth | [![servant-auth](https://img.shields.io/hackage/v/servant-auth?style=flat-square&logo=haskell&label&labelColor=5D4F85)](https://hackage.haskell.org/package/servant-auth) |
|
||||
| servant-auth-server | [![servant-auth-server](https://img.shields.io/hackage/v/servant-auth-server.svg?style=flat-square&logo=haskell&label&labelColor=5D4F85)](https://hackage.haskell.org/package/servant-auth-server) |
|
||||
| servant-auth-client | [![servant-auth-client](https://img.shields.io/hackage/v/servant-auth-client.svg?style=flat-square&logo=haskell&label&labelColor=5D4F85)](https://hackage.haskell.org/package/servant-auth-client) |
|
||||
| servant-auth-swagger | [![servant-auth-swagger](https://img.shields.io/hackage/v/servant-auth-swagger.svg?style=flat-square&logo=haskell&label&labelColor=5D4F85)](https://hackage.haskell.org/package/servant-auth-swagger) |
|
||||
| servant-auth-docs | [![servant-auth-docs](https://img.shields.io/hackage/v/servant-auth-docs.svg?style=flat-square&logo=haskell&label&labelColor=5D4F85)](https://hackage.haskell.org/package/servant-auth-docs) |
|
||||
|
||||
## How it works
|
||||
|
||||
First some imports:
|
||||
|
||||
~~~ haskell
|
||||
{-# OPTIONS_GHC -fno-warn-unused-binds #-}
|
||||
{-# OPTIONS_GHC -fno-warn-deprecations #-}
|
||||
import Control.Concurrent (forkIO)
|
||||
import Control.Monad (forever)
|
||||
import Control.Monad.Trans (liftIO)
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import GHC.Generics (Generic)
|
||||
import Network.Wai.Handler.Warp (run)
|
||||
import System.Environment (getArgs)
|
||||
import Servant
|
||||
import Servant.Auth.Server
|
||||
import Servant.Auth.Server.SetCookieOrphan ()
|
||||
~~~
|
||||
|
||||
`servant-auth` library introduces a combinator `Auth`:
|
||||
|
||||
~~~ haskell
|
||||
data Auth (auths :: [*]) val
|
||||
~~~
|
||||
|
||||
What `Auth [Auth1, Auth2] Something :> API` means is that `API` is protected by
|
||||
*either* `Auth1` *or* `Auth2`, and the result of authentication will be of type
|
||||
`AuthResult Something`, where :
|
||||
|
||||
~~~ haskell
|
||||
data AuthResult val
|
||||
= BadPassword
|
||||
| NoSuchUser
|
||||
| Authenticated val
|
||||
| Indefinite
|
||||
~~~
|
||||
|
||||
Your handlers will get a value of type `AuthResult Something`, and can decide
|
||||
what to do with it.
|
||||
|
||||
~~~ haskell
|
||||
|
||||
data User = User { name :: String, email :: String }
|
||||
deriving (Eq, Show, Read, Generic)
|
||||
|
||||
instance ToJSON User
|
||||
instance ToJWT User
|
||||
instance FromJSON User
|
||||
instance FromJWT User
|
||||
|
||||
data Login = Login { username :: String, password :: String }
|
||||
deriving (Eq, Show, Read, Generic)
|
||||
|
||||
instance ToJSON Login
|
||||
instance FromJSON Login
|
||||
|
||||
type Protected
|
||||
= "name" :> Get '[JSON] String
|
||||
:<|> "email" :> Get '[JSON] String
|
||||
|
||||
|
||||
-- | 'Protected' will be protected by 'auths', which we still have to specify.
|
||||
protected :: Servant.Auth.Server.AuthResult User -> Server Protected
|
||||
-- If we get an "Authenticated v", we can trust the information in v, since
|
||||
-- it was signed by a key we trust.
|
||||
protected (Servant.Auth.Server.Authenticated user) = return (name user) :<|> return (email user)
|
||||
-- Otherwise, we return a 401.
|
||||
protected _ = throwAll err401
|
||||
|
||||
type Unprotected =
|
||||
"login"
|
||||
:> ReqBody '[JSON] Login
|
||||
:> Verb 'POST 204 '[JSON] (Headers '[ Header "Set-Cookie" SetCookie
|
||||
, Header "Set-Cookie" SetCookie]
|
||||
NoContent)
|
||||
:<|> Raw
|
||||
|
||||
unprotected :: CookieSettings -> JWTSettings -> Server Unprotected
|
||||
unprotected cs jwts = checkCreds cs jwts :<|> serveDirectory "example/static"
|
||||
|
||||
type API auths = (Servant.Auth.Server.Auth auths User :> Protected) :<|> Unprotected
|
||||
|
||||
server :: CookieSettings -> JWTSettings -> Server (API auths)
|
||||
server cs jwts = protected :<|> unprotected cs jwts
|
||||
|
||||
~~~
|
||||
|
||||
The code is common to all authentications. In order to pick one or more specific
|
||||
authentication methods, all we need to do is provide the expect configuration
|
||||
parameters.
|
||||
|
||||
## API tokens
|
||||
|
||||
The following example illustrates how to protect an API with tokens.
|
||||
|
||||
|
||||
~~~ haskell
|
||||
-- In main, we fork the server, and allow new tokens to be created in the
|
||||
-- command line for the specified user name and email.
|
||||
mainWithJWT :: IO ()
|
||||
mainWithJWT = do
|
||||
-- We generate the key for signing tokens. This would generally be persisted,
|
||||
-- and kept safely
|
||||
myKey <- generateKey
|
||||
-- Adding some configurations. All authentications require CookieSettings to
|
||||
-- be in the context.
|
||||
let jwtCfg = defaultJWTSettings myKey
|
||||
cfg = defaultCookieSettings :. jwtCfg :. EmptyContext
|
||||
--- Here we actually make concrete
|
||||
api = Proxy :: Proxy (API '[JWT])
|
||||
_ <- forkIO $ run 7249 $ serveWithContext api cfg (server defaultCookieSettings jwtCfg)
|
||||
|
||||
putStrLn "Started server on localhost:7249"
|
||||
putStrLn "Enter name and email separated by a space for a new token"
|
||||
|
||||
forever $ do
|
||||
xs <- words <$> getLine
|
||||
case xs of
|
||||
[name', email'] -> do
|
||||
etoken <- makeJWT (User name' email') jwtCfg Nothing
|
||||
case etoken of
|
||||
Left e -> putStrLn $ "Error generating token:t" ++ show e
|
||||
Right v -> putStrLn $ "New token:\t" ++ show v
|
||||
_ -> putStrLn "Expecting a name and email separated by spaces"
|
||||
|
||||
~~~
|
||||
|
||||
And indeed:
|
||||
|
||||
~~~ bash
|
||||
|
||||
./readme JWT
|
||||
|
||||
Started server on localhost:7249
|
||||
Enter name and email separated by a space for a new token
|
||||
alice alice@gmail.com
|
||||
New token: "eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE"
|
||||
|
||||
curl localhost:7249/name -v
|
||||
|
||||
* Hostname was NOT found in DNS cache
|
||||
* Trying 127.0.0.1...
|
||||
* Connected to localhost (127.0.0.1) port 7249 (#0)
|
||||
> GET /name HTTP/1.1
|
||||
> User-Agent: curl/7.35.0
|
||||
> Host: localhost:7249
|
||||
> Accept: */*
|
||||
>
|
||||
< HTTP/1.1 401 Unauthorized
|
||||
< Transfer-Encoding: chunked
|
||||
< Date: Wed, 07 Sep 2016 20:17:17 GMT
|
||||
* Server Warp/3.2.7 is not blacklisted
|
||||
< Server: Warp/3.2.7
|
||||
<
|
||||
* Connection #0 to host localhost left intact
|
||||
|
||||
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE" \
|
||||
localhost:7249/name -v
|
||||
|
||||
* Hostname was NOT found in DNS cache
|
||||
* Trying 127.0.0.1...
|
||||
* Connected to localhost (127.0.0.1) port 7249 (#0)
|
||||
> GET /name HTTP/1.1
|
||||
> User-Agent: curl/7.35.0
|
||||
> Host: localhost:7249
|
||||
> Accept: */*
|
||||
> Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE
|
||||
>
|
||||
< HTTP/1.1 200 OK
|
||||
< Transfer-Encoding: chunked
|
||||
< Date: Wed, 07 Sep 2016 20:16:11 GMT
|
||||
* Server Warp/3.2.7 is not blacklisted
|
||||
< Server: Warp/3.2.7
|
||||
< Content-Type: application/json
|
||||
< Set-Cookie: JWT-Cookie=eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE; HttpOnly; Secure
|
||||
< Set-Cookie: XSRF-TOKEN=TWcdPnHr2QHcVyTw/TTBLQ==; Secure
|
||||
<
|
||||
* Connection #0 to host localhost left intact
|
||||
"alice"%
|
||||
|
||||
|
||||
~~~
|
||||
|
||||
## Cookies
|
||||
|
||||
What if, in addition to API tokens, we want to expose our API to browsers? All
|
||||
we need to do is say so!
|
||||
|
||||
~~~ haskell
|
||||
mainWithCookies :: IO ()
|
||||
mainWithCookies = do
|
||||
-- We *also* need a key to sign the cookies
|
||||
myKey <- generateKey
|
||||
-- Adding some configurations. 'Cookie' requires, in addition to
|
||||
-- CookieSettings, JWTSettings (for signing), so everything is just as before
|
||||
let jwtCfg = defaultJWTSettings myKey
|
||||
cfg = defaultCookieSettings :. jwtCfg :. EmptyContext
|
||||
--- Here is the actual change
|
||||
api = Proxy :: Proxy (API '[Cookie])
|
||||
run 7249 $ serveWithContext api cfg (server defaultCookieSettings jwtCfg)
|
||||
|
||||
-- Here is the login handler
|
||||
checkCreds :: CookieSettings
|
||||
-> JWTSettings
|
||||
-> Login
|
||||
-> Handler (Headers '[ Header "Set-Cookie" SetCookie
|
||||
, Header "Set-Cookie" SetCookie]
|
||||
NoContent)
|
||||
checkCreds cookieSettings jwtSettings (Login "Ali Baba" "Open Sesame") = do
|
||||
-- Usually you would ask a database for the user info. This is just a
|
||||
-- regular servant handler, so you can follow your normal database access
|
||||
-- patterns (including using 'enter').
|
||||
let usr = User "Ali Baba" "ali@email.com"
|
||||
mApplyCookies <- liftIO $ acceptLogin cookieSettings jwtSettings usr
|
||||
case mApplyCookies of
|
||||
Nothing -> throwError err401
|
||||
Just applyCookies -> return $ applyCookies NoContent
|
||||
checkCreds _ _ _ = throwError err401
|
||||
~~~
|
||||
|
||||
### XSRF and the frontend
|
||||
|
||||
XSRF protection works by requiring that there be a header of the same value as
|
||||
a distinguished cookie that is set by the server on each request. What the
|
||||
cookie and header name are can be configured (see `xsrfCookieName` and
|
||||
`xsrfHeaderName` in `CookieSettings`), but by default they are "XSRF-TOKEN" and
|
||||
"X-XSRF-TOKEN". This means that, if your client is a browser and you're using
|
||||
cookies, Javascript on the client must set the header of each request by
|
||||
reading the cookie. For jQuery, and with the default values, that might be:
|
||||
|
||||
~~~ javascript
|
||||
|
||||
var token = (function() {
|
||||
r = document.cookie.match(new RegExp('XSRF-TOKEN=([^;]+)'))
|
||||
if (r) return r[1];
|
||||
})();
|
||||
|
||||
|
||||
$.ajaxPrefilter(function(opts, origOpts, xhr) {
|
||||
xhr.setRequestHeader('X-XSRF-TOKEN', token);
|
||||
}
|
||||
|
||||
~~~
|
||||
|
||||
I *believe* nothing at all needs to be done if you're using Angular's `$http`
|
||||
directive, but I haven't tested this.
|
||||
|
||||
XSRF protection can be disabled just for `GET` requests by setting
|
||||
`xsrfExcludeGet = False`. You might want this if you're relying on the browser
|
||||
to navigate between pages that require cookie authentication.
|
||||
|
||||
XSRF protection can be completely disabled by setting `cookieXsrfSetting =
|
||||
Nothing` in `CookieSettings`. This is not recommended! If your cookie
|
||||
authenticated web application runs any javascript, it's recommended to send the
|
||||
XSRF header. However, if your web application runs no javascript, disabling
|
||||
XSRF entirely may be required.
|
||||
|
||||
# Note on this README
|
||||
|
||||
This README is a literate haskell file. Here is 'main', allowing you to pick
|
||||
between the examples above.
|
||||
|
||||
~~~ haskell
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
args <- getArgs
|
||||
let usage = "Usage: readme (JWT|Cookie)"
|
||||
case args of
|
||||
["JWT"] -> mainWithJWT
|
||||
["Cookie"] -> mainWithCookies
|
||||
e -> putStrLn $ "Arguments: \"" ++ unwords e ++ "\" not understood\n" ++ usage
|
||||
|
||||
~~~
|
1
servant-auth/servant-auth-server/README.md
Symbolic link
1
servant-auth/servant-auth-server/README.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
README.lhs
|
2
servant-auth/servant-auth-server/Setup.hs
Normal file
2
servant-auth/servant-auth-server/Setup.hs
Normal file
|
@ -0,0 +1,2 @@
|
|||
import Distribution.Simple
|
||||
main = defaultMain
|
134
servant-auth/servant-auth-server/servant-auth-server.cabal
Normal file
134
servant-auth/servant-auth-server/servant-auth-server.cabal
Normal file
|
@ -0,0 +1,134 @@
|
|||
cabal-version: 2.2
|
||||
name: servant-auth-server
|
||||
version: 0.4.7.0
|
||||
synopsis: servant-server/servant-auth compatibility
|
||||
description: This package provides the required instances for using the @Auth@ combinator
|
||||
in your 'servant' server.
|
||||
.
|
||||
Both cookie- and token- (REST API) based authentication is provided.
|
||||
.
|
||||
For a quick overview of the usage, see the <https://github.com/haskell-servant/servant/tree/master/servant-auth#readme README>.
|
||||
category: Web, Servant, Authentication
|
||||
homepage: https://github.com/haskell-servant/servant/tree/master/servant-auth#readme
|
||||
bug-reports: https://github.com/haskell-servant/servant/issues
|
||||
author: Julian K. Arni
|
||||
maintainer: jkarni@gmail.com
|
||||
copyright: (c) Julian K. Arni
|
||||
license: BSD-3-Clause
|
||||
license-file: LICENSE
|
||||
tested-with: GHC ==8.6.5 || ==8.8.4 || ==8.10.4 || ==9.0.1
|
||||
build-type: Simple
|
||||
extra-source-files:
|
||||
CHANGELOG.md
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: https://github.com/haskell-servant/servant
|
||||
|
||||
library
|
||||
hs-source-dirs:
|
||||
src
|
||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
||||
ghc-options: -Wall
|
||||
build-depends:
|
||||
base >= 4.10 && < 4.18
|
||||
, aeson >= 1.0.0.1 && < 3
|
||||
, base64-bytestring >= 1.0.0.1 && < 2
|
||||
, blaze-builder >= 0.4.1.0 && < 0.5
|
||||
, bytestring >= 0.10.6.0 && < 0.12
|
||||
, case-insensitive >= 1.2.0.11 && < 1.3
|
||||
, cookie >= 0.4.4 && < 0.5
|
||||
, data-default-class >= 0.1.2.0 && < 0.2
|
||||
, entropy >= 0.4.1.3 && < 0.5
|
||||
, http-types >= 0.12.2 && < 0.13
|
||||
, jose >= 0.10 && < 0.11
|
||||
, lens >= 4.16.1 && < 5.3
|
||||
, memory >= 0.14.16 && < 0.19
|
||||
, monad-time >= 0.3.1.0 && < 0.4
|
||||
, mtl ^>= 2.2.2 || ^>= 2.3.1
|
||||
, servant >= 0.13 && < 0.20
|
||||
, servant-auth == 0.4.*
|
||||
, servant-server >= 0.13 && < 0.20
|
||||
, tagged >= 0.8.4 && < 0.9
|
||||
, text >= 1.2.3.0 && < 2.1
|
||||
, time >= 1.5.0.1 && < 1.13
|
||||
, unordered-containers >= 0.2.9.0 && < 0.3
|
||||
, wai >= 3.2.1.2 && < 3.3
|
||||
|
||||
if impl(ghc >= 9)
|
||||
build-depends:
|
||||
-- base64-bytestring 1.2.1.0 contains important fix for GHC-9, lower versions
|
||||
-- produce wrong results, thus corrupring JWT via jose package.
|
||||
-- See: https://github.com/haskell/base64-bytestring/pull/46
|
||||
base64-bytestring >= 1.2.1.0
|
||||
|
||||
exposed-modules:
|
||||
Servant.Auth.Server
|
||||
Servant.Auth.Server.Internal
|
||||
Servant.Auth.Server.Internal.AddSetCookie
|
||||
Servant.Auth.Server.Internal.BasicAuth
|
||||
Servant.Auth.Server.Internal.Class
|
||||
Servant.Auth.Server.Internal.ConfigTypes
|
||||
Servant.Auth.Server.Internal.Cookie
|
||||
Servant.Auth.Server.Internal.FormLogin
|
||||
Servant.Auth.Server.Internal.JWT
|
||||
Servant.Auth.Server.Internal.ThrowAll
|
||||
Servant.Auth.Server.Internal.Types
|
||||
Servant.Auth.Server.SetCookieOrphan
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite readme
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: README.lhs
|
||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
||||
ghc-options: -Wall -pgmL markdown-unlit
|
||||
build-tool-depends: markdown-unlit:markdown-unlit
|
||||
build-depends:
|
||||
base
|
||||
, servant-auth
|
||||
, servant-auth-server
|
||||
, servant-server
|
||||
, aeson
|
||||
, mtl
|
||||
, warp
|
||||
default-language: Haskell2010
|
||||
if impl(ghcjs)
|
||||
buildable: False
|
||||
|
||||
test-suite spec
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: Spec.hs
|
||||
hs-source-dirs:
|
||||
test
|
||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
||||
ghc-options: -Wall
|
||||
build-tool-depends: hspec-discover:hspec-discover >=2.5.5 && <2.10
|
||||
|
||||
-- dependencies with bounds inherited from the library stanza
|
||||
build-depends:
|
||||
base
|
||||
, aeson
|
||||
, bytestring
|
||||
, case-insensitive
|
||||
, jose
|
||||
, lens
|
||||
, mtl
|
||||
, time
|
||||
, http-types
|
||||
, wai
|
||||
, servant
|
||||
, servant-server
|
||||
, transformers
|
||||
|
||||
-- test dependencies
|
||||
build-depends:
|
||||
servant-auth-server
|
||||
, hspec >= 2.5.5 && < 2.10
|
||||
, QuickCheck >= 2.11.3 && < 2.15
|
||||
, http-client >= 0.5.13.1 && < 0.8
|
||||
, lens-aeson >= 1.0.2 && < 1.3
|
||||
, warp >= 3.2.25 && < 3.4
|
||||
, wreq >= 0.5.2.1 && < 0.6
|
||||
other-modules:
|
||||
Servant.Auth.ServerSpec
|
||||
default-language: Haskell2010
|
180
servant-auth/servant-auth-server/src/Servant/Auth/Server.hs
Normal file
180
servant-auth/servant-auth-server/src/Servant/Auth/Server.hs
Normal file
|
@ -0,0 +1,180 @@
|
|||
module Servant.Auth.Server
|
||||
(
|
||||
-- | This package provides implementations for some common authentication
|
||||
-- methods. Authentication yields a trustworthy (because generated by the
|
||||
-- server) value of an some arbitrary type:
|
||||
--
|
||||
-- > type MyApi = Protected
|
||||
-- >
|
||||
-- > type Protected = Auth '[JWT, Cookie] User :> Get '[JSON] UserAccountDetails
|
||||
-- >
|
||||
-- > server :: Server Protected
|
||||
-- > server (Authenticated usr) = ... -- here we know the client really is
|
||||
-- > -- who she claims to be
|
||||
-- > server _ = throwAll err401
|
||||
--
|
||||
-- Additional configuration happens via 'Context'.
|
||||
--
|
||||
-- == Example for Custom Handler
|
||||
-- To use a custom 'Servant.Server.Handler' it is necessary to use
|
||||
-- 'Servant.Server.hoistServerWithContext' instead of
|
||||
-- 'Servant.Server.hoistServer' and specify the 'Context'.
|
||||
--
|
||||
-- Below is an example of passing 'CookieSettings' and 'JWTSettings' in the
|
||||
-- 'Context' to create a specialized function equivalent to
|
||||
-- 'Servant.Server.hoistServer' for an API that includes cookie
|
||||
-- authentication.
|
||||
--
|
||||
-- > hoistServerWithAuth
|
||||
-- > :: HasServer api '[CookieSettings, JWTSettings]
|
||||
-- > => Proxy api
|
||||
-- > -> (forall x. m x -> n x)
|
||||
-- > -> ServerT api m
|
||||
-- > -> ServerT api n
|
||||
-- > hoistServerWithAuth api =
|
||||
-- > hoistServerWithContext api (Proxy :: Proxy '[CookieSettings, JWTSettings])
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- * Auth
|
||||
-- | Basic types
|
||||
Auth
|
||||
, AuthResult(..)
|
||||
, AuthCheck(..)
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- * JWT
|
||||
-- | JSON Web Tokens (JWT) are a compact and secure way of transferring
|
||||
-- information between parties. In this library, they are signed by the
|
||||
-- server (or by some other party posessing the relevant key), and used to
|
||||
-- indicate the bearer's identity or authorization.
|
||||
--
|
||||
-- Arbitrary information can be encoded - just declare instances for the
|
||||
-- 'FromJWT' and 'ToJWT' classes. Don't go overboard though - be aware that
|
||||
-- usually you'll be trasmitting this information on each request (and
|
||||
-- response!).
|
||||
--
|
||||
-- Note that, while the tokens are signed, they are not encrypted. Do not put
|
||||
-- any information you do not wish the client to know in them!
|
||||
|
||||
-- ** Combinator
|
||||
-- | Re-exported from 'servant-auth'
|
||||
, JWT
|
||||
|
||||
-- ** Classes
|
||||
, FromJWT(..)
|
||||
, ToJWT(..)
|
||||
|
||||
-- ** Related types
|
||||
, IsMatch(..)
|
||||
|
||||
-- ** Settings
|
||||
, JWTSettings(..)
|
||||
, defaultJWTSettings
|
||||
|
||||
-- ** Create check
|
||||
, jwtAuthCheck
|
||||
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- * Cookie
|
||||
-- | Cookies are also a method of identifying and authenticating a user. They
|
||||
-- are particular common when the client is a browser
|
||||
|
||||
-- ** Combinator
|
||||
-- | Re-exported from 'servant-auth'
|
||||
, Cookie
|
||||
|
||||
-- ** Settings
|
||||
, CookieSettings(..)
|
||||
, XsrfCookieSettings(..)
|
||||
, defaultCookieSettings
|
||||
, defaultXsrfCookieSettings
|
||||
, makeSessionCookie
|
||||
, makeSessionCookieBS
|
||||
, makeXsrfCookie
|
||||
, makeCsrfCookie
|
||||
, makeCookie
|
||||
, makeCookieBS
|
||||
, acceptLogin
|
||||
, clearSession
|
||||
|
||||
|
||||
-- ** Related types
|
||||
, IsSecure(..)
|
||||
, SameSite(..)
|
||||
, AreAuths
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- * BasicAuth
|
||||
-- ** Combinator
|
||||
-- | Re-exported from 'servant-auth'
|
||||
, BasicAuth
|
||||
|
||||
-- ** Classes
|
||||
, FromBasicAuthData(..)
|
||||
|
||||
-- ** Settings
|
||||
, BasicAuthCfg
|
||||
|
||||
-- ** Related types
|
||||
, BasicAuthData(..)
|
||||
, IsPasswordCorrect(..)
|
||||
|
||||
-- ** Authentication request
|
||||
, wwwAuthenticatedErr
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- * Utilies
|
||||
, ThrowAll(throwAll)
|
||||
, generateKey
|
||||
, generateSecret
|
||||
, fromSecret
|
||||
, writeKey
|
||||
, readKey
|
||||
, makeJWT
|
||||
, verifyJWT
|
||||
|
||||
-- ** Re-exports
|
||||
, Default(def)
|
||||
, SetCookie
|
||||
) where
|
||||
|
||||
import Prelude hiding (readFile, writeFile)
|
||||
import Data.ByteString (ByteString, writeFile, readFile)
|
||||
import Data.Default.Class (Default (def))
|
||||
import Servant.Auth
|
||||
import Servant.Auth.JWT
|
||||
import Servant.Auth.Server.Internal ()
|
||||
import Servant.Auth.Server.Internal.BasicAuth
|
||||
import Servant.Auth.Server.Internal.Class
|
||||
import Servant.Auth.Server.Internal.ConfigTypes
|
||||
import Servant.Auth.Server.Internal.Cookie
|
||||
import Servant.Auth.Server.Internal.JWT
|
||||
import Servant.Auth.Server.Internal.ThrowAll
|
||||
import Servant.Auth.Server.Internal.Types
|
||||
|
||||
import Crypto.JOSE as Jose
|
||||
import Servant (BasicAuthData (..))
|
||||
import Web.Cookie (SetCookie)
|
||||
|
||||
-- | Generate a key suitable for use with 'defaultConfig'.
|
||||
generateKey :: IO Jose.JWK
|
||||
generateKey = Jose.genJWK $ Jose.OctGenParam 256
|
||||
|
||||
-- | Generate a bytestring suitable for use with 'fromSecret'.
|
||||
generateSecret :: MonadRandom m => m ByteString
|
||||
generateSecret = Jose.getRandomBytes 256
|
||||
|
||||
-- | Restores a key from a bytestring.
|
||||
fromSecret :: ByteString -> Jose.JWK
|
||||
fromSecret = Jose.fromOctets
|
||||
|
||||
-- | Writes a secret to a file. Can for instance be used from the REPL
|
||||
-- to persist a key to a file, which can then be included with the
|
||||
-- application. Restore the key using 'readKey'.
|
||||
writeKey :: FilePath -> IO ()
|
||||
writeKey fp = writeFile fp =<< generateSecret
|
||||
|
||||
-- | Reads a key from a file.
|
||||
readKey :: FilePath -> IO Jose.JWK
|
||||
readKey fp = fromSecret <$> readFile fp
|
|
@ -0,0 +1,70 @@
|
|||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
|
||||
module Servant.Auth.Server.Internal where
|
||||
|
||||
import Control.Monad.Trans (liftIO)
|
||||
import Servant ((:>), Handler, HasServer (..),
|
||||
Proxy (..),
|
||||
HasContextEntry(getContextEntry))
|
||||
import Servant.Auth
|
||||
import Servant.Auth.JWT (ToJWT)
|
||||
|
||||
import Servant.Auth.Server.Internal.AddSetCookie
|
||||
import Servant.Auth.Server.Internal.Class
|
||||
import Servant.Auth.Server.Internal.Cookie
|
||||
import Servant.Auth.Server.Internal.ConfigTypes
|
||||
import Servant.Auth.Server.Internal.JWT
|
||||
import Servant.Auth.Server.Internal.Types
|
||||
|
||||
import Servant.Server.Internal (DelayedIO, addAuthCheck, withRequest)
|
||||
|
||||
instance ( n ~ 'S ('S 'Z)
|
||||
, HasServer (AddSetCookiesApi n api) ctxs, AreAuths auths ctxs v
|
||||
, HasServer api ctxs -- this constraint is needed to implement hoistServer
|
||||
, AddSetCookies n (ServerT api Handler) (ServerT (AddSetCookiesApi n api) Handler)
|
||||
, ToJWT v
|
||||
, HasContextEntry ctxs CookieSettings
|
||||
, HasContextEntry ctxs JWTSettings
|
||||
) => HasServer (Auth auths v :> api) ctxs where
|
||||
type ServerT (Auth auths v :> api) m = AuthResult v -> ServerT api m
|
||||
|
||||
#if MIN_VERSION_servant_server(0,12,0)
|
||||
hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
|
||||
#endif
|
||||
|
||||
route _ context subserver =
|
||||
route (Proxy :: Proxy (AddSetCookiesApi n api))
|
||||
context
|
||||
(fmap go subserver `addAuthCheck` authCheck)
|
||||
|
||||
where
|
||||
authCheck :: DelayedIO (AuthResult v, SetCookieList ('S ('S 'Z)))
|
||||
authCheck = withRequest $ \req -> liftIO $ do
|
||||
authResult <- runAuthCheck (runAuths (Proxy :: Proxy auths) context) req
|
||||
cookies <- makeCookies authResult
|
||||
return (authResult, cookies)
|
||||
|
||||
jwtSettings :: JWTSettings
|
||||
jwtSettings = getContextEntry context
|
||||
|
||||
cookieSettings :: CookieSettings
|
||||
cookieSettings = getContextEntry context
|
||||
|
||||
makeCookies :: AuthResult v -> IO (SetCookieList ('S ('S 'Z)))
|
||||
makeCookies authResult = do
|
||||
xsrf <- makeXsrfCookie cookieSettings
|
||||
fmap (Just xsrf `SetCookieCons`) $
|
||||
case authResult of
|
||||
(Authenticated v) -> do
|
||||
ejwt <- makeSessionCookie cookieSettings jwtSettings v
|
||||
case ejwt of
|
||||
Nothing -> return $ Nothing `SetCookieCons` SetCookieNil
|
||||
Just jwt -> return $ Just jwt `SetCookieCons` SetCookieNil
|
||||
_ -> return $ Nothing `SetCookieCons` SetCookieNil
|
||||
|
||||
go :: (AuthResult v -> ServerT api Handler)
|
||||
-> (AuthResult v, SetCookieList n)
|
||||
-> ServerT (AddSetCookiesApi n api) Handler
|
||||
go fn (authResult, cookies) = addSetCookies cookies $ fn authResult
|
|
@ -0,0 +1,106 @@
|
|||
{-# LANGUAGE PolyKinds #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
|
||||
module Servant.Auth.Server.Internal.AddSetCookie where
|
||||
|
||||
import Blaze.ByteString.Builder (toByteString)
|
||||
import qualified Data.ByteString as BS
|
||||
import Data.Tagged (Tagged (..))
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import Network.Wai (mapResponseHeaders)
|
||||
import Servant
|
||||
import Servant.API.Generic
|
||||
import Servant.Server.Generic
|
||||
import Web.Cookie
|
||||
|
||||
-- What are we doing here? Well, the idea is to add headers to the response,
|
||||
-- but the headers come from the authentication check. In order to do that, we
|
||||
-- tweak a little the general theme of recursing down the API tree; this time,
|
||||
-- we recurse down a variation of it that adds headers to all the endpoints.
|
||||
-- This involves the usual type-level checks.
|
||||
--
|
||||
-- TODO: If the endpoints already have headers, this will not work as is.
|
||||
|
||||
data Nat = Z | S Nat
|
||||
|
||||
type family AddSetCookiesApi (n :: Nat) a where
|
||||
AddSetCookiesApi ('S 'Z) a = AddSetCookieApi a
|
||||
AddSetCookiesApi ('S n) a = AddSetCookiesApi n (AddSetCookieApi a)
|
||||
|
||||
type family AddSetCookieApiVerb a where
|
||||
AddSetCookieApiVerb (Headers ls a) = Headers (Header "Set-Cookie" SetCookie ': ls) a
|
||||
AddSetCookieApiVerb a = Headers '[Header "Set-Cookie" SetCookie] a
|
||||
|
||||
type family AddSetCookieApi a :: *
|
||||
type instance AddSetCookieApi (a :> b) = a :> AddSetCookieApi b
|
||||
type instance AddSetCookieApi (a :<|> b) = AddSetCookieApi a :<|> AddSetCookieApi b
|
||||
type instance AddSetCookieApi (NamedRoutes api) = AddSetCookieApi (ToServantApi api)
|
||||
type instance AddSetCookieApi (Verb method stat ctyps a)
|
||||
= Verb method stat ctyps (AddSetCookieApiVerb a)
|
||||
type instance AddSetCookieApi Raw = Raw
|
||||
#if MIN_VERSION_servant_server(0,15,0)
|
||||
type instance AddSetCookieApi (Stream method stat framing ctyps a)
|
||||
= Stream method stat framing ctyps (AddSetCookieApiVerb a)
|
||||
#endif
|
||||
type instance AddSetCookieApi (Headers hs a) = AddSetCookieApiVerb (Headers hs a)
|
||||
|
||||
data SetCookieList (n :: Nat) :: * where
|
||||
SetCookieNil :: SetCookieList 'Z
|
||||
SetCookieCons :: Maybe SetCookie -> SetCookieList n -> SetCookieList ('S n)
|
||||
|
||||
class AddSetCookies (n :: Nat) orig new where
|
||||
addSetCookies :: SetCookieList n -> orig -> new
|
||||
|
||||
instance {-# OVERLAPS #-} AddSetCookies ('S n) oldb newb
|
||||
=> AddSetCookies ('S n) (a -> oldb) (a -> newb) where
|
||||
addSetCookies cookies oldfn = addSetCookies cookies . oldfn
|
||||
|
||||
instance AddSetCookies 'Z orig orig where
|
||||
addSetCookies _ = id
|
||||
|
||||
instance {-# OVERLAPPABLE #-}
|
||||
( Functor m
|
||||
, AddSetCookies n (m old) (m cookied)
|
||||
, AddHeader "Set-Cookie" SetCookie cookied new
|
||||
) => AddSetCookies ('S n) (m old) (m new) where
|
||||
addSetCookies (mCookie `SetCookieCons` rest) oldVal =
|
||||
case mCookie of
|
||||
Nothing -> noHeader <$> addSetCookies rest oldVal
|
||||
Just cookie -> addHeader cookie <$> addSetCookies rest oldVal
|
||||
|
||||
instance {-# OVERLAPS #-}
|
||||
(AddSetCookies ('S n) a a', AddSetCookies ('S n) b b')
|
||||
=> AddSetCookies ('S n) (a :<|> b) (a' :<|> b') where
|
||||
addSetCookies cookies (a :<|> b) = addSetCookies cookies a :<|> addSetCookies cookies b
|
||||
|
||||
instance {-# OVERLAPS #-}
|
||||
( AddSetCookies ('S n) (ServerT (ToServantApi api) m) cookiedApi
|
||||
, Generic (api (AsServerT m))
|
||||
, GServantProduct (Rep (api (AsServerT m)))
|
||||
, ToServant api (AsServerT m) ~ ServerT (ToServantApi api) m
|
||||
)
|
||||
=> AddSetCookies ('S n) (api (AsServerT m)) cookiedApi where
|
||||
addSetCookies cookies = addSetCookies cookies . toServant
|
||||
|
||||
-- | for @servant <0.11@
|
||||
instance
|
||||
AddSetCookies ('S n) Application Application where
|
||||
addSetCookies cookies r request respond
|
||||
= r request $ respond . mapResponseHeaders (++ mkHeaders cookies)
|
||||
|
||||
-- | for @servant >=0.11@
|
||||
instance
|
||||
AddSetCookies ('S n) (Tagged m Application) (Tagged m Application) where
|
||||
addSetCookies cookies r = Tagged $ \request respond ->
|
||||
unTagged r request $ respond . mapResponseHeaders (++ mkHeaders cookies)
|
||||
|
||||
mkHeaders :: SetCookieList x -> [HTTP.Header]
|
||||
mkHeaders x = ("Set-Cookie",) <$> mkCookies x
|
||||
where
|
||||
mkCookies :: forall y. SetCookieList y -> [BS.ByteString]
|
||||
mkCookies SetCookieNil = []
|
||||
mkCookies (SetCookieCons Nothing rest) = mkCookies rest
|
||||
mkCookies (SetCookieCons (Just y) rest)
|
||||
= toByteString (renderSetCookie y) : mkCookies rest
|
|
@ -0,0 +1,59 @@
|
|||
{-# LANGUAGE CPP #-}
|
||||
module Servant.Auth.Server.Internal.BasicAuth where
|
||||
|
||||
#if !MIN_VERSION_servant_server(0,16,0)
|
||||
#define ServerError ServantErr
|
||||
#endif
|
||||
|
||||
import qualified Data.ByteString as BS
|
||||
import Servant (BasicAuthData (..),
|
||||
ServerError (..), err401)
|
||||
import Servant.Server.Internal.BasicAuth (decodeBAHdr,
|
||||
mkBAChallengerHdr)
|
||||
|
||||
import Servant.Auth.Server.Internal.Types
|
||||
|
||||
-- | A 'ServerError' that asks the client to authenticate via Basic
|
||||
-- Authentication, should be invoked by an application whenever
|
||||
-- appropriate. The argument is the realm.
|
||||
wwwAuthenticatedErr :: BS.ByteString -> ServerError
|
||||
wwwAuthenticatedErr realm = err401 { errHeaders = [mkBAChallengerHdr realm] }
|
||||
|
||||
-- | A type holding the configuration for Basic Authentication.
|
||||
-- It is defined as a type family with no arguments, so that
|
||||
-- it can be instantiated to whatever type you need to
|
||||
-- authenticate your users (use @type instance BasicAuthCfg = ...@).
|
||||
--
|
||||
-- Note that the instantiation is application-wide,
|
||||
-- i.e. there can be only one instance.
|
||||
-- As a consequence, it should not be instantiated in a library.
|
||||
--
|
||||
-- Basic Authentication expects an element of type 'BasicAuthCfg'
|
||||
-- to be in the 'Context'; that element is then passed automatically
|
||||
-- to the instance of 'FromBasicAuthData' together with the
|
||||
-- authentication data obtained from the client.
|
||||
--
|
||||
-- If you do not need a configuration for Basic Authentication,
|
||||
-- you can use just @BasicAuthCfg = ()@, and recall to also
|
||||
-- add @()@ to the 'Context'.
|
||||
-- A basic but more interesting example is to take as 'BasicAuthCfg'
|
||||
-- a list of authorised username/password pairs:
|
||||
--
|
||||
-- > deriving instance Eq BasicAuthData
|
||||
-- > type instance BasicAuthCfg = [BasicAuthData]
|
||||
-- > instance FromBasicAuthData User where
|
||||
-- > fromBasicAuthData authData authCfg =
|
||||
-- > if elem authData authCfg then ...
|
||||
type family BasicAuthCfg
|
||||
|
||||
class FromBasicAuthData a where
|
||||
-- | Whether the username exists and the password is correct.
|
||||
-- Note that, rather than passing a 'Pass' to the function, we pass a
|
||||
-- function that checks an 'EncryptedPass'. This is to make sure you don't
|
||||
-- accidentally do something untoward with the password, like store it.
|
||||
fromBasicAuthData :: BasicAuthData -> BasicAuthCfg -> IO (AuthResult a)
|
||||
|
||||
basicAuthCheck :: FromBasicAuthData usr => BasicAuthCfg -> AuthCheck usr
|
||||
basicAuthCheck cfg = AuthCheck $ \req -> case decodeBAHdr req of
|
||||
Nothing -> return Indefinite
|
||||
Just baData -> fromBasicAuthData baData cfg
|
|
@ -0,0 +1,72 @@
|
|||
{-# LANGUAGE UndecidableInstances #-}
|
||||
module Servant.Auth.Server.Internal.Class where
|
||||
|
||||
import Servant.Auth
|
||||
import Data.Monoid
|
||||
import Servant hiding (BasicAuth)
|
||||
|
||||
import Servant.Auth.JWT
|
||||
import Servant.Auth.Server.Internal.Types
|
||||
import Servant.Auth.Server.Internal.ConfigTypes
|
||||
import Servant.Auth.Server.Internal.BasicAuth
|
||||
import Servant.Auth.Server.Internal.Cookie
|
||||
import Servant.Auth.Server.Internal.JWT (jwtAuthCheck)
|
||||
|
||||
-- | @IsAuth a ctx v@ indicates that @a@ is an auth type that expects all
|
||||
-- elements of @ctx@ to be the in the Context and whose authentication check
|
||||
-- returns an @AuthCheck v@.
|
||||
class IsAuth a v where
|
||||
type family AuthArgs a :: [*]
|
||||
runAuth :: proxy a -> proxy v -> Unapp (AuthArgs a) (AuthCheck v)
|
||||
|
||||
instance FromJWT usr => IsAuth Cookie usr where
|
||||
type AuthArgs Cookie = '[CookieSettings, JWTSettings]
|
||||
runAuth _ _ = cookieAuthCheck
|
||||
|
||||
instance FromJWT usr => IsAuth JWT usr where
|
||||
type AuthArgs JWT = '[JWTSettings]
|
||||
runAuth _ _ = jwtAuthCheck
|
||||
|
||||
instance FromBasicAuthData usr => IsAuth BasicAuth usr where
|
||||
type AuthArgs BasicAuth = '[BasicAuthCfg]
|
||||
runAuth _ _ = basicAuthCheck
|
||||
|
||||
-- * Helper
|
||||
|
||||
class AreAuths (as :: [*]) (ctxs :: [*]) v where
|
||||
runAuths :: proxy as -> Context ctxs -> AuthCheck v
|
||||
|
||||
instance AreAuths '[] ctxs v where
|
||||
runAuths _ _ = mempty
|
||||
|
||||
instance ( AuthCheck v ~ App (AuthArgs a) (Unapp (AuthArgs a) (AuthCheck v))
|
||||
, IsAuth a v
|
||||
, AreAuths as ctxs v
|
||||
, AppCtx ctxs (AuthArgs a) (Unapp (AuthArgs a) (AuthCheck v))
|
||||
) => AreAuths (a ': as) ctxs v where
|
||||
runAuths _ ctxs = go <> runAuths (Proxy :: Proxy as) ctxs
|
||||
where
|
||||
go = appCtx (Proxy :: Proxy (AuthArgs a))
|
||||
ctxs
|
||||
(runAuth (Proxy :: Proxy a) (Proxy :: Proxy v))
|
||||
|
||||
type family Unapp ls res where
|
||||
Unapp '[] res = res
|
||||
Unapp (arg1 ': rest) res = arg1 -> Unapp rest res
|
||||
|
||||
type family App ls res where
|
||||
App '[] res = res
|
||||
App (arg1 ': rest) (arg1 -> res) = App rest res
|
||||
|
||||
-- | @AppCtx@ applies the function @res@ to the arguments in @ls@ by taking the
|
||||
-- values from the Context provided.
|
||||
class AppCtx ctx ls res where
|
||||
appCtx :: proxy ls -> Context ctx -> res -> App ls res
|
||||
|
||||
instance ( HasContextEntry ctxs ctx
|
||||
, AppCtx ctxs rest res
|
||||
) => AppCtx ctxs (ctx ': rest) (ctx -> res) where
|
||||
appCtx _ ctx fn = appCtx (Proxy :: Proxy rest) ctx $ fn $ getContextEntry ctx
|
||||
|
||||
instance AppCtx ctx '[] res where
|
||||
appCtx _ _ r = r
|
|
@ -0,0 +1,127 @@
|
|||
module Servant.Auth.Server.Internal.ConfigTypes
|
||||
( module Servant.Auth.Server.Internal.ConfigTypes
|
||||
, Servant.API.IsSecure(..)
|
||||
) where
|
||||
|
||||
import Crypto.JOSE as Jose
|
||||
import Crypto.JWT as Jose
|
||||
import qualified Data.ByteString as BS
|
||||
import Data.Default.Class
|
||||
import Data.Time
|
||||
import GHC.Generics (Generic)
|
||||
import Servant.API (IsSecure(..))
|
||||
|
||||
data IsMatch = Matches | DoesNotMatch
|
||||
deriving (Eq, Show, Read, Generic, Ord)
|
||||
|
||||
data IsPasswordCorrect = PasswordCorrect | PasswordIncorrect
|
||||
deriving (Eq, Show, Read, Generic, Ord)
|
||||
|
||||
-- The @SameSite@ attribute of cookies determines whether cookies will be sent
|
||||
-- on cross-origin requests.
|
||||
--
|
||||
-- See <https://tools.ietf.org/html/draft-west-first-party-cookies-07 this document>
|
||||
-- for more information.
|
||||
data SameSite = AnySite | SameSiteStrict | SameSiteLax
|
||||
deriving (Eq, Show, Read, Generic, Ord)
|
||||
|
||||
-- | @JWTSettings@ are used to generate cookies, and to verify JWTs.
|
||||
data JWTSettings = JWTSettings
|
||||
{
|
||||
-- | Key used to sign JWT.
|
||||
signingKey :: Jose.JWK
|
||||
-- | Algorithm used to sign JWT.
|
||||
, jwtAlg :: Maybe Jose.Alg
|
||||
-- | Keys used to validate JWT.
|
||||
, validationKeys :: IO Jose.JWKSet
|
||||
-- | An @aud@ predicate. The @aud@ is a string or URI that identifies the
|
||||
-- intended recipient of the JWT.
|
||||
, audienceMatches :: Jose.StringOrURI -> IsMatch
|
||||
} deriving (Generic)
|
||||
|
||||
-- | A @JWTSettings@ where the audience always matches.
|
||||
defaultJWTSettings :: Jose.JWK -> JWTSettings
|
||||
defaultJWTSettings k = JWTSettings
|
||||
{ signingKey = k
|
||||
, jwtAlg = Nothing
|
||||
, validationKeys = pure $ Jose.JWKSet [k]
|
||||
, audienceMatches = const Matches }
|
||||
|
||||
-- | The policies to use when generating cookies.
|
||||
--
|
||||
-- If *both* 'cookieMaxAge' and 'cookieExpires' are @Nothing@, browsers will
|
||||
-- treat the cookie as a *session cookie*. These will be deleted when the
|
||||
-- browser is closed.
|
||||
--
|
||||
-- Note that having the setting @Secure@ may cause testing failures if you are
|
||||
-- not testing over HTTPS.
|
||||
data CookieSettings = CookieSettings
|
||||
{
|
||||
-- | 'Secure' means browsers will only send cookies over HTTPS. Default:
|
||||
-- @Secure@.
|
||||
cookieIsSecure :: !IsSecure
|
||||
-- | How long from now until the cookie expires. Default: @Nothing@.
|
||||
, cookieMaxAge :: !(Maybe DiffTime)
|
||||
-- | At what time the cookie expires. Default: @Nothing@.
|
||||
, cookieExpires :: !(Maybe UTCTime)
|
||||
-- | The URL path and sub-paths for which this cookie is used. Default: @Just "/"@.
|
||||
, cookiePath :: !(Maybe BS.ByteString)
|
||||
-- | Domain name, if set cookie also allows subdomains. Default: @Nothing@.
|
||||
, cookieDomain :: !(Maybe BS.ByteString)
|
||||
-- | 'SameSite' settings. Default: @SameSiteLax@.
|
||||
, cookieSameSite :: !SameSite
|
||||
-- | What name to use for the cookie used for the session.
|
||||
, sessionCookieName :: !BS.ByteString
|
||||
-- | The optional settings to use for XSRF protection. Default: @Just def@.
|
||||
, cookieXsrfSetting :: !(Maybe XsrfCookieSettings)
|
||||
} deriving (Eq, Show, Generic)
|
||||
|
||||
instance Default CookieSettings where
|
||||
def = defaultCookieSettings
|
||||
|
||||
defaultCookieSettings :: CookieSettings
|
||||
defaultCookieSettings = CookieSettings
|
||||
{ cookieIsSecure = Secure
|
||||
, cookieMaxAge = Nothing
|
||||
, cookieExpires = Nothing
|
||||
, cookiePath = Just "/"
|
||||
, cookieDomain = Nothing
|
||||
, cookieSameSite = SameSiteLax
|
||||
, sessionCookieName = "JWT-Cookie"
|
||||
, cookieXsrfSetting = Just def
|
||||
}
|
||||
|
||||
-- | The policies to use when generating and verifying XSRF cookies
|
||||
data XsrfCookieSettings = XsrfCookieSettings
|
||||
{
|
||||
-- | What name to use for the cookie used for XSRF protection.
|
||||
xsrfCookieName :: !BS.ByteString
|
||||
-- | What path to use for the cookie used for XSRF protection. Default @Just "/"@.
|
||||
, xsrfCookiePath :: !(Maybe BS.ByteString)
|
||||
-- | What name to use for the header used for XSRF protection.
|
||||
, xsrfHeaderName :: !BS.ByteString
|
||||
-- | Exclude GET request method from XSRF protection.
|
||||
, xsrfExcludeGet :: !Bool
|
||||
} deriving (Eq, Show, Generic)
|
||||
|
||||
instance Default XsrfCookieSettings where
|
||||
def = defaultXsrfCookieSettings
|
||||
|
||||
defaultXsrfCookieSettings :: XsrfCookieSettings
|
||||
defaultXsrfCookieSettings = XsrfCookieSettings
|
||||
{ xsrfCookieName = "XSRF-TOKEN"
|
||||
, xsrfCookiePath = Just "/"
|
||||
, xsrfHeaderName = "X-XSRF-TOKEN"
|
||||
, xsrfExcludeGet = False
|
||||
}
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Internal {{{
|
||||
|
||||
jwtSettingsToJwtValidationSettings :: JWTSettings -> Jose.JWTValidationSettings
|
||||
jwtSettingsToJwtValidationSettings s
|
||||
= defaultJWTValidationSettings (toBool <$> audienceMatches s)
|
||||
where
|
||||
toBool Matches = True
|
||||
toBool DoesNotMatch = False
|
||||
-- }}}
|
|
@ -0,0 +1,183 @@
|
|||
{-# LANGUAGE CPP #-}
|
||||
module Servant.Auth.Server.Internal.Cookie where
|
||||
|
||||
import Blaze.ByteString.Builder (toByteString)
|
||||
import Control.Monad (MonadPlus(..), guard)
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Reader
|
||||
import qualified Crypto.JOSE as Jose
|
||||
import qualified Crypto.JWT as Jose
|
||||
import Data.ByteArray (constEq)
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Base64 as BS64
|
||||
import qualified Data.ByteString.Lazy as BSL
|
||||
import Data.CaseInsensitive (mk)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Time.Calendar (Day(..))
|
||||
import Data.Time.Clock (UTCTime(..), secondsToDiffTime)
|
||||
import Network.HTTP.Types (methodGet)
|
||||
import Network.HTTP.Types.Header(hCookie)
|
||||
import Network.Wai (Request, requestHeaders, requestMethod)
|
||||
import Servant (AddHeader, addHeader)
|
||||
import System.Entropy (getEntropy)
|
||||
import Web.Cookie
|
||||
|
||||
import Servant.Auth.JWT (FromJWT (decodeJWT), ToJWT)
|
||||
import Servant.Auth.Server.Internal.ConfigTypes
|
||||
import Servant.Auth.Server.Internal.JWT (makeJWT, verifyJWT)
|
||||
import Servant.Auth.Server.Internal.Types
|
||||
|
||||
|
||||
cookieAuthCheck :: FromJWT usr => CookieSettings -> JWTSettings -> AuthCheck usr
|
||||
cookieAuthCheck ccfg jwtSettings = do
|
||||
req <- ask
|
||||
jwtCookie <- maybe mempty return $ do
|
||||
cookies' <- lookup hCookie $ requestHeaders req
|
||||
let cookies = parseCookies cookies'
|
||||
-- Apply the XSRF check if enabled.
|
||||
guard $ fromMaybe True $ do
|
||||
xsrfCookieCfg <- xsrfCheckRequired ccfg req
|
||||
return $ xsrfCookieAuthCheck xsrfCookieCfg req cookies
|
||||
-- session cookie *must* be HttpOnly and Secure
|
||||
lookup (sessionCookieName ccfg) cookies
|
||||
verifiedJWT <- liftIO $ verifyJWT jwtSettings jwtCookie
|
||||
case verifiedJWT of
|
||||
Nothing -> mzero
|
||||
Just v -> return v
|
||||
|
||||
xsrfCheckRequired :: CookieSettings -> Request -> Maybe XsrfCookieSettings
|
||||
xsrfCheckRequired cookieSettings req = do
|
||||
xsrfCookieCfg <- cookieXsrfSetting cookieSettings
|
||||
let disableForGetReq = xsrfExcludeGet xsrfCookieCfg && requestMethod req == methodGet
|
||||
guard $ not disableForGetReq
|
||||
return xsrfCookieCfg
|
||||
|
||||
xsrfCookieAuthCheck :: XsrfCookieSettings -> Request -> [(BS.ByteString, BS.ByteString)] -> Bool
|
||||
xsrfCookieAuthCheck xsrfCookieCfg req cookies = fromMaybe False $ do
|
||||
xsrfCookie <- lookup (xsrfCookieName xsrfCookieCfg) cookies
|
||||
xsrfHeader <- lookup (mk $ xsrfHeaderName xsrfCookieCfg) $ requestHeaders req
|
||||
return $ xsrfCookie `constEq` xsrfHeader
|
||||
|
||||
-- | Makes a cookie to be used for XSRF.
|
||||
makeXsrfCookie :: CookieSettings -> IO SetCookie
|
||||
makeXsrfCookie cookieSettings = case cookieXsrfSetting cookieSettings of
|
||||
Just xsrfCookieSettings -> makeRealCookie xsrfCookieSettings
|
||||
Nothing -> return $ noXsrfTokenCookie cookieSettings
|
||||
where
|
||||
makeRealCookie xsrfCookieSettings = do
|
||||
xsrfValue <- BS64.encode <$> getEntropy 32
|
||||
return
|
||||
$ applyXsrfCookieSettings xsrfCookieSettings
|
||||
$ applyCookieSettings cookieSettings
|
||||
$ def{ setCookieValue = xsrfValue }
|
||||
|
||||
|
||||
-- | Alias for 'makeXsrfCookie'.
|
||||
makeCsrfCookie :: CookieSettings -> IO SetCookie
|
||||
makeCsrfCookie = makeXsrfCookie
|
||||
{-# DEPRECATED makeCsrfCookie "Use makeXsrfCookie instead" #-}
|
||||
|
||||
|
||||
-- | Makes a cookie with session information.
|
||||
makeSessionCookie :: ToJWT v => CookieSettings -> JWTSettings -> v -> IO (Maybe SetCookie)
|
||||
makeSessionCookie cookieSettings jwtSettings v = do
|
||||
ejwt <- makeJWT v jwtSettings (cookieExpires cookieSettings)
|
||||
case ejwt of
|
||||
Left _ -> return Nothing
|
||||
Right jwt -> return
|
||||
$ Just
|
||||
$ applySessionCookieSettings cookieSettings
|
||||
$ applyCookieSettings cookieSettings
|
||||
$ def{ setCookieValue = BSL.toStrict jwt }
|
||||
|
||||
noXsrfTokenCookie :: CookieSettings -> SetCookie
|
||||
noXsrfTokenCookie cookieSettings =
|
||||
applyCookieSettings cookieSettings $ def{ setCookieName = "NO-XSRF-TOKEN", setCookieValue = "" }
|
||||
|
||||
applyCookieSettings :: CookieSettings -> SetCookie -> SetCookie
|
||||
applyCookieSettings cookieSettings setCookie = setCookie
|
||||
{ setCookieMaxAge = cookieMaxAge cookieSettings
|
||||
, setCookieExpires = cookieExpires cookieSettings
|
||||
, setCookiePath = cookiePath cookieSettings
|
||||
, setCookieDomain = cookieDomain cookieSettings
|
||||
, setCookieSecure = case cookieIsSecure cookieSettings of
|
||||
Secure -> True
|
||||
NotSecure -> False
|
||||
}
|
||||
|
||||
applyXsrfCookieSettings :: XsrfCookieSettings -> SetCookie -> SetCookie
|
||||
applyXsrfCookieSettings xsrfCookieSettings setCookie = setCookie
|
||||
{ setCookieName = xsrfCookieName xsrfCookieSettings
|
||||
, setCookiePath = xsrfCookiePath xsrfCookieSettings
|
||||
, setCookieHttpOnly = False
|
||||
}
|
||||
|
||||
applySessionCookieSettings :: CookieSettings -> SetCookie -> SetCookie
|
||||
applySessionCookieSettings cookieSettings setCookie = setCookie
|
||||
{ setCookieName = sessionCookieName cookieSettings
|
||||
, setCookieSameSite = case cookieSameSite cookieSettings of
|
||||
AnySite -> anySite
|
||||
SameSiteStrict -> Just sameSiteStrict
|
||||
SameSiteLax -> Just sameSiteLax
|
||||
, setCookieHttpOnly = True
|
||||
}
|
||||
where
|
||||
#if MIN_VERSION_cookie(0,4,5)
|
||||
anySite = Just sameSiteNone
|
||||
#else
|
||||
anySite = Nothing
|
||||
#endif
|
||||
|
||||
-- | For a JWT-serializable session, returns a function that decorates a
|
||||
-- provided response object with XSRF and session cookies. This should be used
|
||||
-- when a user successfully authenticates with credentials.
|
||||
acceptLogin :: ( ToJWT session
|
||||
, AddHeader "Set-Cookie" SetCookie response withOneCookie
|
||||
, AddHeader "Set-Cookie" SetCookie withOneCookie withTwoCookies )
|
||||
=> CookieSettings
|
||||
-> JWTSettings
|
||||
-> session
|
||||
-> IO (Maybe (response -> withTwoCookies))
|
||||
acceptLogin cookieSettings jwtSettings session = do
|
||||
mSessionCookie <- makeSessionCookie cookieSettings jwtSettings session
|
||||
case mSessionCookie of
|
||||
Nothing -> pure Nothing
|
||||
Just sessionCookie -> do
|
||||
xsrfCookie <- makeXsrfCookie cookieSettings
|
||||
return $ Just $ addHeader sessionCookie . addHeader xsrfCookie
|
||||
|
||||
-- | Arbitrary cookie expiry time set back in history after unix time 0
|
||||
expireTime :: UTCTime
|
||||
expireTime = UTCTime (ModifiedJulianDay 50000) 0
|
||||
|
||||
-- | Adds headers to a response that clears all session cookies
|
||||
-- | using max-age and expires cookie attributes.
|
||||
clearSession :: ( AddHeader "Set-Cookie" SetCookie response withOneCookie
|
||||
, AddHeader "Set-Cookie" SetCookie withOneCookie withTwoCookies )
|
||||
=> CookieSettings
|
||||
-> response
|
||||
-> withTwoCookies
|
||||
clearSession cookieSettings = addHeader clearedSessionCookie . addHeader clearedXsrfCookie
|
||||
where
|
||||
-- According to RFC6265 max-age takes precedence, but IE/Edge ignore it completely so we set both
|
||||
cookieSettingsExpires = cookieSettings
|
||||
{ cookieExpires = Just expireTime
|
||||
, cookieMaxAge = Just (secondsToDiffTime 0)
|
||||
}
|
||||
clearedSessionCookie = applySessionCookieSettings cookieSettingsExpires $ applyCookieSettings cookieSettingsExpires def
|
||||
clearedXsrfCookie = case cookieXsrfSetting cookieSettings of
|
||||
Just xsrfCookieSettings -> applyXsrfCookieSettings xsrfCookieSettings $ applyCookieSettings cookieSettingsExpires def
|
||||
Nothing -> noXsrfTokenCookie cookieSettingsExpires
|
||||
|
||||
makeSessionCookieBS :: ToJWT v => CookieSettings -> JWTSettings -> v -> IO (Maybe BS.ByteString)
|
||||
makeSessionCookieBS a b c = fmap (toByteString . renderSetCookie) <$> makeSessionCookie a b c
|
||||
|
||||
-- | Alias for 'makeSessionCookie'.
|
||||
makeCookie :: ToJWT v => CookieSettings -> JWTSettings -> v -> IO (Maybe SetCookie)
|
||||
makeCookie = makeSessionCookie
|
||||
{-# DEPRECATED makeCookie "Use makeSessionCookie instead" #-}
|
||||
|
||||
-- | Alias for 'makeSessionCookieBS'.
|
||||
makeCookieBS :: ToJWT v => CookieSettings -> JWTSettings -> v -> IO (Maybe BS.ByteString)
|
||||
makeCookieBS = makeSessionCookieBS
|
||||
{-# DEPRECATED makeCookieBS "Use makeSessionCookieBS instead" #-}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue