shrub/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre.hs

582 lines
18 KiB
Haskell
Raw Normal View History

2020-01-23 07:16:09 +03:00
{-|
Http Server Driver
2020-01-23 07:16:09 +03:00
TODO Make sure that HTTP sockets get closed on shutdown.
TODO What is this about?
// if we don't explicitly set this field, h2o will send with
// transfer-encoding: chunked
//
if ( 1 == has_len_i ) {
rec_u->res.content_length = ( 0 == gen_u->bod_u ) ?
0 : gen_u->bod_u->len_w;
}
TODO Does this matter, is is using WAI's default behavior ok?
rec_u->res.reason = (status < 200) ? "weird" :
(status < 300) ? "ok" :
(status < 400) ? "moved" :
(status < 500) ? "missing" :
"hosed";
-}
2020-05-08 21:29:18 +03:00
module Urbit.Vere.Eyre
( eyre
, multiServ
, ShipAPI(..)
)
where
2019-05-16 03:00:10 +03:00
import Urbit.Prelude hiding (Builder)
2020-05-08 21:29:18 +03:00
import Urbit.Arvo hiding (ServerId, reqUrl, secure)
import Urbit.King.Config
2020-05-08 21:29:18 +03:00
import Urbit.Vere.Eyre.Wai hiding (ReqId)
import Urbit.Vere.Pier.Types
2019-05-16 03:00:10 +03:00
import Data.PEM (pemParseBS, pemWriteBS)
import Network.Socket (SockAddr(..))
import RIO.Prelude (decodeUtf8Lenient)
import System.Directory (doesFileExist, removeFile)
import System.Random (randomIO)
import Urbit.Vere.Http (convertHeaders, unconvertHeaders)
2019-05-31 05:53:00 +03:00
import qualified Network.HTTP.Types as H
import qualified Network.Socket as Net
2019-05-31 05:53:00 +03:00
import qualified Network.Wai as W
import qualified Network.Wai.Handler.Warp as W
2019-05-25 02:03:46 +03:00
import qualified Network.Wai.Handler.WarpTLS as W
2019-05-24 02:58:18 +03:00
-- Internal Types --------------------------------------------------------------
type HasShipEnv e = (HasLogFunc e, HasNetworkConfig e, HasPierConfig e)
2019-12-19 22:30:09 +03:00
type ReqId = UD
data Ports = Ports
{ pHttps :: Maybe Port
, pHttp :: Port
, pLoop :: Port
}
deriving (Eq, Ord, Show)
newtype Drv = Drv { _unDrv :: MVar (Maybe Serv) }
data Serv = Serv
{ sServId :: ServId
, sConfig :: HttpServerConf
, sLoopTid :: Async ()
, sHttpTid :: Async ()
, sHttpsTid :: Maybe (Async ())
, sLoopSock :: Net.Socket
, sHttpSock :: Net.Socket
, sHttpsSock :: Net.Socket
, sPorts :: Ports
, sPortsFile :: FilePath
, sLiveReqs :: TVar LiveReqs
}
2019-08-03 22:26:45 +03:00
-- Generic Service Stop/Restart -- Using an MVar for Atomicity -----------------
2020-01-23 07:16:09 +03:00
{-|
2019-08-02 08:07:20 +03:00
Restart a running service.
2019-05-16 03:00:10 +03:00
2019-08-02 08:07:20 +03:00
This can probably be made simpler, but it
2019-05-16 03:00:10 +03:00
2019-08-02 08:07:20 +03:00
- Sets the MVar to Nothing if there was an exception whil starting
or stopping the service.
2019-05-16 03:00:10 +03:00
2019-08-02 08:07:20 +03:00
- Keeps the MVar lock until the restart process finishes.
-}
2019-08-29 03:26:59 +03:00
restartService :: e s. HasLogFunc e
=> MVar (Maybe s)
-> RIO e s
-> (s -> RIO e ())
-> RIO e (Either SomeException s)
2019-08-02 08:07:20 +03:00
restartService vServ sstart kkill = do
2019-08-29 03:26:59 +03:00
logDebug "restartService"
2019-08-02 08:07:20 +03:00
modifyMVar vServ $ \case
Nothing -> doStart
Just sv -> doRestart sv
where
2019-08-29 03:26:59 +03:00
doRestart :: s -> RIO e (Maybe s, Either SomeException s)
doRestart serv = do
2019-08-29 03:26:59 +03:00
logDebug "doStart"
2019-08-02 08:07:20 +03:00
try (kkill serv) >>= \case
Left exn -> pure (Nothing, Left exn)
Right () -> doStart
2019-05-16 03:04:21 +03:00
2019-08-29 03:26:59 +03:00
doStart :: RIO e (Maybe s, Either SomeException s)
doStart = do
2019-08-29 03:26:59 +03:00
logDebug "doStart"
2019-08-02 08:07:20 +03:00
try sstart <&> \case
Right s -> (Just s, Right s)
Left exn -> (Nothing, Left exn)
2019-08-02 08:07:20 +03:00
2019-08-29 03:26:59 +03:00
stopService :: HasLogFunc e
=> MVar (Maybe s)
-> (s -> RIO e ())
-> RIO e (Either SomeException ())
2019-08-02 08:07:20 +03:00
stopService vServ kkill = do
2019-08-29 03:26:59 +03:00
logDebug "stopService"
2019-08-02 08:07:20 +03:00
modifyMVar vServ $ \case
Nothing -> pure (Nothing, Right ())
Just sv -> do res <- try (kkill sv)
pure (Nothing, res)
2019-05-16 03:00:10 +03:00
2019-05-24 02:58:18 +03:00
-- Ports File ------------------------------------------------------------------
2019-08-29 03:26:59 +03:00
removePortsFile :: FilePath -> RIO e ()
removePortsFile pax =
2019-08-29 03:26:59 +03:00
io (doesFileExist pax) >>= \case
True -> io $ removeFile pax
False -> pure ()
portsFileText :: Ports -> Text
portsFileText Ports{..} =
unlines $ catMaybes
[ pHttps <&> \p -> (tshow p <> " secure public")
, Just (tshow (unPort pHttp) <> " insecure public")
, Just (tshow (unPort pLoop) <> " insecure loopback")
]
2019-08-29 03:26:59 +03:00
writePortsFile :: FilePath -> Ports -> RIO e ()
writePortsFile f = writeFile f . encodeUtf8 . portsFileText
-- Utilities for Constructing Events -------------------------------------------
2019-08-02 08:07:20 +03:00
data WhichServer = Secure | Insecure | Loopback
deriving (Eq)
2019-08-02 08:07:20 +03:00
servEv :: HttpServerEv -> Ev
servEv = EvBlip . BlipEvHttpServer
bornEv :: KingId -> Ev
bornEv king =
2019-08-03 22:26:45 +03:00
servEv $ HttpServerEvBorn (king, ()) ()
2019-08-02 08:07:20 +03:00
liveEv :: ServId -> Ports -> Ev
liveEv sId Ports{..} =
servEv $ HttpServerEvLive (sId, ()) pHttp pHttps
cancelEv :: ServId -> ReqId -> Ev
cancelEv sId reqId =
servEv $ HttpServerEvCancelRequest (sId, reqId, 1, ()) ()
reqEv :: ServId -> ReqId -> WhichServer -> Address -> HttpRequest -> Ev
reqEv sId reqId which addr req =
case which of
Loopback ->
servEv $ HttpServerEvRequestLocal (sId, reqId, 1, ())
$ HttpServerReq False addr req
_ ->
servEv $ HttpServerEvRequest (sId, reqId, 1, ())
$ HttpServerReq (which == Secure) addr req
2019-05-25 02:03:46 +03:00
2019-08-03 22:26:45 +03:00
-- Top-Level Driver Interface --------------------------------------------------
data CantOpenPort = CantOpenPort W.Port
deriving (Eq, Ord, Show, Exception)
data WhichPort
= WPSpecific W.Port
| WPChoices [W.Port]
data SockOpts = SockOpts
{ soLocalhost :: Bool
, soWhich :: WhichPort
}
data PortsToTry = PortsToTry
{ pttSec :: SockOpts
, pttIns :: SockOpts
, pttLop :: SockOpts
}
2020-01-23 07:16:09 +03:00
{-|
2019-12-20 23:58:16 +03:00
Opens a socket on some port, accepting connections from `127.0.0.1`
if fake and `0.0.0.0` if real.
2019-12-20 23:58:16 +03:00
It will attempt to open a socket on each of the supplied ports in
order. If they all fail, it will ask the operating system to give
us an open socket on *any* open port. If that fails, it will throw
an exception.
-}
openPort :: forall e . HasLogFunc e => SockOpts -> RIO e (W.Port, Net.Socket)
openPort SockOpts {..} = case soWhich of
WPSpecific x -> insist (fromIntegral x)
WPChoices xs -> loop (fromIntegral <$> xs)
where
loop :: [W.Port] -> RIO e (W.Port, Net.Socket)
loop = \case
[] -> do
logTrace "Fallback: asking the OS to give us some free port."
ps <- io W.openFreePort
logTrace (display ("Opened port " <> tshow (fst ps)))
pure ps
x : xs -> do
logTrace (display ("Trying to open port " <> tshow x))
io (tryOpen x) >>= \case
Left (err :: IOError) -> do
logWarn (display ("Failed to open port " <> tshow x))
logWarn (display (tshow err))
loop xs
Right ps -> do
logTrace (display ("Opened port " <> tshow (fst ps)))
pure ps
insist :: W.Port -> RIO e (W.Port, Net.Socket)
insist p = do
logTrace (display ("Opening configured port " <> tshow p))
io (tryOpen p) >>= \case
Left (err :: IOError) -> do
logWarn (display ("Failed to open port " <> tshow p))
logWarn (display (tshow err))
throwIO (CantOpenPort p)
Right ps -> do
logTrace (display ("Opened port " <> tshow (fst ps)))
pure ps
bindTo = if soLocalhost then "127.0.0.1" else "0.0.0.0"
getBindAddr :: W.Port -> IO SockAddr
getBindAddr por =
Net.getAddrInfo Nothing (Just bindTo) (Just (show por)) >>= \case
[] -> error "this should never happen."
x : _ -> pure (Net.addrAddress x)
bindListenPort :: W.Port -> Net.Socket -> IO Net.PortNumber
bindListenPort por sok = do
Net.bind sok =<< getBindAddr por
Net.listen sok 1
Net.socketPort sok
-- `inet_addr`, `bind`, and `listen` all throw `IOError` if they fail.
tryOpen :: W.Port -> IO (Either IOError (W.Port, Net.Socket))
tryOpen por = do
sok <- Net.socket Net.AF_INET Net.Stream Net.defaultProtocol
try (bindListenPort por sok) >>= \case
Left exn -> Net.close sok $> Left exn
Right por -> pure (Right (fromIntegral por, sok))
httpServerPorts :: HasShipEnv e => Bool -> RIO e PortsToTry
httpServerPorts fak = do
ins <- view (networkConfigL . ncHttpPort . to (fmap fromIntegral))
sec <- view (networkConfigL . ncHttpsPort . to (fmap fromIntegral))
lop <- view (networkConfigL . ncLocalPort . to (fmap fromIntegral))
localMode <- view (networkConfigL . ncNetMode . to (== NMLocalhost))
let local = localMode || fak
let pttSec = case (sec, fak) of
(Just p , _ ) -> SockOpts local (WPSpecific p)
(Nothing, False) -> SockOpts local (WPChoices (443 : [8443 .. 8448]))
(Nothing, True ) -> SockOpts local (WPChoices ([8443 .. 8448]))
let pttIns = case (ins, fak) of
(Just p , _ ) -> SockOpts local (WPSpecific p)
(Nothing, False) -> SockOpts local (WPChoices (80 : [8080 .. 8085]))
(Nothing, True ) -> SockOpts local (WPChoices [8080 .. 8085])
let pttLop = case (lop, fak) of
(Just p , _) -> SockOpts local (WPSpecific p)
(Nothing, _) -> SockOpts local (WPChoices [12321 .. 12326])
pure (PortsToTry { .. })
eyreApp
:: HasLogFunc e
=> e
-> ServId
-> TVar LiveReqs
-> (Ev -> STM ())
-> WhichServer
-> W.Application
eyreApp env sId vLive plan which =
app env vLive onReq onCancel
where
bodFile "" = Nothing
bodFile bs = Just $ File $ Octs bs
onReq :: Word64 -> ReqInfo -> STM ()
onReq reqId ReqInfo{..} = do
let evBod = bodFile riBod
evHdr = convertHeaders riHdr
evUrl = Cord (decodeUtf8Lenient riUrl)
evReq = HttpRequest riMet evUrl evHdr evBod
reqUd = fromIntegral reqId
event = reqEv sId reqUd which riAdr evReq
plan event
onCancel :: Word64 -> STM ()
onCancel reqId = plan (cancelEv sId (fromIntegral reqId))
parseCerts :: ByteString -> Maybe (ByteString, [ByteString])
parseCerts bs = do
pems <- pemParseBS bs & either (const Nothing) Just
case pems of
[] -> Nothing
p:ps -> pure (pemWriteBS p, pemWriteBS <$> ps)
fByt :: File -> ByteString
fByt = unOcts . unFile
reorgHttpEvent :: HttpEvent -> [RespAct]
reorgHttpEvent = \case
Start h b True -> [RAFull (hSta h) (hHdr h) (fByt $ fromMaybe "" b)]
Start h b False -> [RAHead (hSta h) (hHdr h) (fByt $ fromMaybe "" b)]
Cancel () -> [RADone]
Continue b done -> toList (RABloc . fByt <$> b)
<> if done then [RADone] else []
where
hHdr :: ResponseHeader -> [H.Header]
hHdr = unconvertHeaders . headers
hSta :: ResponseHeader -> H.Status
hSta = toEnum . fromIntegral . statusCode
respond :: HasLogFunc e
=> Drv -> Word64 -> HttpEvent -> RIO e ()
respond (Drv v) reqId ev = do
readMVar v >>= \case
Nothing -> logError "Got a response to a request that does not exist."
Just sv -> do logDebug $ displayShow ev
for_ (reorgHttpEvent ev) $
atomically . routeRespAct (sLiveReqs sv) reqId
wainBytes :: Wain -> ByteString
wainBytes = encodeUtf8 . unWain
startServ :: (HasPierConfig e, HasLogFunc e, HasNetworkConfig e)
=> Bool -> HttpServerConf -> (Ev -> STM ())
2019-08-29 03:26:59 +03:00
-> RIO e Serv
startServ isFake conf plan = do
2019-08-29 03:26:59 +03:00
logDebug "startServ"
let tls = do (PEM key, PEM certs) <- hscSecure conf
(cert, chain) <- parseCerts (wainBytes certs)
pure $ W.tlsSettingsChainMemory cert chain $ wainBytes key
2019-08-02 08:07:20 +03:00
2019-08-29 03:26:59 +03:00
sId <- io $ ServId . UV . fromIntegral <$> (randomIO :: IO Word32)
liv <- newTVarIO emptyLiveReqs
2019-05-25 02:03:46 +03:00
ptt <- httpServerPorts isFake
(httpPortInt, httpSock) <- openPort (pttIns ptt)
(httpsPortInt, httpsSock) <- openPort (pttSec ptt)
(loopPortInt, loopSock) <- openPort (pttLop ptt)
2019-08-22 04:18:10 +03:00
let httpPort = Port (fromIntegral httpPortInt)
httpsPort = Port (fromIntegral httpsPortInt)
loopPort = Port (fromIntegral loopPortInt)
2019-08-22 04:18:10 +03:00
let loopOpts = W.defaultSettings & W.setPort (fromIntegral loopPort)
& W.setHost "127.0.0.1"
& W.setTimeout (5 * 60)
2019-12-20 01:20:04 +03:00
httpOpts = W.defaultSettings & W.setHost "*"
& W.setPort (fromIntegral httpPort)
httpsOpts = W.defaultSettings & W.setHost "*"
& W.setPort (fromIntegral httpsPort)
2019-05-25 02:03:46 +03:00
2019-08-29 03:26:59 +03:00
env <- ask
2019-08-29 03:26:59 +03:00
logDebug "Starting loopback server"
loopTid <- async $ io
$ W.runSettingsSocket loopOpts loopSock
$ eyreApp env sId liv plan Loopback
2019-05-25 02:03:46 +03:00
2019-08-29 03:26:59 +03:00
logDebug "Starting HTTP server"
httpTid <- async $ io
$ W.runSettingsSocket httpOpts httpSock
$ eyreApp env sId liv plan Insecure
2019-08-29 03:26:59 +03:00
logDebug "Starting HTTPS server"
httpsTid <- for tls $ \tlsOpts ->
2019-08-29 03:26:59 +03:00
async $ io
$ W.runTLSSocket tlsOpts httpsOpts httpsSock
$ eyreApp env sId liv plan Secure
pierPath <- view pierPathL
let por = Ports (tls <&> const httpsPort) httpPort loopPort
fil = pierPath <> "/.http.ports"
2019-08-29 03:26:59 +03:00
logDebug $ displayShow (sId, por, fil)
2019-07-22 21:10:27 +03:00
2019-08-29 03:26:59 +03:00
logDebug "Finished started HTTP Servers"
pure $ Serv sId conf
loopTid httpTid httpsTid
httpSock httpsSock loopSock
por fil liv
2019-08-29 03:26:59 +03:00
killServ :: HasLogFunc e => Serv -> RIO e ()
killServ Serv{..} = do
cancel sLoopTid
2019-08-03 22:26:45 +03:00
cancel sHttpTid
traverse_ cancel sHttpsTid
io $ Net.close sHttpSock
io $ Net.close sHttpsSock
io $ Net.close sLoopSock
removePortsFile sPortsFile
(void . waitCatch) sLoopTid
(void . waitCatch) sHttpTid
traverse_ (void . waitCatch) sHttpsTid
2019-08-29 03:26:59 +03:00
kill :: HasLogFunc e => Drv -> RIO e ()
2019-08-03 22:26:45 +03:00
kill (Drv v) = stopService v killServ >>= fromEither
2020-05-08 21:29:18 +03:00
eyre :: e. HasShipEnv e
=> KingId -> QueueEv -> Bool
2019-08-29 03:26:59 +03:00
-> ([Ev], RAcquire e (EffCb e HttpServerEf))
2020-05-08 21:29:18 +03:00
eyre king plan isFake =
2019-08-03 22:26:45 +03:00
(initialEvents, runHttpServer)
where
2019-08-03 22:26:45 +03:00
initialEvents :: [Ev]
initialEvents = [bornEv king]
2019-05-25 02:03:46 +03:00
2019-08-29 03:26:59 +03:00
runHttpServer :: RAcquire e (EffCb e HttpServerEf)
runHttpServer = handleEf <$> mkRAcquire (Drv <$> newMVar Nothing) kill
2019-08-29 03:26:59 +03:00
restart :: Drv -> HttpServerConf -> RIO e Serv
2019-08-03 22:26:45 +03:00
restart (Drv var) conf = do
2019-08-29 03:26:59 +03:00
logDebug "Restarting http server"
res <- fromEither =<<
restartService var (startServ isFake conf plan) killServ
2019-08-29 03:26:59 +03:00
logDebug "Done restating http server"
pure res
2019-08-29 03:26:59 +03:00
handleEf :: Drv -> HttpServerEf -> RIO e ()
2019-08-03 22:26:45 +03:00
handleEf drv = \case
HSESetConfig (i, ()) conf -> do
-- print (i, king)
-- when (i == fromIntegral king) $ do
2019-08-29 03:26:59 +03:00
logDebug "restarting"
Serv{..} <- restart drv conf
2019-08-29 03:26:59 +03:00
logDebug "Enqueue %live"
atomically $ plan (liveEv sServId sPorts)
2019-08-29 03:26:59 +03:00
logDebug "Write ports file"
writePortsFile sPortsFile sPorts
HSEResponse (i, req, _seq, ()) ev -> do
-- print (i, king)
-- when (i == fromIntegral king) $ do
2019-08-29 03:26:59 +03:00
logDebug "respond"
2019-08-03 22:26:45 +03:00
respond drv (fromIntegral req) ev
-- Multi-Tenet HTTP ------------------------------------------------------------
{-
# Very First Phase: Shared HTTP, no SSL.
- Global configuration flag for shared HTTP port.
- Shared server starts before ships.
- Shared server is informed when ships go up and come down.
- Shared server delivers requests to existing HTTP driver.
- Existing HTTP driver can send responses to shared HTTP server.
-}
type ShareRequ = (ServId, ReqId, WhichServer, Address, HttpRequest)
type ShareResp = (ServId, UD, UD, HttpEvent)
data ShipAPI = ShipAPI
{ sapiReq :: ShareRequ -> STM ()
, sapiRes :: STM ShareResp
}
data MultiServ = MultiServ
{ msPort :: Maybe Word16
, msShip :: TVar (Map Ship ShipAPI)
, msBoot :: TMVar (Ship, ShipAPI)
, msDead :: TMVar Ship
, msKill :: STM ()
}
data Hap = Deþ Ship
| Lif (Ship, ShipAPI)
| Res ShareResp
| Kil ()
2020-05-05 20:29:19 +03:00
multiServ :: HasLogFunc e => MultiServ -> RIO e ()
multiServ ms = do
case msPort ms of
Nothing -> doNothing ms
Just po -> doSomething ms po
{-
If the port is set, we do things for real. We run an HTTP server,
sends requests to the appropriate ship, respond to requests when
responses are given, and shuts down when the king shuts down.
-}
2020-05-05 20:29:19 +03:00
doSomething :: HasLogFunc e => MultiServ -> Word16 -> RIO e ()
doSomething MultiServ{..} httpPort = do
2020-05-05 20:29:19 +03:00
logDebug "Starting HTTP server"
let httpOpts = W.defaultSettings & W.setHost "*"
& W.setPort (fromIntegral httpPort)
sId <- io $ ServId . UV . fromIntegral <$> (randomIO :: IO Word32)
vShips :: TVar (Map Ship ShipAPI) <- newTVarIO mempty
liv <- newTVarIO emptyLiveReqs
env <- ask
plan <- error "TODO"
httpTid <- async $ io
$ W.runSettings httpOpts
$ eyreApp env sId liv plan Insecure
2020-05-05 20:29:19 +03:00
let onHapn :: STM Hap
onHapn = asum [ Lif <$> takeTMVar msBoot
, Deþ <$> takeTMVar msDead
, Res <$> (readTVar vShips >>= asum . fmap sapiRes . toList)
, Kil <$> msKill
]
let loop = join $ atomically $ onHapn >>= \case
Deþ s -> modifyTVar' vShips (deleteMap s) >> pure loop
Lif (s,api) -> modifyTVar' vShips (insertMap s api) >> pure loop
Res _ -> error "TODO"
Kil _ -> pure (cancel httpTid)
loop
{-
If the port is not set, we still run a thread for the shared server. It
doesn't run an HTTP server, it ignores all responses, and it shuts
down when the king shuts down.
-}
2020-05-05 20:29:19 +03:00
doNothing :: MultiServ -> RIO e ()
doNothing MultiServ{..} = do
vShips :: TVar (Map Ship ShipAPI) <- newTVarIO mempty
let onHapn :: STM Hap
onHapn = asum [ Lif <$> takeTMVar msBoot
, Deþ <$> takeTMVar msDead
, Res <$> (readTVar vShips >>= asum . fmap sapiRes . toList)
, Kil <$> msKill
]
let loop = join $ atomically $ onHapn >>= \case
Deþ s -> modifyTVar' vShips (deleteMap s) >> pure loop
Lif (s,api) -> modifyTVar' vShips (insertMap s api) >> pure loop
Res _ -> pure loop
Kil _ -> pure (pure ())
loop