~alcinnz/hurl

a70796e500daf85c3910125b712af8e84ec0edfc — Adrian Cochrane 2 years ago 5c4812d
Allow overriding localizations logic, including throwing as error.

Release HURL 2.2!
5 files changed, 162 insertions(+), 147 deletions(-)

M ChangeLog.md
M hurl.cabal
M src/Network/URI/Fetch.hs
M src/Network/URI/Locale.hs
M src/Network/URI/Messages.hs
M ChangeLog.md => ChangeLog.md +7 -0
@@ 1,5 1,12 @@
# Revision history for hurl

## 2.2.0.0 -- 2022-08-06
* Fix webform submission, refine API, & support multiple encodings.
* Switch from OpenSSL to `tls`/Cryptonite for a cryptographic backend for better error reporting & to fix Gemini implementation
* Support clientside certificates in Gemini & HTTPS
* Support HSTS with bypass
* Allow overriding HURL's error-reporting localization

## 2.1.0.1 -- 2021-03-09
* Fixes a build failure.


M hurl.cabal => hurl.cabal +1 -1
@@ 10,7 10,7 @@ name:                hurl
-- PVP summary:      +-+------- breaking API changes
--                   | | +----- non-breaking API additions
--                   | | | +--- code changes with no API change
version:             2.1.0.1
version:             2.2.0.0

-- A short (one-line) description of the package.
synopsis:            Haskell URL resolver

M src/Network/URI/Fetch.hs => src/Network/URI/Fetch.hs +23 -20
@@ 108,6 108,8 @@ data Session = Session {
#endif
    -- | The languages (RFC2616-encoded) to which responses should be localized.
    locale :: [String],
    -- | Callback function for localizing error messages, or throwing exceptions
    trans' :: Errors -> String,
    -- | Additional files to serve from about: URIs.
    aboutPages :: [(FilePath, ByteString)],
    -- | Log of timestamped/profiled URL requests


@@ 201,6 203,7 @@ newSession' appname = do
        rewriter = rewriters,
#endif
        locale = ietfLocale,
        trans' = trans ietfLocale,
        aboutPages = [],
        requestLog = Nothing,
        redirectCount = 5,


@@ 324,8 327,8 @@ encodeQuery query = intercalate "&" $ M.mapMaybe encodePart query

-- | As per `fetchURL`, but also returns the redirected URI.
fetchURL' :: Session -> [String] -> URI -> IO (URI, String, Either Text ByteString)
fetchURL' Session {redirectCount = 0, locale = locale'} _ uri =
    return (uri, mimeERR, Left $ Txt.pack $ trans locale' ExcessiveRedirects)
fetchURL' sess@Session {redirectCount = 0 } _ uri =
    return (uri, mimeERR, Left $ Txt.pack $ trans' sess ExcessiveRedirects)

#ifdef WITH_PLUGIN_REWRITES
fetchURL' session mimes uri


@@ 333,14 336,13 @@ fetchURL' session mimes uri
#endif

#ifdef WITH_PLUGIN_EXEC
fetchURL' session@Session { appName = appname, locale = l } mimes
        uri@(URI "ext:" Nothing path query _) = do
fetchURL' session@Session { appName = appname } mimes uri@(URI "ext:" Nothing path query _) = do
    dir <- getXdgDirectory XdgData "nz.geek.adrian.hurl"
    sysdirs <- getXdgDirectoryList XdgDataDirs
    let dirs = concat [[dir' </> appname, dir'] | dir' <- dir : sysdirs]
    programs <- findExecutablesInDirectories dirs ("bin" </> path)
    case programs of
      [] -> return (uri, mimeERR, Left $ Txt.pack $ trans l $ ReadFailed "404")
      [] -> return (uri, mimeERR, Left $ Txt.pack $ trans' session $ ReadFailed "404")
      program:_ -> do
        let args = case query of {
            '?':rest -> split (== '&') rest;


@@ 384,7 386,7 @@ fetchURL' session accept uri | uriScheme uri `elem` ["http:", "https:"] =
#endif

#ifdef WITH_GEMINI_URI
fetchURL' sess@Session {connCtxt = ctxt, locale = l} mimes uri@URI {
fetchURL' sess@Session { connCtxt = ctxt } mimes uri@URI {
        uriScheme = "gemini:", uriAuthority = Just (URIAuth _ host port)
    } = do
        let params = TLS.defaultParamsClient host "gmni"


@@ 418,12 420,12 @@ fetchURL' sess@Session {connCtxt = ctxt, locale = l} mimes uri@URI {
                    redirectCount = redirectCount sess - 1
                } mimes $ relativeTo redirect' uri
            (x, y, err) -> return (uri, mimeERR, Left $ Txt.pack $
                trans l $ GeminiError x y $ Txt.unpack $
                trans' sess $ GeminiError x y $ Txt.unpack $
                    Txt.replace "<" "&lt;" $ Txt.replace "&" "&amp;" err)
    where
        parseHeader :: String -> (Char, Char, Text)
        parseHeader (major:minor:meta) = (major, minor, Txt.strip $ Txt.pack meta)
        parseHeader header = ('4', '1', Txt.pack $ trans l $ MalformedResponse header)
        parseHeader header = ('4', '1', Txt.pack $ trans' sess $ MalformedResponse header)
        handleIOErr :: IOError -> IO Strict.ByteString
        handleIOErr _ = return ""
        connectionGetChunks conn = do


@@ 432,12 434,12 @@ fetchURL' sess@Session {connCtxt = ctxt, locale = l} mimes uri@URI {
#endif

#ifdef WITH_FILE_URI
fetchURL' Session {locale = l} (defaultMIME:_) uri@URI {uriScheme = "file:"} = do
fetchURL' sess (defaultMIME:_) uri@URI {uriScheme = "file:"} = do
    response <- B.readFile $ uriPath uri
    return (uri, defaultMIME, Right response)
  `catch` \e -> do
    return (uri, mimeERR,
        Left $ Txt.pack $ trans l $ ReadFailed $ displayException (e :: IOException))
        Left $ Txt.pack $ trans' sess $ ReadFailed $ displayException (e :: IOException))
#endif

#ifdef WITH_DATA_URI


@@ 453,21 455,21 @@ fetchURL' _ (defaultMIME:_) uri@URI {uriScheme = "data:"} =
#endif

#ifdef WITH_XDG
fetchURL' Session {locale = l, apps = a} _ uri@(URI {uriScheme = s}) = do
fetchURL' sess@Session { apps = a } _ uri@(URI {uriScheme = s}) = do
        app <- dispatchURIByMIME a uri ("x-scheme-handler/" ++ init s)
        return (uri, htmlERR, Left $ Txt.pack $ trans l $ app)
        return (uri, htmlERR, Left $ Txt.pack $ trans' sess $ app)
#else
fetchURL' Session {locale = l} _ URI {uriScheme = scheme} =
    return (uri, mimeERR, Left $ Txt.pack $ trans l $ UnsupportedScheme scheme)
fetchURL' sess _ URI {uriScheme = scheme} =
    return (uri, mimeERR, Left $ Txt.pack $ trans' sess $ UnsupportedScheme scheme)
#endif

dispatchByMIME :: Session -> String -> URI -> IO (Maybe String)
#if WITH_XDG
dispatchByMIME Session {locale = l, apps = a} mime uri = do
dispatchByMIME sess@Session { apps = a } mime uri = do
    err <- dispatchURIByMIME a uri mime
    return $ case err of
        UnsupportedMIME _ -> Nothing
        _ -> Just $ trans l err
        _ -> Just $ trans' sess err
#else
dispatchByMIME _ _ _ = return Nothing
#endif


@@ 497,7 499,8 @@ dispatchByApp _ _ _ _ = return False
#endif

#ifdef WITH_HTTP_URI
fetchHTTPCached session shouldCache accept@(defaultMIME:_) rawUri cbReq cbResp = do
fetchHTTPCached session @ Session { trans' = t} shouldCache
        accept@(defaultMIME:_) rawUri cbReq cbResp = do
    now <- getCurrentTime
    hsts <- readMVar $ hstsDomains session
    uri <- case (uriScheme rawUri, uriAuthority rawUri) of {


@@ 556,7 559,7 @@ fetchHTTPCached session shouldCache accept@(defaultMIME:_) rawUri cbReq cbResp =
                    return $ Left $ relativeTo uri' uri
                (Status code msg, "", _) -> return $ Right (Txt.pack mimeERR,
                    B.fromStrict $ C8.pack $
                        trans (locale session) $ HTTPStatus code $ C8.unpack msg)
                        trans' session $ HTTPStatus code $ C8.unpack msg)
                (_, body, (mimetype:_)) -> do
                    cacheHTTP uri response
                    forkIO cleanCacheHTTP -- Try to keep diskspace down...


@@ 572,9 575,9 @@ fetchHTTPCached session shouldCache accept@(defaultMIME:_) rawUri cbReq cbResp =
        Right (mime, body) ->
            let mime' = L.map (Txt.unpack . Txt.strip) $ Txt.splitOn ";" mime
            in return $ resolveCharset' uri mime' body
  `catch` \e -> do return (rawUri, mimeERR, Left $ Txt.pack $ transHttp (locale session) e)
  `catch` \e -> do return (rawUri, mimeERR, Left $ Txt.pack $ transHttp t e)
fetchHTTPCached session _ [] uri _ _ =
    return (uri, mimeERR, Left $ Txt.pack $ trans (locale session) $ UnsupportedMIME "")
    return (uri, mimeERR, Left $ Txt.pack $ trans' session $ UnsupportedMIME "")
#endif

#if WITH_HTTP_URI || WITH_GEMINI_URI

M src/Network/URI/Locale.hs => src/Network/URI/Locale.hs +47 -47
@@ 70,64 70,64 @@ split _ [] = [[]]
---- Decoupling Layer
--------
#ifdef WITH_HTTP_URI
transHttp locales (InvalidUrlException url msg) = trans locales $ InvalidUrl url msg
transHttp locales (HttpExceptionRequest _ (TooManyRedirects _)) = trans locales $ ExcessiveRedirects
transHttp locales (HttpExceptionRequest _ ResponseTimeout) = trans locales $ TimeoutResponse
transHttp locales (HttpExceptionRequest _ ConnectionTimeout) = trans locales $ TimeoutConnection
transHttp locales (HttpExceptionRequest _ (ConnectionFailure err)) =
    trans locales $ FailedConnect $ displayException err
transHttp locales (HttpExceptionRequest _ (StatusCodeException _ code)) =
    trans locales $ HTTPStatus (fromMaybe 500 $ readMaybe $ C8.unpack code) ""
transHttp locales (HttpExceptionRequest _ OverlongHeaders) =
    trans locales $ HTTPStatus 431 "Overlong Headers"
transHttp locales (HttpExceptionRequest _ (InvalidStatusLine why)) =
    trans locales $ MalformedResponse $ C8.unpack why
transHttp locales (HttpExceptionRequest _ (InvalidHeader why)) =
    trans locales $ MalformedResponse $ C8.unpack why
transHttp locales (HttpExceptionRequest _ (InvalidRequestHeader why)) =
    trans locales $ InvalidRequest $ C8.unpack why
transHttp locales (HttpExceptionRequest _ (ProxyConnectException a b (Status code msg))) =
    trans locales $ ProxyError (C8.unpack a) b code $ C8.unpack msg
transHttp trans' (InvalidUrlException url msg) = trans' $ InvalidUrl url msg
transHttp trans' (HttpExceptionRequest _ (TooManyRedirects _)) = trans' $ ExcessiveRedirects
transHttp trans' (HttpExceptionRequest _ ResponseTimeout) = trans' $ TimeoutResponse
transHttp trans' (HttpExceptionRequest _ ConnectionTimeout) = trans' $ TimeoutConnection
transHttp trans' (HttpExceptionRequest _ (ConnectionFailure err)) =
    trans' $ FailedConnect $ displayException err
transHttp trans' (HttpExceptionRequest _ (StatusCodeException _ code)) =
    trans' $ HTTPStatus (fromMaybe 500 $ readMaybe $ C8.unpack code) ""
transHttp trans' (HttpExceptionRequest _ OverlongHeaders) =
    trans' $ HTTPStatus 431 "Overlong Headers"
transHttp trans' (HttpExceptionRequest _ (InvalidStatusLine why)) =
    trans' $ MalformedResponse $ C8.unpack why
transHttp trans' (HttpExceptionRequest _ (InvalidHeader why)) =
    trans' $ MalformedResponse $ C8.unpack why
transHttp trans' (HttpExceptionRequest _ (InvalidRequestHeader why)) =
    trans' $ InvalidRequest $ C8.unpack why
transHttp trans' (HttpExceptionRequest _ (ProxyConnectException a b (Status code msg))) =
    trans' $ ProxyError (C8.unpack a) b code $ C8.unpack msg
-- NOTE: Minor details are unlocalized for now... Can always come back to this!
transHttp locales (HttpExceptionRequest _ NoResponseDataReceived) =
    trans locales $ MalformedResponse "Empty"
transHttp locales (HttpExceptionRequest _ TlsNotSupported) =
    trans locales $ HandshakeMisc "Unsupported"
transHttp locales (HttpExceptionRequest _ (WrongRequestBodyStreamSize a b)) =
    trans locales $ OtherException $ unlines ["Wrong request bodysize", show a, show b]
transHttp locales (HttpExceptionRequest _ (ResponseBodyTooShort a b)) =
    trans locales $ MalformedResponse ("Too short " ++ show a ++ '<' : show b)
transHttp locales (HttpExceptionRequest _ InvalidChunkHeaders) =
    trans locales $ MalformedResponse "Chunk headers"
transHttp locales (HttpExceptionRequest _ IncompleteHeaders) =
    trans locales $ MalformedResponse "Incomplete headers"
transHttp locales (HttpExceptionRequest _ (InvalidDestinationHost why)) =
    trans locales $ FailedConnect $ C8.unpack why
transHttp locales (HttpExceptionRequest _ (HttpZlibException _)) =
    trans locales $ MalformedResponse "ZLib compression"
transHttp locales (HttpExceptionRequest _ ConnectionClosed) =
    trans locales $ FailedConnect "already-closed"
transHttp locales (HttpExceptionRequest _ (InvalidProxySettings why)) =
    trans locales $ FailedConnect ("proxy (" ++ Txt.unpack why ++ ")")
transHttp locales (HttpExceptionRequest _ (InvalidProxyEnvironmentVariable key value)) =
    trans locales $ FailedConnect ("proxy (" ++ Txt.unpack key ++ '=' : Txt.unpack value ++ ")")
transHttp locales (HttpExceptionRequest _ (InternalException e)) =
    trans locales $ case fromException e of
transHttp trans' (HttpExceptionRequest _ NoResponseDataReceived) =
    trans' $ MalformedResponse "Empty"
transHttp trans' (HttpExceptionRequest _ TlsNotSupported) =
    trans' $ HandshakeMisc "Unsupported"
transHttp trans' (HttpExceptionRequest _ (WrongRequestBodyStreamSize a b)) =
    trans' $ OtherException $ unlines ["Wrong request bodysize", show a, show b]
transHttp trans' (HttpExceptionRequest _ (ResponseBodyTooShort a b)) =
    trans' $ MalformedResponse ("Too short " ++ show a ++ '<' : show b)
transHttp trans' (HttpExceptionRequest _ InvalidChunkHeaders) =
    trans' $ MalformedResponse "Chunk headers"
transHttp trans' (HttpExceptionRequest _ IncompleteHeaders) =
    trans' $ MalformedResponse "Incomplete headers"
transHttp trans' (HttpExceptionRequest _ (InvalidDestinationHost why)) =
    trans' $ FailedConnect $ C8.unpack why
transHttp trans' (HttpExceptionRequest _ (HttpZlibException _)) =
    trans' $ MalformedResponse "ZLib compression"
transHttp trans' (HttpExceptionRequest _ ConnectionClosed) =
    trans' $ FailedConnect "already-closed"
transHttp trans' (HttpExceptionRequest _ (InvalidProxySettings why)) =
    trans' $ FailedConnect ("proxy (" ++ Txt.unpack why ++ ")")
transHttp trans' (HttpExceptionRequest _ (InvalidProxyEnvironmentVariable key value)) =
    trans' $ FailedConnect ("proxy (" ++ Txt.unpack key ++ '=' : Txt.unpack value ++ ")")
transHttp trans' (HttpExceptionRequest _ (InternalException e)) =
    trans' $ case fromException e of
        Just (Terminated _ why _) -> InsecureTerminated why
        Just (HandshakeFailed (Error_Misc msg)) -> HandshakeMisc msg
        Just (HandshakeFailed (Error_Protocol (_, _, CloseNotify))) -> HandshakeClosed
        Just (HandshakeFailed (Error_Protocol (_, _, HandshakeFailure))) -> HandshakeError
        Just (HandshakeFailed (Error_Protocol (_, _, BadCertificate))) -> InsecureCertificate ""
        Just (HandshakeFailed (Error_Protocol (_, _, UnsupportedCertificate))) ->
            InsecureCertificate $ trans locales InsecureCertificateUnsupported
            InsecureCertificate $ trans' InsecureCertificateUnsupported
        Just (HandshakeFailed (Error_Protocol (_, _, CertificateExpired))) ->
            InsecureCertificate $ trans locales InsecureCertificateExpired
            InsecureCertificate $ trans' InsecureCertificateExpired
        Just (HandshakeFailed (Error_Protocol (_, _, CertificateRevoked))) ->
            InsecureCertificate $ trans locales InsecureCertificateRevoked
            InsecureCertificate $ trans' InsecureCertificateRevoked
        Just (HandshakeFailed (Error_Protocol (_, _, CertificateUnknown))) ->
            InsecureCertificate $ trans locales InsecureCertificateUnknown
            InsecureCertificate $ trans' InsecureCertificateUnknown
        Just (HandshakeFailed (Error_Protocol (_, _, UnknownCa))) ->
            InsecureCertificate $ trans locales InsecureCertificateUnknownCA
            InsecureCertificate $ trans' InsecureCertificateUnknownCA
        Just (HandshakeFailed (Error_Protocol (why, _, _))) -> HandshakeMisc why
        Just (HandshakeFailed (Error_Certificate why)) -> InsecureCertificate why
        Just (HandshakeFailed (Error_HandshakePolicy why)) -> HandshakePolicy why

M src/Network/URI/Messages.hs => src/Network/URI/Messages.hs +84 -79
@@ 13,58 13,60 @@ module Network.URI.Messages (trans, Errors(..)) where

import Data.List (stripPrefix)
import Data.Maybe (fromMaybe)
import Control.Exception (Exception)

trans _ (RawXML markup) = markup
--- BEGIN LOCALIZATION
trans ("en":_) (UnsupportedScheme scheme) = "Unsupported protocol " ++ scheme
trans ("en":_) (UnsupportedMIME mime) = "Unsupported filetype " ++ mime
trans ("en":_) (RequiresInstall mime appsMarkup) =
    "<h1>Please install a compatible app to open <code>" ++ linkType ++ "</code> links</h1>\n" ++ appsMarkup
("en":_) `trans` UnsupportedScheme scheme = "Unsupported protocol " ++ scheme
("en":_) `trans` UnsupportedMIME mime = "Unsupported filetype " ++ mime
("en":_) `trans` RequiresInstall mime appsMarkup =
    "<h1>Please install a compatible app to open <code>" ++ linkType ++
        "</code> links</h1>\n" ++ appsMarkup
  where linkType = fromMaybe mime $ stripPrefix "x-scheme-handler/" mime
trans ("en":_) (OpenedWith app) = "Opened in " ++ app
trans ("en":_) (ReadFailed msg) = "Failed to read file: " ++ msg
trans ("en":_) (MalformedResponse why) = "Invalid response! " ++ why
trans ("en":_) ExcessiveRedirects = "Too many redirects!"
trans ("en":_) (GeminiError '1' '1' label) =
("en":_) `trans` OpenedWith app = "Opened in " ++ app
("en":_) `trans` ReadFailed msg = "Failed to read file: " ++ msg
("en":_) `trans` MalformedResponse why = "Invalid response! " ++ why
("en":_) `trans` ExcessiveRedirects = "Too many redirects!"
("en":_) `trans` GeminiError '1' '1' label =
    "<form><label>" ++ label ++ "<input type=password></form>" 
trans ("en":_) (GeminiError '1' _ label) = "<form><label>" ++ label ++ "<input></form>"
trans ("en":_) (GeminiError '4' '1' _) = "Site unavailable!"
trans ("en":_) (GeminiError '4' '2' _) = "Program error!"
trans ("en":_) (GeminiError '4' '3' _) = "Proxy error!"
trans ("en":_) (GeminiError '4' '4' timeout) =
("en":_) `trans` GeminiError '1' _ label = "<form><label>" ++ label ++ "<input></form>"
("en":_) `trans` GeminiError '4' '1' _ = "Site unavailable!"
("en":_) `trans` GeminiError '4' '2' _ = "Program error!"
("en":_) `trans` GeminiError '4' '3' _ = "Proxy error!"
("en":_) `trans` GeminiError '4' '4' timeout =
    "Site busy! Please reload after at least " ++ timeout ++ " seconds"
trans ("en":_) (GeminiError '5' '1' _) = "Page not found! Try the <a href='/'>homepage</a>."
trans ("en":_) (GeminiError '5' '2' _) = "Page deleted! Try the <a href='/'>homepage</a>."
trans ("en":_) (GeminiError '5' '3' _) = "Contacted wrong server!"
trans ("en":_) (GeminiError '5' '9' _) = "Malformed request, my bad!"
trans ("en":_) (GeminiError '6' '1' _) = "<form><label>Authentication required" ++
("en":_) `trans` GeminiError '5' '1' _ = "Page not found! Try the <a href='/'>homepage</a>."
("en":_) `trans` GeminiError '5' '2' _ = "Page deleted! Try the <a href='/'>homepage</a>."
("en":_) `trans` GeminiError '5' '3' _ = "Contacted wrong server!"
("en":_) `trans` GeminiError '5' '9' _ = "Malformed request, my bad!"
("en":_) `trans` GeminiError '6' '1' _ = "<form><label>Authentication required" ++
    "<-argo-authenticate error='Unauthorized account!'></-argo-authenticate></form>"
trans ("en":_) (GeminiError '6' '2' _) = "<form><label>Authentication required" ++
("en":_) `trans` GeminiError '6' '2' _ = "<form><label>Authentication required" ++
    "<-argo-authenticate error='Invalid account!'></-argo-authenticate></form>"
trans ("en":_) (GeminiError '6' _ _) = "<form><label>Authentication required" ++
("en":_) `trans` GeminiError '6' _ _ = "<form><label>Authentication required" ++
    "<-argo-authenticate></-argo-authenticate></form>"
trans ("en":_) (GeminiError _ _ error) = error
trans ("en":_) (HTTPStatus 400 _) = "I sent a bad request, according to this site."
trans ("en":_) (HTTPStatus 401 _) = "Authentication required!" -- FIXME: Support HTTP Basic Auth.
trans ("en":_) (HTTPStatus 402 _) = "Payment required!"
trans ("en":_) (HTTPStatus 403 _) = "Access denied!"
trans ("en":_) (HTTPStatus 404 _) = "Page not found! Try the <a href='/'>homepage</a>."
trans ("en":_) (HTTPStatus 405 _) = "Bad webform for this destination webaddress! " ++
("en":_) `trans` GeminiError _ _ error = error
("en":_) `trans` HTTPStatus 400 _ = "I sent a bad request, according to this site."
("en":_) `trans` HTTPStatus 401 _ = "Authentication required!" -- FIXME: Support HTTP Basic Auth.
("en":_) `trans` HTTPStatus 402 _ = "Payment required!"
("en":_) `trans` HTTPStatus 403 _ = "Access denied!"
("en":_) `trans` HTTPStatus 404 _ = "Page not found! Try the <a href='/'>homepage</a>."
("en":_) `trans` HTTPStatus 405 _ = "Bad webform for this destination webaddress! " ++
    "<em>Method not allowed</em>."
trans ("en":_) (HTTPStatus 406 _) = "No representation available for given criteria!"
trans ("en":_) (HTTPStatus 407 _) = "Authentication into proxyserver required!"
trans ("en":_) (HTTPStatus 408 _) = "The site took too long to connect! <em>(HTTP 408)</em>"
trans ("en":_) (HTTPStatus 409 _) = "Request is based on outdated state!"
trans ("en":_) (HTTPStatus 410 _) = "Page deleted! Try the <a href='/'>homepage</a>."
trans ("en":_) (HTTPStatus 411 _) = "I sent a bad request, according to this site." ++
("en":_) `trans` HTTPStatus 406 _ = "No representation available for given criteria!"
("en":_) `trans` HTTPStatus 407 _ = "Authentication into proxyserver required!"
("en":_) `trans` HTTPStatus 408 _ = "The site took too long to connect! <em>(HTTP 408)</em>"
("en":_) `trans` HTTPStatus 409 _ = "Request is based on outdated state!"
("en":_) `trans` HTTPStatus 410 _ = "Page deleted! Try the <a href='/'>homepage</a>."
("en":_) `trans` HTTPStatus 411 _ = "I sent a bad request, according to this site." ++
    "<em>(Missing <code>Content-Length</code> header)</em>"
trans ("en":_) (HTTPStatus 412 _) = "Webpage doesn't meet our preconditions."
trans ("en":_) (HTTPStatus 413 _) = "Payload too large, please upload a smaller file!"
trans ("en":_) (HTTPStatus 414 _) = "Web address is too long for the site!"
trans ("en":_) (HTTPStatus 415 _) = "No representation available for supported filetypes!"
trans ("en":_) (HTTPStatus 416 _) = "Invalid byte-range of requested resource!"
trans ("en":_) (HTTPStatus 417 _) = "Site cannot satisfy our stated expectations!"
trans ("en":_) (HTTPStatus 418 _) = unlines [
("en":_) `trans` HTTPStatus 412 _ = "Webpage doesn't meet our preconditions."
("en":_) `trans` HTTPStatus 413 _ = "Payload too large, please upload a smaller file!"
("en":_) `trans` HTTPStatus 414 _ = "Web address is too long for the site!"
("en":_) `trans` HTTPStatus 415 _ = "No representation available for supported filetypes!"
("en":_) `trans` HTTPStatus 416 _ = "Invalid byte-range of requested resource!"
("en":_) `trans` HTTPStatus 417 _ = "Site cannot satisfy our stated expectations!"
("en":_) `trans` HTTPStatus 418 _ = unlines [
    "<p>I'm a little teapot<br/>",
    "Short and stout<br/>",
    "Here is my handle<br/>",


@@ 74,50 76,51 @@ trans ("en":_) (HTTPStatus 418 _) = unlines [
    "<q>Tip me over<br/>",
    "And pour me out!</q></p>"
  ]
trans ("en":_) (HTTPStatus 421 _) = "Contacted wrong server!"
trans ("en":_) (HTTPStatus 422 _) = "Invalid <strong>WebDAV</strong> request!"
trans ("en":_) (HTTPStatus 423 _) = "<strong>WebDAV</strong> resource is locked!"
trans ("en":_) (HTTPStatus 424 _) = "Failed due to previous failure!"
trans ("en":_) (HTTPStatus 425 _) = "Site requires stronger security on our request!"
trans ("en":_) (HTTPStatus 426 _) = "Site requires newer networking-protocols!"
trans ("en":_) (HTTPStatus 428 _) = "Site requires additional protection to avoid loosing changes!"
trans ("en":_) (HTTPStatus 429 _) = "We sent this site too many requests for it to cope with!"
trans ("en":_) (HTTPStatus 431 _) = "I sent more auxiliary data than this site can cope with!"
trans ("en":_) (HTTPStatus 451 _) = "Requested page cannot legally be provided!"
("en":_) `trans` HTTPStatus 421 _ = "Contacted wrong server!"
("en":_) `trans` HTTPStatus 422 _ = "Invalid <strong>WebDAV</strong> request!"
("en":_) `trans` HTTPStatus 423 _ = "<strong>WebDAV</strong> resource is locked!"
("en":_) `trans` HTTPStatus 424 _ = "Failed due to previous failure!"
("en":_) `trans` HTTPStatus 425 _ = "Site requires stronger security on our request!"
("en":_) `trans` HTTPStatus 426 _ = "Site requires newer networking-protocols!"
("en":_) `trans` HTTPStatus 428 _ = "Site requires additional protection to avoid loosing changes!"
("en":_) `trans` HTTPStatus 429 _ = "We sent this site too many requests for it to cope with!"
("en":_) `trans` HTTPStatus 431 _ = "I sent more auxiliary data than this site can cope with!"
("en":_) `trans` HTTPStatus 451 _ = "Requested page cannot legally be provided!"

trans ("en":_) (HTTPStatus 500 _) = "The site experienced an error generating this webpage. <em>The webmasters have probably already been automatically notified.</em>"
trans ("en":_) (HTTPStatus 501 _) = "Bad webform for this destination webaddress! " ++
    "<em>Method not implemented</em>."
trans ("en":_) (HTTPStatus 502 _) = "Proxyserver got a malformed response!"
trans ("en":_) (HTTPStatus 503 _) = "The site is not available right now!"
trans ("en":_) (HTTPStatus 504 _) = "The site took too long to respond! <em>(Behind proxy)</em>"
trans ("en":_) (HTTPStatus 505 _) = "The site does not speak the language as me! " ++
("en":_) `trans` HTTPStatus 500 _ = "The site experienced an error generating this webpage. " ++
    "<em>The webmasters have probably already been automatically notified.</em>"
("en":_) `trans` HTTPStatus 501 _ =
    "Bad webform for this destination webaddress! <em>Method not implemented</em>."
("en":_) `trans` HTTPStatus 502 _ = "Proxyserver got a malformed response!"
("en":_) `trans` HTTPStatus 503 _ = "The site is not available right now!"
("en":_) `trans` HTTPStatus 504 _ = "The site took too long to respond! <em>(Behind proxy)</em>"
("en":_) `trans` HTTPStatus 505 _ = "The site does not speak the language as me! " ++
    "<em>(Unsupported HTTP version)</em>"
trans ("en":_) (HTTPStatus 506 _) = "The site is misconfigured!"
trans ("en":_) (HTTPStatus 507 _) = "Insufficient <strong>WebDAV</strong> storage!"
trans ("en":_) (HTTPStatus 508 _) = "<strong>WebDAV</strong> loop detected!"
trans ("en":_) (HTTPStatus 510 _) = "Further request extensions required!"
trans ("en":_) (HTTPStatus 511 _) = "Authentication into network required!"
trans ("en":_) (HTTPStatus _ error) = error -- Fallback
trans ("en":_) (OtherException error) = "Internal Exception <pre><code>" ++ error ++ "</code></pre>"
trans ("en":_) (InvalidUrl url message) =
("en":_) `trans` HTTPStatus 506 _ = "The site is misconfigured!"
("en":_) `trans` HTTPStatus 507 _ = "Insufficient <strong>WebDAV</strong> storage!"
("en":_) `trans` HTTPStatus 508 _ = "<strong>WebDAV</strong> loop detected!"
("en":_) `trans` HTTPStatus 510 _ = "Further request extensions required!"
("en":_) `trans` HTTPStatus 511 _ = "Authentication into network required!"
("en":_) `trans` HTTPStatus _ error = error -- Fallback
("en":_) `trans` OtherException error = "Internal Exception <pre><code>" ++ error ++ "</code></pre>"
("en":_) `trans` InvalidUrl url message =
    "Invalid web address <code>" ++ url ++ "</code>: <em>" ++ message ++ "</em>"
trans ("en":_) (ProxyError msg code code' msg') = unlines [
("en":_) `trans` ProxyError msg code code' msg' = unlines [
    "<h1>Proxy failed to forward request!<h1>",
    "<p>" ++ show code ++ " " ++ msg ++ "</p>",
    "<p>" ++ show code' ++ " " ++ msg' ++ "</p>"
  ]
trans ("en":_) (InvalidRequest why) =
("en":_) `trans` InvalidRequest why =
    "Attempted to send invalid auxiliary data: <em>" ++ why ++ "</em>"
trans ("en":_) InsecureUnestablished =
("en":_) `trans` InsecureUnestablished =
    "Attempted to send or recieve data before establishing secure connection!"
trans ("en":_) (InsecureCertificate why) = unlines [
("en":_) `trans` InsecureCertificate why = unlines [
    "<h1>The site failed to prove it is who it says it is!</h1>",
    "<p>" ++ why ++ "</p>",
    "<p><a href=action:history/back>Leave Insecure Site</a> | ",
        "<a href=action:novalidate>Accept Imposter Risk &amp; Continue</a></p>"
  ]
trans ("en":_) (InsecureTerminated why) = "Secure session disconnected! <em>" ++ why ++ "</em>"
("en":_) `trans` InsecureTerminated why = "Secure session disconnected! <em>" ++ why ++ "</em>"
trans ("en":_) InsecureCertificateUnknownCA = "The authority vouching for it is unknown to me!"
trans ("en":_) InsecureCertificateUnknown =
    "The cryptographic certificate it has sent us to prove its identity instead " ++


@@ 128,25 131,25 @@ trans ("en":_) InsecureCertificateExpired =
    "The cryptographic certificate it has sent us to prove its identity has expired!"
trans ("en":_) InsecureCertificateUnsupported =
    "It has sent us a cryptographic certificate to prove its identity I failed to make sense of."
trans ("en":_) (HandshakePacketUnparsed why) = "Invalid security packet: <em>" ++ why ++ "</em>"
trans ("en":_) (HandshakePacketUnexpected a b) = unlines [
("en":_) `trans` HandshakePacketUnparsed why = "Invalid security packet: <em>" ++ why ++ "</em>"
("en":_) `trans` HandshakePacketUnexpected a b = unlines [
    "<p>Invalid security packet: <em>" ++ a ++ "</em></p>",
    "<p>" ++ b ++ "</p>"
  ]
trans ("en":_) (HandshakePacketInvalid why) = "Invalid security packet: <em>" ++ why ++ "</em>"
("en":_) `trans` HandshakePacketInvalid why = "Invalid security packet: <em>" ++ why ++ "</em>"
trans ("en":_) HandshakeEOF = "Secure session disconnected!"
trans ("en":_) (HandshakePolicy why) = "Invalid handshake policy: <em>" ++ why ++ "</em>"
trans ("en":_) (HandshakeMisc why) =
("en":_) `trans` HandshakePolicy why = "Invalid handshake policy: <em>" ++ why ++ "</em>"
("en":_) `trans` HandshakeMisc why =
    "Failed to establish secure connection! <em>" ++ why ++ "</em>"
trans ("en":_) HandshakeError = "Failed to negotiate security parameters!"
trans ("en":_) HandshakeClosed = "Secure session disconnected!"
trans ("en":_) (FailedConnect why) = "Failed to open connection to the site: <em>" ++ why ++ "</em>"
("en":_) `trans` FailedConnect why = "Failed to open connection to the site: <em>" ++ why ++ "</em>"
trans ("en":_) TimeoutConnection = "The site took too long to connect!"
trans ("en":_) TimeoutResponse = "The site took too long to respond!"
--- END LOCALIZATION

trans (_:locales) err = trans locales err
trans [] err = trans ["en"] err
trans [] err = show err

data Errors = UnsupportedScheme String | UnsupportedMIME String | RequiresInstall String String
    | OpenedWith String | ReadFailed String | RawXML String | MalformedResponse String


@@ 159,4 162,6 @@ data Errors = UnsupportedScheme String | UnsupportedMIME String | RequiresInstal
    | HandshakePacketUnparsed String | HandshakePacketUnexpected String String
    | HandshakePacketInvalid String
    | HandshakeEOF | HandshakePolicy String | HandshakeMisc String | HandshakeError | HandshakeClosed
    | FailedConnect String | TimeoutConnection | TimeoutResponse
    | FailedConnect String | TimeoutConnection | TimeoutResponse deriving (Show)

instance Exception Errors