Remove pre-release HTTP compatibility surface

This commit is contained in:
ms2k 2026-07-19 17:49:03 +09:00
commit f28810ea33
8 changed files with 104 additions and 329 deletions

View file

@ -32,8 +32,7 @@ framework route and decoding failures retain Servant's response format.
| Surface | Contract | | Surface | Contract |
| --- | --- | | --- | --- |
| Runtime | `GET /healthz` reports process liveness. `GET /readyz` reports declared traffic safety. | | Runtime | `GET /healthz` reports process liveness. `GET /readyz` reports declared traffic safety. |
| Browser | Public `GET /nuthatch.js`, `GET /login`, and `GET /oidc/callback`; authenticated `GET /app` and `/assets/*`; and CSRF-protected authenticated `POST /logout` implement the browser session boundary. | | Browser | Public `GET /login` and `GET /oidc/callback`; authenticated `GET /app` and `/assets/*`; and CSRF-protected authenticated `POST /logout` implement the browser session boundary. |
| Legacy product | Authenticated `GET /`, `GET /download`, `POST /upload`, `POST /delete`, and `POST /delete-prefix` remain supported. |
| Programmatic product | Authenticated `/api/session`, `/api/browse`, `/api/download`, `/api/upload`, `/api/delete`, `/api/delete-plan`, and `/api/delete-batch` expose the JSON product surface. | | Programmatic product | Authenticated `/api/session`, `/api/browse`, `/api/download`, `/api/upload`, `/api/delete`, `/api/delete-plan`, and `/api/delete-batch` expose the JSON product surface. |
Product paths are valid UTF-8, NFC-normalized, relative, and canonical. The Product paths are valid UTF-8, NFC-normalized, relative, and canonical. The

View file

@ -113,7 +113,6 @@ library
Nuthatch.Session Nuthatch.Session
Nuthatch.Streaming Nuthatch.Streaming
Nuthatch.Types Nuthatch.Types
Nuthatch.Ui
executable nuthatch executable nuthatch
import: shared import: shared

View file

@ -2,7 +2,6 @@ module Nuthatch.Api
( Api ( Api
, HealthEndpoint , HealthEndpoint
, ReadyEndpoint , ReadyEndpoint
, BrowserScriptEndpoint
, AppEndpoint , AppEndpoint
, StaticAssetsEndpoint , StaticAssetsEndpoint
, ProductApi , ProductApi
@ -12,17 +11,10 @@ module Nuthatch.Api
, LoginSuccess , LoginSuccess
, LogoutEndpoint , LogoutEndpoint
, LogoutSuccess , LogoutSuccess
, BrowseEndpoint
, DownloadEndpoint
, DownloadSuccess , DownloadSuccess
, UploadEndpoint
, DeleteEndpoint
, DeletePrefixEndpoint
, api , api
, HTML , HTML
, JavaScript
, HtmlPage (..) , HtmlPage (..)
, BrowserScript (..)
, SessionContext (..) , SessionContext (..)
, SessionAuth , SessionAuth
, BrowsePrefix (..) , BrowsePrefix (..)
@ -32,10 +24,7 @@ module Nuthatch.Api
, HealthResponse (..) , HealthResponse (..)
, ReadyResponse (..) , ReadyResponse (..)
, BrowseResponse (..) , BrowseResponse (..)
, BrowseView (..)
, DeleteRequest (..) , DeleteRequest (..)
, DeletePrefixRequest (..)
, DeletePrefixResponse (..)
, SessionResponse (..) , SessionResponse (..)
, DeletePlanResponse (..) , DeletePlanResponse (..)
, DeleteBatchItem (..) , DeleteBatchItem (..)
@ -81,26 +70,15 @@ import Servant.Server (HasServer (..))
import Servant.Server.Experimental.Auth (AuthServerData) import Servant.Server.Experimental.Auth (AuthServerData)
data HTML data HTML
data JavaScript
instance Accept HTML where instance Accept HTML where
contentType _ = "text" // "html" /: ("charset", "utf-8") contentType _ = "text" // "html" /: ("charset", "utf-8")
instance Accept JavaScript where
contentType _ = "text" // "javascript" /: ("charset", "utf-8")
newtype HtmlPage = HtmlPage { unHtmlPage :: Text } newtype HtmlPage = HtmlPage { unHtmlPage :: Text }
deriving stock (Eq, Show) deriving stock (Eq, Show)
instance MimeRender HTML HtmlPage where instance MimeRender HTML HtmlPage where
mimeRender _ = BL.fromStrict . TE.encodeUtf8 . unHtmlPage mimeRender _ = BL.fromStrict . TE.encodeUtf8 . unHtmlPage
newtype BrowserScript = BrowserScript { unBrowserScript :: Text }
deriving stock (Eq, Show)
instance MimeRender JavaScript BrowserScript where
mimeRender _ = BL.fromStrict . TE.encodeUtf8 . unBrowserScript
data SessionContext = SessionContext data SessionContext = SessionContext
{ sessionUser :: SessionUser { sessionUser :: SessionUser
, sessionCsrf :: Maybe CsrfToken , sessionCsrf :: Maybe CsrfToken
@ -152,18 +130,6 @@ instance ToJSON BrowseResponse where
, "partial" .= browsePartial response , "partial" .= browsePartial response
] ]
data BrowseView = BrowseView
{ browseViewHtml :: HtmlPage
, browseViewJson :: BrowseResponse
}
deriving stock (Eq, Show)
instance MimeRender HTML BrowseView where
mimeRender proxy = mimeRender proxy . browseViewHtml
instance ToJSON BrowseView where
toJSON = toJSON . browseViewJson
data DeleteRequest = DeleteRequest data DeleteRequest = DeleteRequest
{ deleteRoot :: Root { deleteRoot :: Root
, deletePath :: RelativePath , deletePath :: RelativePath
@ -180,28 +146,6 @@ instance FromJSON DeleteRequest where
<*> o .: "operation_id" <*> o .: "operation_id"
<*> o .: "csrf" <*> o .: "csrf"
data DeletePrefixRequest = DeletePrefixRequest
{ deletePrefixRoot :: Root
, deletePrefixPath :: RelativePath
, deletePrefixCsrf :: CsrfToken
}
deriving stock (Eq)
instance FromJSON DeletePrefixRequest where
parseJSON = withObject "DeletePrefixRequest" $ \o ->
DeletePrefixRequest
<$> o .: "root"
<*> o .: "path"
<*> o .: "csrf"
newtype DeletePrefixResponse = DeletePrefixResponse
{ deletedObjectCount :: Int
}
deriving stock (Eq, Show)
instance ToJSON DeletePrefixResponse where
toJSON response = object ["deleted" .= deletedObjectCount response]
data UploadResponse = UploadResponse data UploadResponse = UploadResponse
{ uploadResponseOperationId :: OperationId { uploadResponseOperationId :: OperationId
, uploadResponseResult :: MutationResult , uploadResponseResult :: MutationResult
@ -462,9 +406,6 @@ type HealthEndpoint =
type ReadyEndpoint = type ReadyEndpoint =
"readyz" :> Throws ReadyError :> Get '[JSON] ReadyResponse "readyz" :> Throws ReadyError :> Get '[JSON] ReadyResponse
type BrowserScriptEndpoint =
"nuthatch.js" :> Get '[JavaScript] BrowserScript
type AppEndpoint = type AppEndpoint =
"app" "app"
:> AuthProtect SessionAuth :> AuthProtect SessionAuth
@ -524,16 +465,6 @@ type LogoutEndpoint =
:> Header' '[Required, Strict] "X-CSRF-Token" CsrfToken :> Header' '[Required, Strict] "X-CSRF-Token" CsrfToken
:> Post '[JSON] LogoutSuccess :> Post '[JSON] LogoutSuccess
type BrowseEndpoint =
AuthProtect SessionAuth
:> Throws AuthenticationError
:> Throws BrowseError
:> QueryParam "root" Root
:> QueryParam "prefix" BrowsePrefix
:> QueryParam "q" SearchQuery
:> QueryParam "continuation" PaginationToken
:> Get '[HTML, JSON] BrowseView
type DownloadSuccess = type DownloadSuccess =
Headers Headers
'[ Header "Content-Disposition" Text '[ Header "Content-Disposition" Text
@ -543,46 +474,6 @@ type DownloadSuccess =
] ]
(SourceIO BS.ByteString) (SourceIO BS.ByteString)
type DownloadEndpoint =
"download"
:> AuthProtect SessionAuth
:> Throws AuthenticationError
:> Throws DownloadError
:> QueryParam' '[Required, Strict] "root" Root
:> QueryParam' '[Required, Strict] "path" RelativePath
:> Header "Range" Text
:> StreamGet NoFraming OctetStream DownloadSuccess
type UploadEndpoint =
"upload"
:> AuthProtect SessionAuth
:> Throws AuthenticationError
:> Throws UploadError
:> QueryParam' '[Required, Strict] "root" Root
:> QueryParam' '[Required, Strict] "path" RelativePath
:> QueryParam' '[Required, Strict] "operation_id" OperationId
:> Header' '[Required, Strict] "X-Declared-Size" DeclaredSize
:> Header' '[Required, Strict] "X-Content-SHA256" ContentSha256
:> Header' '[Required, Strict] "X-CSRF-Token" CsrfToken
:> StreamBody NoFraming OctetStream (SourceIO BS.ByteString)
:> Post '[JSON] UploadResponse
type DeleteEndpoint =
"delete"
:> AuthProtect SessionAuth
:> Throws AuthenticationError
:> Throws DeleteError
:> ReqBody '[JSON] DeleteRequest
:> Post '[JSON] DeleteResponse
type DeletePrefixEndpoint =
"delete-prefix"
:> AuthProtect SessionAuth
:> Throws AuthenticationError
:> Throws DeletePrefixError
:> ReqBody '[JSON] DeletePrefixRequest
:> Post '[JSON] DeletePrefixResponse
type ProductApi = type ProductApi =
"api" :> "api" :>
( "session" ( "session"
@ -597,7 +488,7 @@ type ProductApi =
:> QueryParam "prefix" BrowsePrefix :> QueryParam "prefix" BrowsePrefix
:> QueryParam "q" SearchQuery :> QueryParam "q" SearchQuery
:> QueryParam "continuation" PaginationToken :> QueryParam "continuation" PaginationToken
:> Get '[JSON] BrowseView :> Get '[JSON] BrowseResponse
:<|> "download" :<|> "download"
:> AuthProtect SessionAuth :> AuthProtect SessionAuth
:> Throws AuthenticationError :> Throws AuthenticationError
@ -643,17 +534,11 @@ type ProductApi =
type Api = type Api =
HealthEndpoint HealthEndpoint
:<|> ReadyEndpoint :<|> ReadyEndpoint
:<|> BrowserScriptEndpoint
:<|> AppEndpoint :<|> AppEndpoint
:<|> StaticAssetsEndpoint :<|> StaticAssetsEndpoint
:<|> LoginEndpoint :<|> LoginEndpoint
:<|> OidcCallbackEndpoint :<|> OidcCallbackEndpoint
:<|> LogoutEndpoint :<|> LogoutEndpoint
:<|> BrowseEndpoint
:<|> DownloadEndpoint
:<|> UploadEndpoint
:<|> DeleteEndpoint
:<|> DeletePrefixEndpoint
:<|> ProductApi :<|> ProductApi
api :: Proxy Api api :: Proxy Api

View file

@ -94,7 +94,6 @@ import Nuthatch.Streaming
, UploadStreamError (..) , UploadStreamError (..)
) )
import Nuthatch.Types import Nuthatch.Types
import Nuthatch.Ui (browserScript, productPage)
import Servant import Servant
import Servant.Server.Experimental.Auth import Servant.Server.Experimental.Auth
( AuthHandler ( AuthHandler
@ -157,17 +156,11 @@ server :: Env -> Server Api
server env = server env =
pure (HealthResponse True) pure (HealthResponse True)
:<|> handleReady env :<|> handleReady env
:<|> pure (BrowserScript browserScript)
:<|> handleAppRequest :<|> handleAppRequest
:<|> staticAssetsServer :<|> staticAssetsServer
:<|> handleLoginRequest env :<|> handleLoginRequest env
:<|> handleOidcCallback env :<|> handleOidcCallback env
:<|> handleLogoutRequest env :<|> handleLogoutRequest env
:<|> handleBrowseRequest env
:<|> handleDownloadRequest env
:<|> handleUploadRequest env
:<|> handleDeleteRequest env
:<|> handleDeletePrefixRequest env
:<|> :<|>
( handleSessionRequest ( handleSessionRequest
:<|> handleBrowseRequest env :<|> handleBrowseRequest env
@ -396,7 +389,7 @@ handleBrowseRequest
-> Maybe BrowsePrefix -> Maybe BrowsePrefix
-> Maybe SearchQuery -> Maybe SearchQuery
-> Maybe PaginationToken -> Maybe PaginationToken
-> Handler BrowseView -> Handler BrowseResponse
handleBrowseRequest env session selectedRoot selectedPrefix search continuation = do handleBrowseRequest env session selectedRoot selectedPrefix search continuation = do
let user = sessionUser session let user = sessionUser session
root = fromMaybe Personal selectedRoot root = fromMaybe Personal selectedRoot
@ -414,14 +407,10 @@ handleBrowseRequest env session selectedRoot selectedPrefix search continuation
(throwOperationError status502Error . BrowseObjectStoreFailed) (throwOperationError status502Error . BrowseObjectStoreFailed)
pure pure
result result
let response = BrowseResponse pure BrowseResponse
{ browseEntries = entries { browseEntries = entries
, browseContinuation = PaginationToken <$> nextContinuation , browseContinuation = PaginationToken <$> nextContinuation
, browsePartial = partial , browsePartial = partial
}
pure BrowseView
{ browseViewHtml = HtmlPage (productPage entries)
, browseViewJson = response
} }
handleDownloadRequest handleDownloadRequest
@ -624,22 +613,6 @@ handleDeleteRequest env session request = do
cfgIdempotencyWindowSec (envConfig env) cfgIdempotencyWindowSec (envConfig env)
} }
handleDeletePrefixRequest
:: Env
-> SessionContext
-> DeletePrefixRequest
-> Handler DeletePrefixResponse
handleDeletePrefixRequest env session request = do
requireCsrf "delete_prefix" DeletePrefixCsrfRejected session (deletePrefixCsrf request)
liftIO $ audit "delete_prefix" "rejected"
(Just (unUserId (suUserId (sessionUser session))))
([ ("root", A.toJSON (rootLabel (deletePrefixRoot request)))
, ("reason", A.String "paged_delete_required")
] <> objectKeyAudit (envConfig env) (deletePrefixPath request))
throwOperationError err409
(DeletePrefixMutationRejected
"use delete-plan and delete-batch with client operation ids")
handleDeletePlanRequest handleDeletePlanRequest
:: Env :: Env
-> SessionContext -> SessionContext

View file

@ -1,55 +0,0 @@
module Nuthatch.Ui
( browserScript
, productPage
, htmlResponse
, escapeHtml
) where
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Lazy (fromStrict)
import qualified Data.Text.Lazy.Encoding as TLE
import Network.HTTP.Types (Status, hContentType)
import Network.Wai (Response, responseLBS)
import Nuthatch.Types
htmlResponse :: Status -> Text -> Response
htmlResponse status body =
responseLBS status
[(hContentType, "text/html; charset=utf-8")]
(TLE.encodeUtf8 (fromStrict body))
escapeHtml :: Text -> Text
escapeHtml =
T.concatMap $ \case
'&' -> "&amp;"
'<' -> "&lt;"
'>' -> "&gt;"
'"' -> "&quot;"
'\'' -> "&#39;"
c -> T.singleton c
productPage :: [ListEntry] -> Text
productPage entries =
T.unlines
[ "<!doctype html>"
, "<html lang=\"en\"><head>"
, "<meta charset=\"utf-8\">"
, "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
, "<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self'; object-src 'none'; base-uri 'none'\">"
, "<title>Nuthatch</title>"
, "</head><body><main>"
, "<h1>Nuthatch</h1>"
, "<p>The browser application has moved to <a href=\"/app\">/app</a>.</p>"
, "<ul>"
, T.concat
[ "<li>" <> escapeHtml (leRelativePath entry) <> "</li>"
| entry <- entries
]
, "</ul>"
, "</main></body></html>"
]
browserScript :: Text
browserScript =
"const nuthatchApp = '/app'; if (window.location.pathname === '/') window.location.assign(nuthatchApp);"

View file

@ -13,6 +13,7 @@ import Control.Exception
, throwIO , throwIO
, try , try
) )
import Control.Monad (forM_)
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
import Control.Retry (simulatePolicy) import Control.Retry (simulatePolicy)
import Control.Lens ((&), (?~)) import Control.Lens ((&), (?~))
@ -129,7 +130,6 @@ import Nuthatch.Streaming
, maximumMultipartBytes , maximumMultipartBytes
, multipartPartBytes , multipartPartBytes
) )
import Nuthatch.Ui (browserScript, productPage)
import Servant import Servant
import Servant.Server.Experimental.Auth (AuthHandler, mkAuthHandler) import Servant.Server.Experimental.Auth (AuthHandler, mkAuthHandler)
import Servant.Types.SourceT import Servant.Types.SourceT
@ -623,8 +623,8 @@ apiTests = testGroup "typed HTTP boundary"
("prefix_mutation_rejected" `BS.isInfixOf` BL.toStrict prefixError) ("prefix_mutation_rejected" `BS.isInfixOf` BL.toStrict prefixError)
, testCase "production authentication validates cookies and callback emits secure cookies" , testCase "production authentication validates cookies and callback emits secure cookies"
testAuthenticationBoundary testAuthenticationBoundary
, testCase "legacy browser entry directs safely to the static application" , testCase "only canonical product routes are exposed"
testBrowserBridge testCanonicalProductRoutes
, testCase "links derive from the type-level API" $ , testCase "links derive from the type-level API" $
show (linkURI (safeLink api (Proxy :: Proxy HealthEndpoint))) show (linkURI (safeLink api (Proxy :: Proxy HealthEndpoint)))
@?= "healthz" @?= "healthz"
@ -648,7 +648,7 @@ testValidDecoding :: IO ()
testValidDecoding = do testValidDecoding = do
calls <- newIORef 0 calls <- newIORef 0
response <- apiRequest calls methodGet response <- apiRequest calls methodGet
"/?root=personal&prefix=docs%2Freports&q=quarterly&continuation=next" "/api/browse?root=personal&prefix=docs%2Freports&q=quarterly&continuation=next"
[] []
"" ""
simpleStatus response @?= status200 simpleStatus response @?= status200
@ -657,16 +657,17 @@ testValidDecoding = do
testInvalidDomains :: IO () testInvalidDomains :: IO ()
testInvalidDomains = do testInvalidDomains = do
calls <- newIORef 0 calls <- newIORef 0
invalidRoot <- apiRequest calls methodGet "/?root=PERSONAL" [] "" invalidRoot <- apiRequest calls methodGet "/api/browse?root=PERSONAL" [] ""
simpleStatus invalidRoot @?= status400 simpleStatus invalidRoot @?= status400
invalidPath <- apiRequest calls methodGet "/?prefix=docs%2F..%2Fprivate" [] "" invalidPath <- apiRequest calls methodGet
"/api/browse?prefix=docs%2F..%2Fprivate" [] ""
simpleStatus invalidPath @?= status400 simpleStatus invalidPath @?= status400
readIORef calls >>= (@?= 0) readIORef calls >>= (@?= 0)
testMalformedUtf8 :: IO () testMalformedUtf8 :: IO ()
testMalformedUtf8 = do testMalformedUtf8 = do
calls <- newIORef 0 calls <- newIORef 0
response <- apiRequest calls methodGet "/?q=%FF" [] "" response <- apiRequest calls methodGet "/api/browse?q=%FF" [] ""
simpleStatus response @?= status400 simpleStatus response @?= status400
readIORef calls >>= (@?= 0) readIORef calls >>= (@?= 0)
@ -681,16 +682,17 @@ testUploadDecoding = do
, ("X-CSRF-Token", csrf) , ("X-CSRF-Token", csrf)
] ]
response <- apiRequest calls methodPost response <- apiRequest calls methodPost
"/upload?root=personal&path=docs%2Ffile.txt&operation_id=bad%20id" "/api/upload?root=personal&path=docs%2Ffile.txt&operation_id=bad%20id"
headers headers
"body" "body"
simpleStatus response @?= status400 simpleStatus response @?= status400
invalidSize <- apiRequest calls methodPost invalidSize <- apiRequest calls methodPost
"/upload?root=personal&path=docs%2Ffile.txt&operation_id=upload-1" "/api/upload?root=personal&path=docs%2Ffile.txt&operation_id=upload-1"
(replaceHeader "X-Declared-Size" "zero" headers) (replaceHeader "X-Declared-Size" "zero" headers)
"body" "body"
simpleStatus invalidSize @?= status400 simpleStatus invalidSize @?= status400
invalidPagination <- apiRequest calls methodGet "/?continuation=" [] "" invalidPagination <- apiRequest calls methodGet
"/api/browse?continuation=" [] ""
simpleStatus invalidPagination @?= status400 simpleStatus invalidPagination @?= status400
readIORef calls >>= (@?= 0) readIORef calls >>= (@?= 0)
@ -705,7 +707,7 @@ testStreamingEndpoints = do
, ("X-CSRF-Token", csrf) , ("X-CSRF-Token", csrf)
] ]
upload <- apiRequest calls methodPost upload <- apiRequest calls methodPost
"/upload?root=personal&path=docs%2Ffile.txt&operation_id=upload-1" "/api/upload?root=personal&path=docs%2Ffile.txt&operation_id=upload-1"
headers headers
"body" "body"
simpleStatus upload @?= status200 simpleStatus upload @?= status200
@ -713,7 +715,7 @@ testStreamingEndpoints = do
("\"idempotency_window_seconds\":3600" ("\"idempotency_window_seconds\":3600"
`BS.isInfixOf` BL.toStrict (simpleBody upload)) `BS.isInfixOf` BL.toStrict (simpleBody upload))
download <- apiRequest calls methodGet download <- apiRequest calls methodGet
"/download?root=shared&path=docs%2Ffile.txt" "/api/download?root=shared&path=docs%2Ffile.txt"
[] []
"" ""
simpleStatus download @?= status200 simpleStatus download @?= status200
@ -735,10 +737,10 @@ testOperationRepresentations = do
[("Cookie", "nuthatch_oidc=transaction")] [("Cookie", "nuthatch_oidc=transaction")]
"" ""
simpleStatus callback @?= status303 simpleStatus callback @?= status303
legacyLogin <- apiRequest calls methodPost "/login" [] "" wrongLoginMethod <- apiRequest calls methodPost "/login" [] ""
simpleStatus legacyLogin @?= status405 simpleStatus wrongLoginMethod @?= status405
deletion <- apiRequest calls methodPost deletion <- apiRequest calls methodPost
"/delete" "/api/delete"
[("Content-Type", "application/json")] [("Content-Type", "application/json")]
"{\"root\":\"shared\",\"path\":\"docs/file.txt\",\"operation_id\":\"delete-1\",\"csrf\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}" "{\"root\":\"shared\",\"path\":\"docs/file.txt\",\"operation_id\":\"delete-1\",\"csrf\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}"
simpleStatus deletion @?= status200 simpleStatus deletion @?= status200
@ -748,7 +750,7 @@ testOperationRepresentations = do
("\"idempotency_window_seconds\":3600" ("\"idempotency_window_seconds\":3600"
`BS.isInfixOf` BL.toStrict (simpleBody deletion)) `BS.isInfixOf` BL.toStrict (simpleBody deletion))
malformed <- apiRequest calls methodPost malformed <- apiRequest calls methodPost
"/delete" "/api/delete"
[("Content-Type", "application/json")] [("Content-Type", "application/json")]
"{\"root\":\"shared\",\"path\":\"../escape\"}" "{\"root\":\"shared\",\"path\":\"../escape\"}"
simpleStatus malformed @?= status400 simpleStatus malformed @?= status400
@ -815,7 +817,6 @@ typedTestServer :: IORef Int -> Server Api
typedTestServer calls = typedTestServer calls =
counted (HealthResponse True) counted (HealthResponse True)
:<|> counted (ReadyResponse True) :<|> counted (ReadyResponse True)
:<|> counted (BrowserScript "script")
:<|> (\_ -> counted (addHeader "default-src 'none'" (HtmlPage "app"))) :<|> (\_ -> counted (addHeader "default-src 'none'" (HtmlPage "app")))
:<|> (\_ -> Tagged $ \_ sendResponse -> :<|> (\_ -> Tagged $ \_ sendResponse ->
sendResponse (responseLBS status404 [] "")) sendResponse (responseLBS status404 [] ""))
@ -831,29 +832,6 @@ typedTestServer calls =
(addHeader "session=expired" (addHeader "session=expired"
(addHeader "csrf=expired" (addHeader "csrf=expired"
(addHeader "transaction=expired" NoContent)))) (addHeader "transaction=expired" NoContent))))
:<|> (\_ _ _ _ _ -> counted
(BrowseView
(HtmlPage "browse")
BrowseResponse
{ browseEntries = []
, browseContinuation = Nothing
, browsePartial = False
}))
:<|> (\_ _ _ _ -> counted
(addHeader "attachment; filename=\"file.txt\""
(addHeader "nosniff"
(addHeader "sandbox" (addHeader 4 (source ["body"]))))))
:<|> (\_ _ _ operation _ _ _ _ -> counted UploadResponse
{ uploadResponseOperationId = operation
, uploadResponseResult = MutationSucceeded
, uploadResponseIdempotencyWindowSeconds = 3600
})
:<|> (\_ request -> counted DeleteResponse
{ deleteResponseOperationId = deleteOperationId request
, deleteResponseResult = MutationSucceeded
, deleteResponseIdempotencyWindowSeconds = 3600
})
:<|> (\_ _ -> counted (DeletePrefixResponse 1))
:<|> :<|>
( (\session -> counted SessionResponse ( (\session -> counted SessionResponse
{ sessionResponseUserId = suUserId (sessionUser session) { sessionResponseUserId = suUserId (sessionUser session)
@ -883,13 +861,11 @@ typedTestServer calls =
counted :: a -> Handler a counted :: a -> Handler a
counted value = liftIO (modifyIORef' calls (+ 1)) >> pure value counted value = liftIO (modifyIORef' calls (+ 1)) >> pure value
emptyBrowse = emptyBrowse =
BrowseView BrowseResponse
(HtmlPage "browse") { browseEntries = []
BrowseResponse , browseContinuation = Nothing
{ browseEntries = [] , browsePartial = False
, browseContinuation = Nothing }
, browsePartial = False
}
downloadResponse = downloadResponse =
addHeader "attachment; filename=\"file.txt\"" $ addHeader "attachment; filename=\"file.txt\"" $
addHeader "nosniff" $ addHeader "nosniff" $
@ -932,7 +908,7 @@ testAuthenticationBoundary = do
"nuthatch_session=" <> TE.encodeUtf8 (unSessionToken token) "nuthatch_session=" <> TE.encodeUtf8 (unSessionToken token)
<> "; nuthatch_csrf=" <> TE.encodeUtf8 (unCsrfToken csrf) <> "; nuthatch_csrf=" <> TE.encodeUtf8 (unCsrfToken csrf)
authenticated <- apiRequestWith app methodGet authenticated <- apiRequestWith app methodGet
"/?root=personal" "/api/browse?root=personal"
[("Cookie", cookie)] [("Cookie", cookie)]
"" ""
simpleStatus authenticated @?= status200 simpleStatus authenticated @?= status200
@ -940,7 +916,7 @@ testAuthenticationBoundary = do
writeIORef calls 0 writeIORef calls 0
malformed <- apiRequestWith app methodGet malformed <- apiRequestWith app methodGet
"/?root=personal" "/api/browse?root=personal"
[("Cookie", "nuthatch_session=" <> BS.singleton 255 [("Cookie", "nuthatch_session=" <> BS.singleton 255
<> "; nuthatch_csrf=" <> TE.encodeUtf8 (unCsrfToken csrf))] <> "; nuthatch_csrf=" <> TE.encodeUtf8 (unCsrfToken csrf))]
"" ""
@ -948,7 +924,7 @@ testAuthenticationBoundary = do
readIORef calls >>= (@?= 0) readIORef calls >>= (@?= 0)
malformedGrammar <- apiRequestWith app methodGet malformedGrammar <- apiRequestWith app methodGet
"/?root=personal" "/api/browse?root=personal"
[("Cookie", "nuthatch_session")] [("Cookie", "nuthatch_session")]
"" ""
simpleStatus malformedGrammar @?= status400 simpleStatus malformedGrammar @?= status400
@ -961,33 +937,33 @@ testAuthenticationBoundary = do
| otherwise -> prefix <> "a" | otherwise -> prefix <> "a"
Nothing -> "invalid" Nothing -> "invalid"
tampered <- apiRequestWith app methodGet tampered <- apiRequestWith app methodGet
"/?root=personal" "/api/browse?root=personal"
[("Cookie", "nuthatch_session=" <> TE.encodeUtf8 tamperedToken)] [("Cookie", "nuthatch_session=" <> TE.encodeUtf8 tamperedToken)]
"" ""
simpleStatus tampered @?= status303 simpleStatus tampered @?= status303
readIORef calls >>= (@?= 0) readIORef calls >>= (@?= 0)
missingCsrf <- apiRequestWith app methodGet missingCsrf <- apiRequestWith app methodGet
"/?root=personal" "/api/browse?root=personal"
[("Cookie", "nuthatch_session=" <> TE.encodeUtf8 (unSessionToken token))] [("Cookie", "nuthatch_session=" <> TE.encodeUtf8 (unSessionToken token))]
"" ""
simpleStatus missingCsrf @?= status200 simpleStatus missingCsrf @?= status200
readIORef calls >>= (@?= 1) readIORef calls >>= (@?= 1)
testBrowserBridge :: IO () testCanonicalProductRoutes :: IO ()
testBrowserBridge = do testCanonicalProductRoutes = do
let entry = ListEntry calls <- newIORef 0
{ leName = "folder" forM_
, leIsPrefix = True [ (methodGet, "/")
, leSize = Nothing , (methodGet, "/nuthatch.js")
, leRelativePath = "folder<script>" , (methodGet, "/download?root=personal&path=file")
} , (methodPost, "/upload")
browse = productPage [entry] , (methodPost, "/delete")
assertBool "legacy page directs browsers to the static application" , (methodPost, "/delete-prefix")
("/app" `T.isInfixOf` browse && "/app" `T.isInfixOf` browserScript) ] $ \(method, path) -> do
assertBool "legacy page escapes object paths" response <- apiRequest calls method path [] ""
("folder&lt;script&gt;" `T.isInfixOf` browse simpleStatus response @?= status404
&& not ("folder<script>" `T.isInfixOf` browse)) readIORef calls >>= (@?= 0)
productionAuthTestApplication :: Config -> IORef Int -> Application productionAuthTestApplication :: Config -> IORef Int -> Application
productionAuthTestApplication cfg calls = productionAuthTestApplication cfg calls =

View file

@ -278,17 +278,6 @@ spec:
grep -F '"ok":true' grep -F '"ok":true'
curl --insecure --fail --silent --show-error "$base/readyz" | curl --insecure --fail --silent --show-error "$base/readyz" |
grep -F '"ready":true' grep -F '"ready":true'
curl --insecure --fail --silent --show-error "$base/nuthatch.js" |
grep -F 'nuthatch'
{
curl --insecure --fail --silent --show-error "$base/nuthatch.js"
curl --insecure --silent --show-error "$base/login"
} > /tmp/browser-visible
if grep -Ei \
'NUTHATCH_S3|AWS_ACCESS_KEY|AWS_SECRET_ACCESS_KEY|client_secret' \
/tmp/browser-visible; then
exit 1
fi
test "$( test "$(
curl --insecure --silent --output /dev/null \ curl --insecure --silent --output /dev/null \
--write-out '%{http_code}' \ --write-out '%{http_code}' \
@ -299,11 +288,11 @@ spec:
curl --insecure --silent --output /dev/null \ curl --insecure --silent --output /dev/null \
--write-out '%{http_code}' \ --write-out '%{http_code}' \
"$base/" "$base/"
)" = 303 )" = 404
test "$( test "$(
curl --insecure --silent --output /dev/null \ curl --insecure --silent --output /dev/null \
--write-out '%{http_code}' \ --write-out '%{http_code}' \
"$base/download?root=personal&path=unauthenticated" "$base/api/download?root=personal&path=unauthenticated"
)" = 303 )" = 303
unauthenticated_digest="$( unauthenticated_digest="$(
printf x | sha256sum | cut -d' ' -f1 printf x | sha256sum | cut -d' ' -f1
@ -315,14 +304,14 @@ spec:
--header "X-Content-SHA256: $unauthenticated_digest" \ --header "X-Content-SHA256: $unauthenticated_digest" \
--header 'X-CSRF-Token: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \ --header 'X-CSRF-Token: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \
--data x \ --data x \
"$base/upload?root=personal&path=unauthenticated&operation_id=40000000-0000-4000-8000-000000000001" "$base/api/upload?root=personal&path=unauthenticated&operation_id=40000000-0000-4000-8000-000000000001"
)" = 303 )" = 303
test "$( test "$(
curl --insecure --silent --output /dev/null \ curl --insecure --silent --output /dev/null \
--write-out '%{http_code}' \ --write-out '%{http_code}' \
--header 'Content-Type: application/json' \ --header 'Content-Type: application/json' \
--data '{"root":"personal","path":"unauthenticated","operation_id":"40000000-0000-4000-8000-000000000002","csrf":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}' \ --data '{"root":"personal","path":"unauthenticated","operation_id":"40000000-0000-4000-8000-000000000002","csrf":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}' \
"$base/delete" "$base/api/delete"
)" = 303 )" = 303
test "$( test "$(
curl --insecure --silent --output /dev/null \ curl --insecure --silent --output /dev/null \
@ -330,7 +319,7 @@ spec:
--header 'Content-Type: application/json' \ --header 'Content-Type: application/json' \
--data '{"root":"personal","path":"unauthenticated","csrf":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}' \ --data '{"root":"personal","path":"unauthenticated","csrf":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}' \
"$base/delete-prefix" "$base/delete-prefix"
)" = 303 )" = 404
test "$( test "$(
curl --insecure --silent --output /dev/null \ curl --insecure --silent --output /dev/null \
--write-out '%{http_code}' \ --write-out '%{http_code}' \
@ -348,14 +337,14 @@ spec:
curl --insecure --silent --output /tmp/authenticated.body \ curl --insecure --silent --output /tmp/authenticated.body \
--write-out '%{http_code}' \ --write-out '%{http_code}' \
--cookie "$cookies" \ --cookie "$cookies" \
"$base/" "$base/api/browse"
)" )"
printf 'Authenticated browse status=%s\n' "$authenticated_status" printf 'Authenticated browse status=%s\n' "$authenticated_status"
test "$authenticated_status" = 200 test "$authenticated_status" = 200
curl --insecure --fail --silent --show-error \ curl --insecure --fail --silent --show-error \
--cookie "$cookies" \ --cookie "$cookies" \
--header 'Accept: application/json' \ --header 'Accept: application/json' \
"$base/" >/tmp/authenticated.json "$base/api/browse" >/tmp/authenticated.json
if grep -Ei \ if grep -Ei \
'NUTHATCH_S3|AWS_ACCESS_KEY|AWS_SECRET_ACCESS_KEY|client_secret' \ 'NUTHATCH_S3|AWS_ACCESS_KEY|AWS_SECRET_ACCESS_KEY|client_secret' \
/tmp/authenticated.body /tmp/authenticated.json; then /tmp/authenticated.body /tmp/authenticated.json; then
@ -514,6 +503,11 @@ spec:
/tmp/product-asset.headers >/dev/null /tmp/product-asset.headers >/dev/null
grep -i '^x-content-type-options: *nosniff' \ grep -i '^x-content-type-options: *nosniff' \
/tmp/product-asset.headers >/dev/null /tmp/product-asset.headers >/dev/null
if grep -Ei \
'NUTHATCH_S3|AWS_ACCESS_KEY|AWS_SECRET_ACCESS_KEY|client_secret' \
/tmp/product-app.html /tmp/product-asset.js; then
exit 1
fi
for marker in \ for marker in \
'Drop a file here' \ 'Drop a file here' \
'Shared root' \ 'Shared root' \
@ -768,7 +762,7 @@ spec:
first_size="$(wc -c </tmp/epoch-one | tr -d ' ')" first_size="$(wc -c </tmp/epoch-one | tr -d ' ')"
first_digest="$(sha256sum /tmp/epoch-one | cut -d' ' -f1)" first_digest="$(sha256sum /tmp/epoch-one | cut -d' ' -f1)"
operation=73000000-0000-4000-8000-000000000001 operation=73000000-0000-4000-8000-000000000001
mutation_url="$base/upload?root=personal&path=idempotency.txt&operation_id=$operation" mutation_url="$base/api/upload?root=personal&path=idempotency.txt&operation_id=$operation"
pod_curl "$NUTHATCH_IDEMPOTENCY_IP_A" \ pod_curl "$NUTHATCH_IDEMPOTENCY_IP_A" \
--fail --silent --show-error \ --fail --silent --show-error \
--cookie "$cookies" \ --cookie "$cookies" \
@ -880,7 +874,7 @@ spec:
pod_curl "$NUTHATCH_IDEMPOTENCY_IP_A" \ pod_curl "$NUTHATCH_IDEMPOTENCY_IP_A" \
--fail --silent --show-error \ --fail --silent --show-error \
--cookie "$cookies" \ --cookie "$cookies" \
"$base/download?root=personal&path=idempotency.txt" \ "$base/api/download?root=personal&path=idempotency.txt" \
>/tmp/epoch-download >/tmp/epoch-download
cmp "$epoch_two_body" /tmp/epoch-download cmp "$epoch_two_body" /tmp/epoch-download
@ -890,7 +884,7 @@ spec:
sha256sum /tmp/active-body | cut -d' ' -f1 sha256sum /tmp/active-body | cut -d' ' -f1
)" )"
active_operation=73000000-0000-4000-8000-000000000002 active_operation=73000000-0000-4000-8000-000000000002
active_url="$base/upload?root=personal&path=active-window.bin&operation_id=$active_operation" active_url="$base/api/upload?root=personal&path=active-window.bin&operation_id=$active_operation"
( (
dd if=/tmp/active-body bs=65536 count=1 status=none dd if=/tmp/active-body bs=65536 count=1 status=none
sleep 5 sleep 5
@ -929,7 +923,7 @@ spec:
--header "X-Content-SHA256: $first_digest" \ --header "X-Content-SHA256: $first_digest" \
--header "X-CSRF-Token: $csrf" \ --header "X-CSRF-Token: $csrf" \
--data-binary @/tmp/epoch-one \ --data-binary @/tmp/epoch-one \
"$base/upload?root=personal&path=active-window.bin&operation_id=73000000-0000-4000-8000-000000000003" "$base/api/upload?root=personal&path=active-window.bin&operation_id=73000000-0000-4000-8000-000000000003"
)" )"
printf 'Active key lock status=%s body=' "$lock_status" printf 'Active key lock status=%s body=' "$lock_status"
cat /tmp/active-lock.json cat /tmp/active-lock.json
@ -983,7 +977,7 @@ spec:
--header "X-CSRF-Token: $csrf" \ --header "X-CSRF-Token: $csrf" \
--request POST \ --request POST \
--upload-file - \ --upload-file - \
"$base/upload?root=personal&path=streaming-large.bin&operation_id=70000000-0000-4000-8000-000000000001" "$base/api/upload?root=personal&path=streaming-large.bin&operation_id=70000000-0000-4000-8000-000000000001"
)" )"
printf 'Large upload status=%s\n' "$status" printf 'Large upload status=%s\n' "$status"
test "$status" = 200 test "$status" = 200
@ -992,7 +986,7 @@ spec:
download_digest="$( download_digest="$(
curl --fail --silent --show-error \ curl --fail --silent --show-error \
--cookie "$cookies" \ --cookie "$cookies" \
"$base/download?root=personal&path=streaming-large.bin" | "$base/api/download?root=personal&path=streaming-large.bin" |
sha256sum | sha256sum |
cut -d' ' -f1 cut -d' ' -f1
)" )"
@ -1013,7 +1007,7 @@ spec:
--header "X-Content-SHA256: $large_digest" \ --header "X-Content-SHA256: $large_digest" \
--header "X-CSRF-Token: $csrf" \ --header "X-CSRF-Token: $csrf" \
--data-binary @/tmp/tiny \ --data-binary @/tmp/tiny \
"$base/upload?root=personal&path=streaming-large.bin&operation_id=70000000-0000-4000-8000-000000000007" "$base/api/upload?root=personal&path=streaming-large.bin&operation_id=70000000-0000-4000-8000-000000000007"
)" )"
test "$status" = 200 test "$status" = 200
grep -F '"result":"succeeded"' \ grep -F '"result":"succeeded"' \
@ -1029,7 +1023,7 @@ spec:
--header "X-Content-SHA256: $tiny_digest" \ --header "X-Content-SHA256: $tiny_digest" \
--header "X-CSRF-Token: $csrf" \ --header "X-CSRF-Token: $csrf" \
--data-binary @/tmp/tiny \ --data-binary @/tmp/tiny \
"$base/upload?root=personal&path=size-mismatch.bin&operation_id=70000000-0000-4000-8000-000000000002" "$base/api/upload?root=personal&path=size-mismatch.bin&operation_id=70000000-0000-4000-8000-000000000002"
)" )"
printf 'Size mismatch status=%s body=' "$status" printf 'Size mismatch status=%s body=' "$status"
cat /tmp/size-mismatch.json cat /tmp/size-mismatch.json
@ -1046,7 +1040,7 @@ spec:
--header "X-Content-SHA256: $(printf '%064d' 0)" \ --header "X-Content-SHA256: $(printf '%064d' 0)" \
--header "X-CSRF-Token: $csrf" \ --header "X-CSRF-Token: $csrf" \
--data-binary @/tmp/tiny \ --data-binary @/tmp/tiny \
"$base/upload?root=personal&path=identity-mismatch.bin&operation_id=70000000-0000-4000-8000-000000000003" "$base/api/upload?root=personal&path=identity-mismatch.bin&operation_id=70000000-0000-4000-8000-000000000003"
)" )"
printf 'Identity mismatch status=%s body=' "$status" printf 'Identity mismatch status=%s body=' "$status"
cat /tmp/identity-mismatch.json cat /tmp/identity-mismatch.json
@ -1069,7 +1063,7 @@ spec:
--header "X-CSRF-Token: $csrf" \ --header "X-CSRF-Token: $csrf" \
--request POST \ --request POST \
--upload-file - \ --upload-file - \
"$base/upload?root=personal&path=too-large.bin&operation_id=70000000-0000-4000-8000-000000000004" "$base/api/upload?root=personal&path=too-large.bin&operation_id=70000000-0000-4000-8000-000000000004"
)" )"
printf 'Over-limit status=%s body=' "$status" printf 'Over-limit status=%s body=' "$status"
cat /tmp/too-large.json cat /tmp/too-large.json
@ -1089,7 +1083,7 @@ spec:
--header "X-CSRF-Token: $csrf" \ --header "X-CSRF-Token: $csrf" \
--request POST \ --request POST \
--upload-file - \ --upload-file - \
"$base/upload?root=personal&path=aborted-upload.bin&operation_id=70000000-0000-4000-8000-000000000005" \ "$base/api/upload?root=personal&path=aborted-upload.bin&operation_id=70000000-0000-4000-8000-000000000005" \
>/tmp/aborted-upload.json; then >/tmp/aborted-upload.json; then
exit 1 exit 1
fi fi
@ -1097,7 +1091,7 @@ spec:
curl --silent --output /dev/null \ curl --silent --output /dev/null \
--write-out '%{http_code}' \ --write-out '%{http_code}' \
--cookie "$cookies" \ --cookie "$cookies" \
"$base/download?root=personal&path=aborted-upload.bin" "$base/api/download?root=personal&path=aborted-upload.bin"
)" )"
printf 'Aborted upload lookup status=%s\n' \ printf 'Aborted upload lookup status=%s\n' \
"$aborted_upload_status" "$aborted_upload_status"
@ -1112,7 +1106,7 @@ spec:
--header "X-Content-SHA256: $large_digest" \ --header "X-Content-SHA256: $large_digest" \
--header "X-CSRF-Token: $csrf" \ --header "X-CSRF-Token: $csrf" \
--data-binary @/tmp/tiny \ --data-binary @/tmp/tiny \
"$base/upload?root=personal&path=aborted-upload.bin&operation_id=70000000-0000-4000-8000-000000000005" "$base/api/upload?root=personal&path=aborted-upload.bin&operation_id=70000000-0000-4000-8000-000000000005"
)" )"
printf 'Aborted upload replay status=%s body=' "$status" printf 'Aborted upload replay status=%s body=' "$status"
cat /tmp/aborted-upload-replay.json cat /tmp/aborted-upload-replay.json
@ -1132,14 +1126,14 @@ spec:
--header "X-CSRF-Token: $csrf" \ --header "X-CSRF-Token: $csrf" \
--request POST \ --request POST \
--upload-file - \ --upload-file - \
"$base/upload?root=personal&path=streaming-large.bin&operation_id=70000000-0000-4000-8000-000000000006" \ "$base/api/upload?root=personal&path=streaming-large.bin&operation_id=70000000-0000-4000-8000-000000000006" \
>/tmp/aborted-overwrite.json; then >/tmp/aborted-overwrite.json; then
exit 1 exit 1
fi fi
preserved_digest="$( preserved_digest="$(
curl --fail --silent --show-error \ curl --fail --silent --show-error \
--cookie "$cookies" \ --cookie "$cookies" \
"$base/download?root=personal&path=streaming-large.bin" | "$base/api/download?root=personal&path=streaming-large.bin" |
sha256sum | sha256sum |
cut -d' ' -f1 cut -d' ' -f1
)" )"
@ -1148,7 +1142,7 @@ spec:
--max-time 1 \ --max-time 1 \
--limit-rate 64k \ --limit-rate 64k \
--cookie "$cookies" \ --cookie "$cookies" \
"$base/download?root=personal&path=streaming-large.bin" \ "$base/api/download?root=personal&path=streaming-large.bin" \
>/tmp/aborted-download; then >/tmp/aborted-download; then
exit 1 exit 1
fi fi
@ -1178,7 +1172,7 @@ spec:
--header "X-Content-SHA256: $joint_digest" \ --header "X-Content-SHA256: $joint_digest" \
--header "X-CSRF-Token: $csrf" \ --header "X-CSRF-Token: $csrf" \
--data-binary @/tmp/joint-upload \ --data-binary @/tmp/joint-upload \
"$base/upload?root=personal&path=joint-throttle.bin&operation_id=70000000-0000-4000-8000-000000000013" "$base/api/upload?root=personal&path=joint-throttle.bin&operation_id=70000000-0000-4000-8000-000000000013"
)" )"
test "$status" = 200 test "$status" = 200
grep -F '"result":"succeeded"' /tmp/joint-upload.json grep -F '"result":"succeeded"' /tmp/joint-upload.json
@ -1197,7 +1191,7 @@ spec:
--header "X-Content-SHA256: $upload_digest" \ --header "X-Content-SHA256: $upload_digest" \
--header "X-CSRF-Token: $csrf_b" \ --header "X-CSRF-Token: $csrf_b" \
--data-binary @/tmp/throttle-upload \ --data-binary @/tmp/throttle-upload \
"$base/upload?root=personal&path=throttled-upload.bin&operation_id=71000000-0000-4000-8000-000000000001" "$base/api/upload?root=personal&path=throttled-upload.bin&operation_id=71000000-0000-4000-8000-000000000001"
)" )"
test "$status" = 429 test "$status" = 429
grep -F '"error":"bandwidth_exceeded"' /tmp/throttle-upload.json grep -F '"error":"bandwidth_exceeded"' /tmp/throttle-upload.json
@ -1211,15 +1205,15 @@ spec:
--header "X-Content-SHA256: $upload_digest" \ --header "X-Content-SHA256: $upload_digest" \
--header "X-CSRF-Token: $csrf_b" \ --header "X-CSRF-Token: $csrf_b" \
--data-binary @/tmp/tiny \ --data-binary @/tmp/tiny \
"$base/upload?root=personal&path=prestart.bin&operation_id=71000000-0000-4000-8000-000000000002" "$base/api/upload?root=personal&path=prestart.bin&operation_id=71000000-0000-4000-8000-000000000002"
)" = 429 )" = 429
curl --silent --show-error --cookie "$cookies" \ curl --silent --show-error --cookie "$cookies" \
"$base/download?root=personal&path=streaming-large.bin" \ "$base/api/download?root=personal&path=streaming-large.bin" \
>/tmp/parallel-a & >/tmp/parallel-a &
first_pid=$! first_pid=$!
curl --silent --show-error --cookie "$cookies" \ curl --silent --show-error --cookie "$cookies" \
"$base/download?root=personal&path=streaming-large.bin" \ "$base/api/download?root=personal&path=streaming-large.bin" \
>/tmp/parallel-b & >/tmp/parallel-b &
second_pid=$! second_pid=$!
first_ok=true first_ok=true
@ -1244,7 +1238,7 @@ spec:
curl --silent --output /tmp/prestart-download.json \ curl --silent --output /tmp/prestart-download.json \
--write-out '%{http_code}' \ --write-out '%{http_code}' \
--cookie "$cookies" \ --cookie "$cookies" \
"$base/download?root=personal&path=streaming-large.bin" "$base/api/download?root=personal&path=streaming-large.bin"
)" )"
printf 'Post-exhaustion download status=%s\n' "$prestart_status" printf 'Post-exhaustion download status=%s\n' "$prestart_status"
test "$prestart_status" = 429 test "$prestart_status" = 429
@ -1275,7 +1269,7 @@ spec:
--header "X-Content-SHA256: $refill_digest" \ --header "X-Content-SHA256: $refill_digest" \
--header "X-CSRF-Token: $csrf" \ --header "X-CSRF-Token: $csrf" \
--data-binary @/tmp/refill-upload \ --data-binary @/tmp/refill-upload \
"$base/upload?root=personal&path=refill-$suffix.bin&operation_id=$refill_operation" "$base/api/upload?root=personal&path=refill-$suffix.bin&operation_id=$refill_operation"
)" )"
test "$status" = 200 test "$status" = 200
grep -F '"result":"succeeded"' "/tmp/refill-$suffix.json" grep -F '"result":"succeeded"' "/tmp/refill-$suffix.json"
@ -1289,7 +1283,7 @@ spec:
--header "X-Content-SHA256: $refill_digest" \ --header "X-Content-SHA256: $refill_digest" \
--header "X-CSRF-Token: $csrf" \ --header "X-CSRF-Token: $csrf" \
--data-binary @/tmp/refill-upload \ --data-binary @/tmp/refill-upload \
"$base/upload?root=personal&path=refill-exhausted.bin&operation_id=72000000-0000-4000-8000-000000000003" "$base/api/upload?root=personal&path=refill-exhausted.bin&operation_id=72000000-0000-4000-8000-000000000003"
)" = 429 )" = 429
fi fi
done done
@ -1329,7 +1323,7 @@ spec:
--header "X-Content-SHA256: $tiny_digest" \ --header "X-Content-SHA256: $tiny_digest" \
--header "X-CSRF-Token: $csrf" \ --header "X-CSRF-Token: $csrf" \
--data-binary @/tmp/tiny \ --data-binary @/tmp/tiny \
"$base/upload?root=personal&path=unavailable.bin&operation_id=72000000-0000-4000-8000-000000000001" "$base/api/upload?root=personal&path=unavailable.bin&operation_id=72000000-0000-4000-8000-000000000001"
)" = 503 )" = 503
grep -F '"error":"throttle_unavailable"' \ grep -F '"error":"throttle_unavailable"' \
/tmp/unavailable-upload.json /tmp/unavailable-upload.json
@ -1337,7 +1331,7 @@ spec:
curl --silent --output /tmp/unavailable-download.json \ curl --silent --output /tmp/unavailable-download.json \
--write-out '%{http_code}' \ --write-out '%{http_code}' \
--cookie "$cookies" \ --cookie "$cookies" \
"$base/download?root=personal&path=$unavailable_path" "$base/api/download?root=personal&path=$unavailable_path"
)" = 503 )" = 503
grep -F '"error":"throttle_unavailable"' \ grep -F '"error":"throttle_unavailable"' \
/tmp/unavailable-download.json /tmp/unavailable-download.json
@ -1363,7 +1357,7 @@ spec:
--header "X-Content-SHA256: $digest" \ --header "X-Content-SHA256: $digest" \
--header "X-CSRF-Token: $csrf" \ --header "X-CSRF-Token: $csrf" \
--data-binary @/tmp/body \ --data-binary @/tmp/body \
"$base/upload?root=$root&path=smoke-$suffix.txt&operation_id=00000000-0000-4000-8000-0000000000${suffix}" "$base/api/upload?root=$root&path=smoke-$suffix.txt&operation_id=00000000-0000-4000-8000-0000000000${suffix}"
)" )"
printf 'Upload %s status=%s\n' "$root" "$upload_status" printf 'Upload %s status=%s\n' "$root" "$upload_status"
test "$upload_status" = 200 test "$upload_status" = 200
@ -1371,11 +1365,11 @@ spec:
curl --fail --silent --show-error \ curl --fail --silent --show-error \
--cookie "$cookies" \ --cookie "$cookies" \
--header 'Accept: application/json' \ --header 'Accept: application/json' \
"$base/?root=$root" | "$base/api/browse?root=$root" |
grep -F "smoke-$suffix.txt" grep -F "smoke-$suffix.txt"
curl --fail --silent --show-error \ curl --fail --silent --show-error \
--cookie "$cookies" \ --cookie "$cookies" \
"$base/download?root=$root&path=smoke-$suffix.txt" >/tmp/download "$base/api/download?root=$root&path=smoke-$suffix.txt" >/tmp/download
test "$(cat /tmp/download)" = "$first" test "$(cat /tmp/download)" = "$first"
printf '%s' "$second" >/tmp/body printf '%s' "$second" >/tmp/body
@ -1388,22 +1382,22 @@ spec:
--header "X-Content-SHA256: $digest" \ --header "X-Content-SHA256: $digest" \
--header "X-CSRF-Token: $csrf" \ --header "X-CSRF-Token: $csrf" \
--data-binary @/tmp/body \ --data-binary @/tmp/body \
"$base/upload?root=$root&path=smoke-$suffix.txt&operation_id=10000000-0000-4000-8000-0000000000${suffix}" | "$base/api/upload?root=$root&path=smoke-$suffix.txt&operation_id=10000000-0000-4000-8000-0000000000${suffix}" |
grep -F '"result":"succeeded"' grep -F '"result":"succeeded"'
curl --fail --silent --show-error \ curl --fail --silent --show-error \
--cookie "$cookies" \ --cookie "$cookies" \
"$base/download?root=$root&path=smoke-$suffix.txt" >/tmp/download "$base/api/download?root=$root&path=smoke-$suffix.txt" >/tmp/download
test "$(cat /tmp/download)" = "$second" test "$(cat /tmp/download)" = "$second"
curl --fail --silent --show-error \ curl --fail --silent --show-error \
--cookie "$cookies" \ --cookie "$cookies" \
--header 'Content-Type: application/json' \ --header 'Content-Type: application/json' \
--data "{\"root\":\"$root\",\"path\":\"smoke-$suffix.txt\",\"operation_id\":\"20000000-0000-4000-8000-0000000000${suffix}\",\"csrf\":\"$csrf\"}" \ --data "{\"root\":\"$root\",\"path\":\"smoke-$suffix.txt\",\"operation_id\":\"20000000-0000-4000-8000-0000000000${suffix}\",\"csrf\":\"$csrf\"}" \
"$base/delete" | "$base/api/delete" |
grep -F '"result":"succeeded"' grep -F '"result":"succeeded"'
if curl --fail --silent \ if curl --fail --silent \
--cookie "$cookies" \ --cookie "$cookies" \
"$base/download?root=$root&path=smoke-$suffix.txt" >/dev/null; then "$base/api/download?root=$root&path=smoke-$suffix.txt" >/dev/null; then
exit 1 exit 1
fi fi
} }
@ -1420,7 +1414,7 @@ spec:
--header "X-Content-SHA256: $digest" \ --header "X-Content-SHA256: $digest" \
--header "X-CSRF-Token: $csrf" \ --header "X-CSRF-Token: $csrf" \
--data-binary @/tmp/body \ --data-binary @/tmp/body \
"$base/upload?root=personal&path=cross-user.txt&operation_id=30000000-0000-4000-8000-000000000001" | "$base/api/upload?root=personal&path=cross-user.txt&operation_id=30000000-0000-4000-8000-000000000001" |
grep -F '"result":"succeeded"' grep -F '"result":"succeeded"'
curl --insecure --fail --silent --show-error \ curl --insecure --fail --silent --show-error \
--cookie "$cookies" \ --cookie "$cookies" \
@ -1429,7 +1423,7 @@ spec:
--header "X-Content-SHA256: $digest" \ --header "X-Content-SHA256: $digest" \
--header "X-CSRF-Token: $csrf" \ --header "X-CSRF-Token: $csrf" \
--data-binary @/tmp/body \ --data-binary @/tmp/body \
"$base/upload?root=shared&path=cross-user.txt&operation_id=30000000-0000-4000-8000-000000000002" | "$base/api/upload?root=shared&path=cross-user.txt&operation_id=30000000-0000-4000-8000-000000000002" |
grep -F '"result":"succeeded"' grep -F '"result":"succeeded"'
csrf_b="$(login "$NUTHATCH_OIDC_USER_B" b)" csrf_b="$(login "$NUTHATCH_OIDC_USER_B" b)"
@ -1472,14 +1466,14 @@ spec:
if curl --insecure --fail --silent --show-error \ if curl --insecure --fail --silent --show-error \
--cookie /tmp/cookies-b \ --cookie /tmp/cookies-b \
--header 'Accept: application/json' \ --header 'Accept: application/json' \
"$base/?root=personal" | "$base/api/browse?root=personal" |
grep -F cross-user.txt; then grep -F cross-user.txt; then
exit 1 exit 1
fi fi
curl --insecure --fail --silent --show-error \ curl --insecure --fail --silent --show-error \
--cookie /tmp/cookies-b \ --cookie /tmp/cookies-b \
--header 'Accept: application/json' \ --header 'Accept: application/json' \
"$base/?root=shared" | "$base/api/browse?root=shared" |
grep -F cross-user.txt grep -F cross-user.txt
if curl --insecure --fail --silent \ if curl --insecure --fail --silent \
@ -1507,7 +1501,7 @@ spec:
curl --insecure --silent --output /dev/null \ curl --insecure --silent --output /dev/null \
--write-out '%{http_code}' \ --write-out '%{http_code}' \
--cookie "$cookies" \ --cookie "$cookies" \
"$base/" "$base/api/browse"
)" = 303 )" = 303
printf 'OIDC app smoke passed\n' printf 'OIDC app smoke passed\n'
env: env:

View file

@ -51,7 +51,11 @@ if grep -Eiq '\b(TODO|TBD|unresolved|open question)\b' "$document"; then
fi fi
grep -F 'e0240ab33521a1882fb647a79f7fd3c74c9b6348b29bdb14e572204517882859' "$document" >/dev/null grep -F 'e0240ab33521a1882fb647a79f7fd3c74c9b6348b29bdb14e572204517882859' "$document" >/dev/null
grep -F 'Public `GET /nuthatch.js`' "$document" >/dev/null grep -F 'Authenticated `/api/session`, `/api/browse`, `/api/download`' "$document" >/dev/null
if grep -Fq '/nuthatch.js' "$document"; then
printf '%s\n' "Durable contracts contain a removed browser bridge" >&2
exit 1
fi
grep -F '`nuthatch-sha256`' "$document" >/dev/null grep -F '`nuthatch-sha256`' "$document" >/dev/null
grep -F '`x-amz-meta-nuthatch-sha256`' "$document" >/dev/null grep -F '`x-amz-meta-nuthatch-sha256`' "$document" >/dev/null
grep -F 'always streams the full body and computes its digest' "$document" >/dev/null grep -F 'always streams the full body and computes its digest' "$document" >/dev/null