{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Retrieves documents for a URL, supporting multiple URL schemes that can be
-- disabled at build-time for reduced dependencies.
module Network.URI.Fetch(
Session(locale, aboutPages, redirectCount, cachingEnabled, validateCertificates, credentials),
newSession,
fetchURL, fetchURL', fetchURLs, submitURL, submitURL', mimeERR, htmlERR,
dispatchByMIME, appsForMIME, Application(..), dispatchByApp,
saveDownload, downloadToURI,
-- logging API
LogRecord(..), enableLogging, retrieveLog, writeLog) where
import Network.URI.Types
import qualified Data.Text as Txt
import Data.Text (Text)
import qualified Data.Text.Encoding as Txt
import Network.URI
import qualified Data.ByteString as Strict
import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy as B
import qualified Data.ByteString.Char8 as C8
import qualified Data.ByteString.Builder as Builder
import Network.URI.Charset
import Control.Exception
import System.IO.Error (isEOFError)
import Control.Concurrent.Async (forConcurrently)
import qualified Data.Maybe as M
-- for about: URIs & port parsing, all standard lib
import Data.Maybe (fromMaybe, listToMaybe, isJust)
import Data.Either (isLeft)
import Text.Read (readMaybe)
-- for executable extensions, all standard lib
import Data.Char (isSpace)
import System.Exit (ExitCode(..))
-- for saveDownload
import System.Directory
import System.FilePath
-- for logging
import Control.Concurrent.MVar
import Data.Time.Clock
import System.IO
import Control.Monad
import Data.List as L
#ifdef WITH_HTTP_URI
import qualified Network.HTTP.Client as HTTP
import qualified Network.HTTP.Client.MultipartFormData as HTTP
import qualified Network.HTTP.Client.TLS as TLS
import Network.HTTP.Types
import Network.PublicSuffixList.Lookup (effectiveTLDPlusOne)
import Data.List (intercalate)
import Control.Concurrent (forkIO)
import Network.URI.Cache
import Network.URI.CookiesDB
#endif
#if WITH_HTTP_URI || WITH_RAW_CONNECTIONS
import qualified Network.Connection as Conn
import qualified Network.TLS as TLS
import qualified Network.TLS.Extra.Cipher as TLS
import Data.Default.Class (def)
#endif
#ifdef WITH_DATA_URI
import qualified Data.ByteString.Base64.URL.Lazy as B64
#endif
import Network.URI.Locale
import Network.URI.Messages
#ifdef WITH_XDG
import Network.URI.XDG
#endif
#ifdef WITH_PLUGIN_REWRITES
import Network.URI.PlugIns.Rewriters
#endif
#ifdef WITH_PLUGIN_EXEC
import System.Process
#endif
-- | Data shared accross multiple URI requests.
data Session = Session {
#ifdef WITH_HTTP_URI
managerHTTP :: HTTP.Manager,
managerHTTPNoValidate :: HTTP.Manager,
globalCookieJar :: MVar HTTP.CookieJar,
cookiesPath :: FilePath,
retroactiveCookies :: Maybe (MVar HTTP.CookieJar),
hstsDomains :: MVar [(String, Bool, UTCTime)],
#endif
#ifdef WITH_RAW_CONNECTIONS
connCtxt :: Conn.ConnectionContext,
#endif
#ifdef WITH_XDG
apps :: XDGConfig,
#endif
#ifdef WITH_PLUGIN_REWRITES
rewriter :: Rewriter,
#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
requestLog :: Maybe (MVar [LogRecord]),
-- | How many redirects to follow for Gemini or HTTP(S) requests
redirectCount :: Int,
-- | Whether to cache network responses, avoiding sending requests
cachingEnabled :: Bool,
-- | App-specific config subdirectory to check
appName :: String,
-- | Whether to validate the server is who they say they are on secured protocols.
validateCertificates :: Bool,
-- | Bytestrings or files containing the client certificate to use for logging into the server.
credentials :: Maybe (Either (FilePath, FilePath) (C8.ByteString, C8.ByteString)),
credentials' :: MVar (Maybe (Either (FilePath, FilePath) (C8.ByteString, C8.ByteString)))
}
data LogRecord = LogRecord {
url :: URI,
accept :: [String],
redirected :: URI,
mimetype :: String,
response :: Either Text ByteString,
begin :: UTCTime,
end :: UTCTime
}
-- | Initializes a default Session object to support HTTPS & Accept-Language
-- if HTTP is enabled.
newSession :: IO Session
newSession = newSession' ""
-- | Variant of `newSession` which loads plugins for the named app.
newSession' :: String -> IO Session
newSession' appname = do
(ietfLocale, unixLocale) <- rfc2616Locale
credentialsMVar <- newMVar Nothing
#ifdef WITH_HTTP_URI
{- let httpsSettings = (TLS.defaultParamsClient "example.com" "https") {
TLS.clientSupported = def { TLS.supportedCiphers = TLS.ciphersuite_default },
TLS.clientHooks = def {
TLS.onCertificateRequest = deliverCredentials credentialsMVar
}
}
let httpsSettingsNoValidate = httpsSettings {
TLS.clientShared = def {
TLS.sharedValidationCache = TLS.ValidationCache
(\_ _ _ -> return TLS.ValidationCachePass)
(\_ _ _ -> return ())
}
} -} -- FIXME: Be nice to support clientside certs... Those are far too strict!
managerHTTP' <- HTTP.newManager $ TLS.mkManagerSettings
(Conn.TLSSettingsSimple False False False) Nothing
managerHTTPnovalidate' <- HTTP.newManager $ TLS.mkManagerSettings
(Conn.TLSSettingsSimple True False False) Nothing
cookiesDir <- getXdgDirectory XdgData "nz.geek.adrian.hurl.cookies2"
let cookiesPath' = cookiesDir </> appname
cookies' <- readCookies cookiesPath'
now <- getCurrentTime
cookieJar <- newMVar $ HTTP.evictExpiredCookies cookies' now
cookieJar' <- newMVar $ HTTP.createCookieJar []
hstsDomains' <- newMVar =<< readHSTS
#endif
#ifdef WITH_RAW_CONNECTIONS
connCtxt <- Conn.initConnectionContext
#endif
#ifdef WITH_XDG
apps' <- loadXDGConfig unixLocale
#endif
#ifdef WITH_PLUGIN_REWRITES
rewriters <- parseRewriters appname
#endif
return Session {
#ifdef WITH_HTTP_URI
managerHTTP = managerHTTP',
managerHTTPNoValidate = managerHTTPnovalidate',
globalCookieJar = cookieJar,
cookiesPath = cookiesPath',
retroactiveCookies = Just cookieJar',
hstsDomains = hstsDomains',
#endif
#ifdef WITH_RAW_CONNECTIONS
connCtxt = connCtxt,
#endif
#ifdef WITH_XDG
apps = apps',
#endif
#ifdef WITH_PLUGIN_REWRITES
rewriter = rewriters,
#endif
locale = ietfLocale,
trans' = trans ietfLocale,
aboutPages = [],
requestLog = Nothing,
redirectCount = 5,
cachingEnabled = True,
validateCertificates = True,
appName = appname,
credentials = Nothing,
credentials' = credentialsMVar
}
llookup key fallback map = fallback `fromMaybe` listToMaybe [v | (k, v) <- map, k == key]
parsePort fallback (':':port) = fallback `fromMaybe` readMaybe port
parsePort fallback _ = fallback
-- | Retrieves a URL-identified resource & it's MIMEtype, possibly decoding it's text.
fetchURL :: Session -- ^ The session of which this request is a part.
-> [String] -- ^ The expected MIMEtypes in priority order.
-> URI -- ^ The URL to retrieve
-> IO (String, Either Text ByteString) -- ^ The MIMEtype & possibly text-decoded response.
fetchURL sess mimes uri = do
(_, mime, resp) <- fetchURL' sess mimes uri
return (mime, resp)
fetchURLLogged log sess mimes uri = do
begin' <- getCurrentTime
res@(redirected', mimetype', response') <- fetchURL' sess mimes uri
end' <- getCurrentTime
modifyMVar_ log $ \log' -> return (
LogRecord uri mimes redirected' mimetype' response' begin' end' : log')
return res
-- | Concurrently fetch given URLs.
fetchURLs :: Session -> [String] -> [URI] -> ((URI, String, Either Text ByteString) -> IO a) -> IO [(URI, a)]
fetchURLs sess mimes uris cb = do
let fetch = case requestLog sess of {Nothing -> fetchURL'; Just log -> fetchURLLogged log}
let sess' = sess {
#ifdef WITH_HTTP_URI
retroactiveCookies = Nothing
#endif
}
forConcurrently uris (\u -> fetch sess' mimes u >>= cb) >>= return . L.zip uris
-- | Internal MIMEtypes for error reporting
mimeERR, htmlERR :: String
mimeERR = "txt/x-error\t"
htmlERR = "html/x-error\t"
submitURL :: Session -> [String] -> URI -> Text -> String -> IO (URI, String, Either Text ByteString)
-- | See submitURL', preserved for backwards compatability.
-- This is a little more cumbersome to use, & doesn't support file uploads.
-- Was designed naively based on convenience of initial caller.
submitURL s a u m q =
submitURL' s a u (Txt.encodeUtf8 m) "application/x-www-form-urlencoded" $
Prelude.map parseQuery $ Txt.splitOn "&" $ Txt.pack q
where
parseQuery q = let (key, value) = Txt.breakOn "=" q in if Txt.null value
then (decode key, Left "")
else (decode key, Left $ decode $ Txt.tail value)
decode = unEscapeString . Txt.unpack
-- | Uploads given key-value pairs to the specified URL using the specified HTTP method & encoding.
-- The key-value pairs may specify filepaths, in which case the method must be "POST"
-- and the encoding must be "multipart/form-data" for that data to get sent.
--
-- Unsupported encodings (values other than "application/x-www-form-urlencoded",
-- "text/plain", or "multipart/form-data") omits the key-value pairs from the request.
submitURL' :: Session -> [String] -> URI -> Strict.ByteString -> Strict.ByteString ->
[(String, Either String FilePath)] -> IO (URI, String, Either Text ByteString)
#ifdef WITH_HTTP_URI
addHTTPBody mime body req = return req {
HTTP.requestHeaders = (hContentType, mime) :
Prelude.filter (\(x, _) -> x /= hContentType) (HTTP.requestHeaders req),
HTTP.requestBody = HTTP.RequestBodyBS $ C8.pack body
}
packQuery :: [(String, Either String FilePath)] -> C8.ByteString -> HTTP.Request -> IO HTTP.Request
packQuery query mime@"application/x-www-form-urlencoded" =
addHTTPBody mime $ encodeQuery query
packQuery query mime@"text/plain" = addHTTPBody mime $
Prelude.unlines [value | (key, Left value) <- query, not $ null value]
packQuery q "multipart/form-data" = HTTP.formDataBody $ Prelude.map encodePart q
where
encodePart (key, Left value) = HTTP.partBS (Txt.pack key) (C8.pack value)
encodePart (key, Right value) =
-- C:\fakepath\ is part of the webstandards now & I might as well use it.
-- Ancient browsers exposed the full filepath which was a security vulnerability.
-- Now to avoid breaking servers relying on this behaviour we send
-- a fake Windows filepath.
HTTP.partFileRequestBodyM (Txt.pack key) ("C:\\fakepath\\" ++ takeFileName value) $ do
size <- fromInteger <$> withBinaryFile value ReadMode hFileSize
body <- B.readFile value
return $ HTTP.RequestBodyBuilder size $ Builder.lazyByteString body
packQuery _ _ = return -- Do not upload data if requested to do so in an invalid format.
submitURL' session mimes uri method "GET" query = fetchURL' session mimes uri {
uriQuery = '?': encodeQuery query } -- Specialcase GET!
submitURL' session accept uri method encoding query | uriScheme uri `elem` ["http:", "https:"] = do
-- HURL is very strict on when it allows cookies to be set: Only POST HTTP requests are considered consent.
-- For the sake of most webframeworks' CSRF protection, cookies from retrieving the form are retroactively set.
csrfCookies <- case retroactiveCookies session of {
Just cookies -> Just <$> readMVar cookies;
Nothing -> return Nothing
}
fetchHTTPCached session False accept uri (\req -> do
ret <- packQuery query encoding req
return ret {
HTTP.cookieJar = firstJust csrfCookies $ HTTP.cookieJar req,
HTTP.method = method
}) $ \resp -> do
let cookies = HTTP.responseCookieJar resp
swapMVar (globalCookieJar session) cookies
writeCookies (cookiesPath session) cookies False
#endif
submitURL' session mimes uri _method _encoding query = fetchURL' session mimes uri {
uriQuery = '?':encodeQuery query }
encodeQuery :: [(String, Either String FilePath)] -> String
encodeQuery [("", Left query)] = query -- Mostly for the sake of Gemini...
encodeQuery query = intercalate "&" $ M.mapMaybe encodePart query
where
encodePart (key, Left "") = Just $ escape key
encodePart (key, Left value) = Just (escape key ++ '=':escape value)
encodePart _ = Nothing
escape = escapeURIString isUnescapedInURIComponent
-- | As per `fetchURL`, but also returns the redirected URI.
fetchURL' :: Session -> [String] -> URI -> IO (URI, String, Either Text ByteString)
fetchURL' sess@Session {redirectCount = 0 } _ uri =
return (uri, mimeERR, Left $ Txt.pack $ trans' sess ExcessiveRedirects)
#ifdef WITH_PLUGIN_REWRITES
fetchURL' session mimes uri
| Just uri' <- applyRewriter (rewriter session) uri = fetchURL' session mimes uri'
#endif
#ifdef WITH_PLUGIN_EXEC
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' session $ ReadFailed "404")
program:_ -> do
let args = case query of {
'?':rest -> split (== '&') rest;
_ -> []
}
(exitcode, stdout, stderr) <- readProcessWithExitCode program args ""
let response = if isSuccess exitcode then stdout else stderr
let (header, body) = breakOn '\n' response
case strip header of
'm':'i':'m':'e':mimetype -> return (uri, strip mimetype, Left $ Txt.pack body)
'u':'r':'l':header' | Just uri' <- parseURIReference $ strip header' ->
fetchURL' (session {redirectCount = redirectCount session - 1}) mimes $
relativeTo uri' uri
_ | isSuccess exitcode -> return (uri, "text/html", Left $ Txt.pack response)
_ -> return (uri, mimeERR, Left $ Txt.pack response)
where
split p s = case dropWhile p s of
"" -> []
s' -> let (w, s'') = break p s' in w : split p s''
strip = dropWhile isSpace . dropWhileEnd isSpace
isSuccess ExitSuccess = True
isSuccess _ = False
#endif
fetchURL' session mimes uri@(URI {uriScheme = "about:", uriPath = ""}) =
fetchURL' session mimes $ uri {uriPath = "version"}
fetchURL' Session {aboutPages = pages} _ url@URI {uriScheme = "about:", uriPath = path} =
return (url,
Txt.unpack $ Txt.strip $ convertCharset "utf-8" $ B.toStrict $
llookup (path ++ ".mime") "text/html" pages,
Right $ llookup path "" pages)
#ifdef WITH_HTTP_URI
fetchURL' session accept uri | uriScheme uri `elem` ["http:", "https:"] =
fetchHTTPCached session (cachingEnabled session) accept uri return saveCookies
where
saveCookies resp
| Just cookies <- retroactiveCookies session =
void $swapMVar cookies $HTTP.responseCookieJar resp
| otherwise = return ()
#endif
#ifdef WITH_GEMINI_URI
fetchURL' sess@Session { connCtxt = ctxt } mimes uri@URI {
uriScheme = "gemini:", uriAuthority = Just (URIAuth _ host port)
} = do
let params = TLS.defaultParamsClient host "gmni"
swapMVar (credentials' sess) (credentials sess)
conn <- Conn.connectTo ctxt Conn.ConnectionParams {
Conn.connectionHostname = host,
Conn.connectionPort = parsePort 1965 port,
-- FIXME Implement certificate validation that actually common geminispace certs...
Conn.connectionUseSecure = Just $ Conn.TLSSettings params {
TLS.clientSupported = def { TLS.supportedCiphers = TLS.ciphersuite_default },
TLS.clientShared = def {
TLS.sharedValidationCache = TLS.ValidationCache
(\_ _ _ -> return TLS.ValidationCachePass)
(\_ _ _ -> return ())
},
TLS.clientHooks = def {
TLS.onCertificateRequest = deliverCredentials $ credentials' sess
}
},
Conn.connectionUseSocks = Nothing
}
Conn.connectionPut conn $ C8.pack $ uriToString id uri "\r\n"
header <- Conn.connectionGetLine 1027 conn
case parseHeader $ C8.unpack header of
('2', _, mime) -> do
body <- B.fromChunks <$> connectionGetChunks conn
let mime' = L.map (Txt.unpack . Txt.strip) $ Txt.splitOn ";" mime
return $ resolveCharset' uri mime' body
('3', _, redirect) | Just redirect' <- parseURIReference $ Txt.unpack redirect ->
fetchURL' sess {
redirectCount = redirectCount sess - 1
} mimes $ relativeTo redirect' uri
(x, y, err) -> return (uri, htmlERR, Left $ Txt.pack $
trans' sess $ GeminiError x y $ Txt.unpack $
Txt.replace "<" "<" $ Txt.replace "&" "&" 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' sess $ MalformedResponse header)
handleIOErr :: IOError -> IO Strict.ByteString
handleIOErr _ = return ""
connectionGetChunks conn = do
chunk <- Conn.connectionGetChunk conn `catch` handleIOErr
if Strict.null chunk then return [] else (chunk:) <$> connectionGetChunks conn
#endif
#ifdef WITH_FILE_URI
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' sess $ ReadFailed $ displayException (e :: IOException))
#endif
#ifdef WITH_DATA_URI
fetchURL' _ (defaultMIME:_) uri@URI {uriScheme = "data:"} =
let request = uriPath uri ++ uriQuery uri ++ uriFragment uri
in case breakOn ',' $ unEscapeString request of
("", response) -> return (uri, defaultMIME, Left $ Txt.pack response)
(mime', response) | '4':'6':'e':'s':'a':'b':';':mime <- reverse mime' ->
return $ case B64.decode $ B.fromStrict $ C8.pack response of
Left str -> (uri, mimeERR, Left $ Txt.pack $ unEscapeString str)
Right bytes -> (uri, reverse mime, Right bytes)
(mime, response) -> return (uri, mime, Left $ Txt.pack response)
#endif
#ifdef WITH_XDG
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' sess $ app)
#else
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 sess@Session { apps = a } mime uri = do
err <- dispatchURIByMIME a uri mime
return $ case err of
UnsupportedMIME _ -> Nothing
_ -> Just $ trans' sess err
#else
dispatchByMIME _ _ _ = return Nothing
#endif
appsForMIME :: Session -> String -> IO [Application]
#if WITH_XDG
appsForMIME Session { apps = a, locale = l } = queryHandlers' a l
#else
appsForMIME _ _ = []
#endif
dispatchByApp :: Session -> Application -> String -> URI -> IO Bool
#if WITH_XDG
dispatchByApp session@Session { locale = l } Application { appId = app} mime uri = do
try1 <- launchApp' l uri app -- First try handing off the URL, feedreaders need this!
case try1 of
Left app -> return True
Right False -> return False
Right True -> do
-- Download as temp file to open locally, the app requires it...
temp <- canonicalizePath =<< getTemporaryDirectory
resp <- fetchURL' session [mime] uri
uri' <- saveDownload (URI "file:" Nothing "" "" "") temp resp
isLeft <$> launchApp' l uri' app
#else
dispatchByApp _ _ _ _ = return False
#endif
#ifdef WITH_HTTP_URI
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 {
(_, Just (URIAuth _ domain _)) | not $ validateCertificates session -> (do
modifyMVar_ (hstsDomains session) $ flip removeHSTS domain
return rawUri);
("http:", Just (URIAuth _ domain _))
| testHSTS now domain hsts -> return rawUri { uriScheme = "https:" };
_ -> return rawUri
}
let manager = (if validateCertificates session
then managerHTTP else managerHTTPNoValidate) session
swapMVar (credentials' session) (credentials session)
cached <- if shouldCache then readCacheHTTP uri else return (Nothing, Nothing)
response <- case cached of
(Just (mime, body), Nothing) -> return $ Right (mime, body)
(cached, cachingHeaders) -> do
request <- HTTP.requestFromURI uri
cookieJar <- readMVar $ globalCookieJar session
request'<- cbReq request {
HTTP.cookieJar = Just $ cookieJar,
HTTP.requestHeaders = [
("Accept", C8.pack $ intercalate ", " accept),
("Accept-Language", C8.pack $ intercalate ", " $ locale session)
] ++ fromMaybe [] cachingHeaders,
HTTP.redirectCount = 0
}
response <- HTTP.httpLbs request' manager
cbResp response
case (
uriScheme uri,
uriAuthority uri,
"strict-transport-security" `lookup` HTTP.responseHeaders response
) of
("https:", Just (URIAuth _ domain _), Just header)
| Just domain' <- effectiveTLDPlusOne $ Txt.pack domain -> do
record <- appendHSTSFromHeader (Txt.unpack domain') header
case record of
Just x -> modifyMVar_ (hstsDomains session) (return . (x:))
_ -> return ()
_ -> return ()
case (
HTTP.responseStatus response,
HTTP.responseBody response,
[val | ("content-type", val) <- HTTP.responseHeaders response]
) of
(Status 304 _, _, _) | Just cached'@(_, body) <- cached -> do
cacheHTTP uri $ response { HTTP.responseBody = body }
return $ Right cached'
-- Manually handle redirects so the caller & HTTP cache gets the correct URI.
(Status code _, _, _) | code > 300 && code < 400,
Just location <- lookup "location" $ HTTP.responseHeaders response,
Just uri' <- parseURIReference $ C8.unpack location ->
return $ Left $ relativeTo uri' uri
(Status code msg, "", _) -> return $ Right (Txt.pack htmlERR,
B.fromStrict $ C8.pack $
trans' session $ HTTPStatus code $ C8.unpack msg)
(_, body, (mimetype:_)) -> do
cacheHTTP uri response
forkIO cleanCacheHTTP -- Try to keep diskspace down...
let mime = Txt.toLower $ convertCharset "utf-8" mimetype
return $ Right (mime, body)
(_, response, []) -> return $ Right (Txt.pack defaultMIME, response)
case response of
Left redirect ->
let session' = session { redirectCount = redirectCount session - 1 }
in fetchURL' session' accept redirect
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, htmlERR, Left $ Txt.pack $ transHttp t e)
fetchHTTPCached session _ [] uri _ _ =
return (uri, htmlERR, Left $ Txt.pack $ trans' session $ UnsupportedMIME "")
#endif
#if WITH_HTTP_URI || WITH_GEMINI_URI
deliverCredentials credentialsMVar _ = do
credentials' <- readMVar credentialsMVar -- workaround for HTTP-Client-TLS
case credentials' of
Just (Left (public, private)) -> right <$> TLS.credentialLoadX509 public private
Just (Right (public, private)) ->
return $ right $ TLS.credentialLoadX509FromMemory public private
Nothing -> return Nothing
where
right (Left _) = Nothing
right (Right x) = Just x
#endif
-- Downloads utilities
-- | write download to a file in the given directory.
saveDownload :: URI -> FilePath -> (URI, String, Either Text ByteString) -> IO URI
saveDownload baseURI dir (URI {uriPath = path}, mime, resp) = do
dest <- unusedFilename (dir </> takeFileName' path)
case resp of
Left txt -> writeFile dest $ Txt.unpack txt
Right bytes -> B.writeFile dest bytes
-- TODO set user.mime file attribute.
return $ baseURI {uriPath = dest}
where
takeFileName' s = case takeFileName s of { "" -> "index"; f -> f}
unusedFilename path = do
exists <- doesFileExist path
if exists then go 0 else return path
where
go n = do
let path' = path ++ show n
exists <- doesFileExist path'
if exists then go (n+1) else return path'
-- | Convert a download into a data: URI
downloadToURI :: (URI, String, Either Text ByteString) -> URI
downloadToURI (_, mime, Left txt) = nullURI {
uriScheme = "data:",
uriPath = mime ++ "," ++ escapeURIString isReserved (Txt.unpack txt)
}
downloadToURI (_, mime, Right bytes) = nullURI {
uriScheme = "data:",
uriPath = mime ++ ";base64," ++ C8.unpack (B.toStrict $ B64.encode bytes)
}
-- Logging API
enableLogging :: Session -> IO Session
enableLogging session = do
log <- newMVar []
return session { requestLog = Just log }
retrieveLog :: Session -> IO [LogRecord]
retrieveLog session@Session { requestLog = Just log } = swapMVar log []
retrieveLog _ = return []
writeLog :: Handle -> Session -> IO ()
writeLog out session = do
writeRow ["URL", "Redirected", "Accept", "MIMEtype", "Size", "Begin", "End", "Duration"]
log <- retrieveLog session
forM log $ \record -> writeRow [
show $ url record, show $ redirected record,
show $ accept record, show $ mimetype record,
case response record of
Left txt -> show $ Txt.length txt
Right bs -> show $ B.length bs,
show $ begin record, show $ end record,
show (end record `diffUTCTime` end record)
]
return ()
where
writeRow = hPutStrLn out . L.intercalate "\t"
-- Utils
breakOn c (a:as) | c == a = ([], as)
| otherwise = let (x, y) = breakOn c as in (a:x, y)
breakOn _ [] = ([], [])
firstJust a@(Just _) _ = a
firstJust Nothing b = b