module Ecluse.Core.Server.Pipeline.Tarball.Relay (
ArtifactServe (..),
withMethod,
withValidators,
relayUpstreamWhen,
acceptArtifact,
relayArtifact,
RelayVerdict (..),
relayVerdict,
observeRelayAnomaly,
) where
import Network.HTTP.Client (Manager)
import Network.HTTP.Client qualified as HTTP
import Network.HTTP.Types (RequestHeaders, ResponseHeaders, Status, hContentType, methodHead, statusCode, statusIsSuccessful)
import Data.ByteString qualified as BS
import Ecluse.Core.Package (PackageName, renderPackageName)
import Ecluse.Core.Server.Conditional (isNotModified)
import Ecluse.Core.Server.Stream (RelayResponder, probeUpstreamWhen, streamUpstreamWhen)
import Ecluse.Core.Telemetry.Metrics qualified as Metric
import Ecluse.Core.Telemetry.Record (MetricsPort (mpPublicRelayAnomaly))
import Ecluse.Core.Version (Version, renderVersion)
import Katip (KatipContext, Severity (WarningS), katipAddContext, logFM, ls, sl)
data ArtifactServe
=
ServeFull
|
ServeHead
withMethod :: ArtifactServe -> HTTP.Request -> HTTP.Request
withMethod :: ArtifactServe -> Request -> Request
withMethod = \case
ArtifactServe
ServeFull -> Request -> Request
forall a. a -> a
id
ArtifactServe
ServeHead -> \Request
req -> Request
req{HTTP.method = methodHead}
withValidators :: RequestHeaders -> HTTP.Request -> HTTP.Request
withValidators :: RequestHeaders -> Request -> Request
withValidators RequestHeaders
validators Request
req =
Request
req{HTTP.requestHeaders = validators <> HTTP.requestHeaders req}
relayUpstreamWhen ::
ArtifactServe ->
Manager ->
HTTP.Request ->
(Status -> Bool) ->
(Status -> ResponseHeaders -> IO (Status, ResponseHeaders)) ->
RelayResponder response ->
IO (Maybe response)
relayUpstreamWhen :: forall response.
ArtifactServe
-> Manager
-> Request
-> (Status -> Bool)
-> (Status -> RequestHeaders -> IO (Status, RequestHeaders))
-> RelayResponder response
-> IO (Maybe response)
relayUpstreamWhen = \case
ArtifactServe
ServeFull -> Manager
-> Request
-> (Status -> Bool)
-> (Status -> RequestHeaders -> IO (Status, RequestHeaders))
-> RelayResponder response
-> IO (Maybe response)
forall response.
Manager
-> Request
-> (Status -> Bool)
-> (Status -> RequestHeaders -> IO (Status, RequestHeaders))
-> RelayResponder response
-> IO (Maybe response)
streamUpstreamWhen
ArtifactServe
ServeHead -> Manager
-> Request
-> (Status -> Bool)
-> (Status -> RequestHeaders -> IO (Status, RequestHeaders))
-> RelayResponder response
-> IO (Maybe response)
forall response.
Manager
-> Request
-> (Status -> Bool)
-> (Status -> RequestHeaders -> IO (Status, RequestHeaders))
-> RelayResponder response
-> IO (Maybe response)
probeUpstreamWhen
acceptArtifact :: Status -> Bool
acceptArtifact :: Status -> Bool
acceptArtifact Status
s = Status -> Bool
statusIsSuccessful Status
s Bool -> Bool -> Bool
|| Status -> Bool
isNotModified Status
s
relayArtifact :: Status -> ResponseHeaders -> (Status, ResponseHeaders)
relayArtifact :: Status -> RequestHeaders -> (Status, RequestHeaders)
relayArtifact Status
status RequestHeaders
headers =
(Status
status, (Header -> Bool) -> RequestHeaders -> RequestHeaders
forall a. (a -> Bool) -> [a] -> [a]
filter (Bool -> Bool
not (Bool -> Bool) -> (Header -> Bool) -> Header -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. HeaderName -> Bool
forall {a}. (Eq a, IsString a) => a -> Bool
isHopByHop (HeaderName -> Bool) -> (Header -> HeaderName) -> Header -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Header -> HeaderName
forall a b. (a, b) -> a
fst) RequestHeaders
headers)
where
isHopByHop :: a -> Bool
isHopByHop a
name = a
name a -> a -> Bool
forall a. Eq a => a -> a -> Bool
== a
"Transfer-Encoding" Bool -> Bool -> Bool
|| a
name a -> a -> Bool
forall a. Eq a => a -> a -> Bool
== a
"Connection"
data RelayVerdict
=
RelayedArtifact
|
RelayedOddShape Text
|
RelayedNonSuccess Status
deriving stock (RelayVerdict -> RelayVerdict -> Bool
(RelayVerdict -> RelayVerdict -> Bool)
-> (RelayVerdict -> RelayVerdict -> Bool) -> Eq RelayVerdict
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: RelayVerdict -> RelayVerdict -> Bool
== :: RelayVerdict -> RelayVerdict -> Bool
$c/= :: RelayVerdict -> RelayVerdict -> Bool
/= :: RelayVerdict -> RelayVerdict -> Bool
Eq, Int -> RelayVerdict -> ShowS
[RelayVerdict] -> ShowS
RelayVerdict -> String
(Int -> RelayVerdict -> ShowS)
-> (RelayVerdict -> String)
-> ([RelayVerdict] -> ShowS)
-> Show RelayVerdict
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> RelayVerdict -> ShowS
showsPrec :: Int -> RelayVerdict -> ShowS
$cshow :: RelayVerdict -> String
show :: RelayVerdict -> String
$cshowList :: [RelayVerdict] -> ShowS
showList :: [RelayVerdict] -> ShowS
Show)
relayVerdict :: Status -> ResponseHeaders -> RelayVerdict
relayVerdict :: Status -> RequestHeaders -> RelayVerdict
relayVerdict Status
status RequestHeaders
headers
| Status -> Bool
isNotModified Status
status = RelayVerdict
RelayedArtifact
| Bool -> Bool
not (Status -> Bool
statusIsSuccessful Status
status) = Status -> RelayVerdict
RelayedNonSuccess Status
status
| Just ByteString
contentType <- Header -> ByteString
forall a b. (a, b) -> b
snd (Header -> ByteString) -> Maybe Header -> Maybe ByteString
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> (Header -> Bool) -> RequestHeaders -> Maybe Header
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
find ((HeaderName -> HeaderName -> Bool
forall a. Eq a => a -> a -> Bool
== HeaderName
hContentType) (HeaderName -> Bool) -> (Header -> HeaderName) -> Header -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Header -> HeaderName
forall a b. (a, b) -> a
fst) RequestHeaders
headers
, ByteString -> Bool
textualContentType ByteString
contentType =
Text -> RelayVerdict
RelayedOddShape (Text
"a success carrying a non-artifact content type: " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> ByteString -> Text
forall a b. ConvertUtf8 a b => b -> a
decodeUtf8 ByteString
contentType)
| Bool
otherwise = RelayVerdict
RelayedArtifact
where
textualContentType :: ByteString -> Bool
textualContentType ByteString
raw =
ByteString
"text/" ByteString -> ByteString -> Bool
`BS.isPrefixOf` ByteString
raw Bool -> Bool -> Bool
|| ByteString
"application/json" ByteString -> ByteString -> Bool
`BS.isPrefixOf` ByteString
raw
observeRelayAnomaly :: forall m. (KatipContext m) => MetricsPort -> PackageName -> Version -> RelayVerdict -> m ()
observeRelayAnomaly :: forall (m :: * -> *).
KatipContext m =>
MetricsPort -> PackageName -> Version -> RelayVerdict -> m ()
observeRelayAnomaly MetricsPort
metrics PackageName
name Version
version = \case
RelayVerdict
RelayedArtifact -> m ()
forall (f :: * -> *). Applicative f => f ()
pass
RelayedOddShape Text
reason -> RelayAnomaly -> Text -> m ()
record RelayAnomaly
Metric.RelayOddShape (Text
"the public upstream answered a success that does not look like the admitted artifact: " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
reason)
RelayedNonSuccess Status
status -> RelayAnomaly -> Text -> m ()
record RelayAnomaly
Metric.RelayNonSuccess (Text
"the public upstream answered a non-success, relayed verbatim: HTTP " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Int -> Text
forall b a. (Show a, IsString b) => a -> b
show (Status -> Int
statusCode Status
status))
where
record :: Metric.RelayAnomaly -> Text -> m ()
record :: RelayAnomaly -> Text -> m ()
record RelayAnomaly
cls Text
message = do
IO () -> m ()
forall a. IO a -> m a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (MetricsPort -> RelayAnomaly -> IO ()
mpPublicRelayAnomaly MetricsPort
metrics RelayAnomaly
cls)
SimpleLogPayload -> m () -> m ()
forall i (m :: * -> *) a.
(LogItem i, KatipContext m) =>
i -> m a -> m a
katipAddContext SimpleLogPayload
payload (Severity -> LogStr -> m ()
forall (m :: * -> *).
(Applicative m, KatipContext m) =>
Severity -> LogStr -> m ()
logFM Severity
WarningS (Text -> LogStr
forall a. StringConv a Text => a -> LogStr
ls Text
message))
payload :: SimpleLogPayload
payload =
Text -> Text -> SimpleLogPayload
forall a. ToJSON a => Text -> a -> SimpleLogPayload
sl Text
"module" (Text
"Ecluse.Core.Server.Pipeline.Tarball.Relay" :: Text)
SimpleLogPayload -> SimpleLogPayload -> SimpleLogPayload
forall a. Semigroup a => a -> a -> a
<> Text -> Text -> SimpleLogPayload
forall a. ToJSON a => Text -> a -> SimpleLogPayload
sl Text
"package" (PackageName -> Text
renderPackageName PackageName
name)
SimpleLogPayload -> SimpleLogPayload -> SimpleLogPayload
forall a. Semigroup a => a -> a -> a
<> Text -> Text -> SimpleLogPayload
forall a. ToJSON a => Text -> a -> SimpleLogPayload
sl Text
"version" (Version -> Text
renderVersion Version
version)