Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/pr-558.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Patch hashes
7 changes: 6 additions & 1 deletion pkgs/nixos/modules/comfyui/module.nix
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ in
"imggen2"
];
};
secretsFile = lib.mkOption {
type = lib.types.str;
default = "/data/andrew/secrets/flask/cozy.json";
description = "Path to JSON file with secret_key and password_hash";
};
};
};

Expand Down Expand Up @@ -211,7 +216,7 @@ in
unitConfig.StartLimitIntervalSec = 0;
serviceConfig = {
Type = "simple";
ExecStart = "${cfg.cozy.package}/bin/cozy --port ${builtins.toString service-ports.cozy} --subdomain /cozy --comfyui-url http://127.0.0.1:${builtins.toString cfg.port} --state-dir ${cfg.cozy.stateDir} --workflow-dir ${cfg.cozy.workflowDir} --input-dir ${cfg.cozy.inputDir} --output-dir ${cfg.cozy.outputDir} --workflows ${lib.concatStringsSep "," cfg.cozy.workflows}";
ExecStart = "${cfg.cozy.package}/bin/cozy --port ${builtins.toString service-ports.cozy} --subdomain /cozy --comfyui-url http://127.0.0.1:${builtins.toString cfg.port} --state-dir ${cfg.cozy.stateDir} --workflow-dir ${cfg.cozy.workflowDir} --input-dir ${cfg.cozy.inputDir} --output-dir ${cfg.cozy.outputDir} --workflows ${lib.concatStringsSep "," cfg.cozy.workflows} --secrets-file ${cfg.cozy.secretsFile}";
ReadWritePaths = [ cfg.cozy.stateDir ];
WorkingDirectory = cfg.cozy.stateDir;
Restart = "always";
Expand Down
37 changes: 29 additions & 8 deletions pkgs/python-packages/flasks/cozy/cozy.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import argparse
import json
import os
import sys

import flask
import flask_login
Expand All @@ -11,11 +13,21 @@
from comfyui_client import ComfyUIClient
from job_store import JobStore, job_duration

# Identical to stampserver's credential hash + secret key.
_PW_HASH = ("scrypt:32768:8:1$acPu0meyxPfx0SnS$26a570af250e0593c2dbb6bfb1d037a7"
"366109a0ba4886e68191237efdabb2fca07de6c81c337b5e275390c2d7ff96f3"
"455f47b7a05027a7e0ebf1628f537498")
_SECRET_KEY = b"71d2dcdb895b367a1d5f0c66ca559c8d69af0c29a7e101c18c7c2d10399f264e"
_PW_HASH = None # populated from the secrets file at startup; see _load_secrets


def _load_secrets(path):
try:
with open(path) as f:
data = json.load(f)
except OSError as e:
sys.exit(f"cozy: cannot read secrets file {path}: {e}")
except json.JSONDecodeError as e:
sys.exit(f"cozy: invalid JSON in secrets file {path}: {e}")
missing = [k for k in ("secret_key", "password_hash") if not data.get(k)]
if missing:
sys.exit(f"cozy: secrets file {path} missing keys: {', '.join(missing)}")
return data


def _check_password(password):
Expand Down Expand Up @@ -89,7 +101,11 @@ def get_id(self):


def create_app(store, workflows, workflow_dir, subdomain="/cozy",
input_dir=None, output_dir=None, workflow_kinds=None):
input_dir=None, output_dir=None, workflow_kinds=None,
secret_key=None, password_hash=None):
global _PW_HASH
if password_hash is not None:
_PW_HASH = password_hash
input_dir = input_dir or os.path.join(workflow_dir, "input")
output_dir = output_dir or os.path.join(workflow_dir, "output")
workflow_kinds = workflow_kinds or {}
Expand All @@ -99,7 +115,7 @@ def create_app(store, workflows, workflow_dir, subdomain="/cozy",
static_url_path = (subdomain.rstrip("/") or "") + "/static"

app = flask.Flask(__name__, static_url_path=static_url_path, static_folder="static")
app.secret_key = _SECRET_KEY
app.secret_key = secret_key or os.urandom(24)
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(minutes=20)
app.config.setdefault("WTF_CSRF_ENABLED", True)

Expand Down Expand Up @@ -232,6 +248,8 @@ def run():
parser.add_argument("--output-dir", type=str, default="",
help="Directory of selectable output images for edit workflows "
"(default <workflow-dir>/output)")
parser.add_argument("--secrets-file", type=str, required=True,
help="Path to JSON file with secret_key and password_hash")
args = parser.parse_args()

state_dir = args.state_dir or os.path.join(os.getcwd(), "cozy-state")
Expand All @@ -245,10 +263,13 @@ def run():
for n in names if os.path.exists(os.path.join(workflow_dir, n + ".api.json"))
}
store = JobStore(state_dir, ComfyUIClient(args.comfyui_url))
secrets = _load_secrets(args.secrets_file)
app = create_app(store=store, workflows=names,
workflow_dir=workflow_dir, subdomain=args.subdomain,
input_dir=input_dir, output_dir=output_dir,
workflow_kinds=workflow_kinds)
workflow_kinds=workflow_kinds,
secret_key=secrets["secret_key"].encode(),
password_hash=secrets["password_hash"])
app.run(host="0.0.0.0", port=args.port)


Expand Down
32 changes: 32 additions & 0 deletions pkgs/python-packages/flasks/cozy/tests/test_secrets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import json

import pytest

import cozy


def test_load_secrets_missing_file(tmp_path):
with pytest.raises(SystemExit):
cozy._load_secrets(str(tmp_path / "nope.json"))


def test_load_secrets_invalid_json(tmp_path):
p = tmp_path / "s.json"
p.write_text("{not json")
with pytest.raises(SystemExit):
cozy._load_secrets(str(p))


def test_load_secrets_missing_field(tmp_path):
p = tmp_path / "s.json"
p.write_text(json.dumps({"secret_key": "abc"}))
with pytest.raises(SystemExit):
cozy._load_secrets(str(p))


def test_load_secrets_ok(tmp_path):
p = tmp_path / "s.json"
p.write_text(json.dumps({"secret_key": "abc", "password_hash": "scrypt:x"}))
data = cozy._load_secrets(str(p))
assert data["secret_key"] == "abc"
assert data["password_hash"] == "scrypt:x"
7 changes: 6 additions & 1 deletion pkgs/python-packages/flasks/rankserver/module.nix
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ in
description = "Home directory (will be cwd of the server)";
default = "/data/andrew";
};
secretsFile = lib.mkOption {
type = lib.types.str;
description = "Path to JSON file with secret_key and password_hash";
default = "/data/andrew/secrets/flask/rankserver.json";
};
};

config = lib.mkIf cfg.enable {
Expand All @@ -44,7 +49,7 @@ in
};
serviceConfig = {
Type = "simple";
ExecStart = "${cfg.package}/bin/rankserver --port ${builtins.toString service-ports.rankserver} --data-dir ${cfg.rootDir}/rankables --subdomain /rank";
ExecStart = "${cfg.package}/bin/rankserver --port ${builtins.toString service-ports.rankserver} --data-dir ${cfg.rootDir}/rankables --subdomain /rank --secrets-file ${cfg.secretsFile}";
ReadWritePaths = [ "/" ];
WorkingDirectory = cfg.rootDir;
Restart = "always";
Expand Down
24 changes: 22 additions & 2 deletions pkgs/python-packages/flasks/rankserver/rankserver.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import os
import json
import sys
import argparse
import flask
import flask_login
Expand All @@ -23,8 +25,26 @@
parser.add_argument("--port", action="store", type=int, default=5000, help="Port to run the server on")
parser.add_argument("--subdomain", action="store", type=str, default="/rank", help="Subdomain for a reverse proxy")
parser.add_argument("--data-dir", action="store", type=str, default="", help="Directory containing the rankable elements")
parser.add_argument("--secrets-file", action="store", type=str, required=True, help="Path to JSON file with secret_key and password_hash")
args = parser.parse_args()


def _load_secrets(path):
try:
with open(path) as f:
data = json.load(f)
except OSError as e:
sys.exit(f"rankserver: cannot read secrets file {path}: {e}")
except json.JSONDecodeError as e:
sys.exit(f"rankserver: invalid JSON in secrets file {path}: {e}")
missing = [k for k in ("secret_key", "password_hash") if not data.get(k)]
if missing:
sys.exit(f"rankserver: secrets file {path} missing keys: {', '.join(missing)}")
return data


_secrets = _load_secrets(args.secrets_file)

urlroot = args.subdomain
if urlroot != "/":
urlroot += "/"
Expand All @@ -41,7 +61,7 @@ class LoginForm(flask_wtf.FlaskForm):

class User(flask_login.UserMixin):
def check_password(self, password):
return check_password_hash("scrypt:32768:8:1$acPu0meyxPfx0SnS$26a570af250e0593c2dbb6bfb1d037a7366109a0ba4886e68191237efdabb2fca07de6c81c337b5e275390c2d7ff96f3455f47b7a05027a7e0ebf1628f537498", password)
return check_password_hash(_secrets["password_hash"], password)
def get_id(self):
return "anonymous"

Expand All @@ -54,7 +74,7 @@ def get_id(self):
RES_DIR = os.path.join(PWD, args.data_dir)

app = flask.Flask(__name__, static_url_path=args.subdomain, static_folder=RES_DIR)
app.secret_key = b"71d2dcdb895b367a1d5f0c66ca559c8d69af0c29a7e101c18c7c2d10399f264e"
app.secret_key = _secrets["secret_key"].encode()
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(minutes=20)
login_manager = flask_login.LoginManager()

Expand Down
7 changes: 6 additions & 1 deletion pkgs/python-packages/flasks/stampserver/module.nix
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ in
description = "Home directory (will be cwd of the server)";
default = "/data/andrew";
};
secretsFile = lib.mkOption {
type = lib.types.str;
description = "Path to JSON file with secret_key and password_hash";
default = "/data/andrew/secrets/flask/stampserver.json";
};
};

config = lib.mkIf cfg.enable {
Expand Down Expand Up @@ -57,7 +62,7 @@ in
};
serviceConfig = {
Type = "simple";
ExecStart = "${cfg.package}/bin/stampserver --port ${builtins.toString service-ports.stampserver} --data-dir ${cfg.rootDir}/stampables --subdomain /stamp";
ExecStart = "${cfg.package}/bin/stampserver --port ${builtins.toString service-ports.stampserver} --data-dir ${cfg.rootDir}/stampables --subdomain /stamp --secrets-file ${cfg.secretsFile}";
ReadWritePaths = [ "/" ];
WorkingDirectory = cfg.rootDir;
Restart = "always";
Expand Down
24 changes: 22 additions & 2 deletions pkgs/python-packages/flasks/stampserver/stampserver.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import os
import json
import sys
import re
import shutil
import subprocess
Expand All @@ -19,8 +21,26 @@
parser.add_argument("--port", action="store", type=int, default=5000, help="Port to run the server on")
parser.add_argument("--subdomain", action="store", type=str, default="/", help="Subdomain for a reverse proxy")
parser.add_argument("--data-dir", action="store", type=str, default="", help="Directory containing the stampable elements")
parser.add_argument("--secrets-file", action="store", type=str, required=True, help="Path to JSON file with secret_key and password_hash")
args = parser.parse_args()


def _load_secrets(path):
try:
with open(path) as f:
data = json.load(f)
except OSError as e:
sys.exit(f"stampserver: cannot read secrets file {path}: {e}")
except json.JSONDecodeError as e:
sys.exit(f"stampserver: invalid JSON in secrets file {path}: {e}")
missing = [k for k in ("secret_key", "password_hash") if not data.get(k)]
if missing:
sys.exit(f"stampserver: secrets file {path} missing keys: {', '.join(missing)}")
return data


_secrets = _load_secrets(args.secrets_file)

urlroot = args.subdomain
if urlroot != "/":
urlroot += "/"
Expand All @@ -37,7 +57,7 @@ class LoginForm(flask_wtf.FlaskForm):

class User(flask_login.UserMixin):
def check_password(self, password):
return check_password_hash("scrypt:32768:8:1$acPu0meyxPfx0SnS$26a570af250e0593c2dbb6bfb1d037a7366109a0ba4886e68191237efdabb2fca07de6c81c337b5e275390c2d7ff96f3455f47b7a05027a7e0ebf1628f537498", password)
return check_password_hash(_secrets["password_hash"], password)
def get_id(self):
return "anonymous"

Expand All @@ -51,7 +71,7 @@ def get_id(self):
SHORT_RESDIR = os.path.basename(os.path.realpath(RES_DIR))

app = flask.Flask(__name__, static_url_path=args.subdomain, static_folder=RES_DIR)
app.secret_key = b"71d2dcdb895b367a1d5f0c66ca559c8d69af0c29a7e101c18c7c2d10399f264e"
app.secret_key = _secrets["secret_key"].encode()
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(minutes=20)
login_manager = flask_login.LoginManager()

Expand Down
Loading