2026-10-02 14:53:02 +08:00
{-# LANGUAGE OverloadedStrings #-}
module ChuSQL.Server.TCP (
ServerConfig ( .. ),
defaultServerConfig ,
loadServerConfigAt ,
ServerEnv ( .. ),
2026-10-03 22:08:40 +08:00
LiveConnections ,
newLiveConnections ,
2026-10-02 14:53:02 +08:00
newServerEnv ,
ServerHandle ( .. ),
startServer ,
runServer ,
2026-10-05 22:22:23 +08:00
isLocalPeer ,
2026-10-02 14:53:02 +08:00
) where
import ChuSQL.Interface.Config ( ServerConfig ( .. ), defaultServerConfig , loadServerConfigAt )
2026-10-05 22:22:23 +08:00
import ChuSQL.Interface.Sudo ( SudoCredential , createSudoCredential , removeSudoCredential , sudoChallenge , validSudoChallenge , isLocalPeer )
import Data.Time.Clock ( UTCTime , getCurrentTime )
2026-10-02 14:53:02 +08:00
import ChuSQL.Interface.Policy ( PasswordPolicy ( .. ))
import ChuSQL.Interface.Protocol ( ClientRequest ( .. ), ServerResponse ( .. ), decodeRequest , encodeResponse , protocolVersion )
import ChuSQL.Interface.RateLimit ( RateLimiter , rateLimitBlock , rateLimitClear , rateLimitRecord )
2026-10-03 02:17:46 +08:00
import qualified ChuSQL.Core.Engine.Error as E
2026-10-05 00:15:01 +08:00
import ChuSQL.Core.Protocol ( Account ( .. ), Request ( ReqIdentityInitialize ))
2026-10-02 14:53:02 +08:00
import ChuSQL.Server.Backend ( Backend ( .. ))
2026-10-05 22:22:23 +08:00
import ChuSQL.Server.Session ( QueryResult ( .. ), Session , SessionError ( .. ), accounts , authenticateSessionCoded , authenticateSudoSessionCoded , isPlainIdentifier , catalog , databases , newSessionWith , policyOf , reloadPolicy , roleViews , runStatementCoded , runStorageCoded , sessionIsAdmin , sessionUser )
import ChuSQL.Server.Security ( writeSecurity )
import ChuSQL.Server.Privileges ( newPrivileges , reconcileObjects )
2026-10-02 14:53:02 +08:00
import Control.Concurrent ( forkIO )
import Control.Concurrent.MVar ( MVar , modifyMVar_ , newMVar , readMVar )
2026-10-05 22:22:23 +08:00
import Control.Exception ( IOException , bracket , finally , onException , try )
2026-10-03 22:08:40 +08:00
import Control.Monad ( filterM , void )
2026-10-02 14:53:02 +08:00
import qualified Data.ByteString.Char8 as BSC
import qualified Data.ByteString.Lazy.Char8 as BLC
2026-10-03 22:08:40 +08:00
import Data.IORef ( IORef , atomicModifyIORef' , newIORef , readIORef , writeIORef )
import qualified Data.Map.Strict as Map
2026-10-02 14:53:02 +08:00
import Data.Maybe ( fromMaybe )
import Data.Text ( Text )
import qualified Data.Text as T
import Network.Socket (
AddrInfo ( .. ),
AddrInfoFlag ( AI_PASSIVE ),
SockAddr ( SockAddrInet , SockAddrInet6 ),
Socket ,
SocketOption ( ReuseAddr ),
SocketType ( Stream ),
accept ,
addrAddress ,
addrFamily ,
bind ,
close ,
defaultHints ,
defaultProtocol ,
getAddrInfo ,
getSocketName ,
2026-10-05 22:22:23 +08:00
getPeerName ,
2026-10-02 14:53:02 +08:00
listen ,
setSocketOption ,
socket ,
socketToHandle ,
)
import System.IO ( BufferMode ( LineBuffering ), IOMode ( ReadWriteMode ), hClose , hFlush , hPutStrLn , hSetBuffering , hSetNewlineMode , noNewlineTranslation , stderr )
import System.IO.Error ( isEOFError )
-- TCP 服务器:监听端口,一连接一会话,按一行一 JSON 收发协议消息。
2026-10-03 22:08:40 +08:00
-- 在线连接与服务器环境
2026-10-04 14:44:40 +08:00
-- | 一条在线连接:登录账号与「已被断开」标记
data LiveEntry = LiveEntry
{ leWho :: IORef ( Maybe Text )
, leDropped :: IORef Bool
2026-10-05 22:22:23 +08:00
, leLocal :: Bool
, leChallenge :: IORef ( Maybe ( Text , UTCTime ))
2026-10-04 14:44:40 +08:00
}
-- | 在线连接登记:自增编号到连接条目
2026-10-03 22:08:40 +08:00
data LiveConnections = LiveConnections
{ lcNext :: IORef Int
2026-10-04 14:44:40 +08:00
, lcLive :: MVar ( Map . Map Int LiveEntry )
2026-10-03 22:08:40 +08:00
}
-- | 建一张空的在线连接登记表
newLiveConnections :: IO LiveConnections
newLiveConnections = LiveConnections <$> newIORef 0 <*> newMVar Map . empty
2026-10-04 14:44:40 +08:00
-- | 新建一条连接条目
2026-10-05 22:22:23 +08:00
newLiveEntry :: Bool -> IO LiveEntry
newLiveEntry local = LiveEntry <$> newIORef Nothing <*> newIORef False <*> pure local <*> newIORef Nothing
2026-10-04 14:44:40 +08:00
2026-10-03 22:08:40 +08:00
-- | 登记一条连接,返回注销动作
2026-10-04 14:44:40 +08:00
registerConnection :: LiveConnections -> LiveEntry -> IO ( IO () )
registerConnection live entry = do
2026-10-03 22:08:40 +08:00
n <- atomicModifyIORef' ( lcNext live ) ( \ i -> ( i + 1 , i ))
2026-10-04 14:44:40 +08:00
modifyMVar_ ( lcLive live ) ( pure . Map . insert n entry )
2026-10-03 22:08:40 +08:00
pure ( modifyMVar_ ( lcLive live ) ( pure . Map . delete n ))
-- | 断开这些账号的在线连接,返回断开的条数
dropConnectionsOf :: LiveConnections -> [ Text ] -> IO Int
dropConnectionsOf live names = do
let wanted = map T . toLower ( filter ( not . T . null ) names )
entries <- readMVar ( lcLive live )
hits <- filterM ( isOf wanted ) ( Map . elems entries )
2026-10-04 14:44:40 +08:00
mapM_ markDropped hits
2026-10-03 22:08:40 +08:00
pure ( length hits )
where
-- | 这条连接登录的账号在名单里吗
2026-10-04 14:44:40 +08:00
isOf wanted entry = do
current <- readIORef ( leWho entry )
2026-10-03 22:08:40 +08:00
pure ( maybe False ( \ name -> T . toLower name ` elem ` wanted ) current )
2026-10-04 14:44:40 +08:00
-- | 只做标记:连接在下一次请求应答后收工
markDropped entry = writeIORef ( leDropped entry ) True
2026-10-03 22:08:40 +08:00
2026-10-02 14:53:02 +08:00
-- 服务器
data ServerEnv = ServerEnv
{ srvBackend :: Backend
2026-10-03 02:17:46 +08:00
, srvRootName :: Text
2026-10-02 14:53:02 +08:00
, srvSettingsFile :: FilePath
, srvConfig :: ServerConfig
, srvLimiter :: RateLimiter
2026-10-03 22:08:40 +08:00
, srvLive :: LiveConnections
2026-10-05 22:22:23 +08:00
, srvSudo :: MVar ( Maybe ( Int , Text , SudoCredential ))
2026-10-02 14:53:02 +08:00
}
-- | 组装服务器环境:每个连接再各开一个会话
2026-10-03 02:17:46 +08:00
newServerEnv :: Backend -> Text -> FilePath -> ServerConfig -> RateLimiter -> IO ServerEnv
2026-10-03 22:08:40 +08:00
newServerEnv backend rootName settingsFile config limiter = do
2026-10-05 00:15:01 +08:00
migrated <- beAccounts backend ( ReqIdentityInitialize rootName )
case migrated of
Left err -> ioError ( userError ( "identity migration failed: " ++ err ++ "; stop all processes and try csql-bootstrap repair --config <settings.toml>; use recover if data restoration is required" ))
Right identities
| any ( \ a -> accountEnabled a && accountCanLogin a && accountIsSuperuser a ) identities -> pure ()
| otherwise -> ioError ( userError "identity migration failed: no enabled login superuser; stop all processes and try csql-bootstrap recover --config <settings.toml>; reset replaces system identities and grants" )
2026-10-03 22:08:40 +08:00
live <- newLiveConnections
2026-10-05 22:22:23 +08:00
privileges <- newPrivileges backend
recovered <- writeSecurity ( reconcileObjects privileges )
case recovered of
Left err -> ioError ( userError ( "object privilege recovery failed: " ++ show err ))
Right () -> pure ()
sudo <- newMVar Nothing
pure ( ServerEnv backend rootName settingsFile config limiter live sudo )
2026-10-02 14:53:02 +08:00
data ServerHandle = ServerHandle
{ shPort :: Int
, shStop :: IO ()
}
-- | 起监听并在线程里收连接(测试与嵌入用)
startServer :: ServerEnv -> IO ServerHandle
startServer env = do
sock <- listenSocket ( scHost ( srvConfig env )) ( scPort ( srvConfig env ))
port <- actualPort sock
2026-10-05 22:22:23 +08:00
enableSudo env port ` onException ` close sock
2026-10-02 14:53:02 +08:00
stopped <- newMVar False
_ <- forkIO ( acceptLoop env sock stopped )
2026-10-05 22:22:23 +08:00
pure ( ServerHandle port ( stopListening sock stopped ` finally ` disableSudo env ))
2026-10-02 14:53:02 +08:00
-- | 在调用线程里收连接(可执行文件用);绑好端口先回调一次
runServer :: ServerEnv -> ( Int -> IO () ) -> IO ()
2026-10-05 22:22:23 +08:00
runServer env announce = bracket ( listenSocket ( scHost ( srvConfig env )) ( scPort ( srvConfig env ))) close $ \ sock -> do
2026-10-02 14:53:02 +08:00
port <- actualPort sock
2026-10-05 22:22:23 +08:00
enableSudo env port
( do
announce port
stopped <- newMVar False
acceptLoop env sock stopped ) ` finally ` disableSudo env
-- 为显式配置的本机身份映射创建凭据。
enableSudo :: ServerEnv -> Int -> IO ()
enableSudo env port
| T . null user = pure ()
| not ( isPlainIdentifier user ) = ioError ( userError "sudo_auth_user must be a plain identity name" )
| otherwise = modifyMVar_ ( srvSudo env ) $ \ current -> case current of
Just _ -> ioError ( userError "sudo authentication is already running" )
Nothing -> do
credential <- createSudoCredential port
hPutStrLn stderr ( "sudo_auth enabled: local elevated identity -> " ++ T . unpack user )
pure ( Just ( port , user , credential ))
where
user = scSudoUser ( srvConfig env )
-- 清理本次服务实例的本机认证凭据。
disableSudo :: ServerEnv -> IO ()
disableSudo env = modifyMVar_ ( srvSudo env ) $ \ current -> do
case current of
Nothing -> pure ()
Just ( port , _ , _ ) -> removeSudoCredential port
pure Nothing
2026-10-02 14:53:02 +08:00
-- | 置停止标记并关掉监听套接字
stopListening :: Socket -> MVar Bool -> IO ()
stopListening sock stopped = do
modifyMVar_ stopped ( const ( pure True ))
void ( try ( close sock ) :: IO ( Either IOException () ))
-- | 建监听套接字;端口 0 表示由系统分配
listenSocket :: String -> Int -> IO Socket
listenSocket host port = do
let hints = defaultHints { addrSocketType = Stream , addrFlags = [ AI_PASSIVE ]}
node = if null host then Nothing else Just host
infos <- getAddrInfo ( Just hints ) node ( Just ( show port ))
case infos of
[] -> ioError ( userError ( "cannot resolve listen address: " ++ host ))
( info : _ ) -> do
sock <- socket ( addrFamily info ) Stream defaultProtocol
setSocketOption sock ReuseAddr 1
bind sock ( addrAddress info )
listen sock 128
pure sock
-- | 系统分配端口后读回真正的端口
actualPort :: Socket -> IO Int
actualPort sock = do
address <- getSocketName sock
pure $ case address of
SockAddrInet port _ -> fromIntegral port
SockAddrInet6 port _ _ _ -> fromIntegral port
_ -> 0
-- | 循环 accept,把每个连接交给独立线程
acceptLoop :: ServerEnv -> Socket -> MVar Bool -> IO ()
acceptLoop env sock stopped = loop
where
-- | 等下一个连接
loop = do
stop <- readMVar stopped
if stop
then pure ()
else do
accepted <- try ( accept sock ) :: IO ( Either IOException ( Socket , SockAddr ))
case accepted of
Left err -> do
stopping <- readMVar stopped
if stopping then pure () else hPutStrLn stderr ( "accept failed: " ++ show err )
Right ( conn , _ ) -> do
void ( forkIO ( reportConnection env conn ))
loop
-- | 连接线程:异常写到 stderr
reportConnection :: ServerEnv -> Socket -> IO ()
reportConnection env conn = do
outcome <- try ( serveConnection env conn ) :: IO ( Either IOException () )
case outcome of
Left err -> hPutStrLn stderr ( "connection failed: " ++ show err )
Right () -> pure ()
-- | 一个连接一个会话:一行一条 JSON,读到 EOF 就收工
serveConnection :: ServerEnv -> Socket -> IO ()
serveConnection env conn = do
2026-10-05 22:22:23 +08:00
peer <- getPeerName conn
2026-10-02 14:53:02 +08:00
h <- socketToHandle conn ReadWriteMode
hSetBuffering h LineBuffering
hSetNewlineMode h noNewlineTranslation
2026-10-05 22:22:23 +08:00
entry <- newLiveEntry ( isLocalPeer peer )
2026-10-04 14:44:40 +08:00
unregister <- registerConnection ( srvLive env ) entry
2026-10-03 22:08:40 +08:00
session <- newSessionWith ( srvBackend env ) ( srvRootName env ) ( srvSettingsFile env ) ( \ names -> void ( dropConnectionsOf ( srvLive env ) names ))
2026-10-04 14:44:40 +08:00
outcome <- try ( loop h session entry ` finally ` unregister ) :: IO ( Either IOException () )
2026-10-02 14:53:02 +08:00
case outcome of
Left err | not ( isEOFError err ) -> hPutStrLn stderr ( "connection error: " ++ show err )
_ -> pure ()
void ( try ( hClose h ) :: IO ( Either IOException () ))
where
-- | 逐行读请求并应答
2026-10-04 14:44:40 +08:00
loop h session entry = do
2026-10-02 14:53:02 +08:00
line <- BSC . hGetLine h
let cleaned = BSC . dropWhileEnd ( == '\ r' ) line
if BSC . length cleaned > scMaxMessage ( srvConfig env )
2026-10-04 14:44:40 +08:00
then send h ( RespError "too_large" "request line too large" ) >> loop h session entry
2026-10-02 14:53:02 +08:00
else case decodeRequest cleaned of
2026-10-04 14:44:40 +08:00
Left message -> send h ( RespError "bad_request" message ) >> loop h session entry
2026-10-02 14:53:02 +08:00
Right request -> do
2026-10-04 14:44:40 +08:00
expired <- readIORef ( leDropped entry )
2026-10-03 22:08:40 +08:00
if expired
then send h ( RespError "unauthorized" "session was dropped by an account change" )
else do
2026-10-04 14:44:40 +08:00
( response , keepGoing ) <- dispatch env session entry request
2026-10-03 22:08:40 +08:00
send h response
2026-10-04 14:44:40 +08:00
if keepGoing then loop h session entry else pure ()
2026-10-02 14:53:02 +08:00
-- | 发一条响应并冲缓冲
send h response = do
BLC . hPutStr h ( encodeResponse response )
BLC . hPutStr h " \n "
hFlush h
-- | 一条请求一条响应,quit 后停连接
2026-10-04 14:44:40 +08:00
dispatch :: ServerEnv -> Session -> LiveEntry -> ClientRequest -> IO ( ServerResponse , Bool )
dispatch env session entry request = case request of
2026-10-02 14:53:02 +08:00
ReqHello version
| version == protocolVersion -> pure ( RespHello protocolVersion , True )
| otherwise -> pure ( RespError "bad_request" ( "unsupported protocol version: " <> tshow version ), True )
ReqLogin user password -> do
2026-10-05 22:22:23 +08:00
writeIORef ( leChallenge entry ) Nothing
2026-10-02 14:53:02 +08:00
response <- login env session user password
2026-10-03 22:08:40 +08:00
case response of
2026-10-04 14:44:40 +08:00
RespLogin who _ -> writeIORef ( leWho entry ) ( Just who )
2026-10-03 22:08:40 +08:00
_ -> pure ()
2026-10-02 14:53:02 +08:00
pure ( response , True )
2026-10-05 22:22:23 +08:00
ReqSudoChallenge -> do
configured <- readMVar ( srvSudo env )
who <- sessionUser session
case configured of
Just _ | leLocal entry && who == Nothing -> do
nonce <- sudoChallenge
now <- getCurrentTime
writeIORef ( leChallenge entry ) ( Just ( nonce , now ))
pure ( RespSudoChallenge nonce , True )
_ -> pure ( RespError "forbidden" "local sudo authentication is unavailable" , True )
ReqSudoLogin user proof -> do
response <- sudoLogin env session entry user proof
pure ( response , True )
2026-10-02 14:53:02 +08:00
ReqQuery sql -> do
who <- sessionUser session
response <- case who of
Nothing -> pure ( RespError "unauthorized" "sign in first" )
Just _
| T . null ( T . strip sql ) -> pure ( RespError "bad_request" "sql must not be empty" )
| otherwise -> do
result <- runStatementCoded session sql
pure $ case result of
Left err -> RespError ( sessCode err ) ( sessMessage err )
Right queryResult -> RespResult ( trimResult ( scMaxRows ( srvConfig env )) queryResult )
pure ( response , True )
ReqStorage payload -> do
2026-10-05 22:22:23 +08:00
result <- runStorageCoded session payload
let response = case result of
Left err -> RespError ( sessCode err ) ( sessMessage err )
Right value -> RespStorage value
2026-10-02 14:53:02 +08:00
pure ( response , True )
ReqCatalog -> do
response <- overSession session ( catalog session ) RespCatalog
pure ( response , True )
ReqDatabases -> do
response <- overSession session ( databases session ) RespDatabases
pure ( response , True )
ReqRoles -> do
response <- overSession session ( roleViews session ) RespRoles
pure ( response , True )
ReqAccounts -> do
response <- overSession session ( accounts session ) RespAccounts
pure ( response , True )
ReqPolicy -> do
response <- overSession session ( policyOf session ) policyResponse
pure ( response , True )
ReqReloadPolicy -> do
response <- overSession session ( reloadPolicy session ) policyResponse
pure ( response , True )
ReqPing -> pure ( RespPong , True )
ReqQuit -> pure ( RespBye , False )
2026-10-05 22:22:23 +08:00
-- 消费一次性证明并登录本机映射账号。
sudoLogin :: ServerEnv -> Session -> LiveEntry -> Text -> Text -> IO ServerResponse
sudoLogin env session entry user proof = do
challenge <- atomicModifyIORef' ( leChallenge entry ) ( \ value -> ( Nothing , value ))
configured <- readMVar ( srvSudo env )
now <- getCurrentTime
who <- sessionUser session
let key = T . toLower ( T . strip user )
mapped = maybe "" ( \ ( _ , name , _ ) -> name ) configured
valid = leLocal entry && who == Nothing && key == mapped
&& case configured of
Just ( _ , _ , credential ) -> validSudoChallenge credential now challenge key proof
Nothing -> False
blocked <- rateLimitBlock ( srvLimiter env ) key
response <- case blocked of
Just _ -> pure ( RespError "too_many_attempts" "too many failed sudo logins" )
Nothing | not valid -> do
rateLimitRecord ( srvLimiter env ) key
pure ( RespError "unauthorized" "sudo authentication refused" )
Nothing -> do
result <- authenticateSudoSessionCoded session key
case result of
Left err -> do
rateLimitRecord ( srvLimiter env ) key
pure ( RespError ( sessCode err ) ( sessMessage err ))
Right () -> do
rateLimitClear ( srvLimiter env ) key
writeIORef ( leWho entry ) ( Just key )
admin <- sessionIsAdmin session
pure ( RespLogin key admin )
hPutStrLn stderr ( "sudo_auth attempt: mapped_user=" ++ T . unpack mapped
++ " local=" ++ show ( leLocal entry ) ++ " result=" ++ auditResult response )
pure response
where
-- 提取不包含凭据的认证审计结果。
auditResult RespLogin {} = "accepted"
auditResult ( RespError code _ ) = T . unpack code
auditResult _ = "refused"
2026-10-02 14:53:02 +08:00
-- | 口令策略在线上只有两个数字:最短长度与至少几类字符
policyResponse :: PasswordPolicy -> ServerResponse
policyResponse ( PasswordPolicy minLength classes ) = RespPolicy minLength classes
-- | 会话方法统一口径:没登录先挡,其余失败按码表落到响应
overSession :: Session -> IO ( Either Text a ) -> ( a -> ServerResponse ) -> IO ServerResponse
overSession session action wrap = do
who <- sessionUser session
case who of
Nothing -> pure ( RespError "unauthorized" "sign in first" )
Just _ -> do
result <- action
pure $ case result of
Left message -> RespError ( sessionCode message ) message
Right value -> wrap value
-- | 会话层的失败文案归一到协议错误码
sessionCode :: Text -> Text
2026-10-03 02:17:46 +08:00
sessionCode = T . pack . E . errorCode . T . unpack
2026-10-02 14:53:02 +08:00
-- | 登录:按账号名限流,成功清账
login :: ServerEnv -> Session -> Text -> Text -> IO ServerResponse
login env session user password = do
let key = T . toLower ( T . strip user )
blocked <- rateLimitBlock ( srvLimiter env ) key
case blocked of
Just seconds -> pure ( RespError "too_many_attempts" ( "too many failed logins, retry in " <> tshow ( ceiling seconds :: Integer ) <> "s" ))
Nothing -> do
result <- authenticateSessionCoded session key password
case result of
Left err -> do
rateLimitRecord ( srvLimiter env ) key
pure ( RespError ( sessCode err ) ( sessMessage err ))
Right () -> do
rateLimitClear ( srvLimiter env ) key
who <- sessionUser session
admin <- sessionIsAdmin session
pure ( RespLogin ( fromMaybe key who ) admin )
-- | 按上限截行,总行数保持不变
trimResult :: Int -> QueryResult -> QueryResult
trimResult limit result
| length ( qrRows result ) > limit = result { qrRows = take limit ( qrRows result ), qrTruncated = True }
| otherwise = result
-- | show 成 Text
tshow :: Show a => a -> Text
tshow = T . pack . show