packages feed

rtorrent-rpc-0.3.0.0: Network/RTorrent/Query.hs

{-# LANGUAGE OverloadedStrings #-}

{-|
Module      : Query
Copyright   : (c) Kai Lindholm, 2014, 2025
License     : MIT
Maintainer  : megantti@gmail.com
Stability   : experimental

An internal module for establishing a connection with RTorrent.
-}

module Network.RTorrent.Query (Headers, Body(..), query) where

import Blaze.ByteString.Builder
import Blaze.ByteString.Builder.Char8
import Blaze.Text
import Control.Applicative
import Data.Char (ord)
import Data.Either (partitionEithers)
import Data.Functor (($>))
import Data.Monoid
import Network.Socket
import Network.URI (URI(..), URIAuth(..), parseURIReference)
import System.IO ( IOMode (..) )
import System.Directory (makeAbsolute, getHomeDirectory)

import qualified Data.Attoparsec.ByteString.Lazy as AT
import Data.Attoparsec.ByteString.Lazy (Parser)

import qualified Data.Aeson as A
import qualified Data.ByteString as BS
import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as LB

import Network.RTorrent.Value

type Headers = [(ByteString, ByteString)]

data Body = Body {
      headers :: Headers
    , body :: LB.ByteString
} deriving Show

makeRequest :: Body -> LB.ByteString
makeRequest (Body hd bd) = res
  where
    res = toLazyByteString . mconcat $ [
        integral len
      , fromChar ':'
      , fromByteString hdbs
      , fromChar ','
      , fromLazyByteString bd
      ]
    fromBS0 bs = fromWrite $ writeByteString bs <> writeWord8 0
    hdbs = toByteString . mconcat $
           (fromBS0 "CONTENT_LENGTH" <> integral (LB.length bd) <> fromWord8 0):
            map (\(a, b) -> fromBS0 a <> fromBS0 b) hd
    len = BS.length hdbs

parseResponse :: Parser Body
parseResponse = parseBody
  where
    lineChange :: Parser ByteString
    lineChange = AT.string "\r\n"
    lineParser :: Parser ByteString
    lineParser =
        mappend <$> AT.takeTill (== (fromIntegral $ ord '\r')) <*> ((lineChange $> "") <|> lineParser)
    headerParser = (,) <$> AT.takeTill (==  (fromIntegral . ord $ ':')) <*> (AT.string ": " *> lineParser)

    takeUntil :: Parser a -> Parser b -> Parser ([a], b)
    takeUntil a b = (end <$> b)
                    <|> (cont <$> a <*> takeUntil a b)
      where
        end x = ([], x)
        cont x (xs, y) = (x : xs, y)

    contentParser = lineChange *> AT.takeLazyByteString
    parseBody = do
        (headers, content) <- takeUntil headerParser contentParser
        return $ Body headers content

querySCGI :: URI -> Body -> IO (Either String Body)
querySCGI uri queryBody = do
    let hints = if uriScheme uri == "tcp:"
        then defaultHints { addrSocketType = Stream, addrFamily = AF_INET }
        else defaultHints { addrSocketType = Stream, addrFamily = AF_UNIX }
    addr <- if uriScheme uri == "tcp:"
        then return $ maybe "" uriRegName (uriAuthority uri)
        else if (uriRegName <$> uriAuthority uri) == Just "~"
                then (++ uriPath uri) <$> getHomeDirectory 
                else return $ uriPath uri
    let port = if uriScheme uri == "tcp:"
        then drop 1 . uriPort <$> uriAuthority uri
        else Nothing
    addrl <- if uriScheme uri == "tcp:"
        then do
            getAddrInfo (Just hints) (Just addr) port
        else do
            return [AddrInfo [] AF_UNIX Stream 0 (SockAddrUnix addr) Nothing]
    case addrl of
        [] -> return (Left "Couldn't resolve SCGI address.")
        (addr:_) -> do
            sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
            connect sock (addrAddress addr)
            h <- socketToHandle sock ReadWriteMode

            LB.hPut h (makeRequest queryBody)
            AT.parseOnly parseResponse <$> LB.hGetContents h

scgi :: URI -> Value -> IO (Either String Value)
scgi addr call = do
    let request = Body [] (A.encode call)
    resp <- querySCGI addr request
    return $ case resp of
        Left err -> Left err
        Right (Body _ content) -> A.eitherDecode content

-- | Send a query in @Value@ to the address and return the response or an error.
query :: String -> Value -> IO (Either String Value)
query addr call =
    case parseURIReference addr of
        Nothing -> return (Left "Invalid URI.")
        Just uri -> maybe (return $ Left "Unsupported URI protocol.")
                        (\f -> f uri call) $ lookup (uriScheme uri) schemes
    where
        schemes = [("tcp:", scgi), ("unix:", scgi)]
            --("http:", http), "https:", http)]