at 22.05-pre 11 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.nitter; 7 configFile = pkgs.writeText "nitter.conf" '' 8 ${generators.toINI { 9 # String values need to be quoted 10 mkKeyValue = generators.mkKeyValueDefault { 11 mkValueString = v: 12 if isString v then "\"" + (strings.escape ["\""] (toString v)) + "\"" 13 else generators.mkValueStringDefault {} v; 14 } " = "; 15 } (lib.recursiveUpdate { 16 Server = cfg.server; 17 Cache = cfg.cache; 18 Config = cfg.config // { hmacKey = "@hmac@"; }; 19 Preferences = cfg.preferences; 20 } cfg.settings)} 21 ''; 22 # `hmac` is a secret used for cryptographic signing of video URLs. 23 # Generate it on first launch, then copy configuration and replace 24 # `@hmac@` with this value. 25 # We are not using sed as it would leak the value in the command line. 26 preStart = pkgs.writers.writePython3 "nitter-prestart" {} '' 27 import os 28 import secrets 29 30 state_dir = os.environ.get("STATE_DIRECTORY") 31 if not os.path.isfile(f"{state_dir}/hmac"): 32 # Generate hmac on first launch 33 hmac = secrets.token_hex(32) 34 with open(f"{state_dir}/hmac", "w") as f: 35 f.write(hmac) 36 else: 37 # Load previously generated hmac 38 with open(f"{state_dir}/hmac", "r") as f: 39 hmac = f.read() 40 41 configFile = "${configFile}" 42 with open(configFile, "r") as f_in: 43 with open(f"{state_dir}/nitter.conf", "w") as f_out: 44 f_out.write(f_in.read().replace("@hmac@", hmac)) 45 ''; 46in 47{ 48 options = { 49 services.nitter = { 50 enable = mkEnableOption "If enabled, start Nitter."; 51 52 server = { 53 address = mkOption { 54 type = types.str; 55 default = "0.0.0.0"; 56 example = "127.0.0.1"; 57 description = "The address to listen on."; 58 }; 59 60 port = mkOption { 61 type = types.port; 62 default = 8080; 63 example = 8000; 64 description = "The port to listen on."; 65 }; 66 67 https = mkOption { 68 type = types.bool; 69 default = false; 70 description = "Set secure attribute on cookies. Keep it disabled to enable cookies when not using HTTPS."; 71 }; 72 73 httpMaxConnections = mkOption { 74 type = types.int; 75 default = 100; 76 description = "Maximum number of HTTP connections."; 77 }; 78 79 staticDir = mkOption { 80 type = types.path; 81 default = "${pkgs.nitter}/share/nitter/public"; 82 defaultText = literalExpression ''"''${pkgs.nitter}/share/nitter/public"''; 83 description = "Path to the static files directory."; 84 }; 85 86 title = mkOption { 87 type = types.str; 88 default = "nitter"; 89 description = "Title of the instance."; 90 }; 91 92 hostname = mkOption { 93 type = types.str; 94 default = "localhost"; 95 example = "nitter.net"; 96 description = "Hostname of the instance."; 97 }; 98 }; 99 100 cache = { 101 listMinutes = mkOption { 102 type = types.int; 103 default = 240; 104 description = "How long to cache list info (not the tweets, so keep it high)."; 105 }; 106 107 rssMinutes = mkOption { 108 type = types.int; 109 default = 10; 110 description = "How long to cache RSS queries."; 111 }; 112 113 redisHost = mkOption { 114 type = types.str; 115 default = "localhost"; 116 description = "Redis host."; 117 }; 118 119 redisPort = mkOption { 120 type = types.port; 121 default = 6379; 122 description = "Redis port."; 123 }; 124 125 redisConnections = mkOption { 126 type = types.int; 127 default = 20; 128 description = "Redis connection pool size."; 129 }; 130 131 redisMaxConnections = mkOption { 132 type = types.int; 133 default = 30; 134 description = '' 135 Maximum number of connections to Redis. 136 137 New connections are opened when none are available, but if the 138 pool size goes above this, they are closed when released, do not 139 worry about this unless you receive tons of requests per second. 140 ''; 141 }; 142 }; 143 144 config = { 145 base64Media = mkOption { 146 type = types.bool; 147 default = false; 148 description = "Use base64 encoding for proxied media URLs."; 149 }; 150 151 tokenCount = mkOption { 152 type = types.int; 153 default = 10; 154 description = '' 155 Minimum amount of usable tokens. 156 157 Tokens are used to authorize API requests, but they expire after 158 ~1 hour, and have a limit of 187 requests. The limit gets reset 159 every 15 minutes, and the pool is filled up so there is always at 160 least tokenCount usable tokens. Only increase this if you receive 161 major bursts all the time. 162 ''; 163 }; 164 }; 165 166 preferences = { 167 replaceTwitter = mkOption { 168 type = types.str; 169 default = ""; 170 example = "nitter.net"; 171 description = "Replace Twitter links with links to this instance (blank to disable)."; 172 }; 173 174 replaceYouTube = mkOption { 175 type = types.str; 176 default = ""; 177 example = "piped.kavin.rocks"; 178 description = "Replace YouTube links with links to this instance (blank to disable)."; 179 }; 180 181 replaceInstagram = mkOption { 182 type = types.str; 183 default = ""; 184 description = "Replace Instagram links with links to this instance (blank to disable)."; 185 }; 186 187 mp4Playback = mkOption { 188 type = types.bool; 189 default = true; 190 description = "Enable MP4 video playback."; 191 }; 192 193 hlsPlayback = mkOption { 194 type = types.bool; 195 default = false; 196 description = "Enable HLS video streaming (requires JavaScript)."; 197 }; 198 199 proxyVideos = mkOption { 200 type = types.bool; 201 default = true; 202 description = "Proxy video streaming through the server (might be slow)."; 203 }; 204 205 muteVideos = mkOption { 206 type = types.bool; 207 default = false; 208 description = "Mute videos by default."; 209 }; 210 211 autoplayGifs = mkOption { 212 type = types.bool; 213 default = true; 214 description = "Autoplay GIFs."; 215 }; 216 217 theme = mkOption { 218 type = types.str; 219 default = "Nitter"; 220 description = "Instance theme."; 221 }; 222 223 infiniteScroll = mkOption { 224 type = types.bool; 225 default = false; 226 description = "Infinite scrolling (requires JavaScript, experimental!)."; 227 }; 228 229 stickyProfile = mkOption { 230 type = types.bool; 231 default = true; 232 description = "Make profile sidebar stick to top."; 233 }; 234 235 bidiSupport = mkOption { 236 type = types.bool; 237 default = false; 238 description = "Support bidirectional text (makes clicking on tweets harder)."; 239 }; 240 241 hideTweetStats = mkOption { 242 type = types.bool; 243 default = false; 244 description = "Hide tweet stats (replies, retweets, likes)."; 245 }; 246 247 hideBanner = mkOption { 248 type = types.bool; 249 default = false; 250 description = "Hide profile banner."; 251 }; 252 253 hidePins = mkOption { 254 type = types.bool; 255 default = false; 256 description = "Hide pinned tweets."; 257 }; 258 259 hideReplies = mkOption { 260 type = types.bool; 261 default = false; 262 description = "Hide tweet replies."; 263 }; 264 }; 265 266 settings = mkOption { 267 type = types.attrs; 268 default = {}; 269 description = '' 270 Add settings here to override NixOS module generated settings. 271 272 Check the official repository for the available settings: 273 https://github.com/zedeus/nitter/blob/master/nitter.conf 274 ''; 275 }; 276 277 redisCreateLocally = mkOption { 278 type = types.bool; 279 default = true; 280 description = "Configure local Redis server for Nitter."; 281 }; 282 283 openFirewall = mkOption { 284 type = types.bool; 285 default = false; 286 description = "Open ports in the firewall for Nitter web interface."; 287 }; 288 }; 289 }; 290 291 config = mkIf cfg.enable { 292 assertions = [ 293 { 294 assertion = !cfg.redisCreateLocally || (cfg.cache.redisHost == "localhost" && cfg.cache.redisPort == 6379); 295 message = "When services.nitter.redisCreateLocally is enabled, you need to use localhost:6379 as a cache server."; 296 } 297 ]; 298 299 systemd.services.nitter = { 300 description = "Nitter (An alternative Twitter front-end)"; 301 wantedBy = [ "multi-user.target" ]; 302 after = [ "syslog.target" "network.target" ]; 303 serviceConfig = { 304 DynamicUser = true; 305 StateDirectory = "nitter"; 306 Environment = [ "NITTER_CONF_FILE=/var/lib/nitter/nitter.conf" ]; 307 # Some parts of Nitter expect `public` folder in working directory, 308 # see https://github.com/zedeus/nitter/issues/414 309 WorkingDirectory = "${pkgs.nitter}/share/nitter"; 310 ExecStart = "${pkgs.nitter}/bin/nitter"; 311 ExecStartPre = "${preStart}"; 312 AmbientCapabilities = lib.mkIf (cfg.server.port < 1024) [ "CAP_NET_BIND_SERVICE" ]; 313 Restart = "on-failure"; 314 RestartSec = "5s"; 315 # Hardening 316 CapabilityBoundingSet = if (cfg.server.port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ]; 317 DeviceAllow = [ "" ]; 318 LockPersonality = true; 319 MemoryDenyWriteExecute = true; 320 PrivateDevices = true; 321 # A private user cannot have process capabilities on the host's user 322 # namespace and thus CAP_NET_BIND_SERVICE has no effect. 323 PrivateUsers = (cfg.server.port >= 1024); 324 ProcSubset = "pid"; 325 ProtectClock = true; 326 ProtectControlGroups = true; 327 ProtectHome = true; 328 ProtectHostname = true; 329 ProtectKernelLogs = true; 330 ProtectKernelModules = true; 331 ProtectKernelTunables = true; 332 ProtectProc = "invisible"; 333 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; 334 RestrictNamespaces = true; 335 RestrictRealtime = true; 336 RestrictSUIDSGID = true; 337 SystemCallArchitectures = "native"; 338 SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ]; 339 UMask = "0077"; 340 }; 341 }; 342 343 services.redis = lib.mkIf (cfg.redisCreateLocally) { 344 enable = true; 345 }; 346 347 networking.firewall = mkIf cfg.openFirewall { 348 allowedTCPPorts = [ cfg.server.port ]; 349 }; 350 }; 351}