Phase 1.1: Organize PROJECTS by difficulty level
- Create beginner/, intermediate/, advanced/ folders in PROJECTS/ - Move all existing projects to appropriate difficulty folders - Update all README.md links to reflect new structure - Update contributor links - Fix pyrightconfig.json trailing comma Projects organized: Beginner (6): simple-port-scanner, keylogger, caesar-cipher, dns-lookup, metadata-scrubber-tool, simple-vulnerability-scanner Intermediate (2): api-security-scanner, docker-security-audit Advanced (4): api-rate-limiter, encrypted-p2p-chat, bug-bounty-platform, Aenebris
This commit is contained in:
parent
d6978c4bd3
commit
a7cae3aa0f
|
|
@ -5,7 +5,7 @@
|
|||
Technical White Paper & Development Roadmap
|
||||
|
||||
Version: 0.1.0
|
||||
Date: 2025-11-12 - Project Name: Ᾰenebris
|
||||
Date: 2025-11-12 - Project Name: Ᾰenebris
|
||||
|
||||
---
|
||||
Abstract
|
||||
|
|
@ -68,14 +68,14 @@ Dhall represents a paradigm shift—a non-Turing-complete functional language sp
|
|||
|
||||
```dhall
|
||||
-- types.dhall
|
||||
let DatabaseConfig =
|
||||
{ Type =
|
||||
let DatabaseConfig =
|
||||
{ Type =
|
||||
{ host : Text
|
||||
, port : Natural
|
||||
, maxConnections : Natural
|
||||
, replica : Optional DatabaseConfig.Type
|
||||
}
|
||||
, default =
|
||||
, default =
|
||||
{ host = "localhost"
|
||||
, port = 5432
|
||||
, maxConnections = 20
|
||||
|
|
@ -87,7 +87,7 @@ let DatabaseConfig =
|
|||
let DB = ./types.dhall
|
||||
let staging = ./staging.dhall
|
||||
|
||||
in DB::{
|
||||
in DB::{
|
||||
, host = "prod-db.internal"
|
||||
, port = 5432
|
||||
, maxConnections = 100
|
||||
|
|
@ -133,11 +133,11 @@ data ServiceConfig = ServiceConfig
|
|||
-- Type-safe DSL in Haskell
|
||||
myService :: ServiceConfig
|
||||
myService = ServiceConfig
|
||||
{ routes =
|
||||
{ routes =
|
||||
[ route "/api" GET apiHandler
|
||||
, route "/health" GET healthCheck
|
||||
]
|
||||
, middleware =
|
||||
, middleware =
|
||||
[ cors allowAll
|
||||
, logging Verbose
|
||||
, auth jwtValidator
|
||||
|
|
@ -205,7 +205,7 @@ let identity : ∀(a : Type) → a → a =
|
|||
λ(a : Type) → λ(x : a) → x
|
||||
|
||||
-- Type-safe record construction
|
||||
let Config : Type =
|
||||
let Config : Type =
|
||||
{ apiKey : Text
|
||||
, endpoints : List { url : Text, port : Natural }
|
||||
, retries : Optional Natural
|
||||
|
|
@ -270,7 +270,7 @@ watchConfigFile = withManager $ \mgr -> do
|
|||
"."
|
||||
(\event -> "config.yaml" `isSuffixOf` eventPath event)
|
||||
handleConfigChange
|
||||
|
||||
|
||||
forever $ threadDelay 1000000
|
||||
|
||||
handleConfigChange :: Event -> IO ()
|
||||
|
|
@ -370,7 +370,7 @@ updateConfigs :: TVar AppConfig -> TVar CacheConfig -> IO ()
|
|||
updateConfigs appVar cacheVar = atomically $ do
|
||||
app <- readTVar appVar
|
||||
cache <- readTVar cacheVar
|
||||
|
||||
|
||||
-- Both updates happen atomically or retry
|
||||
writeTVar appVar (app { maxConnections = 100 })
|
||||
writeTVar cacheVar (cache { ttl = 3600 })
|
||||
|
|
@ -418,16 +418,16 @@ initConfigManager path = do
|
|||
Right config -> do
|
||||
currentRef <- newIORef config
|
||||
lastValidRef <- newIORef config
|
||||
|
||||
|
||||
mgr <- startManager
|
||||
|
||||
|
||||
let manager = ConfigManager currentRef lastValidRef path mgr
|
||||
|
||||
|
||||
-- Start watching
|
||||
_ <- watchDir mgr (takeDirectory path)
|
||||
(matchesFile path)
|
||||
_ <- watchDir mgr (takeDirectory path)
|
||||
(matchesFile path)
|
||||
(handleReload manager)
|
||||
|
||||
|
||||
return $ Right manager
|
||||
where
|
||||
matchesFile target event = target == eventPath event
|
||||
|
|
@ -444,27 +444,27 @@ handleReload manager event = do
|
|||
-- Save current as last valid
|
||||
current <- readIORef (currentConfig manager)
|
||||
writeIORef (lastValidConfig manager) current
|
||||
|
||||
|
||||
-- Atomic swap to new config
|
||||
atomicWriteIORef (currentConfig manager) newConfig
|
||||
|
||||
|
||||
logInfo "Config reloaded successfully"
|
||||
else
|
||||
logError "Validation failed, keeping old config"
|
||||
|
||||
|
||||
Left err -> do
|
||||
logError $ "Reload failed: " ++ err
|
||||
-- Keep running with old config
|
||||
where
|
||||
tryReload :: FilePath -> IO (Either String AppConfig)
|
||||
tryReload path =
|
||||
tryReload path =
|
||||
catch (eitherDecodeFileStrict path)
|
||||
(\(e :: SomeException) -> return $ Left $ show e)
|
||||
|
||||
|
||||
validateConfig :: AppConfig -> Bool
|
||||
validateConfig cfg =
|
||||
validateConfig cfg =
|
||||
-- Custom validation logic
|
||||
not (null $ apiKeys cfg)
|
||||
not (null $ apiKeys cfg)
|
||||
&& all isValidEndpoint (databaseEndpoints $ database cfg)
|
||||
|
||||
-- Access config safely from multiple threads
|
||||
|
|
@ -484,7 +484,7 @@ debounceReload :: IORef UTCTime -> NominalDiffTime -> IO () -> IO ()
|
|||
debounceReload lastReloadRef minInterval action = do
|
||||
now <- getCurrentTime
|
||||
lastReload <- readIORef lastReloadRef
|
||||
|
||||
|
||||
when (diffUTCTime now lastReload > minInterval) $ do
|
||||
writeIORef lastReloadRef now
|
||||
action
|
||||
|
|
@ -502,7 +502,7 @@ data DatabasePool = DatabasePool
|
|||
rotatePoolCredentials :: DatabasePool -> Credentials -> IO ()
|
||||
rotatePoolCredentials dbPool newCreds = do
|
||||
atomicWriteIORef (credentials dbPool) newCreds
|
||||
|
||||
|
||||
-- Drain old connections gradually
|
||||
-- New connections use new credentials from IORef
|
||||
drainPool (pool dbPool) gracefulDrainSeconds
|
||||
|
|
@ -556,7 +556,7 @@ loadConfigFailFast path = do
|
|||
```haskell
|
||||
import Data.Validation
|
||||
|
||||
data ValidationError =
|
||||
data ValidationError =
|
||||
InvalidPort Int
|
||||
| MissingField Text
|
||||
| InvalidFormat Text Text
|
||||
|
|
@ -571,7 +571,7 @@ validateConfig raw = Config
|
|||
validatePort p
|
||||
| p > 0 && p < 65536 = Success p
|
||||
| otherwise = Failure [InvalidPort p]
|
||||
|
||||
|
||||
validateHost h
|
||||
| not (T.null h) = Success h
|
||||
| otherwise = Failure [MissingField "host"]
|
||||
|
|
@ -595,23 +595,23 @@ loadConfigGraceful path = do
|
|||
Left err -> do
|
||||
logWarning $ "Config parse error, using defaults: " ++ err
|
||||
return defaultConfig
|
||||
|
||||
|
||||
Right rawConfig -> do
|
||||
-- Core settings must be valid
|
||||
core <- case validateCore rawConfig of
|
||||
Left errors -> error $ "Core config invalid: " ++ show errors
|
||||
Right validated -> return validated
|
||||
|
||||
|
||||
-- Optional features use defaults on error
|
||||
features <- case validateFeatures rawConfig of
|
||||
Left errors -> do
|
||||
logWarning $ "Feature config invalid, using defaults: " ++ show errors
|
||||
return defaultFeatures
|
||||
Right validated -> return validated
|
||||
|
||||
|
||||
-- Experimental flags filter out invalid entries
|
||||
let flags = filterValidFlags (rawExperimental rawConfig)
|
||||
|
||||
|
||||
return $ ConfigWithDefaults core features flags
|
||||
```
|
||||
|
||||
|
|
@ -656,15 +656,15 @@ instance FromJSON ServerConfig where
|
|||
rawPort <- o .: "port"
|
||||
when (rawPort < 1 || rawPort > 65535) $
|
||||
fail $ "Invalid port: " ++ show rawPort
|
||||
|
||||
|
||||
host <- o .: "host"
|
||||
when (T.null host) $
|
||||
fail "Host cannot be empty"
|
||||
|
||||
|
||||
workers <- o .:? "workers" .!= 4 -- Default to 4
|
||||
when (workers < 1 || workers > 1000) $
|
||||
fail $ "Invalid worker count: " ++ show workers
|
||||
|
||||
|
||||
return $ ServerConfig rawPort host workers
|
||||
```
|
||||
|
||||
|
|
@ -729,9 +729,9 @@ interpolateEnvVars template = do
|
|||
let varName = T.drop 2 $ T.dropEnd 1 match -- Strip ${ }
|
||||
maybeValue <- lookupEnv (T.unpack varName)
|
||||
case maybeValue of
|
||||
Just value ->
|
||||
Just value ->
|
||||
return $ Right $ T.replace match (T.pack value) txt
|
||||
Nothing ->
|
||||
Nothing ->
|
||||
return $ Left $ "Undefined variable: " ++ T.unpack varName
|
||||
|
||||
-- With defaults
|
||||
|
|
@ -755,7 +755,7 @@ import System.Envy
|
|||
|
||||
data AppConfig = AppConfig
|
||||
{ appDatabaseUrl :: String -- DATABASE_URL
|
||||
, appRedisHost :: String -- REDIS_HOST
|
||||
, appRedisHost :: String -- REDIS_HOST
|
||||
, appPort :: Int -- PORT
|
||||
, appDebug :: Bool -- DEBUG
|
||||
, appLogLevel :: Maybe LogLevel -- LOG_LEVEL (optional)
|
||||
|
|
@ -787,9 +787,9 @@ main = do
|
|||
import Options.Applicative
|
||||
import qualified Data.Yaml as Y
|
||||
|
||||
data ConfigSource =
|
||||
data ConfigSource =
|
||||
CLIConfig Config
|
||||
| EnvConfig Config
|
||||
| EnvConfig Config
|
||||
| FileConfig Config
|
||||
| DefaultConfig Config
|
||||
|
||||
|
|
@ -804,7 +804,7 @@ mergeConfigs sources = foldl merge defaultConfig sources
|
|||
merge base (EnvConfig env) = base { port = port env `orDefault` port base }
|
||||
merge base (FileConfig file) = base { port = port file `orDefault` port base }
|
||||
merge base (DefaultConfig _) = base
|
||||
|
||||
|
||||
orDefault :: Maybe a -> a -> a
|
||||
orDefault = fromMaybe
|
||||
|
||||
|
|
@ -813,29 +813,29 @@ loadLayeredConfig :: IO Config
|
|||
loadLayeredConfig = do
|
||||
-- 1. Load defaults
|
||||
let defaults = defaultConfig
|
||||
|
||||
|
||||
-- 2. Load system config
|
||||
systemCfg <- loadSystemConfig `catch` \(_ :: IOException) -> return Nothing
|
||||
|
||||
-- 3. Load project config
|
||||
|
||||
-- 3. Load project config
|
||||
projectCfg <- loadProjectConfig `catch` \(_ :: IOException) -> return Nothing
|
||||
|
||||
|
||||
-- 4. Load local config
|
||||
localCfg <- Y.decodeFileEither "config.yaml" >>= \case
|
||||
Left _ -> return Nothing
|
||||
Right cfg -> return $ Just cfg
|
||||
|
||||
|
||||
-- 5. Load environment variables
|
||||
envCfg <- decodeEnv :: IO (Either String EnvConfig)
|
||||
let env = either (const Nothing) Just envCfg
|
||||
|
||||
|
||||
-- 6. Parse CLI args
|
||||
cliCfg <- execParser cliParser
|
||||
|
||||
|
||||
-- Merge with precedence
|
||||
return $ mergeConfigs
|
||||
return $ mergeConfigs
|
||||
[ maybe DefaultConfig FileConfig systemCfg
|
||||
, maybe DefaultConfig FileConfig projectCfg
|
||||
, maybe DefaultConfig FileConfig projectCfg
|
||||
, maybe DefaultConfig FileConfig localCfg
|
||||
, maybe DefaultConfig EnvConfig env
|
||||
, CLIConfig cliCfg
|
||||
|
|
@ -852,7 +852,7 @@ newtype SafeEnvValue = SafeEnvValue Text
|
|||
validateEnvValue :: Text -> Either String SafeEnvValue
|
||||
validateEnvValue value
|
||||
| T.any isControlChar value = Left "Control characters not allowed"
|
||||
| T.any (== ';') value = Left "Semicolons not allowed"
|
||||
| T.any (== ';') value = Left "Semicolons not allowed"
|
||||
| T.any (== '|') value = Left "Pipes not allowed"
|
||||
| otherwise = Right $ SafeEnvValue value
|
||||
where
|
||||
|
|
@ -903,10 +903,10 @@ loadSecrets :: VaultConnection -> IO (Either String AppSecrets)
|
|||
loadSecrets conn = do
|
||||
-- Get database credentials
|
||||
dbResult <- getSecret conn (SecretPath "myapp/database") Nothing
|
||||
|
||||
|
||||
-- Get API keys
|
||||
apiResult <- getSecret conn (SecretPath "myapp/api-keys") Nothing
|
||||
|
||||
|
||||
case (dbResult, apiResult) of
|
||||
(Right dbData, Right apiData) -> do
|
||||
let dbCreds = parseCredentials $ fromSecretData dbData
|
||||
|
|
@ -923,7 +923,7 @@ updateSecret conn = do
|
|||
NoCheckAndSet
|
||||
(SecretPath "myapp/database")
|
||||
(toSecretData [("password", newPassword), ("username", "admin")])
|
||||
|
||||
|
||||
case result of
|
||||
Right version -> putStrLn $ "Updated to version " ++ show version
|
||||
Left err -> putStrLn $ "Update failed: " ++ err
|
||||
|
|
@ -966,14 +966,14 @@ loadDatabaseConfig :: IO (Either String DatabaseConfig)
|
|||
loadDatabaseConfig = do
|
||||
-- Discover credentials (IAM role, env vars, etc.)
|
||||
env <- newEnv discover
|
||||
|
||||
|
||||
-- Request secret
|
||||
let req = newGetSecretValue "production/database"
|
||||
resp <- runResourceT $ send env req
|
||||
|
||||
|
||||
-- Extract and parse
|
||||
case resp ^. getSecretValueResponse_secretString of
|
||||
Just jsonString ->
|
||||
Just jsonString ->
|
||||
case A.eitherDecode (encodeUtf8 jsonString) of
|
||||
Right config -> return $ Right config
|
||||
Left err -> return $ Left $ "Parse error: " ++ err
|
||||
|
|
@ -983,13 +983,13 @@ loadDatabaseConfig = do
|
|||
rotateSecret :: Text -> IO ()
|
||||
rotateSecret secretId = do
|
||||
env <- newEnv discover
|
||||
|
||||
|
||||
let req = newRotateSecret secretId
|
||||
& rotateSecret_rotationLambdaARN ?~ lambdaArn
|
||||
& rotateSecret_rotationRules ?~
|
||||
& rotateSecret_rotationRules ?~
|
||||
newRotationRulesType
|
||||
& rotationRulesType_automaticallyAfterDays ?~ 30
|
||||
|
||||
|
||||
_ <- runResourceT $ send env req
|
||||
putStrLn "Rotation initiated"
|
||||
```
|
||||
|
|
@ -1038,7 +1038,7 @@ withRotatingCreds rc action = do
|
|||
Just prev -> action prev -- Fallback during rotation window
|
||||
Nothing -> throwIO RotationError
|
||||
where
|
||||
tryAction creds =
|
||||
tryAction creds =
|
||||
catch (Right <$> action creds)
|
||||
(\(e :: SomeException) -> return $ Left e)
|
||||
|
||||
|
|
@ -1047,22 +1047,22 @@ rotationLoop :: IORef RotatingCredentials -> VaultConnection -> IO ()
|
|||
rotationLoop credsRef vault = forever $ do
|
||||
currentTime <- getCurrentTime
|
||||
creds <- readIORef credsRef
|
||||
|
||||
|
||||
when (shouldRotate currentTime (rotationTime creds)) $ do
|
||||
-- Fetch new credentials
|
||||
newCreds <- fetchDynamicCreds vault
|
||||
|
||||
|
||||
-- Update with both old and new
|
||||
atomicModifyIORef' credsRef $ \old ->
|
||||
(RotatingCredentials newCreds (Just $ currentCreds old) currentTime, ())
|
||||
|
||||
|
||||
threadDelay (5 * 60 * 1000000) -- Check every 5 minutes
|
||||
|
||||
-- Vault dynamic secrets with lease renewal
|
||||
requestDynamicCredentials :: VaultConnection -> IO DynamicDBCredentials
|
||||
requestDynamicCredentials conn = do
|
||||
result <- vaultRead conn (VaultSecretPath "database/creds/readonly")
|
||||
|
||||
|
||||
case result of
|
||||
(metadata, Right creds) -> do
|
||||
-- Schedule renewal before expiration
|
||||
|
|
@ -1074,7 +1074,7 @@ renewLeaseLoop :: VaultConnection -> Text -> Int -> IO ()
|
|||
renewLeaseLoop conn leaseId duration = do
|
||||
let renewInterval = duration `div` 2 -- Renew at halfway point
|
||||
threadDelay (renewInterval * 1000000)
|
||||
|
||||
|
||||
success <- renewLease conn leaseId
|
||||
if success
|
||||
then renewLeaseLoop conn leaseId duration -- Continue renewing
|
||||
|
|
@ -1086,7 +1086,7 @@ renewLeaseLoop conn leaseId duration = do
|
|||
```haskell
|
||||
-- Never store secrets in code or config files
|
||||
-- ❌ DON'T DO THIS
|
||||
apiKey = "sk_live_abc123xyz"
|
||||
apiKey = "sk_live_abc123xyz"
|
||||
|
||||
-- ✅ Load from secure source
|
||||
loadSecrets :: IO Secrets
|
||||
|
|
@ -1222,12 +1222,12 @@ databaseCodec = DatabaseConfig
|
|||
|
||||
```dhall
|
||||
-- types/Database.dhall
|
||||
let PoolConfig = { Type =
|
||||
let PoolConfig = { Type =
|
||||
{ minConnections : Natural
|
||||
, maxConnections : Natural
|
||||
, idleTimeout : Natural
|
||||
}
|
||||
, default =
|
||||
, default =
|
||||
{ minConnections = 5
|
||||
, maxConnections = 20
|
||||
, idleTimeout = 30
|
||||
|
|
@ -1259,11 +1259,11 @@ in DatabaseConfig
|
|||
-- config/production.dhall
|
||||
let DB = ../types/Database.dhall
|
||||
|
||||
in DB::{
|
||||
in DB::{
|
||||
, host = "prod-db.internal"
|
||||
, name = "production_db"
|
||||
, pool = DB.default.pool // { maxConnections = 100 }
|
||||
, replicas =
|
||||
, replicas =
|
||||
[ { host = "replica1.internal", port = 5432 }
|
||||
, { host = "replica2.internal", port = 5432 }
|
||||
]
|
||||
|
|
@ -1295,12 +1295,12 @@ initFeatureFlags :: FilePath -> IO FeatureFlagManager
|
|||
initFeatureFlags path = do
|
||||
initial <- loadFeatureFlags path
|
||||
flagsRef <- newIORef initial
|
||||
|
||||
|
||||
let manager = FeatureFlagManager flagsRef path
|
||||
|
||||
|
||||
-- Watch for changes
|
||||
_ <- forkIO $ watchAndReload manager
|
||||
|
||||
|
||||
return manager
|
||||
where
|
||||
loadFeatureFlags :: FilePath -> IO FeatureFlags
|
||||
|
|
@ -1359,25 +1359,25 @@ loadConfig = do
|
|||
Just "production" -> Production
|
||||
Just "staging" -> Staging
|
||||
_ -> Development
|
||||
|
||||
|
||||
-- Load shared config
|
||||
shared <- decodeFileThrow "config/shared.yaml"
|
||||
|
||||
|
||||
-- Load environment-specific config
|
||||
let envFile = case env of
|
||||
Development -> "config/development.yaml"
|
||||
Staging -> "config/staging.yaml"
|
||||
Staging -> "config/staging.yaml"
|
||||
Production -> "config/production.yaml"
|
||||
|
||||
|
||||
envSpecific <- decodeFileThrow envFile
|
||||
|
||||
|
||||
-- Merge with env vars (highest precedence)
|
||||
envOverrides <- decodeEnv :: IO (Either String EnvOverrides)
|
||||
|
||||
|
||||
let finalConfig = case envOverrides of
|
||||
Right overrides -> applyOverrides envSpecific overrides
|
||||
Left _ -> envSpecific
|
||||
|
||||
|
||||
return $ MultiEnvConfig shared env finalConfig
|
||||
```
|
||||
|
||||
|
|
@ -1424,7 +1424,7 @@ handleReload event = do
|
|||
result <- try $ decodeFile path
|
||||
case result of
|
||||
Right newConfig -> atomicWriteIORef configRef newConfig
|
||||
Left (err :: SomeException) ->
|
||||
Left (err :: SomeException) ->
|
||||
logError $ "Reload failed, keeping old config: " ++ show err
|
||||
```
|
||||
|
||||
|
|
@ -1496,7 +1496,7 @@ let baseService = ./types/Service.dhall
|
|||
let makeServiceConfig = λ(name : Text) → λ(port : Natural) →
|
||||
baseService::{ name = name, port = port }
|
||||
|
||||
in { services =
|
||||
in { services =
|
||||
[ makeServiceConfig "api" 8080
|
||||
, makeServiceConfig "auth" 8081
|
||||
-- ... 48 more services with consistent structure
|
||||
|
|
@ -189,16 +189,16 @@ Nginx's event-based architecture provides inherent resistance to connection exha
|
|||
http {
|
||||
# Time to read client request header
|
||||
client_header_timeout 10s;
|
||||
|
||||
|
||||
# Time between successive body data reads
|
||||
client_body_timeout 10s;
|
||||
|
||||
|
||||
# Keep-alive connection timeout
|
||||
keepalive_timeout 15s;
|
||||
|
||||
|
||||
# Time between successive writes to client
|
||||
send_timeout 10s;
|
||||
|
||||
|
||||
# Buffer limits prevent memory exhaustion
|
||||
client_body_buffer_size 128k;
|
||||
client_header_buffer_size 2k;
|
||||
|
|
@ -228,19 +228,19 @@ LoadModule qos_module modules/mod_qos.so
|
|||
<IfModule mod_qos.c>
|
||||
# Track 500,000 unique client IPs (150 bytes per entry)
|
||||
QS_ClientEntries 500000
|
||||
|
||||
|
||||
# Maximum 20 concurrent connections per IP
|
||||
QS_SrvMaxConnPerIP 20
|
||||
|
||||
|
||||
# Maximum 512 total active connections
|
||||
QS_SrvMaxConn 512
|
||||
|
||||
|
||||
# Disable keep-alive when 400 connections active
|
||||
QS_SrvMaxConnClose 400
|
||||
|
||||
|
||||
# Minimum data rates: 200 bytes/sec download, 1500 upload
|
||||
QS_SrvMinDataRate 200 1500
|
||||
|
||||
|
||||
# Protect resource-intensive endpoints
|
||||
QS_LocRequestLimit "/login" 5
|
||||
QS_LocRequestLimit "/admin" 3
|
||||
|
|
@ -264,15 +264,15 @@ http {
|
|||
limit_conn_zone $binary_remote_addr zone=addr:10m;
|
||||
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
|
||||
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
|
||||
|
||||
|
||||
server {
|
||||
# Global: 10 concurrent connections per IP
|
||||
limit_conn addr 10;
|
||||
|
||||
|
||||
location / {
|
||||
limit_req zone=general burst=20 nodelay;
|
||||
}
|
||||
|
||||
|
||||
location /login {
|
||||
# Aggressive limits for authentication
|
||||
limit_conn addr 3;
|
||||
|
|
@ -521,20 +521,20 @@ SEC("xdp")
|
|||
int ddos_protection(struct xdp_md *ctx) {
|
||||
void *data_end = (void *)(long)ctx->data_end;
|
||||
void *data = (void *)(long)ctx->data;
|
||||
|
||||
|
||||
// Parse Ethernet header
|
||||
struct ethhdr *eth = data;
|
||||
if ((void *)(eth + 1) > data_end) return XDP_PASS;
|
||||
if (eth->h_proto != __constant_htons(ETH_P_IP)) return XDP_PASS;
|
||||
|
||||
|
||||
// Parse IP header
|
||||
struct iphdr *iph = (void *)(eth + 1);
|
||||
if ((void *)(iph + 1) > data_end) return XDP_PASS;
|
||||
|
||||
|
||||
__u32 src_ip = iph->saddr;
|
||||
struct rate_limit_entry *entry = bpf_map_lookup_elem(&rate_limit_map, &src_ip);
|
||||
__u64 current_time = bpf_ktime_get_ns();
|
||||
|
||||
|
||||
if (entry) {
|
||||
if (current_time - entry->last_update < TIME_WINDOW_NS) {
|
||||
entry->packet_count++;
|
||||
|
|
@ -1099,7 +1099,7 @@ groups:
|
|||
for: 2m
|
||||
annotations:
|
||||
summary: "High SYN rate detected"
|
||||
|
||||
|
||||
- alert: ConntrackTableFull
|
||||
expr: node_nf_conntrack_entries / node_nf_conntrack_entries_limit > 0.9
|
||||
for: 5m
|
||||
|
|
@ -269,14 +269,14 @@ For complex dependencies, Nix provides reproducible static builds.
|
|||
let
|
||||
pkgs = import <nixpkgs> {};
|
||||
pkgsMusl = pkgs.pkgsMusl;
|
||||
|
||||
|
||||
staticLibs = [
|
||||
(pkgsMusl.gmp6.override { withStatic = true; })
|
||||
pkgsMusl.zlib.static
|
||||
(pkgsMusl.libffi.overrideAttrs (old: { dontDisableStatic = true; }))
|
||||
(pkgsMusl.openssl.override { static = true; })
|
||||
];
|
||||
|
||||
|
||||
in pkgsMusl.haskellPackages.aenebris.overrideAttrs (old: {
|
||||
enableSharedExecutables = false;
|
||||
enableSharedLibraries = false;
|
||||
|
|
@ -384,7 +384,7 @@ packages: .
|
|||
package *
|
||||
ghc-options: -O2 -split-sections
|
||||
gcc-options: -Os -ffunction-sections -fdata-sections
|
||||
|
||||
|
||||
package aenebris
|
||||
ld-options: -Wl,--gc-sections
|
||||
ghc-options: -funbox-strict-fields -fllvm
|
||||
|
|
@ -442,13 +442,13 @@ spec:
|
|||
spec:
|
||||
# Service account for RBAC
|
||||
serviceAccountName: aenebris
|
||||
|
||||
|
||||
# Security context
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
fsGroup: 1000
|
||||
|
||||
|
||||
# Topology spread for high availability
|
||||
topologySpreadConstraints:
|
||||
- maxSkew: 1
|
||||
|
|
@ -463,7 +463,7 @@ spec:
|
|||
labelSelector:
|
||||
matchLabels:
|
||||
app: aenebris
|
||||
|
||||
|
||||
# Node affinity for dedicated nodes
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
|
|
@ -474,7 +474,7 @@ spec:
|
|||
- key: workload-type
|
||||
operator: In
|
||||
values: [proxy, edge]
|
||||
|
||||
|
||||
# Pod anti-affinity
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
|
|
@ -484,15 +484,15 @@ spec:
|
|||
matchLabels:
|
||||
app: aenebris
|
||||
topologyKey: kubernetes.io/hostname
|
||||
|
||||
|
||||
# Graceful shutdown - CRITICAL for WebSockets
|
||||
terminationGracePeriodSeconds: 120
|
||||
|
||||
|
||||
containers:
|
||||
- name: aenebris
|
||||
image: ghcr.io/yourorg/aenebris:1.0.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
|
||||
|
||||
# Ports
|
||||
ports:
|
||||
- name: http
|
||||
|
|
@ -504,7 +504,7 @@ spec:
|
|||
- name: metrics
|
||||
containerPort: 9090
|
||||
protocol: TCP
|
||||
|
||||
|
||||
# Haskell RTS configuration for high performance
|
||||
command:
|
||||
- /usr/local/bin/aenebris
|
||||
|
|
@ -519,14 +519,14 @@ spec:
|
|||
- "-RTS"
|
||||
- "--config"
|
||||
- "/etc/aenebris/config.yaml"
|
||||
|
||||
|
||||
# Environment variables
|
||||
env:
|
||||
- name: LOG_LEVEL
|
||||
value: "info"
|
||||
- name: METRICS_PORT
|
||||
value: "9090"
|
||||
|
||||
|
||||
# Resource requests and limits
|
||||
resources:
|
||||
requests:
|
||||
|
|
@ -535,7 +535,7 @@ spec:
|
|||
limits:
|
||||
cpu: "8000m" # Burst to 8 cores
|
||||
memory: "4Gi" # Hard limit
|
||||
|
||||
|
||||
# Volume mounts
|
||||
volumeMounts:
|
||||
- name: config
|
||||
|
|
@ -549,7 +549,7 @@ spec:
|
|||
readOnly: true
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
|
||||
|
||||
# Startup probe (slow initial startup)
|
||||
startupProbe:
|
||||
httpGet:
|
||||
|
|
@ -561,7 +561,7 @@ spec:
|
|||
timeoutSeconds: 1
|
||||
failureThreshold: 30 # 60 seconds max startup time
|
||||
successThreshold: 1
|
||||
|
||||
|
||||
# Liveness probe (detect deadlocks)
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
|
|
@ -573,7 +573,7 @@ spec:
|
|||
timeoutSeconds: 2
|
||||
failureThreshold: 3
|
||||
successThreshold: 1
|
||||
|
||||
|
||||
# Readiness probe (traffic management)
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
|
|
@ -585,7 +585,7 @@ spec:
|
|||
timeoutSeconds: 2
|
||||
failureThreshold: 2
|
||||
successThreshold: 1
|
||||
|
||||
|
||||
# PreStop hook for graceful shutdown
|
||||
lifecycle:
|
||||
preStop:
|
||||
|
|
@ -599,7 +599,7 @@ spec:
|
|||
kill -TERM 1
|
||||
# Wait for connections to drain (110s, leaving 10s buffer)
|
||||
sleep 110
|
||||
|
||||
|
||||
# Security context
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
|
|
@ -609,7 +609,7 @@ spec:
|
|||
capabilities:
|
||||
drop: ["ALL"]
|
||||
add: ["NET_BIND_SERVICE"]
|
||||
|
||||
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
|
|
@ -683,7 +683,7 @@ spec:
|
|||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
|
||||
|
||||
# Memory-based scaling
|
||||
- type: Resource
|
||||
resource:
|
||||
|
|
@ -691,7 +691,7 @@ spec:
|
|||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 80
|
||||
|
||||
|
||||
# Custom metric: requests per second
|
||||
- type: Pods
|
||||
pods:
|
||||
|
|
@ -700,7 +700,7 @@ spec:
|
|||
target:
|
||||
type: AverageValue
|
||||
averageValue: "2000" # 2k req/s per pod
|
||||
|
||||
|
||||
# Custom metric: active connections
|
||||
- type: Pods
|
||||
pods:
|
||||
|
|
@ -709,7 +709,7 @@ spec:
|
|||
target:
|
||||
type: AverageValue
|
||||
averageValue: "1000" # 1k connections per pod
|
||||
|
||||
|
||||
# Scaling behavior
|
||||
behavior:
|
||||
scaleUp:
|
||||
|
|
@ -971,7 +971,7 @@ probes:
|
|||
timeoutSeconds: 1
|
||||
failureThreshold: 30
|
||||
successThreshold: 1
|
||||
|
||||
|
||||
liveness:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
|
|
@ -981,7 +981,7 @@ probes:
|
|||
timeoutSeconds: 2
|
||||
failureThreshold: 3
|
||||
successThreshold: 1
|
||||
|
||||
|
||||
readiness:
|
||||
httpGet:
|
||||
path: /ready
|
||||
|
|
@ -1009,12 +1009,12 @@ config:
|
|||
enabled: true
|
||||
path: "/health"
|
||||
interval: "10s"
|
||||
|
||||
|
||||
rateLimit:
|
||||
enabled: true
|
||||
requestsPerSecond: 1000
|
||||
burst: 2000
|
||||
|
||||
|
||||
tls:
|
||||
enabled: true
|
||||
minVersion: "1.2"
|
||||
|
|
@ -1022,7 +1022,7 @@ config:
|
|||
- "TLS_AES_256_GCM_SHA384"
|
||||
- "TLS_AES_128_GCM_SHA256"
|
||||
- "TLS_CHACHA20_POLY1305_SHA256"
|
||||
|
||||
|
||||
websocket:
|
||||
enabled: true
|
||||
pingInterval: "30s"
|
||||
|
|
@ -1041,7 +1041,7 @@ tls:
|
|||
secrets:
|
||||
upstreamCredentials:
|
||||
existingSecret: "aenebris-upstream-creds"
|
||||
|
||||
|
||||
# Network policies
|
||||
networkPolicy:
|
||||
enabled: true
|
||||
|
|
@ -1496,7 +1496,7 @@ spec:
|
|||
timeoutSeconds: 1
|
||||
failureThreshold: 30 # 5s + (30 * 2s) = 65s max
|
||||
successThreshold: 1
|
||||
|
||||
|
||||
# Liveness probe - detects deadlocks
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
|
|
@ -1511,7 +1511,7 @@ spec:
|
|||
timeoutSeconds: 2
|
||||
failureThreshold: 3 # Restart after 30s of failures
|
||||
successThreshold: 1
|
||||
|
||||
|
||||
# Readiness probe - traffic management
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
|
|
@ -1541,15 +1541,15 @@ lifecycle:
|
|||
- |
|
||||
# Stop accepting new connections
|
||||
echo "Graceful shutdown initiated at $(date)"
|
||||
|
||||
|
||||
# Send SIGTERM to application (it should stop accepting)
|
||||
kill -TERM 1
|
||||
|
||||
|
||||
# Wait for existing connections to drain
|
||||
# (110 seconds, leaving 10s buffer before SIGKILL)
|
||||
echo "Waiting for connections to drain..."
|
||||
sleep 110
|
||||
|
||||
|
||||
echo "Shutdown complete at $(date)"
|
||||
```
|
||||
|
||||
|
|
@ -1575,23 +1575,23 @@ main :: IO ()
|
|||
main = do
|
||||
shutdownFlag <- newTVarIO False
|
||||
setupGracefulShutdown shutdownFlag
|
||||
|
||||
|
||||
-- Start server in separate thread
|
||||
serverThread <- async $ runServer shutdownFlag
|
||||
|
||||
|
||||
-- Wait for shutdown signal
|
||||
atomically $ do
|
||||
shutdown <- readTVar shutdownFlag
|
||||
unless shutdown retry
|
||||
|
||||
|
||||
putStrLn "Stopping server, draining connections..."
|
||||
|
||||
|
||||
-- Stop accepting new connections
|
||||
stopAcceptingConnections
|
||||
|
||||
|
||||
-- Wait for existing connections to complete
|
||||
waitForConnectionsDrain 110 -- 110 seconds
|
||||
|
||||
|
||||
putStrLn "All connections drained, exiting"
|
||||
|
||||
waitForConnectionsDrain :: Int -> IO ()
|
||||
|
|
@ -1617,10 +1617,10 @@ metadata:
|
|||
# AWS NLB
|
||||
service.beta.kubernetes.io/aws-load-balancer-connection-draining-enabled: "true"
|
||||
service.beta.kubernetes.io/aws-load-balancer-connection-draining-timeout: "120"
|
||||
|
||||
|
||||
# GCP
|
||||
cloud.google.com/neg: '{"ingress": true}'
|
||||
|
||||
|
||||
spec:
|
||||
sessionAffinity: ClientIP
|
||||
sessionAffinityConfig:
|
||||
|
|
@ -1783,7 +1783,7 @@ sum(websocket_connections_total) by (pod)
|
|||
rate(container_cpu_cfs_throttled_seconds_total{pod=~"aenebris.*"}[5m])
|
||||
|
||||
# Memory usage vs limit
|
||||
container_memory_usage_bytes{pod=~"aenebris.*"} /
|
||||
container_memory_usage_bytes{pod=~"aenebris.*"} /
|
||||
container_spec_memory_limit_bytes{pod=~"aenebris.*"}
|
||||
```
|
||||
|
||||
|
|
@ -1853,14 +1853,14 @@ kubectl delete certificate aenebris-tls -n production
|
|||
|
||||
This comprehensive deployment guide provides a production-ready foundation for deploying Ᾰenebris on Kubernetes. The architecture achieves:
|
||||
|
||||
✅ **Ultra-minimal Docker images** (15-50MB) via multi-stage builds and static linking
|
||||
✅ **High availability** with topology spread, pod disruption budgets, and anti-affinity
|
||||
✅ **Zero-downtime deployments** through graceful shutdown and connection draining
|
||||
✅ **Automated TLS management** with cert-manager
|
||||
✅ **Secure secrets handling** via External Secrets Operator and Sealed Secrets
|
||||
✅ **Production-grade monitoring** with Prometheus and Grafana
|
||||
✅ **Dynamic scaling** from 5 to 50 replicas based on traffic
|
||||
✅ **WebSocket support** with proper connection management
|
||||
✅ **Ultra-minimal Docker images** (15-50MB) via multi-stage builds and static linking
|
||||
✅ **High availability** with topology spread, pod disruption budgets, and anti-affinity
|
||||
✅ **Zero-downtime deployments** through graceful shutdown and connection draining
|
||||
✅ **Automated TLS management** with cert-manager
|
||||
✅ **Secure secrets handling** via External Secrets Operator and Sealed Secrets
|
||||
✅ **Production-grade monitoring** with Prometheus and Grafana
|
||||
✅ **Dynamic scaling** from 5 to 50 replicas based on traffic
|
||||
✅ **WebSocket support** with proper connection management
|
||||
|
||||
**Next Steps:**
|
||||
1. Customize values.yaml for your environment
|
||||
|
|
@ -194,7 +194,7 @@ bingExample = do
|
|||
### WaiProxyResponse type
|
||||
|
||||
```haskell
|
||||
data WaiProxyResponse
|
||||
data WaiProxyResponse
|
||||
= WPRResponse WAI.Response -- Return custom response
|
||||
| WPRProxyDest ProxyDest -- Forward to destination
|
||||
| WPRModifiedRequest WAI.Request ProxyDest -- Modify then forward
|
||||
|
|
@ -245,7 +245,7 @@ proxyApp manager request respond = do
|
|||
else give chunk >> loop
|
||||
in loop
|
||||
}
|
||||
|
||||
|
||||
-- Make the request and forward response
|
||||
httpLbs proxiedReq manager >>= \response ->
|
||||
respond $ responseLBS
|
||||
|
|
@ -257,7 +257,7 @@ proxyApp manager request respond = do
|
|||
fixHeaders :: RequestHeaders -> RequestHeaders
|
||||
fixHeaders = filter $ \(name, _) -> name `notElem` hopByHopHeaders
|
||||
where
|
||||
hopByHopHeaders =
|
||||
hopByHopHeaders =
|
||||
[ "connection", "keep-alive", "proxy-authenticate"
|
||||
, "proxy-authorization", "te", "trailers"
|
||||
, "transfer-encoding", "upgrade"
|
||||
|
|
@ -266,7 +266,7 @@ fixHeaders = filter $ \(name, _) -> name `notElem` hopByHopHeaders
|
|||
fixResponseHeaders :: ResponseHeaders -> ResponseHeaders
|
||||
fixResponseHeaders = filter $ \(name, _) -> name `notElem` hopByHopHeaders
|
||||
where
|
||||
hopByHopHeaders =
|
||||
hopByHopHeaders =
|
||||
[ "connection", "keep-alive", "proxy-authenticate"
|
||||
, "proxy-authorization", "te", "trailers"
|
||||
, "transfer-encoding", "upgrade"
|
||||
|
|
@ -286,7 +286,7 @@ main = do
|
|||
```haskell
|
||||
-- Add X-Forwarded-For header
|
||||
addForwardedFor :: Request -> RequestHeaders -> RequestHeaders
|
||||
addForwardedFor req headers =
|
||||
addForwardedFor req headers =
|
||||
let clientIP = show (remoteHost req)
|
||||
existing = lookup "X-Forwarded-For" headers
|
||||
newValue = case existing of
|
||||
|
|
@ -369,7 +369,7 @@ import Network.HTTP.Types
|
|||
import Data.ByteString.Builder (byteString)
|
||||
|
||||
app :: Application
|
||||
app _req respond = respond $
|
||||
app _req respond = respond $
|
||||
responseStream status200 [("Content-Type", "text/plain")] $ \write flush -> do
|
||||
write $ byteString "Hello\n"
|
||||
flush
|
||||
|
|
@ -388,10 +388,10 @@ import Data.Function (fix)
|
|||
import Control.Monad (unless)
|
||||
|
||||
streamFileApp :: Application
|
||||
streamFileApp _req respond =
|
||||
streamFileApp _req respond =
|
||||
withBinaryFile "largefile.txt" ReadMode $ \h ->
|
||||
respond $ responseStream status200 [("Content-Type", "text/plain")] $
|
||||
\chunk _flush ->
|
||||
respond $ responseStream status200 [("Content-Type", "text/plain")] $
|
||||
\chunk _flush ->
|
||||
fix $ \loop -> do
|
||||
bs <- B.hGetSome h 4096 -- Only 4KB in memory at once
|
||||
unless (B.null bs) $ do
|
||||
|
|
@ -414,17 +414,17 @@ import qualified Data.ByteString.Char8 as C8
|
|||
|
||||
sseApp :: Application
|
||||
sseApp _req sendResponse = sendResponse $
|
||||
responseStream status200
|
||||
responseStream status200
|
||||
[("Content-Type", "text/event-stream"),
|
||||
("Cache-Control", "no-cache"),
|
||||
("Connection", "keep-alive")]
|
||||
("Connection", "keep-alive")]
|
||||
myStream
|
||||
|
||||
myStream :: (Builder -> IO ()) -> IO () -> IO ()
|
||||
myStream send flush = do
|
||||
send $ byteString "data: Starting streaming response.\n\n"
|
||||
flush
|
||||
|
||||
|
||||
forM_ [1..50 :: Int] $ \i -> do
|
||||
threadDelay 1000000 -- 1 second
|
||||
send $ byteString "data: Message " <> byteString (C8.pack $ show i) <> byteString "\n\n"
|
||||
|
|
@ -464,10 +464,10 @@ main = do
|
|||
-- Pure operations
|
||||
result <- runConduit $ yieldMany [1..10] .| sumC
|
||||
print result -- 55
|
||||
|
||||
|
||||
-- File operations
|
||||
runConduitRes $
|
||||
sourceFile "input.txt" .|
|
||||
runConduitRes $
|
||||
sourceFile "input.txt" .|
|
||||
sinkFile "output.txt"
|
||||
```
|
||||
|
||||
|
|
@ -480,8 +480,8 @@ import qualified Data.Conduit.Binary as CB
|
|||
import Control.Monad.Trans.Resource
|
||||
|
||||
conduitFileApp :: Application
|
||||
conduitFileApp _req respond =
|
||||
respond $ responseStream status200 [("Content-Type", "text/plain")] $
|
||||
conduitFileApp _req respond =
|
||||
respond $ responseStream status200 [("Content-Type", "text/plain")] $
|
||||
\write flush -> runResourceT $ do
|
||||
CB.sourceFile "largefile.txt" $$ CB.mapM_ $ \chunk -> liftIO $ do
|
||||
write (byteString chunk)
|
||||
|
|
@ -498,8 +498,8 @@ import qualified Data.Conduit.List as CL
|
|||
streamFromDB :: Handler ()
|
||||
streamFromDB = do
|
||||
-- selectSource returns a conduit Source
|
||||
selectSource [] []
|
||||
$$ CL.mapM_ $ \(Entity _ record) ->
|
||||
selectSource [] []
|
||||
$$ CL.mapM_ $ \(Entity _ record) ->
|
||||
liftIO $ processRecord record
|
||||
```
|
||||
|
||||
|
|
@ -531,8 +531,8 @@ import Data.Conduit
|
|||
import qualified Data.Conduit.List as CL
|
||||
|
||||
sumConduit :: IO Int
|
||||
sumConduit = runConduit $
|
||||
yieldMany [1..10] .|
|
||||
sumConduit = runConduit $
|
||||
yieldMany [1..10] .|
|
||||
CL.fold (+) 0
|
||||
```
|
||||
|
||||
|
|
@ -589,17 +589,17 @@ generateCSV write flush = do
|
|||
-- Header
|
||||
write $ byteString "id,name,value\n"
|
||||
flush
|
||||
|
||||
|
||||
-- Generate 1 million rows without buffering
|
||||
forM_ [1..1000000] $ \i -> do
|
||||
let row = byteString $ C8.pack $
|
||||
let row = byteString $ C8.pack $
|
||||
show i ++ ",Item" ++ show i ++ "," ++ show (i * 100) ++ "\n"
|
||||
write row
|
||||
when (i `mod` 1000 == 0) flush
|
||||
|
||||
csvApp :: Application
|
||||
csvApp _req respond = respond $
|
||||
responseStream status200
|
||||
responseStream status200
|
||||
[("Content-Type", "text/csv"),
|
||||
("Content-Disposition", "attachment; filename=data.csv")]
|
||||
generateCSV
|
||||
|
|
@ -611,19 +611,19 @@ csvApp _req respond = respond $
|
|||
processWithProgress :: (Builder -> IO ()) -> IO () -> IO ()
|
||||
processWithProgress write flush = do
|
||||
let total = 100
|
||||
|
||||
|
||||
forM_ [1..total] $ \i -> do
|
||||
threadDelay 100000 -- Simulate work
|
||||
|
||||
let progress = byteString $
|
||||
"data: {\"progress\": " <>
|
||||
byteString (C8.pack $ show i) <>
|
||||
", \"total\": " <>
|
||||
byteString (C8.pack $ show total) <>
|
||||
|
||||
let progress = byteString $
|
||||
"data: {\"progress\": " <>
|
||||
byteString (C8.pack $ show i) <>
|
||||
", \"total\": " <>
|
||||
byteString (C8.pack $ show total) <>
|
||||
"}\n\n"
|
||||
write progress
|
||||
flush
|
||||
|
||||
|
||||
write $ byteString "data: {\"status\": \"complete\"}\n\n"
|
||||
flush
|
||||
```
|
||||
|
|
@ -725,7 +725,7 @@ application state pending = do
|
|||
client = (T.drop (T.length prefix) msg, conn)
|
||||
disconnect = do
|
||||
s <- modifyMVar state $ \s ->
|
||||
let s' = filter ((/= fst client) . fst) s
|
||||
let s' = filter ((/= fst client) . fst) s
|
||||
in return (s', s')
|
||||
broadcast (fst client <> " disconnected") s
|
||||
|
||||
|
|
@ -798,9 +798,9 @@ advancedTLS = do
|
|||
}
|
||||
, onInsecure = DenyInsecure "This server requires HTTPS"
|
||||
}
|
||||
|
||||
|
||||
warpOpts = setPort 443 $ setHost "0.0.0.0" $ defaultSettings
|
||||
|
||||
|
||||
runTLS tlsOpts warpOpts app
|
||||
|
||||
validateClientCert :: CertificateChain -> IO CertificateUsage
|
||||
|
|
@ -821,7 +821,7 @@ secureTLS = do
|
|||
let tlsOpts = tlsSettings "cert.pem" "key.pem"
|
||||
warpOpts = setPort 443 defaultSettings
|
||||
wsApp = websocketsOr WS.defaultConnectionOptions wsHandler httpApp
|
||||
|
||||
|
||||
runTLS tlsOpts warpOpts wsApp
|
||||
|
||||
wsHandler :: WS.ServerApp
|
||||
|
|
@ -844,7 +844,7 @@ dynamicCerts = do
|
|||
Just "example.com" -> loadCredentials "example.pem" "example-key.pem"
|
||||
Just "other.com" -> loadCredentials "other.pem" "other-key.pem"
|
||||
_ -> loadCredentials "default.pem" "default-key.pem"
|
||||
|
||||
|
||||
runTLS tlsOpts defaultSettings app
|
||||
|
||||
loadCredentials :: FilePath -> FilePath -> IO Credentials
|
||||
|
|
@ -909,7 +909,7 @@ gracefulShutdown = do
|
|||
let settings = setInstallShutdownHandler shutdownHandler
|
||||
$ setGracefulShutdownTimeout (Just 30) -- 30 seconds max
|
||||
$ setPort 8080 defaultSettings
|
||||
|
||||
|
||||
runSettings settings app
|
||||
where
|
||||
shutdownHandler closeSocket = do
|
||||
|
|
@ -935,7 +935,7 @@ gracefulShutdown = do
|
|||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
productionSettings :: Settings
|
||||
productionSettings =
|
||||
productionSettings =
|
||||
setPort 443
|
||||
$ setHost "*"
|
||||
$ setOnOpen onOpen
|
||||
|
|
@ -953,13 +953,13 @@ productionSettings =
|
|||
onOpen sockAddr = do
|
||||
putStrLn $ "Connection: " ++ show sockAddr
|
||||
return True
|
||||
|
||||
onClose sockAddr =
|
||||
|
||||
onClose sockAddr =
|
||||
putStrLn $ "Closed: " ++ show sockAddr
|
||||
|
||||
onException _ e =
|
||||
|
||||
onException _ e =
|
||||
putStrLn $ "Exception: " ++ show e
|
||||
|
||||
|
||||
shutdownHandler closeSocket = do
|
||||
_ <- installHandler sigTERM (Catch closeSocket) Nothing
|
||||
_ <- installHandler sigINT (Catch closeSocket) Nothing
|
||||
|
|
@ -1023,16 +1023,16 @@ websocketHandler pending = do
|
|||
|
||||
-- HTTP handler (including streaming)
|
||||
httpHandler :: Application
|
||||
httpHandler req respond =
|
||||
httpHandler req respond =
|
||||
case pathInfo req of
|
||||
["stream"] -> respond $ streamingResponse
|
||||
["ws"] -> respond $ responseLBS status404 [] "Use WebSocket protocol"
|
||||
_ -> respond $ responseLBS status200 [] "Hello HTTP"
|
||||
|
||||
streamingResponse :: Response
|
||||
streamingResponse = responseStream
|
||||
status200
|
||||
[("Content-Type", "text/event-stream")]
|
||||
streamingResponse = responseStream
|
||||
status200
|
||||
[("Content-Type", "text/event-stream")]
|
||||
$ \write flush -> forever $ do
|
||||
threadDelay 1000000
|
||||
write $ byteString "data: tick\n\n"
|
||||
|
|
@ -1067,10 +1067,10 @@ server {
|
|||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
|
||||
# Critical: disable buffering
|
||||
proxy_buffering off;
|
||||
|
||||
|
||||
# Increase timeouts
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
|
|
@ -1080,11 +1080,11 @@ server {
|
|||
location /stream/ {
|
||||
proxy_pass http://localhost:8002;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
|
||||
# Critical: disable buffering for streaming
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
|
||||
|
||||
# Keep connection alive
|
||||
proxy_set_header Connection '';
|
||||
proxy_set_header Cache-Control 'no-cache';
|
||||
|
|
@ -1242,9 +1242,9 @@ For unidirectional streaming, SSE offers advantages:
|
|||
```haskell
|
||||
sseHandler :: Application
|
||||
sseHandler _req respond = respond $
|
||||
responseStream status200
|
||||
responseStream status200
|
||||
[("Content-Type", "text/event-stream"),
|
||||
("Cache-Control", "no-cache")]
|
||||
("Cache-Control", "no-cache")]
|
||||
$ \write flush -> forever $ do
|
||||
threadDelay 1000000
|
||||
write $ byteString "data: update\n\n"
|
||||
|
|
@ -163,14 +163,14 @@ server {
|
|||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
|
||||
|
||||
# HTTP/3 over UDP
|
||||
listen 443 quic reuseport;
|
||||
listen [::]:443 quic reuseport;
|
||||
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
add_header Alt-Svc 'h3=":443"; ma=86400' always;
|
||||
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000; # Warp
|
||||
proxy_http_version 1.1;
|
||||
|
|
@ -221,7 +221,7 @@ import Control.Monad.Trans.Resource
|
|||
|
||||
app :: Application
|
||||
app req respond = runResourceT $ do
|
||||
(releaseKey, handle) <- allocate
|
||||
(releaseKey, handle) <- allocate
|
||||
(openFile "data.txt" ReadMode)
|
||||
hClose
|
||||
content <- liftIO $ hGetContents handle
|
||||
|
|
@ -88,7 +88,7 @@ data Backend = Backend
|
|||
}
|
||||
|
||||
trackConnection :: Backend -> IO a -> IO a
|
||||
trackConnection backend action =
|
||||
trackConnection backend action =
|
||||
bracket_
|
||||
(atomically $ modifyTVar' (activeConnections backend) (+1))
|
||||
(atomically $ modifyTVar' (activeConnections backend) (subtract 1))
|
||||
|
|
@ -110,7 +110,7 @@ This pattern **composes naturally with health checks** - the STM transaction can
|
|||
import Control.Concurrent.LoadDistribution
|
||||
|
||||
lb <- evenlyDistributed (return $ Set.fromList backends)
|
||||
withResource lb $ \maybeBackend ->
|
||||
withResource lb $ \maybeBackend ->
|
||||
case maybeBackend of
|
||||
Just backend -> proxyRequest backend
|
||||
Nothing -> handleNoBackends
|
||||
|
|
@ -132,18 +132,18 @@ data WeightedBackend a = WeightedBackend
|
|||
smoothWeightedSelect :: [WeightedBackend a] -> IO a
|
||||
smoothWeightedSelect backends = atomically $ do
|
||||
-- Increase all current weights by their base weights
|
||||
forM_ backends $ \wb ->
|
||||
forM_ backends $ \wb ->
|
||||
modifyTVar' (currentWeight wb) (+ weight wb)
|
||||
|
||||
|
||||
-- Find backend with maximum current weight
|
||||
weights <- mapM (readTVar . currentWeight) backends
|
||||
let maxWeight = maximum weights
|
||||
selected = backends !! fromJust (findIndex (== maxWeight) weights)
|
||||
|
||||
|
||||
-- Reduce selected backend's current weight by total
|
||||
let totalWeight = sum (map weight backends)
|
||||
modifyTVar' (currentWeight selected) (subtract totalWeight)
|
||||
|
||||
|
||||
return $ backend selected
|
||||
```
|
||||
|
||||
|
|
@ -188,7 +188,7 @@ data BackendState = BackendState
|
|||
|
||||
-- BAD: Touches many TVars in single transaction
|
||||
countAllConnections :: [Backend] -> STM Int
|
||||
countAllConnections backends =
|
||||
countAllConnections backends =
|
||||
sum <$> mapM (readTVar . activeConns) backends
|
||||
|
||||
-- GOOD: Maintain aggregate counter
|
||||
|
|
@ -281,7 +281,7 @@ The `retry` primitive is STM's killer feature - **threads automatically block un
|
|||
badPattern = atomically $ do
|
||||
val <- expensiveComputation -- Recomputed on every retry!
|
||||
tvar <- readTVar someTVar
|
||||
|
||||
|
||||
-- GOOD: Pure computation outside
|
||||
goodPattern = do
|
||||
let val = expensiveComputation
|
||||
|
|
@ -299,7 +299,7 @@ goodPattern = do
|
|||
```haskell
|
||||
-- Update multiple backend states atomically
|
||||
updateHealthChecks :: [Backend] -> [(Backend, Bool)] -> STM ()
|
||||
updateHealthChecks backends results =
|
||||
updateHealthChecks backends results =
|
||||
forM_ results $ \(backend, isHealthy) ->
|
||||
writeTVar (healthy backend) isHealthy
|
||||
```
|
||||
|
|
@ -344,7 +344,7 @@ performHealthCheck manager config backend = do
|
|||
req <- parseRequest url
|
||||
response <- httpLbs req manager
|
||||
return $ statusCode (responseStatus response) == 200
|
||||
|
||||
|
||||
return $ fromMaybe False result
|
||||
```
|
||||
|
||||
|
|
@ -362,13 +362,13 @@ import Control.Monad (forever)
|
|||
healthCheckLoop :: Manager -> HealthCheckConfig -> [Backend] -> IO ()
|
||||
healthCheckLoop manager config backends = forever $ do
|
||||
-- Check all backends concurrently
|
||||
results <- mapConcurrently
|
||||
(performHealthCheck manager config)
|
||||
results <- mapConcurrently
|
||||
(performHealthCheck manager config)
|
||||
backends
|
||||
|
||||
|
||||
-- Update backend states
|
||||
zipWithM_ (updateBackendState config) backends results
|
||||
|
||||
|
||||
-- Wait for next interval
|
||||
threadDelay (hcInterval config * 1000000)
|
||||
|
||||
|
|
@ -414,7 +414,7 @@ updateBackendState config backend healthy = atomically $ do
|
|||
state <- readTVar (backendState backend)
|
||||
failures <- readTVar (backendFailures backend)
|
||||
successes <- readTVar (backendSuccesses backend)
|
||||
|
||||
|
||||
case (state, healthy) of
|
||||
(Healthy, False) -> do
|
||||
let newFailures = failures + 1
|
||||
|
|
@ -422,22 +422,22 @@ updateBackendState config backend healthy = atomically $ do
|
|||
when (newFailures >= hcMaxFailures config) $ do
|
||||
writeTVar (backendState backend) Unhealthy
|
||||
writeTVar (backendFailures backend) 0
|
||||
|
||||
|
||||
(Unhealthy, True) -> do
|
||||
writeTVar (backendState backend) Recovering
|
||||
writeTVar (backendSuccesses backend) 1
|
||||
|
||||
|
||||
(Recovering, True) -> do
|
||||
let newSuccesses = successes + 1
|
||||
writeTVar (backendSuccesses backend) newSuccesses
|
||||
when (newSuccesses >= hcRecoveryAttempts config) $ do
|
||||
writeTVar (backendState backend) Healthy
|
||||
writeTVar (backendSuccesses backend) 0
|
||||
|
||||
|
||||
(Recovering, False) -> do
|
||||
writeTVar (backendState backend) Unhealthy
|
||||
writeTVar (backendSuccesses backend) 0
|
||||
|
||||
|
||||
_ -> return ()
|
||||
```
|
||||
|
||||
|
|
@ -458,15 +458,15 @@ testBreaker = undefined
|
|||
proxyWithCircuitBreaker :: Backend -> Request -> IO Response
|
||||
proxyWithCircuitBreaker backend req = do
|
||||
cbConf <- initialBreakerState
|
||||
|
||||
result <- flip runReaderT cbConf $
|
||||
|
||||
result <- flip runReaderT cbConf $
|
||||
withBreaker testBreaker $ liftIO $ forwardRequest backend req
|
||||
|
||||
|
||||
case result of
|
||||
Left (CircuitBreakerClosed msg) ->
|
||||
Left (CircuitBreakerClosed msg) ->
|
||||
-- Circuit open, return cached response or error
|
||||
return $ errorResponse 503 "Service Temporarily Unavailable"
|
||||
Right response ->
|
||||
Right response ->
|
||||
return response
|
||||
```
|
||||
|
||||
|
|
@ -521,7 +521,7 @@ exponentialBackoffWithJitter config action = go 0 (initialDelay config)
|
|||
```haskell
|
||||
import Control.Retry
|
||||
|
||||
recovering
|
||||
recovering
|
||||
(exponentialBackoff 50000 <> limitRetries 5)
|
||||
[const $ Handler $ \e -> return (isRetryable e)]
|
||||
(\_ -> performHealthCheck backend)
|
||||
|
|
@ -554,16 +554,16 @@ proxyApp :: ProxyConfig -> Application
|
|||
proxyApp config req respond = do
|
||||
mBackend <- selectHealthyBackend (pcBalancer config)
|
||||
case mBackend of
|
||||
Nothing ->
|
||||
Nothing ->
|
||||
respond $ responseLBS status503 [] "No healthy backends available"
|
||||
Just backend ->
|
||||
waiProxyTo
|
||||
(\_ -> return $ WPRProxyDest (ProxyDest
|
||||
(backendHost backend)
|
||||
Just backend ->
|
||||
waiProxyTo
|
||||
(\_ -> return $ WPRProxyDest (ProxyDest
|
||||
(backendHost backend)
|
||||
(backendPort backend)))
|
||||
defaultOnExc
|
||||
defaultOnExc
|
||||
(pcManager config)
|
||||
req
|
||||
req
|
||||
respond
|
||||
```
|
||||
|
||||
|
|
@ -585,7 +585,7 @@ import Network.Wai (Middleware, mapResponseHeaders)
|
|||
-- Add load balancer info header
|
||||
addBackendHeader :: Backend -> Middleware
|
||||
addBackendHeader backend app req respond =
|
||||
app req $ respond . mapResponseHeaders
|
||||
app req $ respond . mapResponseHeaders
|
||||
((hBackend, encodeUtf8 $ backendId backend) :)
|
||||
|
||||
-- Connection tracking middleware
|
||||
|
|
@ -593,18 +593,18 @@ trackingMiddleware :: ServerMetrics -> Middleware
|
|||
trackingMiddleware metrics app req respond = do
|
||||
atomically $ modifyTVar' (activeConnections metrics) (+1)
|
||||
atomically $ modifyTVar' (totalRequests metrics) (+1)
|
||||
|
||||
|
||||
let respond' res = do
|
||||
atomically $ modifyTVar' (activeConnections metrics) (subtract 1)
|
||||
respond res
|
||||
|
||||
|
||||
app req respond' `onException`
|
||||
atomically (modifyTVar' (errorCount metrics) (+1))
|
||||
|
||||
-- Compose middleware
|
||||
main = do
|
||||
config <- initProxyConfig
|
||||
let app = trackingMiddleware metrics
|
||||
let app = trackingMiddleware metrics
|
||||
$ proxyApp config
|
||||
run 8000 app
|
||||
```
|
||||
|
|
@ -624,7 +624,7 @@ main = do
|
|||
**Compile-time optimizations**:
|
||||
|
||||
```cabal
|
||||
ghc-options: -Wall -O2 -threaded
|
||||
ghc-options: -Wall -O2 -threaded
|
||||
-rtsopts -with-rtsopts=-N
|
||||
-fspec-constr -fspecialise
|
||||
-funbox-strict-fields
|
||||
|
|
@ -709,7 +709,7 @@ data ProxyConfig = ProxyConfig
|
|||
|
||||
-- Backend selection dispatcher
|
||||
selectBackend :: ProxyConfig -> IO (Maybe Backend)
|
||||
selectBackend config =
|
||||
selectBackend config =
|
||||
case strategy config of
|
||||
RoundRobin -> selectRoundRobin config
|
||||
LeastConnections -> selectLeastConnections config
|
||||
|
|
@ -722,7 +722,7 @@ selectRoundRobin config = do
|
|||
len = V.length backends'
|
||||
idx <- atomicModifyIORef' (rrCounter config) $ \i ->
|
||||
((i + 1) `mod` len, i)
|
||||
|
||||
|
||||
-- Find next healthy backend
|
||||
findHealthy backends' idx len
|
||||
where
|
||||
|
|
@ -733,7 +733,7 @@ selectRoundRobin config = do
|
|||
isHealthy <- atomically $ (== Healthy) <$> readTVar (state backend)
|
||||
if isHealthy
|
||||
then return (Just backend)
|
||||
else findHealthy backends'
|
||||
else findHealthy backends'
|
||||
((start + 1) `mod` V.length backends')
|
||||
(remaining - 1)
|
||||
|
||||
|
|
@ -756,24 +756,24 @@ selectLeastConnections config = do
|
|||
selectWeightedRR :: ProxyConfig -> IO (Maybe Backend)
|
||||
selectWeightedRR config = atomically $ do
|
||||
let backends' = V.toList (backends config)
|
||||
|
||||
|
||||
-- Increase current weights
|
||||
forM_ backends' $ \wb ->
|
||||
forM_ backends' $ \wb ->
|
||||
modifyTVar' (currentWeight wb) (+ backendWeight wb)
|
||||
|
||||
|
||||
-- Select backend with max current weight
|
||||
weights <- mapM (readTVar . currentWeight) backends'
|
||||
let maxWeight = maximum weights
|
||||
selected = backends' !! fromJust (findIndex (== maxWeight) weights)
|
||||
|
||||
|
||||
-- Check health
|
||||
isHealthy <- (== Healthy) <$> readTVar (state selected)
|
||||
guard isHealthy
|
||||
|
||||
|
||||
-- Reduce selected backend's current weight
|
||||
let totalWeight = sum (map backendWeight backends')
|
||||
modifyTVar' (currentWeight selected) (subtract totalWeight)
|
||||
|
||||
|
||||
return selected
|
||||
|
||||
-- Main proxy application
|
||||
|
|
@ -781,17 +781,17 @@ proxyApp :: ProxyConfig -> Application
|
|||
proxyApp config req respond = do
|
||||
mBackend <- selectBackend config
|
||||
case mBackend of
|
||||
Nothing ->
|
||||
Nothing ->
|
||||
respond $ responseLBS status503 [] "No healthy backends"
|
||||
Just backend -> do
|
||||
-- Track connection
|
||||
atomically $ modifyTVar' (activeConns backend) (+1)
|
||||
|
||||
|
||||
let dest = ProxyDest (backendHost backend) (backendPort backend)
|
||||
respond' res = do
|
||||
atomically $ modifyTVar' (activeConns backend) (subtract 1)
|
||||
respond res
|
||||
|
||||
|
||||
waiProxyTo
|
||||
(\_ -> return $ WPRProxyDest dest)
|
||||
defaultOnExc
|
||||
|
|
@ -807,14 +807,14 @@ healthCheckLoop manager backends = forever $ do
|
|||
threadDelay 10000000 -- 10 seconds
|
||||
where
|
||||
checkHealth mgr backend = do
|
||||
let url = "http://" <> backendHost backend <> ":"
|
||||
let url = "http://" <> backendHost backend <> ":"
|
||||
<> show (backendPort backend) <> "/health"
|
||||
result <- timeout 2000000 $ do
|
||||
req <- parseRequest (unpack url)
|
||||
response <- httpLbs req mgr
|
||||
return $ statusCode (responseStatus response) == 200
|
||||
return $ fromMaybe False result
|
||||
|
||||
|
||||
updateHealth backend healthy = atomically $ do
|
||||
currentState <- readTVar (state backend)
|
||||
failureCount <- readTVar (failures backend)
|
||||
|
|
@ -838,7 +838,7 @@ initProxyConfig specs strat = do
|
|||
manager <- newTlsManager
|
||||
counter <- newIORef 0
|
||||
checker <- async $ healthCheckLoop manager (V.toList backends)
|
||||
|
||||
|
||||
return ProxyConfig
|
||||
{ backends = backends
|
||||
, strategy = strat
|
||||
|
|
@ -859,18 +859,18 @@ initProxyConfig specs strat = do
|
|||
-- Main entry point
|
||||
main :: IO ()
|
||||
main = do
|
||||
let backendSpecs =
|
||||
let backendSpecs =
|
||||
[ BackendSpec "localhost" 8001 5
|
||||
, BackendSpec "localhost" 8002 1
|
||||
, BackendSpec "localhost" 8003 1
|
||||
]
|
||||
|
||||
|
||||
config <- initProxyConfig backendSpecs SmoothWeightedRR
|
||||
|
||||
let settings = setPort 8000
|
||||
$ setTimeout 30
|
||||
|
||||
let settings = setPort 8000
|
||||
$ setTimeout 30
|
||||
$ defaultSettings
|
||||
|
||||
|
||||
putStrLn "Load balancing proxy started on port 8000"
|
||||
runSettings settings (proxyApp config)
|
||||
```
|
||||
|
|
@ -939,8 +939,8 @@ import Test.DejaFu
|
|||
testNoDeadlock :: IO ()
|
||||
testNoDeadlock = autocheck $ do
|
||||
balancer <- setup
|
||||
concurrently_
|
||||
(selectBackend balancer)
|
||||
concurrently_
|
||||
(selectBackend balancer)
|
||||
(selectBackend balancer)
|
||||
```
|
||||
|
||||
|
|
@ -30,13 +30,13 @@ import Foreign.C.Types
|
|||
import System.Posix.Types (Fd(..))
|
||||
|
||||
foreign import ccall unsafe "splice"
|
||||
c_splice :: CInt -> Ptr CLong -> CInt -> Ptr CLong
|
||||
c_splice :: CInt -> Ptr CLong -> CInt -> Ptr CLong
|
||||
-> CSize -> CUInt -> IO CSsize
|
||||
|
||||
splice :: Fd -> Fd -> Int -> [SpliceFlag] -> IO Int
|
||||
splice (Fd fdIn) (Fd fdOut) len flags = do
|
||||
let cflags = foldr (.|.) 0 [f | SpliceFlag f <- flags]
|
||||
result <- c_splice fdIn nullPtr fdOut nullPtr
|
||||
result <- c_splice fdIn nullPtr fdOut nullPtr
|
||||
(fromIntegral len) cflags
|
||||
if result == -1
|
||||
then throwErrno "splice"
|
||||
|
|
@ -59,10 +59,10 @@ The `simple-sendfile` package powers Warp's high-performance static file serving
|
|||
```haskell
|
||||
import Network.Sendfile
|
||||
|
||||
sendfileWithHeader :: Socket -> FilePath -> FileRange
|
||||
sendfileWithHeader :: Socket -> FilePath -> FileRange
|
||||
-> IO () -> [ByteString] -> IO ()
|
||||
-- Sends headers and file data efficiently
|
||||
sendfileWithHeader sock path (PartOfFile offset len)
|
||||
sendfileWithHeader sock path (PartOfFile offset len)
|
||||
tickle headers
|
||||
```
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ spliceWithRetry :: Fd -> Fd -> Int -> IO ()
|
|||
spliceWithRetry fdIn fdOut chunkSize = loop
|
||||
where
|
||||
loop = do
|
||||
result <- try $ splice fdIn fdOut chunkSize
|
||||
result <- try $ splice fdIn fdOut chunkSize
|
||||
[spliceNonBlock, spliceMore]
|
||||
case result of
|
||||
Right 0 -> return () -- EOF
|
||||
|
|
@ -111,15 +111,15 @@ Conversion costs between variants matter significantly. `toStrict` forces entire
|
|||
```haskell
|
||||
import Data.ByteString.Builder
|
||||
|
||||
buildHttpResponse :: Int -> [(ByteString, ByteString)]
|
||||
buildHttpResponse :: Int -> [(ByteString, ByteString)]
|
||||
-> LazyByteString -> LazyByteString
|
||||
buildHttpResponse status headers body = toLazyByteString builder
|
||||
where
|
||||
builder = statusLine <> headerLines
|
||||
builder = statusLine <> headerLines
|
||||
<> byteString "\r\n" <> lazyByteString body
|
||||
statusLine = byteString "HTTP/1.1 " <> intDec status
|
||||
statusLine = byteString "HTTP/1.1 " <> intDec status
|
||||
<> byteString " OK\r\n"
|
||||
headerLines = mconcat
|
||||
headerLines = mconcat
|
||||
[ byteString k <> byteString ": " <> byteString v <> byteString "\r\n"
|
||||
| (k, v) <- headers ]
|
||||
```
|
||||
|
|
@ -134,7 +134,7 @@ import Data.Pool
|
|||
createBackendPool :: HostName -> PortNumber -> IO (Pool Socket)
|
||||
createBackendPool host port = do
|
||||
capabilities <- getNumCapabilities
|
||||
newPool $
|
||||
newPool $
|
||||
defaultPoolConfig
|
||||
(connectBackend host port) -- Create function
|
||||
close -- Destroy function
|
||||
|
|
@ -228,7 +228,7 @@ Interpreting results requires understanding latency distribution. The `--latency
|
|||
```
|
||||
Latency Distribution
|
||||
50% 635.91us
|
||||
75% 712.34us
|
||||
75% 712.34us
|
||||
90% 1.04ms
|
||||
99% 2.87ms
|
||||
```
|
||||
|
|
@ -385,7 +385,7 @@ ThreadScope displays CPU activity across cores, spark creation/conversion (for p
|
|||
**Profiling workflow progresses systematically**:
|
||||
|
||||
1. **Baseline measurement** with `-O2 +RTS -s` establishes initial performance
|
||||
2. **Time profiling** with `-prof -fprof-late +RTS -p` identifies CPU hotspots
|
||||
2. **Time profiling** with `-prof -fprof-late +RTS -p` identifies CPU hotspots
|
||||
3. **Memory profiling** with `-hd -l` and eventlog2html reveals allocation patterns
|
||||
4. **Detailed investigation** using info table profiling for exact source locations
|
||||
5. **Iterative optimization** applying fixes and re-profiling to verify improvements
|
||||
|
|
@ -401,7 +401,7 @@ Successful optimization follows priority order: algorithms trump micro-optimizat
|
|||
**Compilation optimization checklist**:
|
||||
- [ ] Use `-O` or `-O2` for production builds
|
||||
- [ ] Add `-fllvm` for numeric-intensive code after benchmarking
|
||||
- [ ] Enable `-threaded` for concurrent programs
|
||||
- [ ] Enable `-threaded` for concurrent programs
|
||||
- [ ] Include `-rtsopts` to allow runtime tuning
|
||||
- [ ] Set `-with-rtsopts=-N` for automatic parallelism
|
||||
- [ ] Use `optimization: 2` in cabal.project, not `ghc-options`
|
||||
|
|
@ -483,7 +483,7 @@ import System.IO
|
|||
|
||||
data ProxyConfig = ProxyConfig
|
||||
{ listenPort :: PortNumber
|
||||
, targetHost :: HostName
|
||||
, targetHost :: HostName
|
||||
, targetPort :: PortNumber
|
||||
, poolStripes :: Int
|
||||
, poolPerStripe :: Int
|
||||
|
|
@ -492,8 +492,8 @@ data ProxyConfig = ProxyConfig
|
|||
|
||||
-- Create backend connection pool
|
||||
createBackendPool :: ProxyConfig -> IO (Pool Socket)
|
||||
createBackendPool config =
|
||||
newPool $
|
||||
createBackendPool config =
|
||||
newPool $
|
||||
defaultPoolConfig
|
||||
(connectBackend (targetHost config) (targetPort config))
|
||||
close
|
||||
|
|
@ -503,7 +503,7 @@ createBackendPool config =
|
|||
|
||||
connectBackend :: HostName -> PortNumber -> IO Socket
|
||||
connectBackend host port = do
|
||||
addr:_ <- getAddrInfo
|
||||
addr:_ <- getAddrInfo
|
||||
(Just defaultHints { addrSocketType = Stream })
|
||||
(Just host) (Just $ show port)
|
||||
sock <- socket (addrFamily addr) Stream defaultProtocol
|
||||
|
|
@ -517,18 +517,18 @@ runProxy :: ProxyConfig -> IO ()
|
|||
runProxy config = do
|
||||
pool <- createBackendPool config
|
||||
addr:_ <- getAddrInfo
|
||||
(Just defaultHints
|
||||
(Just defaultHints
|
||||
{ addrSocketType = Stream
|
||||
, addrFlags = [AI_PASSIVE] })
|
||||
Nothing (Just $ show $ listenPort config)
|
||||
|
||||
|
||||
sock <- socket (addrFamily addr) Stream defaultProtocol
|
||||
setSocketOption sock ReuseAddr 1
|
||||
bind sock (addrAddress addr)
|
||||
listen sock 128
|
||||
|
||||
|
||||
putStrLn $ "Proxy listening on port " ++ show (listenPort config)
|
||||
|
||||
|
||||
forever $ do
|
||||
(client, clientAddr) <- accept sock
|
||||
forkIO $ handleClient pool client
|
||||
|
|
@ -538,27 +538,27 @@ runProxy config = do
|
|||
handleClient :: Pool Socket -> Socket -> IO ()
|
||||
handleClient pool client = do
|
||||
done <- newEmptyMVar
|
||||
|
||||
|
||||
withResource pool $ \backend -> do
|
||||
-- Bidirectional zero-copy forwarding
|
||||
let chunkSize = 65536 -- 64KB chunks
|
||||
|
||||
|
||||
forkIO $ do
|
||||
result <- try $ splice chunkSize (client, Nothing)
|
||||
result <- try $ splice chunkSize (client, Nothing)
|
||||
(backend, Nothing)
|
||||
case result of
|
||||
Left (e :: SomeException) ->
|
||||
Left (e :: SomeException) ->
|
||||
putStrLn $ "Client->Backend error: " ++ show e
|
||||
Right _ -> return ()
|
||||
putMVar done ()
|
||||
|
||||
|
||||
result <- try $ splice chunkSize (backend, Nothing)
|
||||
(client, Nothing)
|
||||
case result of
|
||||
Left (e :: SomeException) ->
|
||||
putStrLn $ "Backend->Client error: " ++ show e
|
||||
Right _ -> return ()
|
||||
|
||||
|
||||
takeMVar done
|
||||
|
||||
main :: IO ()
|
||||
|
|
@ -582,7 +582,7 @@ main = do
|
|||
import Data.ByteString.Builder
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
|
||||
buildHttpResponse :: Int -> [(ByteString, ByteString)]
|
||||
buildHttpResponse :: Int -> [(ByteString, ByteString)]
|
||||
-> BL.ByteString -> BL.ByteString
|
||||
buildHttpResponse status headers body = toLazyByteString $
|
||||
mconcat
|
||||
|
|
@ -591,7 +591,7 @@ buildHttpResponse status headers body = toLazyByteString $
|
|||
, byteString " "
|
||||
, statusText status
|
||||
, byteString "\r\n"
|
||||
, mconcat [ byteString k <> byteString ": "
|
||||
, mconcat [ byteString k <> byteString ": "
|
||||
<> byteString v <> byteString "\r\n"
|
||||
| (k, v) <- headers ]
|
||||
, byteString "\r\n"
|
||||
|
|
@ -599,7 +599,7 @@ buildHttpResponse status headers body = toLazyByteString $
|
|||
]
|
||||
where
|
||||
statusText 200 = byteString "OK"
|
||||
statusText 404 = byteString "Not Found"
|
||||
statusText 404 = byteString "Not Found"
|
||||
statusText 500 = byteString "Internal Server Error"
|
||||
statusText _ = byteString "Unknown"
|
||||
```
|
||||
|
|
@ -616,14 +616,14 @@ parseRequestLine :: ByteString -> Maybe (ByteString, ByteString, ByteString)
|
|||
parseRequestLine bs = do
|
||||
let (method, rest1) = BS.break (== space) bs
|
||||
guard (not $ BS.null rest1)
|
||||
|
||||
|
||||
let rest2 = BS.drop 1 rest1
|
||||
(path, rest3) = BS.break (== space) rest2
|
||||
guard (not $ BS.null rest3)
|
||||
|
||||
|
||||
let rest4 = BS.drop 1 rest3
|
||||
(version, _) = BS.break (== cr) rest4
|
||||
|
||||
|
||||
return (method, path, version)
|
||||
where
|
||||
space = 32; cr = 13
|
||||
|
|
@ -635,11 +635,11 @@ parseHeaders = go . BC.lines
|
|||
go [] = []
|
||||
go (line:rest)
|
||||
| BS.null line = []
|
||||
| otherwise =
|
||||
| otherwise =
|
||||
case BC.break (== ':') line of
|
||||
(key, value)
|
||||
(key, value)
|
||||
| BS.null value -> go rest
|
||||
| otherwise ->
|
||||
| otherwise ->
|
||||
let val = BS.dropWhile (== 32) (BS.drop 1 value)
|
||||
in (key, val) : go rest
|
||||
```
|
||||
|
|
@ -210,16 +210,16 @@ RateLimit: "default";r=0;t=60
|
|||
async function fetchWithRetry(url, maxRetries = 3) {
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
const res = await fetch(url);
|
||||
|
||||
|
||||
if (res.status === 429) {
|
||||
const retryAfter = res.headers.get('retry-after');
|
||||
const delay = parseInt(retryAfter) * 1000 || (1000 * Math.pow(2, attempt));
|
||||
const jitter = delay * 0.2 * (Math.random() - 0.5);
|
||||
|
||||
|
||||
await sleep(delay + jitter);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
return res;
|
||||
}
|
||||
throw new Error('Max retries exceeded');
|
||||
|
|
@ -91,10 +91,10 @@ tlsClient hostname port = do
|
|||
addr:_ <- getAddrInfo Nothing (Just hostname) (Just port)
|
||||
sock <- socket (addrFamily addr) Stream defaultProtocol
|
||||
connect sock (addrAddress addr)
|
||||
|
||||
|
||||
-- Configure TLS parameters
|
||||
let params = (defaultParamsClient hostname "")
|
||||
{ clientSupported = def
|
||||
{ clientSupported = def
|
||||
{ supportedCiphers = ciphersuite_default
|
||||
, supportedVersions = [TLS13, TLS12]
|
||||
, supportedGroups = [X25519, P256]
|
||||
|
|
@ -103,16 +103,16 @@ tlsClient hostname port = do
|
|||
{ sharedCAStore = systemStore
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-- Perform handshake
|
||||
ctx <- contextNew sock params
|
||||
handshake ctx
|
||||
|
||||
|
||||
-- Send HTTP request
|
||||
sendData ctx "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
|
||||
response <- recvData ctx
|
||||
print response
|
||||
|
||||
|
||||
-- Clean shutdown
|
||||
bye ctx
|
||||
close sock
|
||||
|
|
@ -131,8 +131,8 @@ import qualified Network.TLS as TLS
|
|||
import Network.TLS.Extra.Cipher
|
||||
|
||||
app :: Application
|
||||
app _ respond =
|
||||
respond $ responseLBS status200
|
||||
app _ respond =
|
||||
respond $ responseLBS status200
|
||||
[("Content-Type", "text/plain")
|
||||
,("Strict-Transport-Security", "max-age=31536000; includeSubDomains")]
|
||||
"Secure HTTPS Server"
|
||||
|
|
@ -144,11 +144,11 @@ main = do
|
|||
, tlsCiphers = ciphersuite_strong
|
||||
, onInsecure = DenyInsecure "HTTPS required"
|
||||
}
|
||||
|
||||
|
||||
warpConfig = defaultSettings
|
||||
& setPort 443
|
||||
& setHost "0.0.0.0"
|
||||
|
||||
|
||||
putStrLn "HTTPS server on :443"
|
||||
runTLS tlsConfig warpConfig app
|
||||
```
|
||||
|
|
@ -169,14 +169,14 @@ validateCertificate certFile hostname = do
|
|||
-- Load certificate chain
|
||||
certs <- readSignedObject certFile
|
||||
let chain = CertificateChain certs
|
||||
|
||||
|
||||
-- Get system CA store
|
||||
store <- getSystemCertificateStore
|
||||
|
||||
|
||||
-- Validate with default checks
|
||||
let cache = exceptionValidationCache []
|
||||
failures <- validateDefault store cache (hostname, ":443") chain
|
||||
|
||||
|
||||
case failures of
|
||||
[] -> putStrLn "✓ Certificate valid" >> return True
|
||||
errs -> do
|
||||
|
|
@ -206,10 +206,10 @@ initializeKeys :: CertConfig -> IO ()
|
|||
initializeKeys cfg = do
|
||||
accountExists <- doesFileExist (accountKey cfg)
|
||||
domainExists <- doesFileExist (domainKey cfg)
|
||||
|
||||
unless accountExists $
|
||||
|
||||
unless accountExists $
|
||||
callCommand $ "openssl genrsa 4096 > " ++ accountKey cfg
|
||||
|
||||
|
||||
unless domainExists $
|
||||
callCommand $ "openssl genrsa 4096 > " ++ domainKey cfg
|
||||
|
||||
|
|
@ -337,7 +337,7 @@ import qualified Network.TLS as TLS
|
|||
customTlsManager :: IO Manager
|
||||
customTlsManager = do
|
||||
let tlsParams = (TLS.defaultParamsClient "example.com" "")
|
||||
{ TLS.clientSupported = def
|
||||
{ TLS.clientSupported = def
|
||||
{ TLS.supportedCiphers = ciphersuite_strong
|
||||
, TLS.supportedVersions = [TLS.TLS13, TLS.TLS12]
|
||||
}
|
||||
|
|
@ -346,15 +346,15 @@ customTlsManager = do
|
|||
}
|
||||
}
|
||||
tlsSettings = TLSSettings tlsParams
|
||||
|
||||
|
||||
newManager $ mkManagerSettings tlsSettings Nothing
|
||||
|
||||
customValidation :: CertificateStore -> ValidationCache -> ServiceID
|
||||
customValidation :: CertificateStore -> ValidationCache -> ServiceID
|
||||
-> CertificateChain -> IO [FailedReason]
|
||||
customValidation store cache sid chain = do
|
||||
-- Perform standard validation
|
||||
failures <- validateDefault store cache sid chain
|
||||
|
||||
|
||||
-- Add custom checks (certificate pinning, etc.)
|
||||
if null failures
|
||||
then return []
|
||||
|
|
@ -375,11 +375,11 @@ loadCredentials hostname = do
|
|||
|
||||
main :: IO ()
|
||||
main = do
|
||||
let tlsConfig = tlsSettingsSni
|
||||
let tlsConfig = tlsSettingsSni
|
||||
(return $ Just loadCredentials)
|
||||
"default.crt"
|
||||
"default.key"
|
||||
|
||||
|
||||
runTLS tlsConfig defaultSettings app
|
||||
```
|
||||
|
||||
|
|
@ -392,15 +392,15 @@ import Data.IORef
|
|||
setupSessionManager :: IO SessionManager
|
||||
setupSessionManager = do
|
||||
sessions <- newIORef Map.empty
|
||||
|
||||
|
||||
return SessionManager
|
||||
{ sessionResume = \sessionID -> do
|
||||
cache <- readIORef sessions
|
||||
return $ Map.lookup sessionID cache
|
||||
|
||||
|
||||
, sessionEstablish = \sessionID sessionData -> do
|
||||
modifyIORef' sessions (Map.insert sessionID sessionData)
|
||||
|
||||
|
||||
, sessionInvalidate = \sessionID -> do
|
||||
modifyIORef' sessions (Map.delete sessionID)
|
||||
}
|
||||
|
|
@ -408,7 +408,7 @@ setupSessionManager = do
|
|||
clientWithResumption :: HostName -> IO ()
|
||||
clientWithResumption hostname = do
|
||||
manager <- setupSessionManager
|
||||
|
||||
|
||||
let params = (defaultParamsClient hostname "")
|
||||
{ clientShared = def
|
||||
{ sharedSessionManager = manager
|
||||
|
|
@ -68,14 +68,14 @@ Dhall represents a paradigm shift—a non-Turing-complete functional language sp
|
|||
|
||||
```dhall
|
||||
-- types.dhall
|
||||
let DatabaseConfig =
|
||||
{ Type =
|
||||
let DatabaseConfig =
|
||||
{ Type =
|
||||
{ host : Text
|
||||
, port : Natural
|
||||
, maxConnections : Natural
|
||||
, replica : Optional DatabaseConfig.Type
|
||||
}
|
||||
, default =
|
||||
, default =
|
||||
{ host = "localhost"
|
||||
, port = 5432
|
||||
, maxConnections = 20
|
||||
|
|
@ -87,7 +87,7 @@ let DatabaseConfig =
|
|||
let DB = ./types.dhall
|
||||
let staging = ./staging.dhall
|
||||
|
||||
in DB::{
|
||||
in DB::{
|
||||
, host = "prod-db.internal"
|
||||
, port = 5432
|
||||
, maxConnections = 100
|
||||
|
|
@ -133,11 +133,11 @@ data ServiceConfig = ServiceConfig
|
|||
-- Type-safe DSL in Haskell
|
||||
myService :: ServiceConfig
|
||||
myService = ServiceConfig
|
||||
{ routes =
|
||||
{ routes =
|
||||
[ route "/api" GET apiHandler
|
||||
, route "/health" GET healthCheck
|
||||
]
|
||||
, middleware =
|
||||
, middleware =
|
||||
[ cors allowAll
|
||||
, logging Verbose
|
||||
, auth jwtValidator
|
||||
|
|
@ -205,7 +205,7 @@ let identity : ∀(a : Type) → a → a =
|
|||
λ(a : Type) → λ(x : a) → x
|
||||
|
||||
-- Type-safe record construction
|
||||
let Config : Type =
|
||||
let Config : Type =
|
||||
{ apiKey : Text
|
||||
, endpoints : List { url : Text, port : Natural }
|
||||
, retries : Optional Natural
|
||||
|
|
@ -270,7 +270,7 @@ watchConfigFile = withManager $ \mgr -> do
|
|||
"."
|
||||
(\event -> "config.yaml" `isSuffixOf` eventPath event)
|
||||
handleConfigChange
|
||||
|
||||
|
||||
forever $ threadDelay 1000000
|
||||
|
||||
handleConfigChange :: Event -> IO ()
|
||||
|
|
@ -370,7 +370,7 @@ updateConfigs :: TVar AppConfig -> TVar CacheConfig -> IO ()
|
|||
updateConfigs appVar cacheVar = atomically $ do
|
||||
app <- readTVar appVar
|
||||
cache <- readTVar cacheVar
|
||||
|
||||
|
||||
-- Both updates happen atomically or retry
|
||||
writeTVar appVar (app { maxConnections = 100 })
|
||||
writeTVar cacheVar (cache { ttl = 3600 })
|
||||
|
|
@ -418,16 +418,16 @@ initConfigManager path = do
|
|||
Right config -> do
|
||||
currentRef <- newIORef config
|
||||
lastValidRef <- newIORef config
|
||||
|
||||
|
||||
mgr <- startManager
|
||||
|
||||
|
||||
let manager = ConfigManager currentRef lastValidRef path mgr
|
||||
|
||||
|
||||
-- Start watching
|
||||
_ <- watchDir mgr (takeDirectory path)
|
||||
(matchesFile path)
|
||||
_ <- watchDir mgr (takeDirectory path)
|
||||
(matchesFile path)
|
||||
(handleReload manager)
|
||||
|
||||
|
||||
return $ Right manager
|
||||
where
|
||||
matchesFile target event = target == eventPath event
|
||||
|
|
@ -444,27 +444,27 @@ handleReload manager event = do
|
|||
-- Save current as last valid
|
||||
current <- readIORef (currentConfig manager)
|
||||
writeIORef (lastValidConfig manager) current
|
||||
|
||||
|
||||
-- Atomic swap to new config
|
||||
atomicWriteIORef (currentConfig manager) newConfig
|
||||
|
||||
|
||||
logInfo "Config reloaded successfully"
|
||||
else
|
||||
logError "Validation failed, keeping old config"
|
||||
|
||||
|
||||
Left err -> do
|
||||
logError $ "Reload failed: " ++ err
|
||||
-- Keep running with old config
|
||||
where
|
||||
tryReload :: FilePath -> IO (Either String AppConfig)
|
||||
tryReload path =
|
||||
tryReload path =
|
||||
catch (eitherDecodeFileStrict path)
|
||||
(\(e :: SomeException) -> return $ Left $ show e)
|
||||
|
||||
|
||||
validateConfig :: AppConfig -> Bool
|
||||
validateConfig cfg =
|
||||
validateConfig cfg =
|
||||
-- Custom validation logic
|
||||
not (null $ apiKeys cfg)
|
||||
not (null $ apiKeys cfg)
|
||||
&& all isValidEndpoint (databaseEndpoints $ database cfg)
|
||||
|
||||
-- Access config safely from multiple threads
|
||||
|
|
@ -484,7 +484,7 @@ debounceReload :: IORef UTCTime -> NominalDiffTime -> IO () -> IO ()
|
|||
debounceReload lastReloadRef minInterval action = do
|
||||
now <- getCurrentTime
|
||||
lastReload <- readIORef lastReloadRef
|
||||
|
||||
|
||||
when (diffUTCTime now lastReload > minInterval) $ do
|
||||
writeIORef lastReloadRef now
|
||||
action
|
||||
|
|
@ -502,7 +502,7 @@ data DatabasePool = DatabasePool
|
|||
rotatePoolCredentials :: DatabasePool -> Credentials -> IO ()
|
||||
rotatePoolCredentials dbPool newCreds = do
|
||||
atomicWriteIORef (credentials dbPool) newCreds
|
||||
|
||||
|
||||
-- Drain old connections gradually
|
||||
-- New connections use new credentials from IORef
|
||||
drainPool (pool dbPool) gracefulDrainSeconds
|
||||
|
|
@ -556,7 +556,7 @@ loadConfigFailFast path = do
|
|||
```haskell
|
||||
import Data.Validation
|
||||
|
||||
data ValidationError =
|
||||
data ValidationError =
|
||||
InvalidPort Int
|
||||
| MissingField Text
|
||||
| InvalidFormat Text Text
|
||||
|
|
@ -571,7 +571,7 @@ validateConfig raw = Config
|
|||
validatePort p
|
||||
| p > 0 && p < 65536 = Success p
|
||||
| otherwise = Failure [InvalidPort p]
|
||||
|
||||
|
||||
validateHost h
|
||||
| not (T.null h) = Success h
|
||||
| otherwise = Failure [MissingField "host"]
|
||||
|
|
@ -595,23 +595,23 @@ loadConfigGraceful path = do
|
|||
Left err -> do
|
||||
logWarning $ "Config parse error, using defaults: " ++ err
|
||||
return defaultConfig
|
||||
|
||||
|
||||
Right rawConfig -> do
|
||||
-- Core settings must be valid
|
||||
core <- case validateCore rawConfig of
|
||||
Left errors -> error $ "Core config invalid: " ++ show errors
|
||||
Right validated -> return validated
|
||||
|
||||
|
||||
-- Optional features use defaults on error
|
||||
features <- case validateFeatures rawConfig of
|
||||
Left errors -> do
|
||||
logWarning $ "Feature config invalid, using defaults: " ++ show errors
|
||||
return defaultFeatures
|
||||
Right validated -> return validated
|
||||
|
||||
|
||||
-- Experimental flags filter out invalid entries
|
||||
let flags = filterValidFlags (rawExperimental rawConfig)
|
||||
|
||||
|
||||
return $ ConfigWithDefaults core features flags
|
||||
```
|
||||
|
||||
|
|
@ -656,15 +656,15 @@ instance FromJSON ServerConfig where
|
|||
rawPort <- o .: "port"
|
||||
when (rawPort < 1 || rawPort > 65535) $
|
||||
fail $ "Invalid port: " ++ show rawPort
|
||||
|
||||
|
||||
host <- o .: "host"
|
||||
when (T.null host) $
|
||||
fail "Host cannot be empty"
|
||||
|
||||
|
||||
workers <- o .:? "workers" .!= 4 -- Default to 4
|
||||
when (workers < 1 || workers > 1000) $
|
||||
fail $ "Invalid worker count: " ++ show workers
|
||||
|
||||
|
||||
return $ ServerConfig rawPort host workers
|
||||
```
|
||||
|
||||
|
|
@ -729,9 +729,9 @@ interpolateEnvVars template = do
|
|||
let varName = T.drop 2 $ T.dropEnd 1 match -- Strip ${ }
|
||||
maybeValue <- lookupEnv (T.unpack varName)
|
||||
case maybeValue of
|
||||
Just value ->
|
||||
Just value ->
|
||||
return $ Right $ T.replace match (T.pack value) txt
|
||||
Nothing ->
|
||||
Nothing ->
|
||||
return $ Left $ "Undefined variable: " ++ T.unpack varName
|
||||
|
||||
-- With defaults
|
||||
|
|
@ -755,7 +755,7 @@ import System.Envy
|
|||
|
||||
data AppConfig = AppConfig
|
||||
{ appDatabaseUrl :: String -- DATABASE_URL
|
||||
, appRedisHost :: String -- REDIS_HOST
|
||||
, appRedisHost :: String -- REDIS_HOST
|
||||
, appPort :: Int -- PORT
|
||||
, appDebug :: Bool -- DEBUG
|
||||
, appLogLevel :: Maybe LogLevel -- LOG_LEVEL (optional)
|
||||
|
|
@ -787,9 +787,9 @@ main = do
|
|||
import Options.Applicative
|
||||
import qualified Data.Yaml as Y
|
||||
|
||||
data ConfigSource =
|
||||
data ConfigSource =
|
||||
CLIConfig Config
|
||||
| EnvConfig Config
|
||||
| EnvConfig Config
|
||||
| FileConfig Config
|
||||
| DefaultConfig Config
|
||||
|
||||
|
|
@ -804,7 +804,7 @@ mergeConfigs sources = foldl merge defaultConfig sources
|
|||
merge base (EnvConfig env) = base { port = port env `orDefault` port base }
|
||||
merge base (FileConfig file) = base { port = port file `orDefault` port base }
|
||||
merge base (DefaultConfig _) = base
|
||||
|
||||
|
||||
orDefault :: Maybe a -> a -> a
|
||||
orDefault = fromMaybe
|
||||
|
||||
|
|
@ -813,29 +813,29 @@ loadLayeredConfig :: IO Config
|
|||
loadLayeredConfig = do
|
||||
-- 1. Load defaults
|
||||
let defaults = defaultConfig
|
||||
|
||||
|
||||
-- 2. Load system config
|
||||
systemCfg <- loadSystemConfig `catch` \(_ :: IOException) -> return Nothing
|
||||
|
||||
-- 3. Load project config
|
||||
|
||||
-- 3. Load project config
|
||||
projectCfg <- loadProjectConfig `catch` \(_ :: IOException) -> return Nothing
|
||||
|
||||
|
||||
-- 4. Load local config
|
||||
localCfg <- Y.decodeFileEither "config.yaml" >>= \case
|
||||
Left _ -> return Nothing
|
||||
Right cfg -> return $ Just cfg
|
||||
|
||||
|
||||
-- 5. Load environment variables
|
||||
envCfg <- decodeEnv :: IO (Either String EnvConfig)
|
||||
let env = either (const Nothing) Just envCfg
|
||||
|
||||
|
||||
-- 6. Parse CLI args
|
||||
cliCfg <- execParser cliParser
|
||||
|
||||
|
||||
-- Merge with precedence
|
||||
return $ mergeConfigs
|
||||
return $ mergeConfigs
|
||||
[ maybe DefaultConfig FileConfig systemCfg
|
||||
, maybe DefaultConfig FileConfig projectCfg
|
||||
, maybe DefaultConfig FileConfig projectCfg
|
||||
, maybe DefaultConfig FileConfig localCfg
|
||||
, maybe DefaultConfig EnvConfig env
|
||||
, CLIConfig cliCfg
|
||||
|
|
@ -852,7 +852,7 @@ newtype SafeEnvValue = SafeEnvValue Text
|
|||
validateEnvValue :: Text -> Either String SafeEnvValue
|
||||
validateEnvValue value
|
||||
| T.any isControlChar value = Left "Control characters not allowed"
|
||||
| T.any (== ';') value = Left "Semicolons not allowed"
|
||||
| T.any (== ';') value = Left "Semicolons not allowed"
|
||||
| T.any (== '|') value = Left "Pipes not allowed"
|
||||
| otherwise = Right $ SafeEnvValue value
|
||||
where
|
||||
|
|
@ -903,10 +903,10 @@ loadSecrets :: VaultConnection -> IO (Either String AppSecrets)
|
|||
loadSecrets conn = do
|
||||
-- Get database credentials
|
||||
dbResult <- getSecret conn (SecretPath "myapp/database") Nothing
|
||||
|
||||
|
||||
-- Get API keys
|
||||
apiResult <- getSecret conn (SecretPath "myapp/api-keys") Nothing
|
||||
|
||||
|
||||
case (dbResult, apiResult) of
|
||||
(Right dbData, Right apiData) -> do
|
||||
let dbCreds = parseCredentials $ fromSecretData dbData
|
||||
|
|
@ -923,7 +923,7 @@ updateSecret conn = do
|
|||
NoCheckAndSet
|
||||
(SecretPath "myapp/database")
|
||||
(toSecretData [("password", newPassword), ("username", "admin")])
|
||||
|
||||
|
||||
case result of
|
||||
Right version -> putStrLn $ "Updated to version " ++ show version
|
||||
Left err -> putStrLn $ "Update failed: " ++ err
|
||||
|
|
@ -966,14 +966,14 @@ loadDatabaseConfig :: IO (Either String DatabaseConfig)
|
|||
loadDatabaseConfig = do
|
||||
-- Discover credentials (IAM role, env vars, etc.)
|
||||
env <- newEnv discover
|
||||
|
||||
|
||||
-- Request secret
|
||||
let req = newGetSecretValue "production/database"
|
||||
resp <- runResourceT $ send env req
|
||||
|
||||
|
||||
-- Extract and parse
|
||||
case resp ^. getSecretValueResponse_secretString of
|
||||
Just jsonString ->
|
||||
Just jsonString ->
|
||||
case A.eitherDecode (encodeUtf8 jsonString) of
|
||||
Right config -> return $ Right config
|
||||
Left err -> return $ Left $ "Parse error: " ++ err
|
||||
|
|
@ -983,13 +983,13 @@ loadDatabaseConfig = do
|
|||
rotateSecret :: Text -> IO ()
|
||||
rotateSecret secretId = do
|
||||
env <- newEnv discover
|
||||
|
||||
|
||||
let req = newRotateSecret secretId
|
||||
& rotateSecret_rotationLambdaARN ?~ lambdaArn
|
||||
& rotateSecret_rotationRules ?~
|
||||
& rotateSecret_rotationRules ?~
|
||||
newRotationRulesType
|
||||
& rotationRulesType_automaticallyAfterDays ?~ 30
|
||||
|
||||
|
||||
_ <- runResourceT $ send env req
|
||||
putStrLn "Rotation initiated"
|
||||
```
|
||||
|
|
@ -1038,7 +1038,7 @@ withRotatingCreds rc action = do
|
|||
Just prev -> action prev -- Fallback during rotation window
|
||||
Nothing -> throwIO RotationError
|
||||
where
|
||||
tryAction creds =
|
||||
tryAction creds =
|
||||
catch (Right <$> action creds)
|
||||
(\(e :: SomeException) -> return $ Left e)
|
||||
|
||||
|
|
@ -1047,22 +1047,22 @@ rotationLoop :: IORef RotatingCredentials -> VaultConnection -> IO ()
|
|||
rotationLoop credsRef vault = forever $ do
|
||||
currentTime <- getCurrentTime
|
||||
creds <- readIORef credsRef
|
||||
|
||||
|
||||
when (shouldRotate currentTime (rotationTime creds)) $ do
|
||||
-- Fetch new credentials
|
||||
newCreds <- fetchDynamicCreds vault
|
||||
|
||||
|
||||
-- Update with both old and new
|
||||
atomicModifyIORef' credsRef $ \old ->
|
||||
(RotatingCredentials newCreds (Just $ currentCreds old) currentTime, ())
|
||||
|
||||
|
||||
threadDelay (5 * 60 * 1000000) -- Check every 5 minutes
|
||||
|
||||
-- Vault dynamic secrets with lease renewal
|
||||
requestDynamicCredentials :: VaultConnection -> IO DynamicDBCredentials
|
||||
requestDynamicCredentials conn = do
|
||||
result <- vaultRead conn (VaultSecretPath "database/creds/readonly")
|
||||
|
||||
|
||||
case result of
|
||||
(metadata, Right creds) -> do
|
||||
-- Schedule renewal before expiration
|
||||
|
|
@ -1074,7 +1074,7 @@ renewLeaseLoop :: VaultConnection -> Text -> Int -> IO ()
|
|||
renewLeaseLoop conn leaseId duration = do
|
||||
let renewInterval = duration `div` 2 -- Renew at halfway point
|
||||
threadDelay (renewInterval * 1000000)
|
||||
|
||||
|
||||
success <- renewLease conn leaseId
|
||||
if success
|
||||
then renewLeaseLoop conn leaseId duration -- Continue renewing
|
||||
|
|
@ -1086,7 +1086,7 @@ renewLeaseLoop conn leaseId duration = do
|
|||
```haskell
|
||||
-- Never store secrets in code or config files
|
||||
-- ❌ DON'T DO THIS
|
||||
apiKey = "sk_live_abc123xyz"
|
||||
apiKey = "sk_live_abc123xyz"
|
||||
|
||||
-- ✅ Load from secure source
|
||||
loadSecrets :: IO Secrets
|
||||
|
|
@ -1222,12 +1222,12 @@ databaseCodec = DatabaseConfig
|
|||
|
||||
```dhall
|
||||
-- types/Database.dhall
|
||||
let PoolConfig = { Type =
|
||||
let PoolConfig = { Type =
|
||||
{ minConnections : Natural
|
||||
, maxConnections : Natural
|
||||
, idleTimeout : Natural
|
||||
}
|
||||
, default =
|
||||
, default =
|
||||
{ minConnections = 5
|
||||
, maxConnections = 20
|
||||
, idleTimeout = 30
|
||||
|
|
@ -1259,11 +1259,11 @@ in DatabaseConfig
|
|||
-- config/production.dhall
|
||||
let DB = ../types/Database.dhall
|
||||
|
||||
in DB::{
|
||||
in DB::{
|
||||
, host = "prod-db.internal"
|
||||
, name = "production_db"
|
||||
, pool = DB.default.pool // { maxConnections = 100 }
|
||||
, replicas =
|
||||
, replicas =
|
||||
[ { host = "replica1.internal", port = 5432 }
|
||||
, { host = "replica2.internal", port = 5432 }
|
||||
]
|
||||
|
|
@ -1295,12 +1295,12 @@ initFeatureFlags :: FilePath -> IO FeatureFlagManager
|
|||
initFeatureFlags path = do
|
||||
initial <- loadFeatureFlags path
|
||||
flagsRef <- newIORef initial
|
||||
|
||||
|
||||
let manager = FeatureFlagManager flagsRef path
|
||||
|
||||
|
||||
-- Watch for changes
|
||||
_ <- forkIO $ watchAndReload manager
|
||||
|
||||
|
||||
return manager
|
||||
where
|
||||
loadFeatureFlags :: FilePath -> IO FeatureFlags
|
||||
|
|
@ -1359,25 +1359,25 @@ loadConfig = do
|
|||
Just "production" -> Production
|
||||
Just "staging" -> Staging
|
||||
_ -> Development
|
||||
|
||||
|
||||
-- Load shared config
|
||||
shared <- decodeFileThrow "config/shared.yaml"
|
||||
|
||||
|
||||
-- Load environment-specific config
|
||||
let envFile = case env of
|
||||
Development -> "config/development.yaml"
|
||||
Staging -> "config/staging.yaml"
|
||||
Staging -> "config/staging.yaml"
|
||||
Production -> "config/production.yaml"
|
||||
|
||||
|
||||
envSpecific <- decodeFileThrow envFile
|
||||
|
||||
|
||||
-- Merge with env vars (highest precedence)
|
||||
envOverrides <- decodeEnv :: IO (Either String EnvOverrides)
|
||||
|
||||
|
||||
let finalConfig = case envOverrides of
|
||||
Right overrides -> applyOverrides envSpecific overrides
|
||||
Left _ -> envSpecific
|
||||
|
||||
|
||||
return $ MultiEnvConfig shared env finalConfig
|
||||
```
|
||||
|
||||
|
|
@ -1424,7 +1424,7 @@ handleReload event = do
|
|||
result <- try $ decodeFile path
|
||||
case result of
|
||||
Right newConfig -> atomicWriteIORef configRef newConfig
|
||||
Left (err :: SomeException) ->
|
||||
Left (err :: SomeException) ->
|
||||
logError $ "Reload failed, keeping old config: " ++ show err
|
||||
```
|
||||
|
||||
|
|
@ -1496,7 +1496,7 @@ let baseService = ./types/Service.dhall
|
|||
let makeServiceConfig = λ(name : Text) → λ(port : Natural) →
|
||||
baseService::{ name = name, port = port }
|
||||
|
||||
in { services =
|
||||
in { services =
|
||||
[ makeServiceConfig "api" 8080
|
||||
, makeServiceConfig "auth" 8081
|
||||
-- ... 48 more services with consistent structure
|
||||
|
|
@ -189,16 +189,16 @@ Nginx's event-based architecture provides inherent resistance to connection exha
|
|||
http {
|
||||
# Time to read client request header
|
||||
client_header_timeout 10s;
|
||||
|
||||
|
||||
# Time between successive body data reads
|
||||
client_body_timeout 10s;
|
||||
|
||||
|
||||
# Keep-alive connection timeout
|
||||
keepalive_timeout 15s;
|
||||
|
||||
|
||||
# Time between successive writes to client
|
||||
send_timeout 10s;
|
||||
|
||||
|
||||
# Buffer limits prevent memory exhaustion
|
||||
client_body_buffer_size 128k;
|
||||
client_header_buffer_size 2k;
|
||||
|
|
@ -228,19 +228,19 @@ LoadModule qos_module modules/mod_qos.so
|
|||
<IfModule mod_qos.c>
|
||||
# Track 500,000 unique client IPs (150 bytes per entry)
|
||||
QS_ClientEntries 500000
|
||||
|
||||
|
||||
# Maximum 20 concurrent connections per IP
|
||||
QS_SrvMaxConnPerIP 20
|
||||
|
||||
|
||||
# Maximum 512 total active connections
|
||||
QS_SrvMaxConn 512
|
||||
|
||||
|
||||
# Disable keep-alive when 400 connections active
|
||||
QS_SrvMaxConnClose 400
|
||||
|
||||
|
||||
# Minimum data rates: 200 bytes/sec download, 1500 upload
|
||||
QS_SrvMinDataRate 200 1500
|
||||
|
||||
|
||||
# Protect resource-intensive endpoints
|
||||
QS_LocRequestLimit "/login" 5
|
||||
QS_LocRequestLimit "/admin" 3
|
||||
|
|
@ -264,15 +264,15 @@ http {
|
|||
limit_conn_zone $binary_remote_addr zone=addr:10m;
|
||||
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
|
||||
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
|
||||
|
||||
|
||||
server {
|
||||
# Global: 10 concurrent connections per IP
|
||||
limit_conn addr 10;
|
||||
|
||||
|
||||
location / {
|
||||
limit_req zone=general burst=20 nodelay;
|
||||
}
|
||||
|
||||
|
||||
location /login {
|
||||
# Aggressive limits for authentication
|
||||
limit_conn addr 3;
|
||||
|
|
@ -521,20 +521,20 @@ SEC("xdp")
|
|||
int ddos_protection(struct xdp_md *ctx) {
|
||||
void *data_end = (void *)(long)ctx->data_end;
|
||||
void *data = (void *)(long)ctx->data;
|
||||
|
||||
|
||||
// Parse Ethernet header
|
||||
struct ethhdr *eth = data;
|
||||
if ((void *)(eth + 1) > data_end) return XDP_PASS;
|
||||
if (eth->h_proto != __constant_htons(ETH_P_IP)) return XDP_PASS;
|
||||
|
||||
|
||||
// Parse IP header
|
||||
struct iphdr *iph = (void *)(eth + 1);
|
||||
if ((void *)(iph + 1) > data_end) return XDP_PASS;
|
||||
|
||||
|
||||
__u32 src_ip = iph->saddr;
|
||||
struct rate_limit_entry *entry = bpf_map_lookup_elem(&rate_limit_map, &src_ip);
|
||||
__u64 current_time = bpf_ktime_get_ns();
|
||||
|
||||
|
||||
if (entry) {
|
||||
if (current_time - entry->last_update < TIME_WINDOW_NS) {
|
||||
entry->packet_count++;
|
||||
|
|
@ -1099,7 +1099,7 @@ groups:
|
|||
for: 2m
|
||||
annotations:
|
||||
summary: "High SYN rate detected"
|
||||
|
||||
|
||||
- alert: ConntrackTableFull
|
||||
expr: node_nf_conntrack_entries / node_nf_conntrack_entries_limit > 0.9
|
||||
for: 5m
|
||||
|
|
@ -269,14 +269,14 @@ For complex dependencies, Nix provides reproducible static builds.
|
|||
let
|
||||
pkgs = import <nixpkgs> {};
|
||||
pkgsMusl = pkgs.pkgsMusl;
|
||||
|
||||
|
||||
staticLibs = [
|
||||
(pkgsMusl.gmp6.override { withStatic = true; })
|
||||
pkgsMusl.zlib.static
|
||||
(pkgsMusl.libffi.overrideAttrs (old: { dontDisableStatic = true; }))
|
||||
(pkgsMusl.openssl.override { static = true; })
|
||||
];
|
||||
|
||||
|
||||
in pkgsMusl.haskellPackages.aenebris.overrideAttrs (old: {
|
||||
enableSharedExecutables = false;
|
||||
enableSharedLibraries = false;
|
||||
|
|
@ -384,7 +384,7 @@ packages: .
|
|||
package *
|
||||
ghc-options: -O2 -split-sections
|
||||
gcc-options: -Os -ffunction-sections -fdata-sections
|
||||
|
||||
|
||||
package aenebris
|
||||
ld-options: -Wl,--gc-sections
|
||||
ghc-options: -funbox-strict-fields -fllvm
|
||||
|
|
@ -442,13 +442,13 @@ spec:
|
|||
spec:
|
||||
# Service account for RBAC
|
||||
serviceAccountName: aenebris
|
||||
|
||||
|
||||
# Security context
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
fsGroup: 1000
|
||||
|
||||
|
||||
# Topology spread for high availability
|
||||
topologySpreadConstraints:
|
||||
- maxSkew: 1
|
||||
|
|
@ -463,7 +463,7 @@ spec:
|
|||
labelSelector:
|
||||
matchLabels:
|
||||
app: aenebris
|
||||
|
||||
|
||||
# Node affinity for dedicated nodes
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
|
|
@ -474,7 +474,7 @@ spec:
|
|||
- key: workload-type
|
||||
operator: In
|
||||
values: [proxy, edge]
|
||||
|
||||
|
||||
# Pod anti-affinity
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
|
|
@ -484,15 +484,15 @@ spec:
|
|||
matchLabels:
|
||||
app: aenebris
|
||||
topologyKey: kubernetes.io/hostname
|
||||
|
||||
|
||||
# Graceful shutdown - CRITICAL for WebSockets
|
||||
terminationGracePeriodSeconds: 120
|
||||
|
||||
|
||||
containers:
|
||||
- name: aenebris
|
||||
image: ghcr.io/yourorg/aenebris:1.0.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
|
||||
|
||||
# Ports
|
||||
ports:
|
||||
- name: http
|
||||
|
|
@ -504,7 +504,7 @@ spec:
|
|||
- name: metrics
|
||||
containerPort: 9090
|
||||
protocol: TCP
|
||||
|
||||
|
||||
# Haskell RTS configuration for high performance
|
||||
command:
|
||||
- /usr/local/bin/aenebris
|
||||
|
|
@ -519,14 +519,14 @@ spec:
|
|||
- "-RTS"
|
||||
- "--config"
|
||||
- "/etc/aenebris/config.yaml"
|
||||
|
||||
|
||||
# Environment variables
|
||||
env:
|
||||
- name: LOG_LEVEL
|
||||
value: "info"
|
||||
- name: METRICS_PORT
|
||||
value: "9090"
|
||||
|
||||
|
||||
# Resource requests and limits
|
||||
resources:
|
||||
requests:
|
||||
|
|
@ -535,7 +535,7 @@ spec:
|
|||
limits:
|
||||
cpu: "8000m" # Burst to 8 cores
|
||||
memory: "4Gi" # Hard limit
|
||||
|
||||
|
||||
# Volume mounts
|
||||
volumeMounts:
|
||||
- name: config
|
||||
|
|
@ -549,7 +549,7 @@ spec:
|
|||
readOnly: true
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
|
||||
|
||||
# Startup probe (slow initial startup)
|
||||
startupProbe:
|
||||
httpGet:
|
||||
|
|
@ -561,7 +561,7 @@ spec:
|
|||
timeoutSeconds: 1
|
||||
failureThreshold: 30 # 60 seconds max startup time
|
||||
successThreshold: 1
|
||||
|
||||
|
||||
# Liveness probe (detect deadlocks)
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
|
|
@ -573,7 +573,7 @@ spec:
|
|||
timeoutSeconds: 2
|
||||
failureThreshold: 3
|
||||
successThreshold: 1
|
||||
|
||||
|
||||
# Readiness probe (traffic management)
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
|
|
@ -585,7 +585,7 @@ spec:
|
|||
timeoutSeconds: 2
|
||||
failureThreshold: 2
|
||||
successThreshold: 1
|
||||
|
||||
|
||||
# PreStop hook for graceful shutdown
|
||||
lifecycle:
|
||||
preStop:
|
||||
|
|
@ -599,7 +599,7 @@ spec:
|
|||
kill -TERM 1
|
||||
# Wait for connections to drain (110s, leaving 10s buffer)
|
||||
sleep 110
|
||||
|
||||
|
||||
# Security context
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
|
|
@ -609,7 +609,7 @@ spec:
|
|||
capabilities:
|
||||
drop: ["ALL"]
|
||||
add: ["NET_BIND_SERVICE"]
|
||||
|
||||
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
|
|
@ -683,7 +683,7 @@ spec:
|
|||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
|
||||
|
||||
# Memory-based scaling
|
||||
- type: Resource
|
||||
resource:
|
||||
|
|
@ -691,7 +691,7 @@ spec:
|
|||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 80
|
||||
|
||||
|
||||
# Custom metric: requests per second
|
||||
- type: Pods
|
||||
pods:
|
||||
|
|
@ -700,7 +700,7 @@ spec:
|
|||
target:
|
||||
type: AverageValue
|
||||
averageValue: "2000" # 2k req/s per pod
|
||||
|
||||
|
||||
# Custom metric: active connections
|
||||
- type: Pods
|
||||
pods:
|
||||
|
|
@ -709,7 +709,7 @@ spec:
|
|||
target:
|
||||
type: AverageValue
|
||||
averageValue: "1000" # 1k connections per pod
|
||||
|
||||
|
||||
# Scaling behavior
|
||||
behavior:
|
||||
scaleUp:
|
||||
|
|
@ -971,7 +971,7 @@ probes:
|
|||
timeoutSeconds: 1
|
||||
failureThreshold: 30
|
||||
successThreshold: 1
|
||||
|
||||
|
||||
liveness:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
|
|
@ -981,7 +981,7 @@ probes:
|
|||
timeoutSeconds: 2
|
||||
failureThreshold: 3
|
||||
successThreshold: 1
|
||||
|
||||
|
||||
readiness:
|
||||
httpGet:
|
||||
path: /ready
|
||||
|
|
@ -1009,12 +1009,12 @@ config:
|
|||
enabled: true
|
||||
path: "/health"
|
||||
interval: "10s"
|
||||
|
||||
|
||||
rateLimit:
|
||||
enabled: true
|
||||
requestsPerSecond: 1000
|
||||
burst: 2000
|
||||
|
||||
|
||||
tls:
|
||||
enabled: true
|
||||
minVersion: "1.2"
|
||||
|
|
@ -1022,7 +1022,7 @@ config:
|
|||
- "TLS_AES_256_GCM_SHA384"
|
||||
- "TLS_AES_128_GCM_SHA256"
|
||||
- "TLS_CHACHA20_POLY1305_SHA256"
|
||||
|
||||
|
||||
websocket:
|
||||
enabled: true
|
||||
pingInterval: "30s"
|
||||
|
|
@ -1041,7 +1041,7 @@ tls:
|
|||
secrets:
|
||||
upstreamCredentials:
|
||||
existingSecret: "aenebris-upstream-creds"
|
||||
|
||||
|
||||
# Network policies
|
||||
networkPolicy:
|
||||
enabled: true
|
||||
|
|
@ -1496,7 +1496,7 @@ spec:
|
|||
timeoutSeconds: 1
|
||||
failureThreshold: 30 # 5s + (30 * 2s) = 65s max
|
||||
successThreshold: 1
|
||||
|
||||
|
||||
# Liveness probe - detects deadlocks
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
|
|
@ -1511,7 +1511,7 @@ spec:
|
|||
timeoutSeconds: 2
|
||||
failureThreshold: 3 # Restart after 30s of failures
|
||||
successThreshold: 1
|
||||
|
||||
|
||||
# Readiness probe - traffic management
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
|
|
@ -1541,15 +1541,15 @@ lifecycle:
|
|||
- |
|
||||
# Stop accepting new connections
|
||||
echo "Graceful shutdown initiated at $(date)"
|
||||
|
||||
|
||||
# Send SIGTERM to application (it should stop accepting)
|
||||
kill -TERM 1
|
||||
|
||||
|
||||
# Wait for existing connections to drain
|
||||
# (110 seconds, leaving 10s buffer before SIGKILL)
|
||||
echo "Waiting for connections to drain..."
|
||||
sleep 110
|
||||
|
||||
|
||||
echo "Shutdown complete at $(date)"
|
||||
```
|
||||
|
||||
|
|
@ -1575,23 +1575,23 @@ main :: IO ()
|
|||
main = do
|
||||
shutdownFlag <- newTVarIO False
|
||||
setupGracefulShutdown shutdownFlag
|
||||
|
||||
|
||||
-- Start server in separate thread
|
||||
serverThread <- async $ runServer shutdownFlag
|
||||
|
||||
|
||||
-- Wait for shutdown signal
|
||||
atomically $ do
|
||||
shutdown <- readTVar shutdownFlag
|
||||
unless shutdown retry
|
||||
|
||||
|
||||
putStrLn "Stopping server, draining connections..."
|
||||
|
||||
|
||||
-- Stop accepting new connections
|
||||
stopAcceptingConnections
|
||||
|
||||
|
||||
-- Wait for existing connections to complete
|
||||
waitForConnectionsDrain 110 -- 110 seconds
|
||||
|
||||
|
||||
putStrLn "All connections drained, exiting"
|
||||
|
||||
waitForConnectionsDrain :: Int -> IO ()
|
||||
|
|
@ -1617,10 +1617,10 @@ metadata:
|
|||
# AWS NLB
|
||||
service.beta.kubernetes.io/aws-load-balancer-connection-draining-enabled: "true"
|
||||
service.beta.kubernetes.io/aws-load-balancer-connection-draining-timeout: "120"
|
||||
|
||||
|
||||
# GCP
|
||||
cloud.google.com/neg: '{"ingress": true}'
|
||||
|
||||
|
||||
spec:
|
||||
sessionAffinity: ClientIP
|
||||
sessionAffinityConfig:
|
||||
|
|
@ -1783,7 +1783,7 @@ sum(websocket_connections_total) by (pod)
|
|||
rate(container_cpu_cfs_throttled_seconds_total{pod=~"aenebris.*"}[5m])
|
||||
|
||||
# Memory usage vs limit
|
||||
container_memory_usage_bytes{pod=~"aenebris.*"} /
|
||||
container_memory_usage_bytes{pod=~"aenebris.*"} /
|
||||
container_spec_memory_limit_bytes{pod=~"aenebris.*"}
|
||||
```
|
||||
|
||||
|
|
@ -1853,14 +1853,14 @@ kubectl delete certificate aenebris-tls -n production
|
|||
|
||||
This comprehensive deployment guide provides a production-ready foundation for deploying Ᾰenebris on Kubernetes. The architecture achieves:
|
||||
|
||||
✅ **Ultra-minimal Docker images** (15-50MB) via multi-stage builds and static linking
|
||||
✅ **High availability** with topology spread, pod disruption budgets, and anti-affinity
|
||||
✅ **Zero-downtime deployments** through graceful shutdown and connection draining
|
||||
✅ **Automated TLS management** with cert-manager
|
||||
✅ **Secure secrets handling** via External Secrets Operator and Sealed Secrets
|
||||
✅ **Production-grade monitoring** with Prometheus and Grafana
|
||||
✅ **Dynamic scaling** from 5 to 50 replicas based on traffic
|
||||
✅ **WebSocket support** with proper connection management
|
||||
✅ **Ultra-minimal Docker images** (15-50MB) via multi-stage builds and static linking
|
||||
✅ **High availability** with topology spread, pod disruption budgets, and anti-affinity
|
||||
✅ **Zero-downtime deployments** through graceful shutdown and connection draining
|
||||
✅ **Automated TLS management** with cert-manager
|
||||
✅ **Secure secrets handling** via External Secrets Operator and Sealed Secrets
|
||||
✅ **Production-grade monitoring** with Prometheus and Grafana
|
||||
✅ **Dynamic scaling** from 5 to 50 replicas based on traffic
|
||||
✅ **WebSocket support** with proper connection management
|
||||
|
||||
**Next Steps:**
|
||||
1. Customize values.yaml for your environment
|
||||
|
|
@ -194,7 +194,7 @@ bingExample = do
|
|||
### WaiProxyResponse type
|
||||
|
||||
```haskell
|
||||
data WaiProxyResponse
|
||||
data WaiProxyResponse
|
||||
= WPRResponse WAI.Response -- Return custom response
|
||||
| WPRProxyDest ProxyDest -- Forward to destination
|
||||
| WPRModifiedRequest WAI.Request ProxyDest -- Modify then forward
|
||||
|
|
@ -245,7 +245,7 @@ proxyApp manager request respond = do
|
|||
else give chunk >> loop
|
||||
in loop
|
||||
}
|
||||
|
||||
|
||||
-- Make the request and forward response
|
||||
httpLbs proxiedReq manager >>= \response ->
|
||||
respond $ responseLBS
|
||||
|
|
@ -257,7 +257,7 @@ proxyApp manager request respond = do
|
|||
fixHeaders :: RequestHeaders -> RequestHeaders
|
||||
fixHeaders = filter $ \(name, _) -> name `notElem` hopByHopHeaders
|
||||
where
|
||||
hopByHopHeaders =
|
||||
hopByHopHeaders =
|
||||
[ "connection", "keep-alive", "proxy-authenticate"
|
||||
, "proxy-authorization", "te", "trailers"
|
||||
, "transfer-encoding", "upgrade"
|
||||
|
|
@ -266,7 +266,7 @@ fixHeaders = filter $ \(name, _) -> name `notElem` hopByHopHeaders
|
|||
fixResponseHeaders :: ResponseHeaders -> ResponseHeaders
|
||||
fixResponseHeaders = filter $ \(name, _) -> name `notElem` hopByHopHeaders
|
||||
where
|
||||
hopByHopHeaders =
|
||||
hopByHopHeaders =
|
||||
[ "connection", "keep-alive", "proxy-authenticate"
|
||||
, "proxy-authorization", "te", "trailers"
|
||||
, "transfer-encoding", "upgrade"
|
||||
|
|
@ -286,7 +286,7 @@ main = do
|
|||
```haskell
|
||||
-- Add X-Forwarded-For header
|
||||
addForwardedFor :: Request -> RequestHeaders -> RequestHeaders
|
||||
addForwardedFor req headers =
|
||||
addForwardedFor req headers =
|
||||
let clientIP = show (remoteHost req)
|
||||
existing = lookup "X-Forwarded-For" headers
|
||||
newValue = case existing of
|
||||
|
|
@ -369,7 +369,7 @@ import Network.HTTP.Types
|
|||
import Data.ByteString.Builder (byteString)
|
||||
|
||||
app :: Application
|
||||
app _req respond = respond $
|
||||
app _req respond = respond $
|
||||
responseStream status200 [("Content-Type", "text/plain")] $ \write flush -> do
|
||||
write $ byteString "Hello\n"
|
||||
flush
|
||||
|
|
@ -388,10 +388,10 @@ import Data.Function (fix)
|
|||
import Control.Monad (unless)
|
||||
|
||||
streamFileApp :: Application
|
||||
streamFileApp _req respond =
|
||||
streamFileApp _req respond =
|
||||
withBinaryFile "largefile.txt" ReadMode $ \h ->
|
||||
respond $ responseStream status200 [("Content-Type", "text/plain")] $
|
||||
\chunk _flush ->
|
||||
respond $ responseStream status200 [("Content-Type", "text/plain")] $
|
||||
\chunk _flush ->
|
||||
fix $ \loop -> do
|
||||
bs <- B.hGetSome h 4096 -- Only 4KB in memory at once
|
||||
unless (B.null bs) $ do
|
||||
|
|
@ -414,17 +414,17 @@ import qualified Data.ByteString.Char8 as C8
|
|||
|
||||
sseApp :: Application
|
||||
sseApp _req sendResponse = sendResponse $
|
||||
responseStream status200
|
||||
responseStream status200
|
||||
[("Content-Type", "text/event-stream"),
|
||||
("Cache-Control", "no-cache"),
|
||||
("Connection", "keep-alive")]
|
||||
("Connection", "keep-alive")]
|
||||
myStream
|
||||
|
||||
myStream :: (Builder -> IO ()) -> IO () -> IO ()
|
||||
myStream send flush = do
|
||||
send $ byteString "data: Starting streaming response.\n\n"
|
||||
flush
|
||||
|
||||
|
||||
forM_ [1..50 :: Int] $ \i -> do
|
||||
threadDelay 1000000 -- 1 second
|
||||
send $ byteString "data: Message " <> byteString (C8.pack $ show i) <> byteString "\n\n"
|
||||
|
|
@ -464,10 +464,10 @@ main = do
|
|||
-- Pure operations
|
||||
result <- runConduit $ yieldMany [1..10] .| sumC
|
||||
print result -- 55
|
||||
|
||||
|
||||
-- File operations
|
||||
runConduitRes $
|
||||
sourceFile "input.txt" .|
|
||||
runConduitRes $
|
||||
sourceFile "input.txt" .|
|
||||
sinkFile "output.txt"
|
||||
```
|
||||
|
||||
|
|
@ -480,8 +480,8 @@ import qualified Data.Conduit.Binary as CB
|
|||
import Control.Monad.Trans.Resource
|
||||
|
||||
conduitFileApp :: Application
|
||||
conduitFileApp _req respond =
|
||||
respond $ responseStream status200 [("Content-Type", "text/plain")] $
|
||||
conduitFileApp _req respond =
|
||||
respond $ responseStream status200 [("Content-Type", "text/plain")] $
|
||||
\write flush -> runResourceT $ do
|
||||
CB.sourceFile "largefile.txt" $$ CB.mapM_ $ \chunk -> liftIO $ do
|
||||
write (byteString chunk)
|
||||
|
|
@ -498,8 +498,8 @@ import qualified Data.Conduit.List as CL
|
|||
streamFromDB :: Handler ()
|
||||
streamFromDB = do
|
||||
-- selectSource returns a conduit Source
|
||||
selectSource [] []
|
||||
$$ CL.mapM_ $ \(Entity _ record) ->
|
||||
selectSource [] []
|
||||
$$ CL.mapM_ $ \(Entity _ record) ->
|
||||
liftIO $ processRecord record
|
||||
```
|
||||
|
||||
|
|
@ -531,8 +531,8 @@ import Data.Conduit
|
|||
import qualified Data.Conduit.List as CL
|
||||
|
||||
sumConduit :: IO Int
|
||||
sumConduit = runConduit $
|
||||
yieldMany [1..10] .|
|
||||
sumConduit = runConduit $
|
||||
yieldMany [1..10] .|
|
||||
CL.fold (+) 0
|
||||
```
|
||||
|
||||
|
|
@ -589,17 +589,17 @@ generateCSV write flush = do
|
|||
-- Header
|
||||
write $ byteString "id,name,value\n"
|
||||
flush
|
||||
|
||||
|
||||
-- Generate 1 million rows without buffering
|
||||
forM_ [1..1000000] $ \i -> do
|
||||
let row = byteString $ C8.pack $
|
||||
let row = byteString $ C8.pack $
|
||||
show i ++ ",Item" ++ show i ++ "," ++ show (i * 100) ++ "\n"
|
||||
write row
|
||||
when (i `mod` 1000 == 0) flush
|
||||
|
||||
csvApp :: Application
|
||||
csvApp _req respond = respond $
|
||||
responseStream status200
|
||||
responseStream status200
|
||||
[("Content-Type", "text/csv"),
|
||||
("Content-Disposition", "attachment; filename=data.csv")]
|
||||
generateCSV
|
||||
|
|
@ -611,19 +611,19 @@ csvApp _req respond = respond $
|
|||
processWithProgress :: (Builder -> IO ()) -> IO () -> IO ()
|
||||
processWithProgress write flush = do
|
||||
let total = 100
|
||||
|
||||
|
||||
forM_ [1..total] $ \i -> do
|
||||
threadDelay 100000 -- Simulate work
|
||||
|
||||
let progress = byteString $
|
||||
"data: {\"progress\": " <>
|
||||
byteString (C8.pack $ show i) <>
|
||||
", \"total\": " <>
|
||||
byteString (C8.pack $ show total) <>
|
||||
|
||||
let progress = byteString $
|
||||
"data: {\"progress\": " <>
|
||||
byteString (C8.pack $ show i) <>
|
||||
", \"total\": " <>
|
||||
byteString (C8.pack $ show total) <>
|
||||
"}\n\n"
|
||||
write progress
|
||||
flush
|
||||
|
||||
|
||||
write $ byteString "data: {\"status\": \"complete\"}\n\n"
|
||||
flush
|
||||
```
|
||||
|
|
@ -725,7 +725,7 @@ application state pending = do
|
|||
client = (T.drop (T.length prefix) msg, conn)
|
||||
disconnect = do
|
||||
s <- modifyMVar state $ \s ->
|
||||
let s' = filter ((/= fst client) . fst) s
|
||||
let s' = filter ((/= fst client) . fst) s
|
||||
in return (s', s')
|
||||
broadcast (fst client <> " disconnected") s
|
||||
|
||||
|
|
@ -798,9 +798,9 @@ advancedTLS = do
|
|||
}
|
||||
, onInsecure = DenyInsecure "This server requires HTTPS"
|
||||
}
|
||||
|
||||
|
||||
warpOpts = setPort 443 $ setHost "0.0.0.0" $ defaultSettings
|
||||
|
||||
|
||||
runTLS tlsOpts warpOpts app
|
||||
|
||||
validateClientCert :: CertificateChain -> IO CertificateUsage
|
||||
|
|
@ -821,7 +821,7 @@ secureTLS = do
|
|||
let tlsOpts = tlsSettings "cert.pem" "key.pem"
|
||||
warpOpts = setPort 443 defaultSettings
|
||||
wsApp = websocketsOr WS.defaultConnectionOptions wsHandler httpApp
|
||||
|
||||
|
||||
runTLS tlsOpts warpOpts wsApp
|
||||
|
||||
wsHandler :: WS.ServerApp
|
||||
|
|
@ -844,7 +844,7 @@ dynamicCerts = do
|
|||
Just "example.com" -> loadCredentials "example.pem" "example-key.pem"
|
||||
Just "other.com" -> loadCredentials "other.pem" "other-key.pem"
|
||||
_ -> loadCredentials "default.pem" "default-key.pem"
|
||||
|
||||
|
||||
runTLS tlsOpts defaultSettings app
|
||||
|
||||
loadCredentials :: FilePath -> FilePath -> IO Credentials
|
||||
|
|
@ -909,7 +909,7 @@ gracefulShutdown = do
|
|||
let settings = setInstallShutdownHandler shutdownHandler
|
||||
$ setGracefulShutdownTimeout (Just 30) -- 30 seconds max
|
||||
$ setPort 8080 defaultSettings
|
||||
|
||||
|
||||
runSettings settings app
|
||||
where
|
||||
shutdownHandler closeSocket = do
|
||||
|
|
@ -935,7 +935,7 @@ gracefulShutdown = do
|
|||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
productionSettings :: Settings
|
||||
productionSettings =
|
||||
productionSettings =
|
||||
setPort 443
|
||||
$ setHost "*"
|
||||
$ setOnOpen onOpen
|
||||
|
|
@ -953,13 +953,13 @@ productionSettings =
|
|||
onOpen sockAddr = do
|
||||
putStrLn $ "Connection: " ++ show sockAddr
|
||||
return True
|
||||
|
||||
onClose sockAddr =
|
||||
|
||||
onClose sockAddr =
|
||||
putStrLn $ "Closed: " ++ show sockAddr
|
||||
|
||||
onException _ e =
|
||||
|
||||
onException _ e =
|
||||
putStrLn $ "Exception: " ++ show e
|
||||
|
||||
|
||||
shutdownHandler closeSocket = do
|
||||
_ <- installHandler sigTERM (Catch closeSocket) Nothing
|
||||
_ <- installHandler sigINT (Catch closeSocket) Nothing
|
||||
|
|
@ -1023,16 +1023,16 @@ websocketHandler pending = do
|
|||
|
||||
-- HTTP handler (including streaming)
|
||||
httpHandler :: Application
|
||||
httpHandler req respond =
|
||||
httpHandler req respond =
|
||||
case pathInfo req of
|
||||
["stream"] -> respond $ streamingResponse
|
||||
["ws"] -> respond $ responseLBS status404 [] "Use WebSocket protocol"
|
||||
_ -> respond $ responseLBS status200 [] "Hello HTTP"
|
||||
|
||||
streamingResponse :: Response
|
||||
streamingResponse = responseStream
|
||||
status200
|
||||
[("Content-Type", "text/event-stream")]
|
||||
streamingResponse = responseStream
|
||||
status200
|
||||
[("Content-Type", "text/event-stream")]
|
||||
$ \write flush -> forever $ do
|
||||
threadDelay 1000000
|
||||
write $ byteString "data: tick\n\n"
|
||||
|
|
@ -1067,10 +1067,10 @@ server {
|
|||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
|
||||
# Critical: disable buffering
|
||||
proxy_buffering off;
|
||||
|
||||
|
||||
# Increase timeouts
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
|
|
@ -1080,11 +1080,11 @@ server {
|
|||
location /stream/ {
|
||||
proxy_pass http://localhost:8002;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
|
||||
# Critical: disable buffering for streaming
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
|
||||
|
||||
# Keep connection alive
|
||||
proxy_set_header Connection '';
|
||||
proxy_set_header Cache-Control 'no-cache';
|
||||
|
|
@ -1242,9 +1242,9 @@ For unidirectional streaming, SSE offers advantages:
|
|||
```haskell
|
||||
sseHandler :: Application
|
||||
sseHandler _req respond = respond $
|
||||
responseStream status200
|
||||
responseStream status200
|
||||
[("Content-Type", "text/event-stream"),
|
||||
("Cache-Control", "no-cache")]
|
||||
("Cache-Control", "no-cache")]
|
||||
$ \write flush -> forever $ do
|
||||
threadDelay 1000000
|
||||
write $ byteString "data: update\n\n"
|
||||
|
|
@ -163,14 +163,14 @@ server {
|
|||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
|
||||
|
||||
# HTTP/3 over UDP
|
||||
listen 443 quic reuseport;
|
||||
listen [::]:443 quic reuseport;
|
||||
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
add_header Alt-Svc 'h3=":443"; ma=86400' always;
|
||||
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000; # Warp
|
||||
proxy_http_version 1.1;
|
||||
|
|
@ -221,7 +221,7 @@ import Control.Monad.Trans.Resource
|
|||
|
||||
app :: Application
|
||||
app req respond = runResourceT $ do
|
||||
(releaseKey, handle) <- allocate
|
||||
(releaseKey, handle) <- allocate
|
||||
(openFile "data.txt" ReadMode)
|
||||
hClose
|
||||
content <- liftIO $ hGetContents handle
|
||||
|
|
@ -88,7 +88,7 @@ data Backend = Backend
|
|||
}
|
||||
|
||||
trackConnection :: Backend -> IO a -> IO a
|
||||
trackConnection backend action =
|
||||
trackConnection backend action =
|
||||
bracket_
|
||||
(atomically $ modifyTVar' (activeConnections backend) (+1))
|
||||
(atomically $ modifyTVar' (activeConnections backend) (subtract 1))
|
||||
|
|
@ -110,7 +110,7 @@ This pattern **composes naturally with health checks** - the STM transaction can
|
|||
import Control.Concurrent.LoadDistribution
|
||||
|
||||
lb <- evenlyDistributed (return $ Set.fromList backends)
|
||||
withResource lb $ \maybeBackend ->
|
||||
withResource lb $ \maybeBackend ->
|
||||
case maybeBackend of
|
||||
Just backend -> proxyRequest backend
|
||||
Nothing -> handleNoBackends
|
||||
|
|
@ -132,18 +132,18 @@ data WeightedBackend a = WeightedBackend
|
|||
smoothWeightedSelect :: [WeightedBackend a] -> IO a
|
||||
smoothWeightedSelect backends = atomically $ do
|
||||
-- Increase all current weights by their base weights
|
||||
forM_ backends $ \wb ->
|
||||
forM_ backends $ \wb ->
|
||||
modifyTVar' (currentWeight wb) (+ weight wb)
|
||||
|
||||
|
||||
-- Find backend with maximum current weight
|
||||
weights <- mapM (readTVar . currentWeight) backends
|
||||
let maxWeight = maximum weights
|
||||
selected = backends !! fromJust (findIndex (== maxWeight) weights)
|
||||
|
||||
|
||||
-- Reduce selected backend's current weight by total
|
||||
let totalWeight = sum (map weight backends)
|
||||
modifyTVar' (currentWeight selected) (subtract totalWeight)
|
||||
|
||||
|
||||
return $ backend selected
|
||||
```
|
||||
|
||||
|
|
@ -188,7 +188,7 @@ data BackendState = BackendState
|
|||
|
||||
-- BAD: Touches many TVars in single transaction
|
||||
countAllConnections :: [Backend] -> STM Int
|
||||
countAllConnections backends =
|
||||
countAllConnections backends =
|
||||
sum <$> mapM (readTVar . activeConns) backends
|
||||
|
||||
-- GOOD: Maintain aggregate counter
|
||||
|
|
@ -281,7 +281,7 @@ The `retry` primitive is STM's killer feature - **threads automatically block un
|
|||
badPattern = atomically $ do
|
||||
val <- expensiveComputation -- Recomputed on every retry!
|
||||
tvar <- readTVar someTVar
|
||||
|
||||
|
||||
-- GOOD: Pure computation outside
|
||||
goodPattern = do
|
||||
let val = expensiveComputation
|
||||
|
|
@ -299,7 +299,7 @@ goodPattern = do
|
|||
```haskell
|
||||
-- Update multiple backend states atomically
|
||||
updateHealthChecks :: [Backend] -> [(Backend, Bool)] -> STM ()
|
||||
updateHealthChecks backends results =
|
||||
updateHealthChecks backends results =
|
||||
forM_ results $ \(backend, isHealthy) ->
|
||||
writeTVar (healthy backend) isHealthy
|
||||
```
|
||||
|
|
@ -344,7 +344,7 @@ performHealthCheck manager config backend = do
|
|||
req <- parseRequest url
|
||||
response <- httpLbs req manager
|
||||
return $ statusCode (responseStatus response) == 200
|
||||
|
||||
|
||||
return $ fromMaybe False result
|
||||
```
|
||||
|
||||
|
|
@ -362,13 +362,13 @@ import Control.Monad (forever)
|
|||
healthCheckLoop :: Manager -> HealthCheckConfig -> [Backend] -> IO ()
|
||||
healthCheckLoop manager config backends = forever $ do
|
||||
-- Check all backends concurrently
|
||||
results <- mapConcurrently
|
||||
(performHealthCheck manager config)
|
||||
results <- mapConcurrently
|
||||
(performHealthCheck manager config)
|
||||
backends
|
||||
|
||||
|
||||
-- Update backend states
|
||||
zipWithM_ (updateBackendState config) backends results
|
||||
|
||||
|
||||
-- Wait for next interval
|
||||
threadDelay (hcInterval config * 1000000)
|
||||
|
||||
|
|
@ -414,7 +414,7 @@ updateBackendState config backend healthy = atomically $ do
|
|||
state <- readTVar (backendState backend)
|
||||
failures <- readTVar (backendFailures backend)
|
||||
successes <- readTVar (backendSuccesses backend)
|
||||
|
||||
|
||||
case (state, healthy) of
|
||||
(Healthy, False) -> do
|
||||
let newFailures = failures + 1
|
||||
|
|
@ -422,22 +422,22 @@ updateBackendState config backend healthy = atomically $ do
|
|||
when (newFailures >= hcMaxFailures config) $ do
|
||||
writeTVar (backendState backend) Unhealthy
|
||||
writeTVar (backendFailures backend) 0
|
||||
|
||||
|
||||
(Unhealthy, True) -> do
|
||||
writeTVar (backendState backend) Recovering
|
||||
writeTVar (backendSuccesses backend) 1
|
||||
|
||||
|
||||
(Recovering, True) -> do
|
||||
let newSuccesses = successes + 1
|
||||
writeTVar (backendSuccesses backend) newSuccesses
|
||||
when (newSuccesses >= hcRecoveryAttempts config) $ do
|
||||
writeTVar (backendState backend) Healthy
|
||||
writeTVar (backendSuccesses backend) 0
|
||||
|
||||
|
||||
(Recovering, False) -> do
|
||||
writeTVar (backendState backend) Unhealthy
|
||||
writeTVar (backendSuccesses backend) 0
|
||||
|
||||
|
||||
_ -> return ()
|
||||
```
|
||||
|
||||
|
|
@ -458,15 +458,15 @@ testBreaker = undefined
|
|||
proxyWithCircuitBreaker :: Backend -> Request -> IO Response
|
||||
proxyWithCircuitBreaker backend req = do
|
||||
cbConf <- initialBreakerState
|
||||
|
||||
result <- flip runReaderT cbConf $
|
||||
|
||||
result <- flip runReaderT cbConf $
|
||||
withBreaker testBreaker $ liftIO $ forwardRequest backend req
|
||||
|
||||
|
||||
case result of
|
||||
Left (CircuitBreakerClosed msg) ->
|
||||
Left (CircuitBreakerClosed msg) ->
|
||||
-- Circuit open, return cached response or error
|
||||
return $ errorResponse 503 "Service Temporarily Unavailable"
|
||||
Right response ->
|
||||
Right response ->
|
||||
return response
|
||||
```
|
||||
|
||||
|
|
@ -521,7 +521,7 @@ exponentialBackoffWithJitter config action = go 0 (initialDelay config)
|
|||
```haskell
|
||||
import Control.Retry
|
||||
|
||||
recovering
|
||||
recovering
|
||||
(exponentialBackoff 50000 <> limitRetries 5)
|
||||
[const $ Handler $ \e -> return (isRetryable e)]
|
||||
(\_ -> performHealthCheck backend)
|
||||
|
|
@ -554,16 +554,16 @@ proxyApp :: ProxyConfig -> Application
|
|||
proxyApp config req respond = do
|
||||
mBackend <- selectHealthyBackend (pcBalancer config)
|
||||
case mBackend of
|
||||
Nothing ->
|
||||
Nothing ->
|
||||
respond $ responseLBS status503 [] "No healthy backends available"
|
||||
Just backend ->
|
||||
waiProxyTo
|
||||
(\_ -> return $ WPRProxyDest (ProxyDest
|
||||
(backendHost backend)
|
||||
Just backend ->
|
||||
waiProxyTo
|
||||
(\_ -> return $ WPRProxyDest (ProxyDest
|
||||
(backendHost backend)
|
||||
(backendPort backend)))
|
||||
defaultOnExc
|
||||
defaultOnExc
|
||||
(pcManager config)
|
||||
req
|
||||
req
|
||||
respond
|
||||
```
|
||||
|
||||
|
|
@ -585,7 +585,7 @@ import Network.Wai (Middleware, mapResponseHeaders)
|
|||
-- Add load balancer info header
|
||||
addBackendHeader :: Backend -> Middleware
|
||||
addBackendHeader backend app req respond =
|
||||
app req $ respond . mapResponseHeaders
|
||||
app req $ respond . mapResponseHeaders
|
||||
((hBackend, encodeUtf8 $ backendId backend) :)
|
||||
|
||||
-- Connection tracking middleware
|
||||
|
|
@ -593,18 +593,18 @@ trackingMiddleware :: ServerMetrics -> Middleware
|
|||
trackingMiddleware metrics app req respond = do
|
||||
atomically $ modifyTVar' (activeConnections metrics) (+1)
|
||||
atomically $ modifyTVar' (totalRequests metrics) (+1)
|
||||
|
||||
|
||||
let respond' res = do
|
||||
atomically $ modifyTVar' (activeConnections metrics) (subtract 1)
|
||||
respond res
|
||||
|
||||
|
||||
app req respond' `onException`
|
||||
atomically (modifyTVar' (errorCount metrics) (+1))
|
||||
|
||||
-- Compose middleware
|
||||
main = do
|
||||
config <- initProxyConfig
|
||||
let app = trackingMiddleware metrics
|
||||
let app = trackingMiddleware metrics
|
||||
$ proxyApp config
|
||||
run 8000 app
|
||||
```
|
||||
|
|
@ -624,7 +624,7 @@ main = do
|
|||
**Compile-time optimizations**:
|
||||
|
||||
```cabal
|
||||
ghc-options: -Wall -O2 -threaded
|
||||
ghc-options: -Wall -O2 -threaded
|
||||
-rtsopts -with-rtsopts=-N
|
||||
-fspec-constr -fspecialise
|
||||
-funbox-strict-fields
|
||||
|
|
@ -709,7 +709,7 @@ data ProxyConfig = ProxyConfig
|
|||
|
||||
-- Backend selection dispatcher
|
||||
selectBackend :: ProxyConfig -> IO (Maybe Backend)
|
||||
selectBackend config =
|
||||
selectBackend config =
|
||||
case strategy config of
|
||||
RoundRobin -> selectRoundRobin config
|
||||
LeastConnections -> selectLeastConnections config
|
||||
|
|
@ -722,7 +722,7 @@ selectRoundRobin config = do
|
|||
len = V.length backends'
|
||||
idx <- atomicModifyIORef' (rrCounter config) $ \i ->
|
||||
((i + 1) `mod` len, i)
|
||||
|
||||
|
||||
-- Find next healthy backend
|
||||
findHealthy backends' idx len
|
||||
where
|
||||
|
|
@ -733,7 +733,7 @@ selectRoundRobin config = do
|
|||
isHealthy <- atomically $ (== Healthy) <$> readTVar (state backend)
|
||||
if isHealthy
|
||||
then return (Just backend)
|
||||
else findHealthy backends'
|
||||
else findHealthy backends'
|
||||
((start + 1) `mod` V.length backends')
|
||||
(remaining - 1)
|
||||
|
||||
|
|
@ -756,24 +756,24 @@ selectLeastConnections config = do
|
|||
selectWeightedRR :: ProxyConfig -> IO (Maybe Backend)
|
||||
selectWeightedRR config = atomically $ do
|
||||
let backends' = V.toList (backends config)
|
||||
|
||||
|
||||
-- Increase current weights
|
||||
forM_ backends' $ \wb ->
|
||||
forM_ backends' $ \wb ->
|
||||
modifyTVar' (currentWeight wb) (+ backendWeight wb)
|
||||
|
||||
|
||||
-- Select backend with max current weight
|
||||
weights <- mapM (readTVar . currentWeight) backends'
|
||||
let maxWeight = maximum weights
|
||||
selected = backends' !! fromJust (findIndex (== maxWeight) weights)
|
||||
|
||||
|
||||
-- Check health
|
||||
isHealthy <- (== Healthy) <$> readTVar (state selected)
|
||||
guard isHealthy
|
||||
|
||||
|
||||
-- Reduce selected backend's current weight
|
||||
let totalWeight = sum (map backendWeight backends')
|
||||
modifyTVar' (currentWeight selected) (subtract totalWeight)
|
||||
|
||||
|
||||
return selected
|
||||
|
||||
-- Main proxy application
|
||||
|
|
@ -781,17 +781,17 @@ proxyApp :: ProxyConfig -> Application
|
|||
proxyApp config req respond = do
|
||||
mBackend <- selectBackend config
|
||||
case mBackend of
|
||||
Nothing ->
|
||||
Nothing ->
|
||||
respond $ responseLBS status503 [] "No healthy backends"
|
||||
Just backend -> do
|
||||
-- Track connection
|
||||
atomically $ modifyTVar' (activeConns backend) (+1)
|
||||
|
||||
|
||||
let dest = ProxyDest (backendHost backend) (backendPort backend)
|
||||
respond' res = do
|
||||
atomically $ modifyTVar' (activeConns backend) (subtract 1)
|
||||
respond res
|
||||
|
||||
|
||||
waiProxyTo
|
||||
(\_ -> return $ WPRProxyDest dest)
|
||||
defaultOnExc
|
||||
|
|
@ -807,14 +807,14 @@ healthCheckLoop manager backends = forever $ do
|
|||
threadDelay 10000000 -- 10 seconds
|
||||
where
|
||||
checkHealth mgr backend = do
|
||||
let url = "http://" <> backendHost backend <> ":"
|
||||
let url = "http://" <> backendHost backend <> ":"
|
||||
<> show (backendPort backend) <> "/health"
|
||||
result <- timeout 2000000 $ do
|
||||
req <- parseRequest (unpack url)
|
||||
response <- httpLbs req mgr
|
||||
return $ statusCode (responseStatus response) == 200
|
||||
return $ fromMaybe False result
|
||||
|
||||
|
||||
updateHealth backend healthy = atomically $ do
|
||||
currentState <- readTVar (state backend)
|
||||
failureCount <- readTVar (failures backend)
|
||||
|
|
@ -838,7 +838,7 @@ initProxyConfig specs strat = do
|
|||
manager <- newTlsManager
|
||||
counter <- newIORef 0
|
||||
checker <- async $ healthCheckLoop manager (V.toList backends)
|
||||
|
||||
|
||||
return ProxyConfig
|
||||
{ backends = backends
|
||||
, strategy = strat
|
||||
|
|
@ -859,18 +859,18 @@ initProxyConfig specs strat = do
|
|||
-- Main entry point
|
||||
main :: IO ()
|
||||
main = do
|
||||
let backendSpecs =
|
||||
let backendSpecs =
|
||||
[ BackendSpec "localhost" 8001 5
|
||||
, BackendSpec "localhost" 8002 1
|
||||
, BackendSpec "localhost" 8003 1
|
||||
]
|
||||
|
||||
|
||||
config <- initProxyConfig backendSpecs SmoothWeightedRR
|
||||
|
||||
let settings = setPort 8000
|
||||
$ setTimeout 30
|
||||
|
||||
let settings = setPort 8000
|
||||
$ setTimeout 30
|
||||
$ defaultSettings
|
||||
|
||||
|
||||
putStrLn "Load balancing proxy started on port 8000"
|
||||
runSettings settings (proxyApp config)
|
||||
```
|
||||
|
|
@ -939,8 +939,8 @@ import Test.DejaFu
|
|||
testNoDeadlock :: IO ()
|
||||
testNoDeadlock = autocheck $ do
|
||||
balancer <- setup
|
||||
concurrently_
|
||||
(selectBackend balancer)
|
||||
concurrently_
|
||||
(selectBackend balancer)
|
||||
(selectBackend balancer)
|
||||
```
|
||||
|
||||
|
|
@ -30,13 +30,13 @@ import Foreign.C.Types
|
|||
import System.Posix.Types (Fd(..))
|
||||
|
||||
foreign import ccall unsafe "splice"
|
||||
c_splice :: CInt -> Ptr CLong -> CInt -> Ptr CLong
|
||||
c_splice :: CInt -> Ptr CLong -> CInt -> Ptr CLong
|
||||
-> CSize -> CUInt -> IO CSsize
|
||||
|
||||
splice :: Fd -> Fd -> Int -> [SpliceFlag] -> IO Int
|
||||
splice (Fd fdIn) (Fd fdOut) len flags = do
|
||||
let cflags = foldr (.|.) 0 [f | SpliceFlag f <- flags]
|
||||
result <- c_splice fdIn nullPtr fdOut nullPtr
|
||||
result <- c_splice fdIn nullPtr fdOut nullPtr
|
||||
(fromIntegral len) cflags
|
||||
if result == -1
|
||||
then throwErrno "splice"
|
||||
|
|
@ -59,10 +59,10 @@ The `simple-sendfile` package powers Warp's high-performance static file serving
|
|||
```haskell
|
||||
import Network.Sendfile
|
||||
|
||||
sendfileWithHeader :: Socket -> FilePath -> FileRange
|
||||
sendfileWithHeader :: Socket -> FilePath -> FileRange
|
||||
-> IO () -> [ByteString] -> IO ()
|
||||
-- Sends headers and file data efficiently
|
||||
sendfileWithHeader sock path (PartOfFile offset len)
|
||||
sendfileWithHeader sock path (PartOfFile offset len)
|
||||
tickle headers
|
||||
```
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ spliceWithRetry :: Fd -> Fd -> Int -> IO ()
|
|||
spliceWithRetry fdIn fdOut chunkSize = loop
|
||||
where
|
||||
loop = do
|
||||
result <- try $ splice fdIn fdOut chunkSize
|
||||
result <- try $ splice fdIn fdOut chunkSize
|
||||
[spliceNonBlock, spliceMore]
|
||||
case result of
|
||||
Right 0 -> return () -- EOF
|
||||
|
|
@ -111,15 +111,15 @@ Conversion costs between variants matter significantly. `toStrict` forces entire
|
|||
```haskell
|
||||
import Data.ByteString.Builder
|
||||
|
||||
buildHttpResponse :: Int -> [(ByteString, ByteString)]
|
||||
buildHttpResponse :: Int -> [(ByteString, ByteString)]
|
||||
-> LazyByteString -> LazyByteString
|
||||
buildHttpResponse status headers body = toLazyByteString builder
|
||||
where
|
||||
builder = statusLine <> headerLines
|
||||
builder = statusLine <> headerLines
|
||||
<> byteString "\r\n" <> lazyByteString body
|
||||
statusLine = byteString "HTTP/1.1 " <> intDec status
|
||||
statusLine = byteString "HTTP/1.1 " <> intDec status
|
||||
<> byteString " OK\r\n"
|
||||
headerLines = mconcat
|
||||
headerLines = mconcat
|
||||
[ byteString k <> byteString ": " <> byteString v <> byteString "\r\n"
|
||||
| (k, v) <- headers ]
|
||||
```
|
||||
|
|
@ -134,7 +134,7 @@ import Data.Pool
|
|||
createBackendPool :: HostName -> PortNumber -> IO (Pool Socket)
|
||||
createBackendPool host port = do
|
||||
capabilities <- getNumCapabilities
|
||||
newPool $
|
||||
newPool $
|
||||
defaultPoolConfig
|
||||
(connectBackend host port) -- Create function
|
||||
close -- Destroy function
|
||||
|
|
@ -228,7 +228,7 @@ Interpreting results requires understanding latency distribution. The `--latency
|
|||
```
|
||||
Latency Distribution
|
||||
50% 635.91us
|
||||
75% 712.34us
|
||||
75% 712.34us
|
||||
90% 1.04ms
|
||||
99% 2.87ms
|
||||
```
|
||||
|
|
@ -385,7 +385,7 @@ ThreadScope displays CPU activity across cores, spark creation/conversion (for p
|
|||
**Profiling workflow progresses systematically**:
|
||||
|
||||
1. **Baseline measurement** with `-O2 +RTS -s` establishes initial performance
|
||||
2. **Time profiling** with `-prof -fprof-late +RTS -p` identifies CPU hotspots
|
||||
2. **Time profiling** with `-prof -fprof-late +RTS -p` identifies CPU hotspots
|
||||
3. **Memory profiling** with `-hd -l` and eventlog2html reveals allocation patterns
|
||||
4. **Detailed investigation** using info table profiling for exact source locations
|
||||
5. **Iterative optimization** applying fixes and re-profiling to verify improvements
|
||||
|
|
@ -401,7 +401,7 @@ Successful optimization follows priority order: algorithms trump micro-optimizat
|
|||
**Compilation optimization checklist**:
|
||||
- [ ] Use `-O` or `-O2` for production builds
|
||||
- [ ] Add `-fllvm` for numeric-intensive code after benchmarking
|
||||
- [ ] Enable `-threaded` for concurrent programs
|
||||
- [ ] Enable `-threaded` for concurrent programs
|
||||
- [ ] Include `-rtsopts` to allow runtime tuning
|
||||
- [ ] Set `-with-rtsopts=-N` for automatic parallelism
|
||||
- [ ] Use `optimization: 2` in cabal.project, not `ghc-options`
|
||||
|
|
@ -483,7 +483,7 @@ import System.IO
|
|||
|
||||
data ProxyConfig = ProxyConfig
|
||||
{ listenPort :: PortNumber
|
||||
, targetHost :: HostName
|
||||
, targetHost :: HostName
|
||||
, targetPort :: PortNumber
|
||||
, poolStripes :: Int
|
||||
, poolPerStripe :: Int
|
||||
|
|
@ -492,8 +492,8 @@ data ProxyConfig = ProxyConfig
|
|||
|
||||
-- Create backend connection pool
|
||||
createBackendPool :: ProxyConfig -> IO (Pool Socket)
|
||||
createBackendPool config =
|
||||
newPool $
|
||||
createBackendPool config =
|
||||
newPool $
|
||||
defaultPoolConfig
|
||||
(connectBackend (targetHost config) (targetPort config))
|
||||
close
|
||||
|
|
@ -503,7 +503,7 @@ createBackendPool config =
|
|||
|
||||
connectBackend :: HostName -> PortNumber -> IO Socket
|
||||
connectBackend host port = do
|
||||
addr:_ <- getAddrInfo
|
||||
addr:_ <- getAddrInfo
|
||||
(Just defaultHints { addrSocketType = Stream })
|
||||
(Just host) (Just $ show port)
|
||||
sock <- socket (addrFamily addr) Stream defaultProtocol
|
||||
|
|
@ -517,18 +517,18 @@ runProxy :: ProxyConfig -> IO ()
|
|||
runProxy config = do
|
||||
pool <- createBackendPool config
|
||||
addr:_ <- getAddrInfo
|
||||
(Just defaultHints
|
||||
(Just defaultHints
|
||||
{ addrSocketType = Stream
|
||||
, addrFlags = [AI_PASSIVE] })
|
||||
Nothing (Just $ show $ listenPort config)
|
||||
|
||||
|
||||
sock <- socket (addrFamily addr) Stream defaultProtocol
|
||||
setSocketOption sock ReuseAddr 1
|
||||
bind sock (addrAddress addr)
|
||||
listen sock 128
|
||||
|
||||
|
||||
putStrLn $ "Proxy listening on port " ++ show (listenPort config)
|
||||
|
||||
|
||||
forever $ do
|
||||
(client, clientAddr) <- accept sock
|
||||
forkIO $ handleClient pool client
|
||||
|
|
@ -538,27 +538,27 @@ runProxy config = do
|
|||
handleClient :: Pool Socket -> Socket -> IO ()
|
||||
handleClient pool client = do
|
||||
done <- newEmptyMVar
|
||||
|
||||
|
||||
withResource pool $ \backend -> do
|
||||
-- Bidirectional zero-copy forwarding
|
||||
let chunkSize = 65536 -- 64KB chunks
|
||||
|
||||
|
||||
forkIO $ do
|
||||
result <- try $ splice chunkSize (client, Nothing)
|
||||
result <- try $ splice chunkSize (client, Nothing)
|
||||
(backend, Nothing)
|
||||
case result of
|
||||
Left (e :: SomeException) ->
|
||||
Left (e :: SomeException) ->
|
||||
putStrLn $ "Client->Backend error: " ++ show e
|
||||
Right _ -> return ()
|
||||
putMVar done ()
|
||||
|
||||
|
||||
result <- try $ splice chunkSize (backend, Nothing)
|
||||
(client, Nothing)
|
||||
case result of
|
||||
Left (e :: SomeException) ->
|
||||
putStrLn $ "Backend->Client error: " ++ show e
|
||||
Right _ -> return ()
|
||||
|
||||
|
||||
takeMVar done
|
||||
|
||||
main :: IO ()
|
||||
|
|
@ -582,7 +582,7 @@ main = do
|
|||
import Data.ByteString.Builder
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
|
||||
buildHttpResponse :: Int -> [(ByteString, ByteString)]
|
||||
buildHttpResponse :: Int -> [(ByteString, ByteString)]
|
||||
-> BL.ByteString -> BL.ByteString
|
||||
buildHttpResponse status headers body = toLazyByteString $
|
||||
mconcat
|
||||
|
|
@ -591,7 +591,7 @@ buildHttpResponse status headers body = toLazyByteString $
|
|||
, byteString " "
|
||||
, statusText status
|
||||
, byteString "\r\n"
|
||||
, mconcat [ byteString k <> byteString ": "
|
||||
, mconcat [ byteString k <> byteString ": "
|
||||
<> byteString v <> byteString "\r\n"
|
||||
| (k, v) <- headers ]
|
||||
, byteString "\r\n"
|
||||
|
|
@ -599,7 +599,7 @@ buildHttpResponse status headers body = toLazyByteString $
|
|||
]
|
||||
where
|
||||
statusText 200 = byteString "OK"
|
||||
statusText 404 = byteString "Not Found"
|
||||
statusText 404 = byteString "Not Found"
|
||||
statusText 500 = byteString "Internal Server Error"
|
||||
statusText _ = byteString "Unknown"
|
||||
```
|
||||
|
|
@ -616,14 +616,14 @@ parseRequestLine :: ByteString -> Maybe (ByteString, ByteString, ByteString)
|
|||
parseRequestLine bs = do
|
||||
let (method, rest1) = BS.break (== space) bs
|
||||
guard (not $ BS.null rest1)
|
||||
|
||||
|
||||
let rest2 = BS.drop 1 rest1
|
||||
(path, rest3) = BS.break (== space) rest2
|
||||
guard (not $ BS.null rest3)
|
||||
|
||||
|
||||
let rest4 = BS.drop 1 rest3
|
||||
(version, _) = BS.break (== cr) rest4
|
||||
|
||||
|
||||
return (method, path, version)
|
||||
where
|
||||
space = 32; cr = 13
|
||||
|
|
@ -635,11 +635,11 @@ parseHeaders = go . BC.lines
|
|||
go [] = []
|
||||
go (line:rest)
|
||||
| BS.null line = []
|
||||
| otherwise =
|
||||
| otherwise =
|
||||
case BC.break (== ':') line of
|
||||
(key, value)
|
||||
(key, value)
|
||||
| BS.null value -> go rest
|
||||
| otherwise ->
|
||||
| otherwise ->
|
||||
let val = BS.dropWhile (== 32) (BS.drop 1 value)
|
||||
in (key, val) : go rest
|
||||
```
|
||||
|
|
@ -210,16 +210,16 @@ RateLimit: "default";r=0;t=60
|
|||
async function fetchWithRetry(url, maxRetries = 3) {
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
const res = await fetch(url);
|
||||
|
||||
|
||||
if (res.status === 429) {
|
||||
const retryAfter = res.headers.get('retry-after');
|
||||
const delay = parseInt(retryAfter) * 1000 || (1000 * Math.pow(2, attempt));
|
||||
const jitter = delay * 0.2 * (Math.random() - 0.5);
|
||||
|
||||
|
||||
await sleep(delay + jitter);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
return res;
|
||||
}
|
||||
throw new Error('Max retries exceeded');
|
||||
|
|
@ -34,7 +34,7 @@ For a Haskell reverse proxy, the recommended approach uses WAI's `responseRaw` c
|
|||
|
||||
```haskell
|
||||
handleWebSocketProxy :: Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
|
||||
handleWebSocketProxy req respond = respond $
|
||||
handleWebSocketProxy req respond = respond $
|
||||
responseRaw (rawProxy targetHost targetPort) backupResponse
|
||||
```
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ The **recommended hybrid approach**: forward all ping/pong frames transparently
|
|||
Per-connection state:
|
||||
last_activity: timestamp
|
||||
pong_expected: bool
|
||||
|
||||
|
||||
Periodic check (every 10s):
|
||||
if pong_expected and (now - ping_sent_at) > pong_timeout:
|
||||
terminate_connection("pong timeout")
|
||||
|
|
@ -104,7 +104,7 @@ HAProxy's model provides the blueprint for proper timeout architecture:
|
|||
```
|
||||
defaults
|
||||
timeout client 25s # HTTP inactivity
|
||||
timeout server 25s # HTTP inactivity
|
||||
timeout server 25s # HTTP inactivity
|
||||
timeout connect 5s # TCP establishment
|
||||
timeout tunnel 3600s # WebSocket/upgraded connections
|
||||
```
|
||||
|
|
@ -129,7 +129,7 @@ Auto-detect connection type via `Connection: Upgrade` + `Upgrade: websocket` hea
|
|||
The connection state machine should explicitly model different phases:
|
||||
|
||||
```haskell
|
||||
data ConnectionState
|
||||
data ConnectionState
|
||||
= HttpRequest -- Initial HTTP parsing
|
||||
| HttpResponse -- Response from upstream
|
||||
| ProtocolUpgrade -- Detected upgrade, awaiting 101
|
||||
|
|
@ -155,14 +155,14 @@ transitionState :: ConnectionState -> Event -> ConnectionState
|
|||
|
||||
**Recommended Haskell stack:**
|
||||
- `wai` + `warp`: HTTP server foundation
|
||||
- `streaming-commons`: TCP client connections
|
||||
- `streaming-commons`: TCP client connections
|
||||
- `splice`: Zero-copy socket forwarding on Linux
|
||||
- `async`: Concurrent bidirectional copy with `race_`
|
||||
- `websockets` + `wai-websockets`: Only if frame-level access needed
|
||||
|
||||
**Architecture flow:**
|
||||
```
|
||||
Listener → HTTP Parser → Route Match →
|
||||
Listener → HTTP Parser → Route Match →
|
||||
├─ Regular HTTP → Buffer Pool → Upstream → Response Buffer → Client
|
||||
├─ WebSocket → Upgrade Handler → Bidirectional Tunnel (splice)
|
||||
└─ SSE/Stream → Detect Header → Immediate Flush → Client
|
||||
|
|
@ -91,10 +91,10 @@ tlsClient hostname port = do
|
|||
addr:_ <- getAddrInfo Nothing (Just hostname) (Just port)
|
||||
sock <- socket (addrFamily addr) Stream defaultProtocol
|
||||
connect sock (addrAddress addr)
|
||||
|
||||
|
||||
-- Configure TLS parameters
|
||||
let params = (defaultParamsClient hostname "")
|
||||
{ clientSupported = def
|
||||
{ clientSupported = def
|
||||
{ supportedCiphers = ciphersuite_default
|
||||
, supportedVersions = [TLS13, TLS12]
|
||||
, supportedGroups = [X25519, P256]
|
||||
|
|
@ -103,16 +103,16 @@ tlsClient hostname port = do
|
|||
{ sharedCAStore = systemStore
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-- Perform handshake
|
||||
ctx <- contextNew sock params
|
||||
handshake ctx
|
||||
|
||||
|
||||
-- Send HTTP request
|
||||
sendData ctx "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
|
||||
response <- recvData ctx
|
||||
print response
|
||||
|
||||
|
||||
-- Clean shutdown
|
||||
bye ctx
|
||||
close sock
|
||||
|
|
@ -131,8 +131,8 @@ import qualified Network.TLS as TLS
|
|||
import Network.TLS.Extra.Cipher
|
||||
|
||||
app :: Application
|
||||
app _ respond =
|
||||
respond $ responseLBS status200
|
||||
app _ respond =
|
||||
respond $ responseLBS status200
|
||||
[("Content-Type", "text/plain")
|
||||
,("Strict-Transport-Security", "max-age=31536000; includeSubDomains")]
|
||||
"Secure HTTPS Server"
|
||||
|
|
@ -144,11 +144,11 @@ main = do
|
|||
, tlsCiphers = ciphersuite_strong
|
||||
, onInsecure = DenyInsecure "HTTPS required"
|
||||
}
|
||||
|
||||
|
||||
warpConfig = defaultSettings
|
||||
& setPort 443
|
||||
& setHost "0.0.0.0"
|
||||
|
||||
|
||||
putStrLn "HTTPS server on :443"
|
||||
runTLS tlsConfig warpConfig app
|
||||
```
|
||||
|
|
@ -169,14 +169,14 @@ validateCertificate certFile hostname = do
|
|||
-- Load certificate chain
|
||||
certs <- readSignedObject certFile
|
||||
let chain = CertificateChain certs
|
||||
|
||||
|
||||
-- Get system CA store
|
||||
store <- getSystemCertificateStore
|
||||
|
||||
|
||||
-- Validate with default checks
|
||||
let cache = exceptionValidationCache []
|
||||
failures <- validateDefault store cache (hostname, ":443") chain
|
||||
|
||||
|
||||
case failures of
|
||||
[] -> putStrLn "✓ Certificate valid" >> return True
|
||||
errs -> do
|
||||
|
|
@ -206,10 +206,10 @@ initializeKeys :: CertConfig -> IO ()
|
|||
initializeKeys cfg = do
|
||||
accountExists <- doesFileExist (accountKey cfg)
|
||||
domainExists <- doesFileExist (domainKey cfg)
|
||||
|
||||
unless accountExists $
|
||||
|
||||
unless accountExists $
|
||||
callCommand $ "openssl genrsa 4096 > " ++ accountKey cfg
|
||||
|
||||
|
||||
unless domainExists $
|
||||
callCommand $ "openssl genrsa 4096 > " ++ domainKey cfg
|
||||
|
||||
|
|
@ -337,7 +337,7 @@ import qualified Network.TLS as TLS
|
|||
customTlsManager :: IO Manager
|
||||
customTlsManager = do
|
||||
let tlsParams = (TLS.defaultParamsClient "example.com" "")
|
||||
{ TLS.clientSupported = def
|
||||
{ TLS.clientSupported = def
|
||||
{ TLS.supportedCiphers = ciphersuite_strong
|
||||
, TLS.supportedVersions = [TLS.TLS13, TLS.TLS12]
|
||||
}
|
||||
|
|
@ -346,15 +346,15 @@ customTlsManager = do
|
|||
}
|
||||
}
|
||||
tlsSettings = TLSSettings tlsParams
|
||||
|
||||
|
||||
newManager $ mkManagerSettings tlsSettings Nothing
|
||||
|
||||
customValidation :: CertificateStore -> ValidationCache -> ServiceID
|
||||
customValidation :: CertificateStore -> ValidationCache -> ServiceID
|
||||
-> CertificateChain -> IO [FailedReason]
|
||||
customValidation store cache sid chain = do
|
||||
-- Perform standard validation
|
||||
failures <- validateDefault store cache sid chain
|
||||
|
||||
|
||||
-- Add custom checks (certificate pinning, etc.)
|
||||
if null failures
|
||||
then return []
|
||||
|
|
@ -375,11 +375,11 @@ loadCredentials hostname = do
|
|||
|
||||
main :: IO ()
|
||||
main = do
|
||||
let tlsConfig = tlsSettingsSni
|
||||
let tlsConfig = tlsSettingsSni
|
||||
(return $ Just loadCredentials)
|
||||
"default.crt"
|
||||
"default.key"
|
||||
|
||||
|
||||
runTLS tlsConfig defaultSettings app
|
||||
```
|
||||
|
||||
|
|
@ -392,15 +392,15 @@ import Data.IORef
|
|||
setupSessionManager :: IO SessionManager
|
||||
setupSessionManager = do
|
||||
sessions <- newIORef Map.empty
|
||||
|
||||
|
||||
return SessionManager
|
||||
{ sessionResume = \sessionID -> do
|
||||
cache <- readIORef sessions
|
||||
return $ Map.lookup sessionID cache
|
||||
|
||||
|
||||
, sessionEstablish = \sessionID sessionData -> do
|
||||
modifyIORef' sessions (Map.insert sessionID sessionData)
|
||||
|
||||
|
||||
, sessionInvalidate = \sessionID -> do
|
||||
modifyIORef' sessions (Map.delete sessionID)
|
||||
}
|
||||
|
|
@ -408,7 +408,7 @@ setupSessionManager = do
|
|||
clientWithResumption :: HostName -> IO ()
|
||||
clientWithResumption hostname = do
|
||||
manager <- setupSessionManager
|
||||
|
||||
|
||||
let params = (defaultParamsClient hostname "")
|
||||
{ clientShared = def
|
||||
{ sharedSessionManager = manager
|
||||
|
|
@ -139,8 +139,8 @@ warn_required_dynamic_aliases = true
|
|||
py-version = "3.11"
|
||||
jobs = 4
|
||||
load-plugins = [
|
||||
"pylint_pydantic",
|
||||
"pylint_per_file_ignores",
|
||||
"pylint_pydantic",
|
||||
"pylint_per_file_ignores",
|
||||
]
|
||||
persistent = true
|
||||
ignore = [
|
||||
|
|
@ -239,4 +239,3 @@ possibly-missing-import = "error"
|
|||
unused-ignore-comment = "warn"
|
||||
redundant-cast = "warn"
|
||||
undefined-reveal = "warn"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue