1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
|
module Sproxy.Server (
server
) where
import Control.Concurrent (forkIO)
import Control.Exception (bracketOnError)
import Control.Monad (void, when)
import Data.ByteString as BS (hGetLine, readFile)
import Data.ByteString.Char8 (pack)
import Data.HashMap.Strict as HM (fromList, lookup, toList)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Word (Word16)
import Data.Yaml (decodeFileEither)
import Network.HTTP.Client (Manager, ManagerSettings(..), defaultManagerSettings, newManager, socketConnection)
import Network.HTTP.Client.Internal (Connection)
import Network.Socket ( Family(AF_INET, AF_UNIX), SockAddr(SockAddrInet, SockAddrUnix),
SocketOption(ReuseAddr), SocketType(Stream), bind, close, connect, inet_addr,
listen, maxListenQueue, setSocketOption, socket )
import Network.Wai.Handler.WarpTLS (tlsSettingsChain, runTLSSocket)
import Network.Wai.Handler.Warp ( defaultSettings, runSettingsSocket,
setHTTP2Disabled, setOnException )
import System.Entropy (getEntropy)
import System.Environment (setEnv)
import System.Exit (exitFailure)
import System.FilePath.Glob (compile)
import System.IO (IOMode(ReadMode), hIsEOF, hPutStrLn, stderr, withFile)
import System.Posix.User ( GroupEntry(..), UserEntry(..),
getAllGroupEntries, getRealUserID,
getUserEntryForName, setGroupID, setGroups, setUserID )
import Sproxy.Application (sproxy, redirect)
import Sproxy.Application.OAuth2.Common (OAuth2Client)
import Sproxy.Config (BackendConf(..), ConfigFile(..), OAuth2Conf(..))
import qualified Sproxy.Application.OAuth2 as OAuth2
import qualified Sproxy.Logging as Log
import qualified Sproxy.Server.DB as DB
server :: FilePath -> IO ()
server configFile = do
cf <- readConfigFile configFile
Log.start $ cfLogLevel cf
Log.debug $ show cf
sock <- socket AF_INET Stream 0
setSocketOption sock ReuseAddr 1
bind sock $ SockAddrInet (fromIntegral $ cfListen cf) 0
maybe80 <- if fromMaybe (443 == cfListen cf) (cfListen80 cf)
then do
sock80 <- socket AF_INET Stream 0
setSocketOption sock80 ReuseAddr 1
bind sock80 $ SockAddrInet 80 0
return (Just sock80)
else
return Nothing
uid <- getRealUserID
when (0 == uid) $ do
let user = cfUser cf
Log.info $ "switching to user " ++ show user
u <- getUserEntryForName user
groupIDs <- map groupID . filter (elem user . groupMembers)
<$> getAllGroupEntries
setGroups groupIDs
setGroupID $ userGroupID u
setUserID $ userID u
ds <- newDataSource cf
db <- DB.start (cfHome cf) ds
key <- maybe
(Log.info "using new random key" >> getEntropy 32)
(\f -> Log.info ("reading key from " ++ f) >> BS.readFile f)
(cfKey cf)
case maybe80 of
Nothing -> return ()
Just sock80 -> do
Log.info "listening on port 80 (HTTP redirect)"
listen sock80 maxListenQueue
void . forkIO $ runSettingsSocket defaultSettings sock80 (redirect $ cfListen cf)
oauth2clients <- HM.fromList <$> mapM newOAuth2Client (HM.toList (cfOAuth2 cf))
backends <-
mapM (\be -> do
m <- newBackendManager be
return (compile $ beName be, be, m)
) $ cfBackends cf
let
settings =
(if cfHTTP2 cf then id else setHTTP2Disabled) $
setOnException (\_ _ -> return ())
defaultSettings
-- XXX 2048 is from bindPortTCP from streaming-commons used internally by runTLS.
-- XXX Since we don't call runTLS, we listen socket here with the same options.
Log.info $ "listening on port " ++ show (cfListen cf) ++ " (HTTPS)"
listen sock (max 2048 maxListenQueue)
runTLSSocket
(tlsSettingsChain (cfSslCert cf) (cfSslCertChain cf) (cfSslKey cf))
settings
sock
(sproxy key db oauth2clients backends)
newDataSource :: ConfigFile -> IO (Maybe DB.DataSource)
newDataSource cf =
case (cfDataFile cf, cfDatabase cf) of
(Nothing, Just str) -> do
case cfPgPassFile cf of
Nothing -> return ()
Just f -> do
Log.info $ "pgpassfile: " ++ show f
setEnv "PGPASSFILE" f
return . Just $ DB.PostgreSQL str
(Just f, Nothing) -> return . Just $ DB.File f
(Nothing, Nothing) -> return Nothing
_ -> do
Log.error "only one data source can be used"
exitFailure
newOAuth2Client :: (Text, OAuth2Conf) -> IO (Text, OAuth2Client)
newOAuth2Client (name, cfg) =
case HM.lookup name OAuth2.providers of
Nothing -> do Log.error $ "OAuth2 provider " ++ show name ++ " is not supported"
exitFailure
Just provider -> do
Log.info $ "oauth2: adding " ++ show name
client_secret <- withFile secret_file ReadMode $ \h -> do
empty <- hIsEOF h
if empty then do
Log.error $ "oauth2: empty secret file for "
++ show name ++ ": " ++ show secret_file
return $ pack ""
else BS.hGetLine h
return (name, provider (pack client_id, client_secret))
where client_id = oa2ClientId cfg
secret_file = oa2ClientSecret cfg
newBackendManager :: BackendConf -> IO Manager
newBackendManager be = do
openConn <-
case (beSocket be, bePort be) of
(Just f, Nothing) -> do
Log.info $ "backend `" ++ beName be ++ "' on UNIX socket " ++ f
return $ openUnixSocketConnection f
(Nothing, Just n) -> do
Log.info $ "backend `" ++ beName be ++ "' on " ++ beAddress be ++ ":" ++ show n
return $ openTCPConnection (beAddress be) n
_ -> do
Log.error "either backend port number or UNIX socket path is required."
exitFailure
newManager defaultManagerSettings {
managerRawConnection = return $ \_ _ _ -> openConn
, managerConnCount = beConnCount be
}
openUnixSocketConnection :: FilePath -> IO Connection
openUnixSocketConnection f =
bracketOnError
(socket AF_UNIX Stream 0)
close
(\s -> do
connect s (SockAddrUnix f)
socketConnection s 8192)
openTCPConnection :: String -> Word16 -> IO Connection
openTCPConnection addr port =
bracketOnError
(socket AF_INET Stream 0)
close
(\s -> do
a <- inet_addr addr
connect s (SockAddrInet (fromIntegral port) a)
socketConnection s 8192)
readConfigFile :: FilePath -> IO ConfigFile
readConfigFile f = do
r <- decodeFileEither f
case r of
Left e -> do
hPutStrLn stderr $ "FATAL: " ++ f ++ ": " ++ show e
exitFailure
Right cf -> return cf
|