{- Copyright 2016 Joey Hess <id@joeyh.name>
 -
 - Licensed under the GNU AGPL version 3 or higher.
 -}

module HTTP.Client where

import HTTP
import HTTP.ProofOfWork
import Types
import Types.Server
import Types.Storage
import Types.Cost
import Servant.API
import Servant.Client
import Data.Proxy
import Network.HTTP.Client hiding (port, host, Proxy)
import Network.HTTP.Client.Internal (Connection)
import Control.Exception
import Network.Socks5
import qualified Data.ByteString.UTF8 as BU8
import Data.List
import Data.Char

httpAPI :: Proxy HttpAPI
httpAPI = Proxy

motd :: ClientM Motd
getObject :: StorableObjectIdent -> Maybe ProofOfWork -> ClientM (POWGuarded StorableObject)
putObject :: StorableObjectIdent -> Maybe ProofOfWork -> StorableObject -> ClientM (POWGuarded StoreResult)
countObjects :: Maybe ProofOfWork -> ClientM (POWGuarded CountResult)
motd :<|> getObject :<|> putObject :<|> countObjects = client httpAPI

tryA :: IO a -> IO (Either SomeException a)
tryA = try

serverRequest
	:: POWIdent p
	=> Server
	-> (String -> a)
	-> (r -> a)
	-> p
	-> (Maybe ProofOfWork -> ClientM (POWGuarded r))
	-> IO a
serverRequest srv onerr onsuccess p a = do
	r <- tryA $ go Nothing maxProofOfWork
	case r of
		Left e -> return $ onerr (show e)
		Right v -> return v
  where
	go pow (Seconds timeleft)
		| timeleft <= 0 = return $ onerr "server asked for too much proof of work; gave up"
		| otherwise = do
			res <- serverRequest' srv (a pow)
			case res of
				Left err -> return $ onerr err
				Right (Result r) -> return $ onsuccess r
				Right (NeedProofOfWork req) -> go
					(Just $ genProofOfWork req p)
					(Seconds timeleft - generationTime req)

-- A new Manager is allocated for each request, rather than reusing
-- any connection. This is a feature; it makes correlation attacks
-- harder because the server can't tell if two connections
-- accessing different objects came from the same user, except by
-- comparing IP addresses (which are masked somewhat by using tor).
serverRequest'
	:: Server
	-> (ClientM r)
	-> IO (Either String r)
serverRequest' srv a = go Nothing (serverUrls srv)
  where
	go lasterr [] = return $ Left $
		maybe "no known address" (\err -> "server failure: " ++ show err) lasterr
	go _ (url:urls) = do
		mymanager <- torableManager
		res <- runClientM a (ClientEnv mymanager url Nothing)
		case res of
			Left err -> go (Just err) urls
			Right r -> return (Right r)

-- | HTTP Manager supporting tor .onion and regular hosts
torableManager :: IO Manager
torableManager = newManager $ defaultManagerSettings
	{ managerRawConnection = return conn
	}
  where
	conn addr host port
		| ".onion" `isSuffixOf` map toLower host = torConnection host port
		| otherwise = do
			regular <- managerRawConnection defaultManagerSettings
			regular addr host port

torConnection :: String -> Port -> IO Connection
torConnection onionaddress p = do
	(socket, _) <- socksConnect torsockconf socksaddr
	socketConnection socket 8192
  where
	torsocksport = 9050
	torsockconf = defaultSocksConf "127.0.0.1" torsocksport
	socksdomain = SocksAddrDomainName (BU8.fromString onionaddress)
	socksaddr = SocksAddress socksdomain (fromIntegral p)

serverUrls :: Server -> [BaseUrl]
serverUrls srv = map go (serverAddress srv)
  where
	go (ServerAddress addr port) = BaseUrl Http addr port ""
