module Ecluse.Core.Server.Pipeline.Publish (
PublishReplies (..),
servePublish,
) where
import Data.ByteString.Lazy qualified as LBS
import Network.HTTP.Types (ResponseHeaders, Status, mkStatus, status403, status405, status413, status500, status502)
import Network.Wai (Request, RequestBodyLength (ChunkedBody, KnownLength), ResponseReceived, getRequestBodyChunk, requestBodyLength)
import Ecluse.Core.Package (
PackageName,
Scope,
pkgNamespace,
renderPackageName,
)
import Ecluse.Core.Registry (PublishRelayFault (RelayBoundExceeded, RelayTransport, RelayUrlUnformable), PublishRelayResponse (PublishRelayResponse))
import Ecluse.Core.Security (Limits (maxBodyBytes), boundedRead)
import Ecluse.Core.Server.Admission.Bytes (withByteAdmission)
import Ecluse.Core.Server.Context (
Handler,
MountBinding (bindingPublishDeps),
PublishDeps (..),
ServeRuntime (srMetrics, srPrivateManager),
ctxMount,
ctxRuntime,
)
import Ecluse.Core.Server.Pipeline.Shared
import Ecluse.Core.Server.Response (appendHelp)
data PublishReplies response = PublishReplies
{ forall response.
PublishReplies response
-> Status -> ResponseHeaders -> LByteString -> response
publishRelayed :: Status -> ResponseHeaders -> LByteString -> response
, forall response.
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
publishError :: Status -> ResponseHeaders -> Text -> response
}
servePublish ::
PublishReplies response ->
PackageName ->
Request ->
(response -> IO ResponseReceived) ->
Handler ResponseReceived
servePublish :: forall response.
PublishReplies response
-> PackageName
-> Request
-> (response -> IO ResponseReceived)
-> Handler ResponseReceived
servePublish PublishReplies response
replies PackageName
name Request
request response -> IO ResponseReceived
respond = do
(RequestCtx -> Maybe PublishDeps) -> Handler (Maybe PublishDeps)
forall r (m :: * -> *) a. MonadReader r m => (r -> a) -> m a
asks (MountBinding -> Maybe PublishDeps
bindingPublishDeps (MountBinding -> Maybe PublishDeps)
-> (RequestCtx -> MountBinding) -> RequestCtx -> Maybe PublishDeps
forall b c a. (b -> c) -> (a -> b) -> a -> c
. RequestCtx -> MountBinding
ctxMount) Handler (Maybe PublishDeps)
-> (Maybe PublishDeps -> Handler ResponseReceived)
-> Handler ResponseReceived
forall a b. Handler a -> (a -> Handler b) -> Handler b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
Maybe PublishDeps
Nothing -> IO ResponseReceived -> Handler ResponseReceived
forall a. IO a -> Handler a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (response -> IO ResponseReceived
respond (PublishReplies response -> response
forall response. PublishReplies response -> response
publishDisabled PublishReplies response
replies))
Just PublishDeps
deps -> PublishReplies response
-> PublishDeps
-> PackageName
-> Request
-> (response -> IO ResponseReceived)
-> Handler ResponseReceived
forall response.
PublishReplies response
-> PublishDeps
-> PackageName
-> Request
-> (response -> IO ResponseReceived)
-> Handler ResponseReceived
publishWithDeps PublishReplies response
replies PublishDeps
deps PackageName
name Request
request response -> IO ResponseReceived
respond
publishWithDeps ::
PublishReplies response ->
PublishDeps ->
PackageName ->
Request ->
(response -> IO ResponseReceived) ->
Handler ResponseReceived
publishWithDeps :: forall response.
PublishReplies response
-> PublishDeps
-> PackageName
-> Request
-> (response -> IO ResponseReceived)
-> Handler ResponseReceived
publishWithDeps PublishReplies response
replies PublishDeps
deps PackageName
name Request
request response -> IO ResponseReceived
respond
| Bool -> Bool
not (Maybe Secret -> Maybe Secret -> Bool
edgeTokenMatches (PublishDeps -> Maybe Secret
pubInboundToken PublishDeps
deps) Maybe Secret
clientToken) =
IO ResponseReceived -> Handler ResponseReceived
forall a. IO a -> Handler a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (response -> IO ResponseReceived
respond (PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
forall response.
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
publishError PublishReplies response
replies (Int -> ByteString -> Status
mkStatus Int
401 ByteString
"Unauthorized") [] Text
"authentication required"))
| Bool -> Bool
not ([Scope] -> PackageName -> Bool
inPublishScope (PublishDeps -> [Scope]
pubScopes PublishDeps
deps) PackageName
name) =
IO ResponseReceived -> Handler ResponseReceived
forall a. IO a -> Handler a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (response -> IO ResponseReceived
respond (PublishReplies response -> PublishDeps -> PackageName -> response
forall response.
PublishReplies response -> PublishDeps -> PackageName -> response
outOfScope PublishReplies response
replies PublishDeps
deps PackageName
name))
| Bool
overDeclaredCap =
IO ResponseReceived -> Handler ResponseReceived
forall a. IO a -> Handler a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (response -> IO ResponseReceived
respond (PublishReplies response -> PublishDeps -> response
forall response. PublishReplies response -> PublishDeps -> response
publishTooLarge PublishReplies response
replies PublishDeps
deps))
| Bool
otherwise = do
rt <- (RequestCtx -> ServeRuntime) -> Handler ServeRuntime
forall r (m :: * -> *) a. MonadReader r m => (r -> a) -> m a
asks RequestCtx -> ServeRuntime
ctxRuntime
outcome <- withByteAdmission (srMetrics rt) (pubBodyBudget deps) bodyWeight $ do
liftIO (boundedRead requestBodyLimits (getRequestBodyChunk request)) >>= \case
Left LimitError
_ -> response -> Handler response
forall a. a -> Handler a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (PublishReplies response -> PublishDeps -> response
forall response. PublishReplies response -> PublishDeps -> response
publishTooLarge PublishReplies response
replies PublishDeps
deps)
Right ByteString
body -> case (LByteString -> [Text])
-> (Text -> Maybe PackageName)
-> PackageName
-> LByteString
-> Maybe Text
bodyNameDisagreement (PublishDeps -> LByteString -> [Text]
pubDeclaredNames PublishDeps
deps) (PublishDeps -> Text -> Maybe PackageName
pubCanonicaliseName PublishDeps
deps) PackageName
name (ByteString -> LByteString
LBS.fromStrict ByteString
body) of
Just Text
declared -> response -> Handler response
forall a. a -> Handler a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (PublishReplies response
-> PublishDeps -> PackageName -> Text -> response
forall response.
PublishReplies response
-> PublishDeps -> PackageName -> Text -> response
bodyNameMismatch PublishReplies response
replies PublishDeps
deps PackageName
name Text
declared)
Maybe Text
Nothing ->
PublishReplies response
-> PublishDeps
-> Either PublishRelayFault PublishRelayResponse
-> response
forall response.
PublishReplies response
-> PublishDeps
-> Either PublishRelayFault PublishRelayResponse
-> response
renderRelay PublishReplies response
replies PublishDeps
deps
(Either PublishRelayFault PublishRelayResponse -> response)
-> Handler (Either PublishRelayFault PublishRelayResponse)
-> Handler response
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> IO (Either PublishRelayFault PublishRelayResponse)
-> Handler (Either PublishRelayFault PublishRelayResponse)
forall a. IO a -> Handler a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (PublishDeps
-> Limits
-> Manager
-> Text
-> Maybe Secret
-> PackageName
-> ByteString
-> IO (Either PublishRelayFault PublishRelayResponse)
pubRelayPublish PublishDeps
deps (PublishDeps -> Limits
pubLimits PublishDeps
deps) (ServeRuntime -> Manager
srPrivateManager ServeRuntime
rt) (PublishDeps -> Text
pubTargetUrl PublishDeps
deps) (Maybe Secret
clientToken Maybe Secret -> Maybe Secret -> Maybe Secret
forall a. Maybe a -> Maybe a -> Maybe a
forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> PublishDeps -> Maybe Secret
pubStaticToken PublishDeps
deps) PackageName
name ByteString
body)
liftIO (respond (fromMaybe (bodyBudgetShed replies deps) outcome))
where
clientToken :: Maybe Secret
clientToken = Request -> Maybe Secret
forwardedToken Request
request
requestBodyLimits :: Limits
requestBodyLimits = (PublishDeps -> Limits
pubLimits PublishDeps
deps){maxBodyBytes = pubMaxRequestBytes deps}
overDeclaredCap :: Bool
overDeclaredCap = case Request -> RequestBodyLength
requestBodyLength Request
request of
KnownLength Word64
n -> Word64
n Word64 -> Word64 -> Bool
forall a. Ord a => a -> a -> Bool
> Int -> Word64
forall a b. (Integral a, Num b) => a -> b
fromIntegral (PublishDeps -> Int
pubMaxRequestBytes PublishDeps
deps)
RequestBodyLength
ChunkedBody -> Bool
False
bodyWeight :: Int
bodyWeight = case Request -> RequestBodyLength
requestBodyLength Request
request of
KnownLength Word64
n -> Word64 -> Int
forall a b. (Integral a, Num b) => a -> b
fromIntegral Word64
n
RequestBodyLength
ChunkedBody -> PublishDeps -> Int
pubMaxRequestBytes PublishDeps
deps
inPublishScope :: [Scope] -> PackageName -> Bool
inPublishScope :: [Scope] -> PackageName -> Bool
inPublishScope [Scope]
scopes PackageName
name = case PackageName -> Maybe Scope
pkgNamespace PackageName
name of
Just Scope
scope -> Scope
scope Scope -> [Scope] -> Bool
forall (f :: * -> *) a.
(Foldable f, DisallowElem f, Eq a) =>
a -> f a -> Bool
`elem` [Scope]
scopes
Maybe Scope
Nothing -> Bool
False
renderRelay ::
PublishReplies response ->
PublishDeps ->
Either PublishRelayFault PublishRelayResponse ->
response
renderRelay :: forall response.
PublishReplies response
-> PublishDeps
-> Either PublishRelayFault PublishRelayResponse
-> response
renderRelay PublishReplies response
replies PublishDeps
deps = \case
Right (PublishRelayResponse Int
code LByteString
relayed) ->
PublishReplies response
-> Status -> ResponseHeaders -> LByteString -> response
forall response.
PublishReplies response
-> Status -> ResponseHeaders -> LByteString -> response
publishRelayed PublishReplies response
replies (Int -> ByteString -> Status
mkStatus Int
code ByteString
"") [] LByteString
relayed
Left (RelayUrlUnformable UrlFormationError
_urlErr) ->
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
forall response.
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
publishError PublishReplies response
replies Status
status500 [] (Maybe HelpMessage -> Text -> Text
appendHelp (PublishDeps -> Maybe HelpMessage
pubHelp PublishDeps
deps) Text
"the publication target URL is misconfigured")
Left (RelayTransport TransportFault
_fault) ->
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
forall response.
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
publishError PublishReplies response
replies Status
status502 [] (Maybe HelpMessage -> Text -> Text
appendHelp (PublishDeps -> Maybe HelpMessage
pubHelp PublishDeps
deps) Text
"the publication target could not be reached")
Left (RelayBoundExceeded LimitError
_limit) ->
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
forall response.
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
publishError PublishReplies response
replies Status
status502 [] (Maybe HelpMessage -> Text -> Text
appendHelp (PublishDeps -> Maybe HelpMessage
pubHelp PublishDeps
deps) Text
"the publication target could not be reached")
bodyBudgetShed :: PublishReplies response -> PublishDeps -> response
bodyBudgetShed :: forall response. PublishReplies response -> PublishDeps -> response
bodyBudgetShed PublishReplies response
replies PublishDeps
deps =
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
forall response.
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
publishError PublishReplies response
replies Status
shedStatus [Header
shedRetryAfter] (Maybe HelpMessage -> Text -> Text
appendHelp (PublishDeps -> Maybe HelpMessage
pubHelp PublishDeps
deps) Text
"the server is at its publish-body capacity; retry shortly")
publishTooLarge :: PublishReplies response -> PublishDeps -> response
publishTooLarge :: forall response. PublishReplies response -> PublishDeps -> response
publishTooLarge PublishReplies response
replies PublishDeps
deps =
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
forall response.
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
publishError PublishReplies response
replies Status
status413 [] (Maybe HelpMessage -> Text -> Text
appendHelp (PublishDeps -> Maybe HelpMessage
pubHelp PublishDeps
deps) Text
"the publish body exceeds the maximum accepted request size")
publishDisabled :: PublishReplies response -> response
publishDisabled :: forall response. PublishReplies response -> response
publishDisabled PublishReplies response
replies =
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
forall response.
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
publishError PublishReplies response
replies Status
status405 [(HeaderName
"Allow", ByteString
"GET, HEAD")] Text
"publishing is not enabled on this proxy (no publication target is configured)"
outOfScope :: PublishReplies response -> PublishDeps -> PackageName -> response
outOfScope :: forall response.
PublishReplies response -> PublishDeps -> PackageName -> response
outOfScope PublishReplies response
replies PublishDeps
deps PackageName
name =
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
forall response.
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
publishError PublishReplies response
replies Status
status403 [] (Maybe HelpMessage -> Text -> Text
appendHelp (PublishDeps -> Maybe HelpMessage
pubHelp PublishDeps
deps) Text
message)
where
message :: Text
message :: Text
message =
Text
"refusing to publish '"
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> PackageName -> Text
renderPackageName PackageName
name
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"': its name is outside the configured publish-scope allow-list (the anti-shadowing guard against publishing a name that shadows a public package)"
bodyNameMismatch :: PublishReplies response -> PublishDeps -> PackageName -> Text -> response
bodyNameMismatch :: forall response.
PublishReplies response
-> PublishDeps -> PackageName -> Text -> response
bodyNameMismatch PublishReplies response
replies PublishDeps
deps PackageName
name Text
declared =
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
forall response.
PublishReplies response
-> Status -> ResponseHeaders -> Text -> response
publishError PublishReplies response
replies Status
status403 [] (Maybe HelpMessage -> Text -> Text
appendHelp (PublishDeps -> Maybe HelpMessage
pubHelp PublishDeps
deps) Text
message)
where
message :: Text
message :: Text
message =
Text
"refusing to publish '"
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> PackageName -> Text
renderPackageName PackageName
name
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"': the document body declares the name '"
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
declared
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"', which disagrees with the URL-path package name the scope guard authorised (the anti-shadowing guard against publishing a name the allow-list never saw)"
bodyNameDisagreement :: (LByteString -> [Text]) -> (Text -> Maybe PackageName) -> PackageName -> LByteString -> Maybe Text
bodyNameDisagreement :: (LByteString -> [Text])
-> (Text -> Maybe PackageName)
-> PackageName
-> LByteString
-> Maybe Text
bodyNameDisagreement LByteString -> [Text]
declaredNames Text -> Maybe PackageName
canonicalise PackageName
name LByteString
body =
(Text -> Bool) -> [Text] -> Maybe Text
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
find Text -> Bool
disagrees (LByteString -> [Text]
declaredNames LByteString
body)
where
disagrees :: Text -> Bool
disagrees :: Text -> Bool
disagrees Text
declared = case Text -> Maybe PackageName
canonicalise Text
declared of
Just PackageName
declaredName -> PackageName
declaredName PackageName -> PackageName -> Bool
forall a. Eq a => a -> a -> Bool
/= PackageName
name
Maybe PackageName
Nothing -> Bool
True