diff --git a/modules/common/documentation.nix b/modules/common/documentation.nix
index f9e0fcb..b28988d 100644
--- a/modules/common/documentation.nix
+++ b/modules/common/documentation.nix
@@ -7,23 +7,28 @@
}:
{
config = {
- hm.manual = {
- manpages.enable = this.isHeadful;
- html.enable = false;
- json.enable = false;
+ hm = {
+ manual = {
+ manpages.enable = this.isHeadful;
+ html.enable = false;
+ json.enable = false;
+ };
+
+ # Fixes a wierd issue with `direnv` and an unbound value.
+ home.sessionVariables.MANPATH = "";
};
documentation = {
enable = this.isHeadful;
dev.enable = true;
doc.enable = false;
- info.enable = false;
+ info.enable = true;
nixos.enable = true;
man.man-db.manualPages =
(pkgs.buildEnv {
name = "man-paths";
- paths = with config; environment.systemPackages ++ hm.home.packages;
+ paths = config.environment.systemPackages ++ config.hm.home.packages;
pathsToLink = [ "/share/man" ];
extraOutputsToInstall = [ "man" ];
ignoreCollisions = true;
diff --git a/modules/piracy/default.nix b/modules/piracy/default.nix
index 7682356..3554a02 100644
--- a/modules/piracy/default.nix
+++ b/modules/piracy/default.nix
@@ -55,6 +55,9 @@ in
user = "rtorrent";
inherit (cfg) group;
+ port = 1337;
+ openFirewall = true;
+
rpcSocket = socket;
configText =
with config.services.rtorrent;
diff --git a/modules/profiles/headful.nix b/modules/profiles/headful.nix
index 186d97d..a315af4 100644
--- a/modules/profiles/headful.nix
+++ b/modules/profiles/headful.nix
@@ -54,6 +54,7 @@ in
packages = with pkgs; [
anki
audacity
+ ayugram-desktop
byedpi
eaglemode
easyeffects
diff --git a/modules/soju.nix b/modules/soju.nix
index 2060eca..dbf069d 100644
--- a/modules/soju.nix
+++ b/modules/soju.nix
@@ -12,24 +12,30 @@ in
options.nixfiles.modules.soju = {
enable = mkEnableOption "soju";
- address = mkOption {
- description = "Address.";
- type = with types; str;
- default = "";
- };
-
port = mkOption {
description = "Port.";
type = with types; port;
default = 6697;
};
+ httpPort = mkOption {
+ description = "HTTP Port.";
+ type = with types; port;
+ default = 9981;
+ };
+
domain = mkOption {
description = "Domain.";
type = with types; str;
default = config.networking.fqdn;
};
+ uploadsDir = mkOption {
+ description = "Uploads directory.";
+ type = with types; str;
+ default = "/srv/soju/uploads";
+ };
+
prometheus = {
enable = mkEnableOption "Prometheus exporter" // {
default = true;
@@ -50,7 +56,25 @@ in
mkIf cfg.enable {
nixfiles.modules = {
acme.enable = true;
- nginx.enable = true;
+ nginx = {
+ enable = true;
+ upstreams.soju.servers."127.0.0.1:${toString cfg.httpPort}" = { };
+ virtualHosts.${cfg.domain}.locations = {
+ "/_irc" = {
+ proxyPass = "http://soju";
+ proxyWebsockets = true;
+ extraConfig = ''
+ rewrite ^/_irc/(.*)$ /$1 break;
+ '';
+ };
+ "/_irc/uploads" = {
+ root = "/srv/soju";
+ extraConfig = ''
+ rewrite ^/_irc/(.*)$ /$1 break;
+ '';
+ };
+ };
+ };
postgresql = {
enable = true;
extraPostStart = [
@@ -71,76 +95,87 @@ in
];
};
- systemd.services.soju = {
- description = "soju IRC bouncer";
- wantedBy = [ "multi-user.target" ];
- wants = [ "network-online.target" ];
- requires = [ "postgresql.service" ];
- after = [
- "network-online.target"
- "postgresql.service"
- ];
- serviceConfig = {
- ExecStart =
- let
- # https://soju.im/doc/soju.1.html
- configFile = pkgs.writeText "soju.conf" ''
- listen ircs://${cfg.address}:${toString cfg.port}
- tls ${with config.certs.${cfg.domain}; "${directory}/fullchain.pem ${directory}/key.pem"}
- ${with cfg.prometheus; optionalString enable "listen http+prometheus://localhost:${toString port}"}
- db postgres "${
- concatStringsSep " " [
- "host=/run/postgresql"
- "user=${db}"
- "dbname=${db}"
- "sslmode=disable"
- ]
- }"
- message-store db
- hostname ${cfg.domain}
- title ${cfg.domain}
- '';
- in
- concatStringsSep " " [
- (getExe' pkgs.soju "soju")
- "-config ${configFile}"
- ];
- DynamicUser = true;
- SupplementaryGroups = [ config.services.nginx.group ];
- AmbientCapabilities = [ "" ];
- CapabilityBoundingSet = [ "" ];
- UMask = "0077";
- LockPersonality = true;
- MemoryDenyWriteExecute = true;
- NoNewPrivileges = true;
- PrivateDevices = true;
- PrivateTmp = true;
- PrivateUsers = true;
- ProtectClock = true;
- ProtectControlGroups = true;
- ProtectHome = true;
- ProtectHostname = true;
- ProtectKernelLogs = true;
- ProtectKernelModules = true;
- ProtectKernelTunables = true;
- ProtectSystem = "strict";
- ProtectProc = "invisible";
- ProcSubset = "pid";
- RemoveIPC = true;
- RestrictAddressFamilies = [
- "AF_UNIX"
- "AF_INET"
- "AF_INET6"
+ systemd = {
+ services.soju = {
+ description = "soju IRC bouncer";
+ documentation = [
+ "https://soju.im/"
+ "man:soju(1)"
+ "man:sojuctl(1)"
];
- RestrictNamespaces = true;
- RestrictRealtime = true;
- RestrictSUIDSGID = true;
- SystemCallArchitectures = "native";
- SystemCallFilter = [
- "@system-service"
- "~@privileged"
+ wantedBy = [ "multi-user.target" ];
+ wants = [ "network-online.target" ];
+ requires = [ "postgresql.service" ];
+ after = [
+ "network-online.target"
+ "postgresql.service"
];
+ serviceConfig = {
+ ExecStart =
+ let
+ # https://soju.im/doc/soju.1.html
+ configFile = pkgs.writeText "soju.conf" ''
+ listen ircs://:${toString cfg.port}
+ listen http://localhost:${toString cfg.httpPort}
+ tls ${with config.certs.${cfg.domain}; "${directory}/fullchain.pem ${directory}/key.pem"}
+ ${with cfg.prometheus; optionalString enable "listen http+prometheus://localhost:${toString port}"}
+ db postgres "${
+ concatStringsSep " " [
+ "host=/run/postgresql"
+ "user=${db}"
+ "dbname=${db}"
+ "sslmode=disable"
+ ]
+ }"
+ message-store db
+ file-upload fs ${cfg.uploadsDir}
+ hostname ${cfg.domain}
+ http-ingress https://${cfg.domain}/_irc
+ '';
+ in
+ "${pkgs.soju}/bin/soju -config ${configFile}";
+ ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+ DynamicUser = true;
+ ReadWritePaths = [ cfg.uploadsDir ];
+ SupplementaryGroups = [ config.services.nginx.group ];
+ AmbientCapabilities = [ "" ];
+ CapabilityBoundingSet = [ "" ];
+ LockPersonality = true;
+ MemoryDenyWriteExecute = true;
+ NoNewPrivileges = true;
+ PrivateDevices = true;
+ PrivateTmp = true;
+ PrivateUsers = true;
+ ProtectClock = true;
+ ProtectControlGroups = true;
+ ProtectHome = true;
+ ProtectHostname = true;
+ ProtectKernelLogs = true;
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ ProtectSystem = "strict";
+ ProtectProc = "invisible";
+ ProcSubset = "pid";
+ RemoveIPC = true;
+ RestrictAddressFamilies = [
+ "AF_UNIX"
+ "AF_INET"
+ "AF_INET6"
+ ];
+ RestrictNamespaces = true;
+ RestrictRealtime = true;
+ RestrictSUIDSGID = true;
+ SystemCallArchitectures = "native";
+ SystemCallFilter = [
+ "@system-service"
+ "~@privileged"
+ ];
+ };
};
+
+ tmpfiles.rules = [
+ "d ${cfg.uploadsDir} 0755 soju soju"
+ ];
};
};
}
diff --git a/modules/throttled.nix b/modules/throttled.nix
deleted file mode 100644
index 7d37cd4..0000000
--- a/modules/throttled.nix
+++ /dev/null
@@ -1,105 +0,0 @@
-{ config, lib, ... }:
-with lib;
-let
- cfg = config.nixfiles.modules.throttled;
-in
-{
- options.nixfiles.modules.throttled.enable = mkEnableOption "Throttled";
-
- config = mkIf cfg.enable {
- services.throttled.enable = true;
-
- environment.etc."throttled.conf".text = mkDefault ''
- [GENERAL]
- # Enable or disable the script execution
- Enabled: True
- # SYSFS path for checking if the system is running on AC power
- Sysfs_Power_Path: /sys/class/power_supply/AC*/online
- # Auto reload config on changes
- Autoreload: True
-
- ## Settings to apply while connected to Battery power
- [BATTERY]
- # Update the registers every this many seconds
- Update_Rate_s: 30
- # Max package power for time window #1
- PL1_Tdp_W: 29
- # Time window #1 duration
- PL1_Duration_s: 28
- # Max package power for time window #2
- PL2_Tdp_W: 44
- # Time window #2 duration
- PL2_Duration_S: 0.002
- # Max allowed temperature before throttling
- Trip_Temp_C: 85
- # Set cTDP to normal=0, down=1 or up=2 (EXPERIMENTAL)
- cTDP: 0
- # Disable BDPROCHOT (EXPERIMENTAL)
- Disable_BDPROCHOT: False
-
- ## Settings to apply while connected to AC power
- [AC]
- # Update the registers every this many seconds
- Update_Rate_s: 5
- # Max package power for time window #1
- PL1_Tdp_W: 44
- # Time window #1 duration
- PL1_Duration_s: 28
- # Max package power for time window #2
- PL2_Tdp_W: 44
- # Time window #2 duration
- PL2_Duration_S: 0.002
- # Max allowed temperature before throttling
- Trip_Temp_C: 95
- # Set HWP energy performance hints to 'performance' on high load (EXPERIMENTAL)
- # Uncomment only if you really want to use it
- # HWP_Mode: False
- # Set cTDP to normal=0, down=1 or up=2 (EXPERIMENTAL)
- cTDP: 0
- # Disable BDPROCHOT (EXPERIMENTAL)
- Disable_BDPROCHOT: False
-
- # All voltage values are expressed in mV and *MUST* be negative (i.e. undervolt)!
- [UNDERVOLT.BATTERY]
- # CPU core voltage offset (mV)
- CORE: 0
- # Integrated GPU voltage offset (mV)
- GPU: 0
- # CPU cache voltage offset (mV)
- CACHE: 0
- # System Agent voltage offset (mV)
- UNCORE: 0
- # Analog I/O voltage offset (mV)
- ANALOGIO: 0
-
- # All voltage values are expressed in mV and *MUST* be negative (i.e. undervolt)!
- [UNDERVOLT.AC]
- # CPU core voltage offset (mV)
- CORE: 0
- # Integrated GPU voltage offset (mV)
- GPU: 0
- # CPU cache voltage offset (mV)
- CACHE: 0
- # System Agent voltage offset (mV)
- UNCORE: 0
- # Analog I/O voltage offset (mV)
- ANALOGIO: 0
-
- # [ICCMAX.AC]
- # # CPU core max current (A)
- # CORE:
- # # Integrated GPU max current (A)
- # GPU:
- # # CPU cache max current (A)
- # CACHE:
-
- # [ICCMAX.BATTERY]
- # # CPU core max current (A)
- # CORE:
- # # Integrated GPU max current (A)
- # GPU:
- # # CPU cache max current (A)
- # CACHE:
- '';
- };
-}
diff --git a/overlays.nix b/overlays.nix
index f5c3aac..3c64def 100644
--- a/overlays.nix
+++ b/overlays.nix
@@ -16,6 +16,7 @@
'';
grc.__output = {
+ version.__assign = "nixfiles";
src.__assign = final.fetchFromGitHub {
owner = "garabik";
repo = "grc";
@@ -63,13 +64,6 @@
openssl_1_0_0.__assign = prev.callPackage ./packages/openssl_1_0_0.nix { };
- telegram-desktop = {
- __input.stdenv = final.useMoldLinker;
- # __output.patches.__append = [
- # ./packages/telegram-no-ads.patch
- # ];
- };
-
vesktop = {
__input = {
withMiddleClickScroll.__assign = true;
@@ -92,5 +86,18 @@
dendrite =
_: (lib.packages.fromPR 366129 "sha256-oI9Afm3azJyEz4SJJIwuzeyuH7IaiGNTSA442vFlfv4=").dendrite;
+
+ soju.__output = {
+ version.__assign = "nixfiles";
+ src.__assign = final.fetchFromGitea {
+ domain = "codeberg.org";
+ owner = "emersion";
+ repo = "soju";
+ rev = "5f68e69529d8da21b22d073f07278722488d507d";
+ hash = "sha256-oJDw2AzUBfU2KkG4YzzAXiE5UZj0m0/zw/GuHVrqddM=";
+ };
+ vendorHash.__assign = "sha256-tq9FI8A3pi3ztcLYF6sZ4wmwTD0HWq4g2EAl7eLo+po=";
+ patches.__append = [ ./packages/soju-upload.patch ];
+ };
};
}
diff --git a/packages/soju-upload.patch b/packages/soju-upload.patch
new file mode 100644
index 0000000..03efe76
--- /dev/null
+++ b/packages/soju-upload.patch
@@ -0,0 +1,222 @@
+diff --git i/fileupload/fileupload.go w/fileupload/fileupload.go
+index 07fccf1..55104f8 100644
+--- i/fileupload/fileupload.go
++++ w/fileupload/fileupload.go
+@@ -7,6 +7,7 @@ import (
+ "errors"
+ "fmt"
+ "io"
++ "log"
+ "mime"
+ "net/http"
+ "net/url"
+@@ -153,30 +154,35 @@ func (h *Handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
+ return
+ }
+
++ var err error
+ switch req.Method {
+ case http.MethodOptions:
+ resp.WriteHeader(http.StatusNoContent)
+ case http.MethodHead, http.MethodGet:
+- h.fetch(resp, req)
++ err = h.fetch(resp, req)
+ case http.MethodPost:
+- h.store(resp, req)
++ err = h.store(resp, req)
+ default:
+ http.Error(resp, "only OPTIONS, HEAD, GET and POST are allowed", http.StatusMethodNotAllowed)
++ err = errors.New(http.StatusText(http.StatusMethodNotAllowed))
++ }
++ if err != nil {
++ log.Printf("upload: %s", err.Error())
+ }
+ }
+
+-func (h *Handler) fetch(resp http.ResponseWriter, req *http.Request) {
++func (h *Handler) fetch(resp http.ResponseWriter, req *http.Request) error {
+ prefix := "/uploads/"
+ if !strings.HasPrefix(req.URL.Path, prefix) {
+ http.Error(resp, "invalid path", http.StatusNotFound)
+- return
++ return fmt.Errorf("invalid path")
+ }
+
+ filename := strings.TrimPrefix(req.URL.Path, prefix)
+ filename = path.Join("/", filename)[1:] // prevent directory traversal
+ if filename == "" {
+ http.Error(resp, "invalid path", http.StatusNotFound)
+- return
++ return fmt.Errorf("invalid path")
+ }
+
+ basename, modTime, content, err := h.Uploader.load(req.Context(), filename)
+@@ -184,10 +190,10 @@ func (h *Handler) fetch(resp http.ResponseWriter, req *http.Request) {
+ var httpErr *httpError
+ if errors.As(err, &httpErr) {
+ httpErr.Write(resp)
+- return
++ return err
+ }
+ http.Error(resp, "failed to open file", http.StatusNotFound)
+- return
++ return fmt.Errorf("failed to open file: %v", err)
+ }
+ defer content.Close()
+
+@@ -200,7 +206,7 @@ func (h *Handler) fetch(resp http.ResponseWriter, req *http.Request) {
+ _, err := content.Seek(0, io.SeekStart) // rewind to output whole file
+ if err != nil {
+ http.Error(resp, "failed to seek file", http.StatusInternalServerError)
+- return
++ return fmt.Errorf("failed to seek file: %v", err)
+ }
+ }
+
+@@ -219,18 +225,20 @@ func (h *Handler) fetch(resp http.ResponseWriter, req *http.Request) {
+ resp.Header().Set("Content-Disposition", contentDisp)
+
+ http.ServeContent(resp, req, basename, modTime, content)
++
++ return nil
+ }
+
+-func (h *Handler) store(resp http.ResponseWriter, req *http.Request) {
++func (h *Handler) store(resp http.ResponseWriter, req *http.Request) error {
+ if req.URL.Path != "/uploads" {
+ http.Error(resp, "invalid path", http.StatusNotFound)
+- return
++ return fmt.Errorf("invalid path")
+ }
+
+ authz := req.Header.Get("Authorization")
+ if authz == "" {
+ http.Error(resp, "missing Authorization header", http.StatusUnauthorized)
+- return
++ return fmt.Errorf("missing Authorization header")
+ }
+
+ var (
+@@ -243,26 +251,26 @@ func (h *Handler) store(resp http.ResponseWriter, req *http.Request) {
+ plainAuth := h.Auth.Plain
+ if plainAuth == nil {
+ http.Error(resp, "Basic scheme in Authorization header not supported", http.StatusBadRequest)
+- return
++ return fmt.Errorf("Basic scheme in Authorization header not supported")
+ }
+ var password string
+ var ok bool
+ username, password, ok = req.BasicAuth()
+ if !ok {
+ http.Error(resp, "invalid Authorization header", http.StatusBadRequest)
+- return
++ return fmt.Errorf("invalid Authorization header")
+ }
+ err = plainAuth.AuthPlain(req.Context(), h.DB, username, password)
+ case "bearer":
+ oauthAuth := h.Auth.OAuthBearer
+ if oauthAuth == nil {
+ http.Error(resp, "Bearer scheme in Authorization header not supported", http.StatusBadRequest)
+- return
++ return fmt.Errorf("Bearer scheme in Authorization header not supported")
+ }
+ username, err = oauthAuth.AuthOAuthBearer(req.Context(), h.DB, param)
+ default:
+ http.Error(resp, "unsupported Authorization header scheme", http.StatusBadRequest)
+- return
++ return fmt.Errorf("unsupported Authorization header scheme")
+ }
+ if err != nil {
+ var msg string
+@@ -272,7 +280,7 @@ func (h *Handler) store(resp http.ResponseWriter, req *http.Request) {
+ msg = "authentication failed"
+ }
+ http.Error(resp, msg, http.StatusForbidden)
+- return
++ return fmt.Errorf("%s: %v", msg, err)
+ }
+
+ var mimeType string
+@@ -284,7 +292,7 @@ func (h *Handler) store(resp http.ResponseWriter, req *http.Request) {
+ mimeType, params, err = mime.ParseMediaType(contentType)
+ if err != nil {
+ http.Error(resp, "failed to parse Content-Type", http.StatusBadRequest)
+- return
++ return fmt.Errorf("failed to parse Content-Type: %v", err)
+ }
+ if mimeType == "application/octet-stream" {
+ mimeType = ""
+@@ -295,7 +303,7 @@ func (h *Handler) store(resp http.ResponseWriter, req *http.Request) {
+ // OK
+ default:
+ http.Error(resp, "unsupported charset", http.StatusUnsupportedMediaType)
+- return
++ return fmt.Errorf("unsupported charset")
+ }
+ }
+
+@@ -304,7 +312,7 @@ func (h *Handler) store(resp http.ResponseWriter, req *http.Request) {
+ _, params, err := mime.ParseMediaType(contentDisp)
+ if err != nil {
+ http.Error(resp, "failed to parse Content-Disposition", http.StatusBadRequest)
+- return
++ return fmt.Errorf("failed to parse Content-Disposition: %v", err)
+ }
+ basename = path.Base(params["filename"])
+ }
+@@ -325,7 +333,7 @@ func (h *Handler) store(resp http.ResponseWriter, req *http.Request) {
+ var httpErr *httpError
+ if errors.As(err, &httpErr) {
+ httpErr.Write(resp)
+- return
++ return err
+ }
+ status := http.StatusInternalServerError
+ var maxBytesErr *http.MaxBytesError
+@@ -335,19 +343,21 @@ func (h *Handler) store(resp http.ResponseWriter, req *http.Request) {
+ resp.Header().Set("Upload-Limit", fmt.Sprintf("maxsize=%v", maxBytesErr.Limit))
+ }
+ http.Error(resp, "failed to write file", status)
+- return
++ return fmt.Errorf("failed to write file: %v", err)
+ }
+
+ u, err := url.Parse(out)
+ if err != nil {
+ http.Error(resp, "failed to write file", http.StatusInternalServerError)
+- return
++ return fmt.Errorf("failed to write file: %v", err)
+ }
+- baseURL := url.URL{Path: "/uploads/"}
++ baseURL := url.URL{Path: "/_irc/uploads/"}
+ out = baseURL.ResolveReference(u).String()
+
+ resp.Header().Set("Location", out)
+ resp.WriteHeader(http.StatusCreated)
++
++ return nil
+ }
+
+ func generateToken(n int) (string, error) {
+diff --git i/fileupload/fs.go w/fileupload/fs.go
+index c0c401d..66337a1 100644
+--- i/fileupload/fs.go
++++ w/fileupload/fs.go
+@@ -56,7 +56,7 @@ func (fs *fileuploadFS) store(ctx context.Context, r io.Reader, username, mimeTy
+ }
+
+ dir := filepath.Join(fs.dir, username)
+- if err := os.MkdirAll(dir, 0700); err != nil {
++ if err := os.MkdirAll(dir, 0755); err != nil {
+ return "", fmt.Errorf("failed to create user upload directory: %w", err)
+ }
+
+@@ -77,7 +77,7 @@ func (fs *fileuploadFS) store(ctx context.Context, r io.Reader, username, mimeTy
+ }
+ basename += suffix
+
+- f, err = os.OpenFile(filepath.Join(dir, basename), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
++ f, err = os.OpenFile(filepath.Join(dir, basename), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
+ if err == nil {
+ break
+ } else if !os.IsExist(err) {
|