{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}

module Server (server, defaultDomain, defaultPort) where

import Control.Concurrent (runInUnboundThread)
import Control.Exception (try)
import Control.Monad (unless, when)
import Data.Either (fromRight)
import qualified Data.ByteString.Char8 as BS
import Data.Streaming.Network (bindPortTCP)
import qualified Network.HTTP.Client as H
import Network.Socket (Socket, close)
import Network.Wai.Application.Classic hiding ((</>))
import Network.Wai.Handler.Warp
import Network.Wai.Logger
import System.Exit (ExitCode(..), exitSuccess)
import System.IO
import System.IO.Error (ioeGetErrorString)
import System.Posix (exitImmediately, Handler(..), getProcessID, setFileMode)
import System.Posix.Signals (sigCHLD)
import qualified System.TimeManager as T

import Program.Mighty
import WaiApp

#ifdef HTTP_OVER_TLS
import Control.Concurrent.Async (concurrently_)
import Data.Char (isSpace)
import Data.List (dropWhileEnd)
import Network.TLS (Credentials(..),SessionManager)
import qualified Network.TLS as TLS
import qualified Network.TLS.SessionTicket as SM
import Network.Wai.Handler.WarpTLS
#ifdef HTTP_OVER_QUIC
import Control.Concurrent.Async (mapConcurrently_)
import Data.Bits
import Data.ByteString (ByteString)
import Data.List (find)
import Data.Maybe (fromJust)
import qualified Network.QUIC.Internal as Q
import Network.Wai.Handler.WarpQUIC
#ifdef DROP_EXCEPT_BIND
import Control.Monad (forM_)
import Foreign.C.Types (CInt(..))
import System.Directory (listDirectory)
import System.Posix.Signals (sigUSR1)
#endif
#endif
#else
data Credentials
data SessionManager
#endif

----------------------------------------------------------------

defaultDomain :: Domain
defaultDomain = "localhost"

defaultPort :: Natural
defaultPort = 80

openFileNumber :: Integer
openFileNumber = 10000

logBufferSize :: Natural
logBufferSize = 4 * 1024 * 10

managerNumber :: Int
managerNumber = 1024 -- FIXME

----------------------------------------------------------------

type LogRemover = IO ()

----------------------------------------------------------------

server :: Option -> Reporter -> RouteDB -> IO ()
server opt rpt route = reportDo rpt $ do
    unlimit openFileNumber
    svc <- openService opt
    unless debug writePidFile
    rdr <- newRouteDBRef route
    tmgr <- T.initialize (naturalToInt (opt_connection_timeout opt) * 1000000)
    (mcred, smgr) <- setup opt
    _changed <- setGroupUser (opt_user opt) (opt_group opt)
#ifdef DROP_EXCEPT_BIND
    when _changed dropExceptBind
#endif
    logCheck logtype
    (zdater,_) <- clockDateCacher
    ap <- initLogger FromSocket logtype zdater
    let lgr = apacheLogger ap
        remover = logRemover ap
        pushlgr = serverpushLogger ap
    mgr <- getManager opt
    setHandlers opt rpt svc remover rdr

    report rpt "Mighty started"
    runInUnboundThread $ mighty opt rpt svc lgr pushlgr mgr rdr mcred smgr tmgr
    report rpt "Mighty retired"
    remover
    exitSuccess
  where
    debug = opt_debug_mode opt
    port = opt_port opt
    pidfile
        | port == 80 = opt_pid_file opt
        | otherwise  = opt_pid_file opt ++ show port
    writePidFile = do
        pid <- getProcessID
        writeFile pidfile $ show pid ++ "\n"
        setFileMode pidfile 0o644
    logspec = FileLogSpec {
        log_file          = opt_log_file opt
      , log_file_size     = fromIntegral $ opt_log_file_size opt
      , log_backup_number = naturalToInt $ opt_log_backup_number opt
      }
    logtype
      | not (opt_logging opt) = LogNone
      | debug                 = LogStdout $ naturalToInt logBufferSize
      | otherwise             = LogFile logspec $ naturalToInt logBufferSize

setHandlers :: Option -> Reporter -> Service -> LogRemover -> RouteDBRef -> IO ()
setHandlers opt rpt svc remover rdr = do
    setHandler sigStop   stopHandler
    setHandler sigRetire retireHandler
    setHandler sigInfo   infoHandler
    setHandler sigReload reloadHandler
    setHandler sigCHLD   Ignore        -- for CGI
  where
    stopHandler = Catch $ do
        report rpt "Mighty finished"
        closeService svc
        remover
        exitImmediately ExitSuccess
    retireHandler = Catch $ do
        report rpt "Mighty retiring"
        closeService svc -- this lets warp break
    infoHandler = Catch $ report rpt "obsoleted"
    reloadHandler = Catch $ do
        ifRouteFileIsValid rpt opt $ \newroute -> do
            writeRouteDBRef rdr newroute
            report rpt "Mighty reloaded"

#ifdef HTTP_OVER_TLS
loadCredentials :: Option -> IO Credentials
loadCredentials opt = do
    cert   <- BS.readFile $ opt_tls_cert_file opt
    chains <- mapM BS.readFile chain_files
    key    <- BS.readFile $ opt_tls_key_file opt
    let cred = fromRight (error "loadCredentials") $ TLS.credentialLoadX509ChainFromMemory cert chains key
    return $ Credentials [cred]
  where
    strip = dropWhileEnd isSpace . dropWhile isSpace
    split "" = []
    split s = case break (',' ==) s of
      ("",r)  -> split (tail r)
      (s',"") -> [s']
      (s',r)  -> s' : split (tail r)
    chain_files = map strip $ split $ opt_tls_chain_files opt
#endif

----------------------------------------------------------------

ifRouteFileIsValid :: Reporter -> Option -> (RouteDB -> IO ()) -> IO ()
ifRouteFileIsValid rpt opt act = case opt_routing_file opt of
    Nothing    -> return ()
    Just rfile -> try (parseRoute rfile defaultDomain defaultPort) >>= either reportError act
  where
    reportError = report rpt . BS.pack . ioeGetErrorString

----------------------------------------------------------------

mighty :: Option -> Reporter -> Service
       -> ApacheLogger -> ServerPushLogger
       -> ConnPool -> RouteDBRef
       -> Maybe Credentials -> Maybe SessionManager -> T.Manager
       -> IO ()
mighty opt rpt svc lgr pushlgr mgr rdr _mcreds _msmgr tmgr
  = reportDo rpt $ case svc of
    HttpOnly s  -> runSettingsSocket setting s app
#ifdef HTTP_OVER_TLS
    HttpsOnly s -> runTLSSocket tlsSetting setting s app
    HttpAndHttps s1 s2 -> concurrently_
        (runSettingsSocket setting s1 app)
        (runTLSSocket tlsSetting setting s2 app)
#ifdef HTTP_OVER_QUIC
    QUIC s1 s2 -> do
        let quicPort' = BS.pack $ show quicPort
            strver Q.Version1 = ""
            strver Q.Version2 = ""
            strver v = BS.append "-" $ BS.pack $ show $ fromVersion v
            quicDrafts = map strver quicVersions
            value v = BS.concat ["h3",v,"=\":",quicPort',"\""]
            altsvc = BS.intercalate "," $ map value quicDrafts
            settingT = setAltSvc altsvc setting
        mapConcurrently_ id [runSettingsSocket        setting  s1 app
                            ,runTLSSocket  tlsSetting settingT s2 app
                            ,runQUIC       qconf      setting     app
                            ]
#else
    _ -> error "never reach"
#endif
#else
    _ -> error "never reach"
#endif
  where
    app = fileCgiApp cspec filespec cgispec revproxyspec rdr
    debug = opt_debug_mode opt
    -- We don't use setInstallShutdownHandler because we may use
    -- two sockets for HTTP and HTTPS.
    setting = setPort            (naturalToInt $ opt_port opt) -- just in case
            $ setHost            (fromString (opt_host opt))  -- just in case
            $ setOnException     (if debug then printStdout else warpHandler rpt)
            $ setManager         tmgr
            $ setFdCacheDuration (naturalToInt $ opt_fd_cache_duration opt)
            $ setFileInfoCacheDuration 10
            $ setServerName      serverName
            $ setLogger          lgr
            $ setServerPushLogger pushlgr
            defaultSettings
#ifdef HTTP_OVER_TLS
    ~tlsSetting = defaultTlsSettings {
        tlsCredentials    = _mcreds
      , tlsSessionManager = _msmgr
      , tlsAllowedVersions = [TLS.TLS13,TLS.TLS12]
      }
#endif
    serverName = BS.pack $ opt_server_name opt
    cspec = ClassicAppSpec {
        softwareName = serverName
      , statusFileDir = fromString $ opt_status_file_dir opt
      }
    filespec = FileAppSpec {
        indexFile = fromString $ opt_index_file opt
      , isHTML = \x -> ".html" `isSuffixOf` x || ".htm" `isSuffixOf` x
      }
    cgispec = CgiAppSpec {
        indexCgi = "index.cgi"
      }
    revproxyspec = RevProxyAppSpec {
        revProxyManager = mgr
      }
#ifdef HTTP_OVER_QUIC
    ~quicAddr = read <$> opt_quic_addr opt
    ~quicPort = fromIntegral $ opt_quic_port opt
    ~quicVersions = Q.scVersions Q.defaultServerConfig
    -- Lazy binding for opt_service == 0 so that
    -- 'fromJust' is not called.
    ~qconf = Q.defaultServerConfig {
            Q.scAddresses      = (,quicPort) <$> quicAddr
          , Q.scALPN           = Just chooseALPN
          , Q.scRequireRetry   = False
          , Q.scSessionManager = fromJust _msmgr
          , Q.scUse0RTT        = True
          , Q.scDebugLog       = opt_quic_debug_dir opt
          , Q.scQLog           = opt_quic_qlog_dir opt
          , Q.scCredentials    = fromJust _mcreds
          }

chooseALPN :: Q.Version -> [ByteString] -> IO ByteString
chooseALPN ver protos = case find (\x -> x == h3 || x == hq) protos of
  Nothing    -> return ""
  Just proto -> return proto
  where
    h3 | ver == Q.Version1 = "h3"
       | ver == Q.Version2 = "h3"
       | otherwise = "h3-" `BS.append` BS.pack (show (fromVersion ver))
    hq | ver == Q.Version1 = "hq-interop"
       | ver == Q.Version2 = "hq-interop"
       | otherwise = "hq-" `BS.append` BS.pack (show (fromVersion ver))

fromVersion :: Q.Version -> Int
fromVersion (Q.Version ver) = fromIntegral (0x000000ff .&. ver)
#endif

----------------------------------------------------------------

data Service = HttpOnly Socket
             | HttpsOnly Socket
             | HttpAndHttps Socket Socket
             | QUIC Socket Socket

instance Show Service where
    show HttpOnly{}     = "HttpOnly"
    show HttpsOnly{}    = "HttpOnlys"
    show HttpAndHttps{} = "HttpAndHttps"
    show QUIC{}         = "QUIC"

----------------------------------------------------------------

openService :: Option -> IO Service
openService opt
  | service == 1 = do
      s <- bindPortTCP httpsPort hostpref
      debugMessage $ urlForHTTPS httpsPort
      return $ HttpsOnly s
  | service == 2 = do
      s1 <- bindPortTCP httpPort hostpref
      s2 <- bindPortTCP httpsPort hostpref
      debugMessage $ urlForHTTP httpPort
      debugMessage $ urlForHTTPS httpsPort
      return $ HttpAndHttps s1 s2
  | service == 3 = do
      s1 <- bindPortTCP httpPort hostpref
      s2 <- bindPortTCP httpsPort hostpref
      debugMessage $ urlForHTTP httpPort
      debugMessage $ urlForHTTPS httpsPort
      debugMessage "QUIC is also available via Alt-Svc"
      return $ QUIC s1 s2
  | otherwise = do
      s <- bindPortTCP httpPort hostpref
      debugMessage $ urlForHTTP httpPort
      return $ HttpOnly s
  where
    httpPort  = naturalToInt $ opt_port opt
    httpsPort = naturalToInt $ opt_tls_port opt
    hostpref  = fromString $ opt_host opt
    service = opt_service opt
    debug = opt_debug_mode opt
    debugMessage msg = when debug $ do
        putStrLn msg
        hFlush stdout
    urlForHTTP  80  =  "http://localhost/"
    urlForHTTP  p   =  "http://localhost:" ++ show p ++ "/"
    urlForHTTPS 443 = "https://localhost/"
    urlForHTTPS p   = "https://localhost:" ++ show p ++ "/"


----------------------------------------------------------------

closeService :: Service -> IO ()
closeService (HttpOnly s)         = close s
closeService (HttpsOnly s)        = close s
closeService (HttpAndHttps s1 s2) = close s1 >> close s2
closeService (QUIC s1 s2)         = close s1 >> close s2

----------------------------------------------------------------

type ConnPool = H.Manager

getManager :: Option -> IO ConnPool
getManager opt = H.newManager H.defaultManagerSettings {
    H.managerConnCount = managerNumber
  , H.managerResponseTimeout = responseTimeout
  }
  where
    responseTimeout
      | opt_proxy_timeout opt == 0 = H.managerResponseTimeout H.defaultManagerSettings
      | otherwise                  = H.responseTimeoutMicro (naturalToInt $ opt_proxy_timeout opt * 1000000) -- micro seconds

#ifdef DROP_EXCEPT_BIND
foreign import ccall unsafe "send_signal"
  c_send_signal :: CInt -> CInt -> IO ()

dropExceptBind :: IO ()
dropExceptBind = do
    pid <- getProcessID
    strtids <- listDirectory ("/proc/" ++ show pid ++ "/task")
    let tids = map read strtids :: [Int]
    forM_ tids $ \tid -> c_send_signal (fromIntegral tid) sigUSR1
#endif

----------------------------------------------------------------

setup :: Option -> IO (Maybe Credentials, Maybe SessionManager)
#ifdef HTTP_OVER_TLS
setup opt
  | 1 <= service && service <= 3 = do
        mcred <- Just <$> loadCredentials opt
        smgr <- Just <$> SM.newSessionTicketManager SM.defaultConfig
        return (mcred, smgr)
  | otherwise = return (Nothing, Nothing)
  where
    service = opt_service opt
#else
setup _ = return (Nothing, Nothing)
#endif
