de138c642b
* p2phttp: Allow unauthenticated users to lock content by default. * p2phttp: Added --unauth-nolocking option to prevent unauthenticated users from locking content. The rationalle for this is that locking is not really a write operation, so makes sense to allow in a repository that only allows read-only access. Not supporting locking in that situation will prevent the user from dropping content from a special remote they control in cases where the other copy of the content is on the p2phttp server. Also, when p2phttp is configured to also allow authenticated access, lockcontent was resulting in a password prompt for users who had no way to authenticate. And there is no good way to distinguish between the two types of users client side. --unauth-nolocking anticipates that this might be abused, and seems better than disabling unauthenticated access entirely if a server is being attacked. It may be that rate limiting locking by IP address or similar would be an effective measure in such a situation. Or just limiting the number of locks by anonymous users that can be live at any one time. Since the impact of such an DOS attempt is limited to preventing dropping content from the server, it seems not a very appealing target anyway.
199 lines
5.8 KiB
Haskell
199 lines
5.8 KiB
Haskell
{- git-annex command
|
|
-
|
|
- Copyright 2024 Joey Hess <id@joeyh.name>
|
|
-
|
|
- Licensed under the GNU AGPL version 3 or higher.
|
|
-}
|
|
|
|
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE TypeOperators #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
module Command.P2PHttp where
|
|
|
|
import Command
|
|
import P2P.Http.Server
|
|
import P2P.Http.Url
|
|
import qualified P2P.Protocol as P2P
|
|
import Utility.Env
|
|
|
|
import Servant
|
|
import qualified Network.Wai.Handler.Warp as Warp
|
|
import qualified Network.Wai.Handler.WarpTLS as Warp
|
|
import Network.Socket (PortNumber)
|
|
import qualified Data.Map as M
|
|
import Data.String
|
|
|
|
cmd :: Command
|
|
cmd = noMessages $ withAnnexOptions [jobsOption] $
|
|
command "p2phttp" SectionPlumbing
|
|
"communicate in P2P protocol over http"
|
|
paramNothing (seek <$$> optParser)
|
|
|
|
data Options = Options
|
|
{ portOption :: Maybe PortNumber
|
|
, bindOption :: Maybe String
|
|
, certFileOption :: Maybe FilePath
|
|
, privateKeyFileOption :: Maybe FilePath
|
|
, chainFileOption :: [FilePath]
|
|
, authEnvOption :: Bool
|
|
, authEnvHttpOption :: Bool
|
|
, unauthReadOnlyOption :: Bool
|
|
, unauthAppendOnlyOption :: Bool
|
|
, unauthNoLockingOption :: Bool
|
|
, wideOpenOption :: Bool
|
|
, proxyConnectionsOption :: Maybe Integer
|
|
, clusterJobsOption :: Maybe Int
|
|
}
|
|
|
|
optParser :: CmdParamsDesc -> Parser Options
|
|
optParser _ = Options
|
|
<$> optional (option auto
|
|
( long "port" <> metavar paramNumber
|
|
<> help "specify port to listen on"
|
|
))
|
|
<*> optional (strOption
|
|
( long "bind" <> metavar paramAddress
|
|
<> help "specify address to bind to"
|
|
))
|
|
<*> optional (strOption
|
|
( long "certfile" <> metavar paramFile
|
|
<> help "TLS certificate file for HTTPS"
|
|
))
|
|
<*> optional (strOption
|
|
( long "privatekeyfile" <> metavar paramFile
|
|
<> help "TLS private key file for HTTPS"
|
|
))
|
|
<*> many (strOption
|
|
( long "chainfile" <> metavar paramFile
|
|
<> help "TLS chain file"
|
|
))
|
|
<*> switch
|
|
( long "authenv"
|
|
<> help "authenticate users from environment (https only)"
|
|
)
|
|
<*> switch
|
|
( long "authenv-http"
|
|
<> help "authenticate users from environment (including http)"
|
|
)
|
|
<*> switch
|
|
( long "unauth-readonly"
|
|
<> help "allow unauthenticated users to read the repository"
|
|
)
|
|
<*> switch
|
|
( long "unauth-appendonly"
|
|
<> help "allow unauthenticated users to read and append to the repository"
|
|
)
|
|
<*> switch
|
|
( long "unauth-nolocking"
|
|
<> help "prevent unauthenticated users from locking content in the repository"
|
|
)
|
|
<*> switch
|
|
( long "wideopen"
|
|
<> help "give unauthenticated users full read+write access"
|
|
)
|
|
<*> optional (option auto
|
|
( long "proxyconnections" <> metavar paramNumber
|
|
<> help "maximum number of idle connections when proxying"
|
|
))
|
|
<*> optional (option auto
|
|
( long "clusterjobs" <> metavar paramNumber
|
|
<> help "number of concurrent node accesses per connection"
|
|
))
|
|
|
|
seek :: Options -> CommandSeek
|
|
seek o = getAnnexWorkerPool $ \workerpool ->
|
|
withP2PConnections workerpool
|
|
(fromMaybe 1 $ proxyConnectionsOption o)
|
|
(fromMaybe 1 $ clusterJobsOption o)
|
|
(go workerpool)
|
|
where
|
|
go workerpool acquireconn = liftIO $ do
|
|
authenv <- getAuthEnv
|
|
st <- mkP2PHttpServerState acquireconn workerpool $
|
|
mkGetServerMode authenv o
|
|
let settings = Warp.setPort port $ Warp.setHost host $
|
|
Warp.defaultSettings
|
|
case (certFileOption o, privateKeyFileOption o) of
|
|
(Nothing, Nothing) -> Warp.runSettings settings (p2pHttpApp st)
|
|
(Just certfile, Just privatekeyfile) -> do
|
|
let tlssettings = Warp.tlsSettingsChain
|
|
certfile (chainFileOption o) privatekeyfile
|
|
Warp.runTLS tlssettings settings (p2pHttpApp st)
|
|
_ -> giveup "You must use both --certfile and --privatekeyfile options to enable HTTPS."
|
|
|
|
port = maybe
|
|
(fromIntegral defaultP2PHttpProtocolPort)
|
|
fromIntegral
|
|
(portOption o)
|
|
host = maybe
|
|
(fromString "*") -- both ipv4 and ipv6
|
|
fromString
|
|
(bindOption o)
|
|
|
|
mkGetServerMode :: M.Map Auth P2P.ServerMode -> Options -> GetServerMode
|
|
mkGetServerMode _ o _ Nothing
|
|
| wideOpenOption o = ServerMode
|
|
{ serverMode = P2P.ServeReadWrite
|
|
, unauthenticatedLockingAllowed = unauthlock
|
|
, authenticationAllowed = False
|
|
}
|
|
| unauthAppendOnlyOption o = ServerMode
|
|
{ serverMode = P2P.ServeAppendOnly
|
|
, unauthenticatedLockingAllowed = unauthlock
|
|
, authenticationAllowed = canauth
|
|
}
|
|
| unauthReadOnlyOption o = ServerMode
|
|
{ serverMode = P2P.ServeReadOnly
|
|
, unauthenticatedLockingAllowed = unauthlock
|
|
, authenticationAllowed = canauth
|
|
}
|
|
| otherwise = CannotServeRequests
|
|
where
|
|
canauth = authEnvOption o || authEnvHttpOption o
|
|
unauthlock = not (unauthNoLockingOption o)
|
|
mkGetServerMode authenv o issecure (Just auth) =
|
|
case (issecure, authEnvOption o, authEnvHttpOption o) of
|
|
(Secure, True, _) -> checkauth
|
|
(NotSecure, _, True) -> checkauth
|
|
_ -> noauth
|
|
where
|
|
checkauth = case M.lookup auth authenv of
|
|
Just servermode -> ServerMode
|
|
{ serverMode = servermode
|
|
, authenticationAllowed = False
|
|
, unauthenticatedLockingAllowed = False
|
|
}
|
|
Nothing -> noauth
|
|
noauth = mkGetServerMode authenv noautho issecure Nothing
|
|
noautho = o { authEnvOption = False, authEnvHttpOption = False }
|
|
|
|
getAuthEnv :: IO (M.Map Auth P2P.ServerMode)
|
|
getAuthEnv = do
|
|
environ <- getEnvironment
|
|
let permmap = M.fromList (mapMaybe parseperms environ)
|
|
return $ M.fromList $
|
|
map (addperms permmap) $
|
|
mapMaybe parseusername environ
|
|
where
|
|
parseperms (k, v) = case deprefix "GIT_ANNEX_P2PHTTP_PERMISSIONS_" k of
|
|
Nothing -> Nothing
|
|
Just username -> case v of
|
|
"readonly" -> Just
|
|
(encodeBS username, P2P.ServeReadOnly)
|
|
"appendonly" -> Just
|
|
(encodeBS username, P2P.ServeAppendOnly)
|
|
_ -> Nothing
|
|
|
|
parseusername (k, v) = case deprefix "GIT_ANNEX_P2PHTTP_PASSWORD_" k of
|
|
Nothing -> Nothing
|
|
Just username -> Just $ Auth (encodeBS username) (encodeBS v)
|
|
|
|
deprefix prefix s
|
|
| prefix `isPrefixOf` s = Just (drop (length prefix) s)
|
|
| otherwise = Nothing
|
|
|
|
addperms permmap auth@(Auth user _) =
|
|
case M.lookup user permmap of
|
|
Nothing -> (auth, P2P.ServeReadWrite)
|
|
Just perms -> (auth, perms)
|