mirror of https://github.com/voidlizard/hbs2
384 lines
11 KiB
Haskell
384 lines
11 KiB
Haskell
{-# LANGUAGE PatternSynonyms #-}
|
|
{-# Language TypeOperators #-}
|
|
module HttpWorker where
|
|
|
|
import HBS2.Prelude.Plated
|
|
import HBS2.OrDie
|
|
import HBS2.Base58
|
|
import HBS2.Hash
|
|
import HBS2.Actors.Peer
|
|
import HBS2.Storage
|
|
import HBS2.Data.Detect
|
|
import HBS2.Data.Types.Refs
|
|
import HBS2.Merkle
|
|
import HBS2.Net.Messaging.Pipe
|
|
import HBS2.Peer.Proto hiding (Request)
|
|
import HBS2.Peer.Proto.BrowserPlugin hiding (Request)
|
|
import HBS2.Peer.Proto.LWWRef
|
|
import HBS2.Peer.Browser.Assets
|
|
import HBS2.Net.Auth.Schema (HBS2Basic)
|
|
import HBS2.Data.Types.SignedBox
|
|
import HBS2.Events
|
|
import HBS2.Storage.Operations.ByteString
|
|
import HBS2.Misc.PrettyStuff
|
|
|
|
|
|
import PeerTypes
|
|
import PeerConfig
|
|
import RefLog ( doRefLogBroadCast )
|
|
import Browser
|
|
|
|
import Data.Config.Suckless
|
|
|
|
import Data.ByteString.Lazy qualified as LBS
|
|
import Network.HTTP.Types.Status
|
|
import Network.Wai.Middleware.RequestLogger
|
|
import Network.Wai.Middleware.StaticEmbedded
|
|
import Network.Wai
|
|
import Web.Scotty as Scotty
|
|
|
|
import Data.ByteString.Builder (byteString, Builder)
|
|
|
|
import Codec.Serialise (deserialiseOrFail)
|
|
import Control.Monad.Except
|
|
import Control.Monad.Reader
|
|
import Control.Monad.Trans.Cont
|
|
import Control.Monad.Trans.Maybe
|
|
import Data.Function
|
|
import Data.Aeson (object, (.=))
|
|
import Data.ByteString.Lazy.Char8 qualified as LBS8
|
|
import Data.ByteString.Char8 qualified as BS8
|
|
import Data.Either
|
|
import Data.HashMap.Strict qualified as HM
|
|
import Data.HashMap.Strict (HashMap)
|
|
import Data.Maybe
|
|
import Data.List qualified as List
|
|
import Data.Text qualified as Text
|
|
import Data.Text.Lazy qualified as LT
|
|
import Data.Text.Encoding qualified as Text
|
|
import Lens.Micro.Platform (view)
|
|
import Streaming.Prelude qualified as S
|
|
import System.FilePath
|
|
import Text.InterpolatedString.Perl6 (qc)
|
|
import System.Process.Typed
|
|
import System.Environment
|
|
|
|
import UnliftIO hiding (orElse)
|
|
|
|
{- HLINT ignore "Functor law" -}
|
|
|
|
-- TODO: introduce-http-of-off-feature
|
|
|
|
extractMetadataHash :: Hash HbSync -> LBS.ByteString -> Maybe (Hash HbSync)
|
|
extractMetadataHash what blob =
|
|
case tryDetect what blob of
|
|
MerkleAnn (MTreeAnn {_mtaMeta = AnnHashRef h, _mtaCrypt = NullEncryption}) -> Just h
|
|
_ -> Nothing
|
|
|
|
orElse :: m r -> Maybe a -> ContT r m a
|
|
orElse a mb = ContT $ maybe1 mb a
|
|
|
|
|
|
data Plugin =
|
|
Plugin
|
|
{
|
|
}
|
|
|
|
pattern Spawn :: forall {c}. [Syntax c] -> Syntax c
|
|
pattern Spawn args <- ListVal (SymbolVal "spawn" : args)
|
|
|
|
runPlugin :: forall m . MonadUnliftIO m
|
|
=> RefChanId L4Proto
|
|
-> [FilePath]
|
|
-> TVar (HashMap (RefChanId L4Proto) (ServiceCaller BrowserPluginAPI PIPE))
|
|
-> m ()
|
|
|
|
runPlugin _ [] _ = pure ()
|
|
runPlugin pks (self:args) handles = do
|
|
|
|
let cmd = proc self args
|
|
& setStdin createPipe
|
|
& setStdout createPipe
|
|
-- & setStderr closed
|
|
|
|
forever do
|
|
flip runContT pure do
|
|
|
|
debug $ yellow "started channel plugin" <+> pretty (AsBase58 pks) <+> pretty self
|
|
|
|
p <- ContT $ withProcessWait cmd
|
|
|
|
let ssin = getStdin p
|
|
let sout = getStdout p
|
|
client <- newMessagingPipe (sout,ssin)
|
|
|
|
void $ ContT $ withAsync $ runMessagingPipe client
|
|
|
|
debug $ red "RUNNING PLUGIN!"
|
|
|
|
caller <- makeServiceCaller @BrowserPluginAPI @PIPE (localPeer client)
|
|
|
|
void $ ContT $ withAsync $ liftIO $ runReaderT (runServiceClient caller) client
|
|
|
|
ContT $ bracket (atomically $ modifyTVar handles (HM.insert pks caller))
|
|
(const $ atomically $ modifyTVar handles (HM.delete pks))
|
|
|
|
void $ waitExitCode p
|
|
|
|
|
|
findPlugins :: forall m . MonadIO m => [Syntax C] -> m [(Maybe Text, RefChanId L4Proto, [FilePath])]
|
|
findPlugins syn = w $ S.toList_ $ do
|
|
|
|
let chans = mconcat [ channels b | ListVal (SymbolVal "browser" : b) <- syn ]
|
|
|
|
for_ chans $ \cha -> void $ runMaybeT do
|
|
|
|
rchan <- toMPlus $ headMay $
|
|
catMaybes [ fromStringMay @(RefChanId L4Proto) (Text.unpack x)
|
|
| ListVal [SymbolVal "refchan", LitStrVal x] <- cha
|
|
]
|
|
|
|
let alias = headMay [ x
|
|
| ListVal [SymbolVal "alias", LitStrVal x] <- cha
|
|
]
|
|
|
|
plug <- toMPlus $ headMay $ catMaybes $
|
|
[ mkProcessArgs what
|
|
| ListVal [ SymbolVal "plugin", Spawn what ] <- cha
|
|
]
|
|
|
|
debug $ red "FOUND CHANNEL" <+> pretty (AsBase58 rchan) <+> parens (pretty plug)
|
|
|
|
lift $ S.yield (alias, rchan, plug)
|
|
|
|
where
|
|
|
|
w l = l >>= uniq
|
|
|
|
uniq s = pure (List.nubBy ((==) `on` ukey) s)
|
|
where ukey (a,b,_) = (a,b)
|
|
|
|
mkProcessArgs ssyn = sequence $
|
|
flip fmap ssyn \case
|
|
LitStrVal s -> Just (Text.unpack s)
|
|
SymbolVal (Id s) -> Just (Text.unpack s)
|
|
_ -> Nothing
|
|
|
|
|
|
channels bro = [ chan
|
|
| ListVal (SymbolVal "channel" : chan) <- bro
|
|
]
|
|
|
|
httpWorker :: forall e s m . ( MyPeer e
|
|
, MonadIO m
|
|
, HasStorage m
|
|
, IsRefPubKey s
|
|
, s ~ Encryption e
|
|
, m ~ PeerM e IO
|
|
, e ~ L4Proto
|
|
-- , ForLWWRefProto e
|
|
) => PeerConfig -> AnnMetaData -> DownloadEnv e -> m ()
|
|
|
|
httpWorker (PeerConfig syn) pmeta e = do
|
|
|
|
sto <- getStorage
|
|
let port' = runReader (cfgValue @PeerHttpPortKey) syn <&> fromIntegral
|
|
let bro = runReader (cfgValue @PeerBrowserEnable) syn == FeatureOn
|
|
penv <- ask
|
|
|
|
void $ flip runContT pure do
|
|
|
|
handles <- newTVarIO mempty
|
|
|
|
aliases <- newTVarIO (mempty :: HashMap Text (RefChanId L4Proto))
|
|
|
|
plugins <- findPlugins syn
|
|
|
|
for_ plugins $ \(a, r, args) -> do
|
|
for_ a $ \alias -> atomically $ modifyTVar aliases (HM.insert alias r)
|
|
void $ ContT $ withAsync (runPlugin r args handles)
|
|
|
|
port <- ContT $ maybe1 port' none
|
|
|
|
liftIO $ scotty port $ do
|
|
middleware logStdout
|
|
|
|
defaultHandler $ const do
|
|
status status500
|
|
|
|
get "/size/:hash" do
|
|
|
|
what <- param @String "hash" <&> fromString
|
|
size <- liftIO $ hasBlock sto what
|
|
case size of
|
|
Nothing -> status status404
|
|
Just n -> do
|
|
json n
|
|
|
|
-- TODO: key-to-disable-tree-streaming
|
|
|
|
get "/ref/:key" do
|
|
|
|
void $ flip runContT pure do
|
|
what <- lift (param @String "key" <&> fromStringMay @(LWWRefKey HBS2Basic))
|
|
>>= orElse (status status404)
|
|
|
|
rv <- getRef sto what
|
|
>>= orElse (status status404)
|
|
>>= getBlock sto
|
|
>>= orElse (status status404)
|
|
<&> either (const Nothing) Just . deserialiseOrFail @(SignedBox (LWWRef e) e)
|
|
>>= orElse (status status404)
|
|
<&> unboxSignedBox0 @(LWWRef e)
|
|
>>= orElse (status status404)
|
|
<&> lwwValue . snd
|
|
|
|
lift $ getTreeHash sto rv
|
|
|
|
-- TODO: define-parsable-instance-for-our-types
|
|
get "/tree/:hash" do
|
|
what <- param @String "hash" <&> fromString
|
|
getTreeHash sto what
|
|
|
|
get "/cat/:hash" do
|
|
what <- param @String "hash" <&> fromString
|
|
blob <- liftIO $ getBlock sto what
|
|
case blob of
|
|
Nothing -> status status404
|
|
Just lbs -> do
|
|
addHeader "content-type" "application/octet-stream"
|
|
addHeader "content-length" [qc|{LBS.length lbs}|]
|
|
raw lbs
|
|
|
|
get "/reflog/:ref" do
|
|
re <- param @String "ref" <&> fromStringMay
|
|
case re of
|
|
Nothing -> status status404
|
|
Just ref -> do
|
|
va <- liftIO $ getRef sto (RefLogKey @s ref)
|
|
maybe1 va (status status404) $ \val -> do
|
|
text [qc|{pretty val}|]
|
|
|
|
-- FIXME: to-replace-to-rpc
|
|
post "/reflog" do
|
|
bs <- LBS.take 4194304 <$> body
|
|
let msg' =
|
|
deserialiseOrFail @(RefLogUpdate L4Proto) bs
|
|
& either (const Nothing) Just
|
|
case msg' of
|
|
Nothing -> do
|
|
status status400
|
|
json $ object ["error" .= "unable to parse RefLogUpdate message"]
|
|
Just msg -> do
|
|
let pubk = view refLogId msg
|
|
liftIO $ withPeerM penv $ do
|
|
emit @e RefLogUpdateEvKey (RefLogUpdateEvData (pubk, msg, Nothing))
|
|
doRefLogBroadCast msg
|
|
status status200
|
|
|
|
get "/metadata" do
|
|
raw $ serialise $ pmeta
|
|
|
|
middleware (static cssDir)
|
|
|
|
|
|
let pluginPath = function $ \r -> case splitDirectories (BS8.unpack (rawPathInfo r)) of
|
|
("/" : "browser" : plugin : _ ) -> Just [("plugin", LT.pack plugin)]
|
|
_ -> Nothing
|
|
|
|
when bro do
|
|
|
|
get "/browser" do
|
|
renderTextT (browserRootPage syn) >>= html
|
|
|
|
get pluginPath do
|
|
|
|
req <- Scotty.request
|
|
|
|
debug $ red "BROWSER" <+> viaShow (splitDirectories (BS8.unpack (rawPathInfo req)))
|
|
|
|
url <- param @Text "plugin"
|
|
alias <- readTVarIO aliases <&> HM.lookup url
|
|
|
|
-- args <- param @String "1"
|
|
|
|
void $ flip runContT pure do
|
|
|
|
chan <- maybe (fromStringMay $ Text.unpack url) pure alias
|
|
& orElse (status status404)
|
|
|
|
plugin <- readTVarIO handles <&> HM.lookup chan
|
|
>>= orElse (status status404)
|
|
|
|
let req = Get mempty mempty
|
|
|
|
lift $ renderTextT (pluginPage plugin req) >>= html
|
|
|
|
|
|
put "/" do
|
|
-- FIXME: optional-header-based-authorization
|
|
-- signed nonce + peer key?
|
|
|
|
-- TODO: ddos-protection
|
|
-- FIXME: fix-max-size-hardcode
|
|
bs <- LBS.take 4194304 <$> body
|
|
-- let ha = hashObject @HbSync bs
|
|
-- here <- liftIO $ hasBlock sto ha <&> isJust
|
|
|
|
mbHash <- liftIO $ putBlock sto bs
|
|
|
|
case mbHash of
|
|
Nothing -> status status500
|
|
Just h -> text [qc|{pretty h}|]
|
|
|
|
warn "http port not set"
|
|
forever $ pause @'Seconds 600
|
|
|
|
|
|
|
|
getTreeHash :: AnyStorage -> HashRef -> ActionM ()
|
|
getTreeHash sto what' = void $ flip runContT pure do
|
|
blob <- liftIO (getBlock sto what)
|
|
>>= orElse (status status404)
|
|
|
|
mh <- orElse (status status404) (extractMetadataHash what blob)
|
|
|
|
meta <- lift (getBlock sto mh) >>= orElse (status status404)
|
|
<&> LBS8.unpack
|
|
<&> fromRight mempty . parseTop
|
|
|
|
let tp = headDef "application/octet-stream"
|
|
[ show (pretty w)
|
|
| ListVal [SymbolVal "mime-type:", LitStrVal w] <- meta
|
|
]
|
|
|
|
let fn = headMay
|
|
[ show (pretty w)
|
|
| ListVal [SymbolVal "file-name:", LitStrVal w] <- meta
|
|
]
|
|
|
|
-- liftIO $ print $ pretty meta
|
|
|
|
case fn of
|
|
Just x | takeExtension x == ".html" -> pure ()
|
|
| otherwise -> lift $ do
|
|
addHeader "content-disposition" [qc|attachment; filename="{x}"|]
|
|
|
|
_ -> pure ()
|
|
|
|
lift $ addHeader "content-type" (fromString tp)
|
|
|
|
elbs <- lift $ runExceptT $ readFromMerkle sto (SimpleKey what)
|
|
|
|
case elbs of
|
|
Left{} -> lift $ status status404
|
|
Right lbs -> lift do
|
|
stream $ \write flush -> do
|
|
for_ (LBS.toChunks lbs) $ \chunk -> do
|
|
write $ byteString chunk
|
|
flush
|
|
where
|
|
what = fromHashRef what'
|
|
|
|
|