From a45a6698839247a9657dc0d94c116bdacf2607aa Mon Sep 17 00:00:00 2001 From: Stefan Darius Date: Wed, 9 Sep 2026 15:49:07 +0300 Subject: [PATCH 1/7] tls: add db_* commands for tls_mgm provisioning The tls module could only write certificates to files, leaving the tls_mgm table to be provisioned by hand. Add CRUD commands over it: tls db_add [type] column=value ... tls db_update [type] column=value ... tls db_show [type] tls db_delete [type] tls db_list A domain is identified by its name and its type (server or client), matching the UNIQUE (domain, type) constraint of the table. Every settable column is passed as 'column=value', so the whole schema is reachable, not just the certificate and the key. The columns holding PEM content take the path of the file holding it, which is read and stored as a BLOB; every other column is stored as given. Unknown columns, unreadable files and files that do not hold PEM are rejected before the database is touched. After every change the tls_reload MI command is issued, so a running OpenSIPS picks up the domains without a restart. --- docs/modules/tls.md | 86 ++++++++++- opensipscli/modules/tls.py | 307 +++++++++++++++++++++++++++++++++++++ 2 files changed, 392 insertions(+), 1 deletion(-) diff --git a/docs/modules/tls.md b/docs/modules/tls.md index f7ff16f..c798d8c 100644 --- a/docs/modules/tls.md +++ b/docs/modules/tls.md @@ -2,11 +2,51 @@ Using the `tls` module, you can generate TLS certificates and private keys. -The module has two subcommands: +The module has the following subcommands: * `rootCA` - generates a CA (certification authority) self signed certificate and private key pair. These are to be used by a TLS server. * `userCERT` - generates a certificate signed by a given CA, a private key and a CA list (chain of trust) file. These are to be used by TLS clients (users). +* `db_add` - adds a new TLS domain to the `tls_mgm` table. +* `db_update` - changes the columns of an existing TLS domain. +* `db_list` - lists the TLS domains provisioned in the `tls_mgm` table. +* `db_show` - prints the columns of a TLS domain. +* `db_delete` - removes a TLS domain from the `tls_mgm` table. + +The `db_*` subcommands provision the `tls_mgm` module over the database, where +the certificate, private key and CA list are stored as BLOB values rather than +as paths to files. A TLS domain is identified by its name and its type +(`server` or `client`), both passed as arguments: +``` +opensips-cli -x tls db_delete a.example.org server +``` + +`db_add` and `db_update` take the remaining `tls_mgm` columns as `column=value` +arguments, in any order and after the domain and the type: +``` +opensips-cli -x tls db_add a.example.org server method=TLSv1_2 verify_cert=1 +``` +The settable columns are `match_ip_address`, `match_sip_domain`, `method`, +`verify_cert`, `require_cert`, `certificate`, `private_key`, `crl_check_all`, +`crl_dir`, `ca_list`, `ca_dir`, `cipher_list`, `dh_params` and `ec_curve`. A +column that is not given is left to its default in the database schema; +`db_update` only changes the columns it is given. + +The `certificate`, `private_key`, `ca_list` and `dh_params` columns hold PEM +content, so their value is the path of the file holding it, and that file is +read and stored in the table: +``` +opensips-cli -x tls db_add a.example.org server \ + certificate=/etc/opensips/tls/user/user-cert.pem \ + private_key=/etc/opensips/tls/user/user-privkey.pem +``` +Every other column is stored as the value it is given, paths included: for +example, `ca_list` reads the file it points to, while `ca_dir` and `crl_dir` +keep the directory as such, which is what `tls_mgm` expects of them. + +After every change, the `tls_reload` MI command is issued so that a running +OpenSIPS picks up the new domains. If OpenSIPS cannot be reached, a warning is +logged and the domains are loaded at the next restart. ## Configuration @@ -47,6 +87,14 @@ List of `opensips-cli.cfg` settings for configuring user certificates: * tls_user_key_size - the size of the RSA key, in bits (e.g. 4096) * tls_user_md - the digest algorithm to use for signing (e.g. SHA256) +List of `opensips-cli.cfg` settings for the `db_*` subcommands: + +* database_tls_url - URL of the database holding the `tls_mgm` table; falls +back to `database_url` +* database_tls_name - name of the database; falls back to `database_name` +* tls_db_type - the default TLS domain type ("server" or "client"), used when +no type is passed as an argument + ## Examples @@ -97,3 +145,39 @@ tls_user_notafter: 315360000 tls_user_key_size: 4096 tls_user_md: SHA256 ``` + +To provision the certificate generated above as a TLS domain in the database: +``` +opensips-cli -x tls db_add a.example.org server \ + certificate=/etc/opensips/tls/user/user-cert.pem \ + private_key=/etc/opensips/tls/user/user-privkey.pem \ + ca_list=/etc/opensips/tls/user/user-calist.pem +``` +Certificates issued by a public CA are provisioned the same way: +``` +opensips-cli -x tls db_add a.example.org server \ + certificate=/etc/letsencrypt/live/a.example.org/fullchain.pem \ + private_key=/etc/letsencrypt/live/a.example.org/privkey.pem +``` +Configuration file example for the `db_*` subcommands: +``` +[default] +database_url: mysql://opensips:opensipsrw@localhost +database_name: opensips +tls_db_type: server +``` + +To renew the certificate of a domain, or to change any of its other columns: +``` +opensips-cli -x tls db_update a.example.org server \ + certificate=/etc/letsencrypt/live/a.example.org/fullchain.pem \ + private_key=/etc/letsencrypt/live/a.example.org/privkey.pem +opensips-cli -x tls db_update a.example.org server cipher_list=HIGH +``` + +To inspect and remove the provisioned domains: +``` +opensips-cli -x tls db_list +opensips-cli -x tls db_show a.example.org server +opensips-cli -x tls db_delete a.example.org server +``` diff --git a/opensipscli/modules/tls.py b/opensipscli/modules/tls.py index d9cfbec..8ffbf09 100644 --- a/opensipscli/modules/tls.py +++ b/opensipscli/modules/tls.py @@ -25,8 +25,33 @@ from os.path import exists, join, dirname from os import makedirs from opensipscli.config import cfg, OpenSIPSCLIConfig +from opensipscli.db import osdb, osdbError +from opensipscli import comm from random import randrange +DEFAULT_DB_NAME = "opensips" +TLS_MGM_TABLE = "tls_mgm" +TLS_DOMAIN_COL = "domain" +TLS_TYPE_COL = "type" +TLS_CERT_COL = "certificate" +TLS_PK_COL = "private_key" +TLS_CALIST_COL = "ca_list" +TLS_DH_COL = "dh_params" + +# the tls_mgm columns that can be provisioned; 'id' is auto-incremented, while +# 'domain' and 'type' identify the row and are passed as arguments +TLS_MGM_COLUMNS = ["match_ip_address", "match_sip_domain", "method", + "verify_cert", "require_cert", TLS_CERT_COL, TLS_PK_COL, + "crl_check_all", "crl_dir", TLS_CALIST_COL, "ca_dir", "cipher_list", + TLS_DH_COL, "ec_curve"] + +# columns holding PEM content, which is read from the file they point to +TLS_PEM_COLUMNS = [TLS_CERT_COL, TLS_PK_COL, TLS_CALIST_COL, TLS_DH_COL] + +# as defined by CLIENT_DOMAIN_TYPE/SERVER_DOMAIN_TYPE in tls_mgm/tls_domain.h +TLS_DOMAIN_TYPES = {"client": 1, "server": 2} +TLS_TYPE_NAMES = {v: k for k, v in TLS_DOMAIN_TYPES.items()} + openssl_version = None try: @@ -207,6 +232,7 @@ def load(self, key): password=None) class tls(Module): + def do_rootCA(self, params, modifiers=None): global cfg logger.info("Preparing to generate CA cert + key...") @@ -349,6 +375,287 @@ def do_userCERT(self, params, modifiers=None): logger.info("user private key created in " + k_f) logger.info("user CA list (chain of trust) created in " + ca_f) + def tls_db_connect(self): + """ + connects to the database holding the tls_mgm table + """ + if not osdb.has_sqlalchemy(): + logger.error("SQLAlchemy not available: cannot access the database") + return None + + engine = osdb.get_db_engine() + + db_url = cfg.read_param(["database_tls_url", "database_url"], + "Please provide us the URL of the database") + if db_url is None: + print() + logger.error("no URL specified: aborting!") + return None + + db_url = osdb.set_url_driver(db_url, engine) + db_name = cfg.read_param(["database_tls_name", "database_name"], + "Please provide the database storing the TLS domains", + DEFAULT_DB_NAME) + + try: + db = osdb(db_url, db_name) + except osdbError: + logger.error("failed to connect to database %s", db_name) + return None + + if not db.connect(): + return None + + return db + + def tls_db_domain(self, params): + """ + resolves the (domain, type) pair identifying a tls_mgm row + """ + if len(params) > 0: + domain = params[0] + else: + domain = cfg.read_param(None, + "Please provide the name of the TLS domain") + if not domain: + logger.error("no TLS domain specified!") + return None, None + + if len(params) > 1: + dtype = params[1] + else: + dtype = cfg.read_param("tls_db_type", + "TLS domain type (server/client)", "server") + + if dtype.lower() not in TLS_DOMAIN_TYPES: + logger.error("invalid TLS domain type '%s': " + "expected 'server' or 'client'", dtype) + return None, None + + return domain, TLS_DOMAIN_TYPES[dtype.lower()] + + def tls_db_reload(self): + """ + makes a running OpenSIPS pick up the tls_mgm changes + """ + if comm.execute('tls_reload') is None: + logger.warning("could not reload the TLS domains; " + "OpenSIPS will load them at the next restart") + + def tls_db_params(self, params): + """ + splits the params into the (domain, type) pair identifying the row and + the 'column=value' assignments; the value of a PEM column is the path + of the file holding it + """ + domain, dtype = self.tls_db_domain([p for p in params if '=' not in p]) + if not domain: + return None, None, None + + cols = {} + for param in [p for p in params if '=' in p]: + col, val = param.split('=', 1) + if col not in TLS_MGM_COLUMNS: + logger.error("unknown %s column '%s'", TLS_MGM_TABLE, col) + return None, None, None + + if col in TLS_PEM_COLUMNS: + path = val + try: + with open(path, "rt") as f: + val = f.read() + except Exception as e: + logger.exception(e) + logger.error("Failed to read %s", path) + return None, None, None + + if "-----BEGIN" not in val: + logger.error("%s is not in PEM format", path) + return None, None, None + + cols[col] = val + + return domain, dtype, cols + + def do_db_add(self, params=None, modifiers=None): + """ + provisions a new TLS domain in the database + """ + domain, dtype, cols = self.tls_db_params(params or []) + if not domain: + return -1 + + db = self.tls_db_connect() + if not db: + return -1 + + row = {TLS_DOMAIN_COL: domain, TLS_TYPE_COL: dtype} + if db.entry_exists(TLS_MGM_TABLE, row): + logger.error("TLS %s domain '%s' already exists", + TLS_TYPE_NAMES[dtype], domain) + db.destroy() + return -1 + + row.update(cols) + if db.insert(TLS_MGM_TABLE, row) is False: + db.destroy() + return -1 + + db.destroy() + logger.info("Successfully added TLS %s domain '%s'", + TLS_TYPE_NAMES[dtype], domain) + self.tls_db_reload() + return True + + def do_db_update(self, params=None, modifiers=None): + """ + changes the given columns of an existing TLS domain + """ + domain, dtype, cols = self.tls_db_params(params or []) + if not domain: + return -1 + + if not cols: + logger.error("no column to update: expected 'column=value'") + return -1 + + db = self.tls_db_connect() + if not db: + return -1 + + row = {TLS_DOMAIN_COL: domain, TLS_TYPE_COL: dtype} + if not db.entry_exists(TLS_MGM_TABLE, row): + logger.error("TLS %s domain '%s' does not exist", + TLS_TYPE_NAMES[dtype], domain) + db.destroy() + return -1 + + if db.update(TLS_MGM_TABLE, cols, row) is False: + db.destroy() + return -1 + + db.destroy() + logger.info("Successfully updated TLS %s domain '%s'", + TLS_TYPE_NAMES[dtype], domain) + self.tls_db_reload() + return True + + def do_db_list(self, params=None, modifiers=None): + """ + lists the TLS domains provisioned in the database + """ + db = self.tls_db_connect() + if not db: + return -1 + + res = db.find(TLS_MGM_TABLE, + ["id", TLS_DOMAIN_COL, TLS_TYPE_COL, "method", + "verify_cert", "require_cert"], None) + if res is None: + db.destroy() + return -1 + + rows = res.fetchall() + db.destroy() + + if not rows: + logger.info("no TLS domain provisioned in %s", TLS_MGM_TABLE) + return True + + print("{:<5} {:<32} {:<8} {:<8} {:<8} {:<8}".format( + "id", "domain", "type", "method", "verify", "require")) + for r in rows: + print("{:<5} {:<32} {:<8} {:<8} {:<8} {:<8}".format( + r[0], r[1], TLS_TYPE_NAMES.get(r[2], r[2]), + str(r[3]), str(r[4]), str(r[5]))) + return True + + def do_db_show(self, params=None, modifiers=None): + """ + prints the columns of a TLS domain + """ + domain, dtype = self.tls_db_domain(params or []) + if not domain: + return -1 + + db = self.tls_db_connect() + if not db: + return -1 + + res = db.find(TLS_MGM_TABLE, TLS_MGM_COLUMNS, + {TLS_DOMAIN_COL: domain, TLS_TYPE_COL: dtype}) + row = res.first() if res is not None else None + db.destroy() + + if not row: + logger.error("TLS %s domain '%s' does not exist", + TLS_TYPE_NAMES[dtype], domain) + return -1 + + def decode(val): + return val.decode('utf-8') if isinstance(val, bytes) else val + + values = dict(zip(TLS_MGM_COLUMNS, row)) + + print("{} domain: {}".format(TLS_TYPE_NAMES[dtype], domain)) + for col in TLS_MGM_COLUMNS: + if col in TLS_PEM_COLUMNS: + continue + print("{}: {}".format(col, + "" if values[col] is None else values[col])) + + # the private key is never printed back + print("{}: {}".format(TLS_PK_COL, + "" if values[TLS_PK_COL] else "")) + + for col in TLS_PEM_COLUMNS: + if col == TLS_PK_COL: + continue + print("\n{}:\n{}".format(col, + decode(values[col]) if values[col] else "")) + return True + + def do_db_delete(self, params=None, modifiers=None): + """ + removes a TLS domain from the database + """ + domain, dtype = self.tls_db_domain(params or []) + if not domain: + return -1 + + db = self.tls_db_connect() + if not db: + return -1 + + row = {TLS_DOMAIN_COL: domain, TLS_TYPE_COL: dtype} + if not db.entry_exists(TLS_MGM_TABLE, row): + logger.error("TLS %s domain '%s' does not exist", + TLS_TYPE_NAMES[dtype], domain) + db.destroy() + return -1 + + if db.delete(TLS_MGM_TABLE, row) is False: + db.destroy() + return -1 + + db.destroy() + logger.info("Successfully deleted TLS %s domain '%s'", + TLS_TYPE_NAMES[dtype], domain) + self.tls_db_reload() + return True + + def __complete__(self, command, text, line, begidx, endidx): + """ + helper for autocompletion in interactive mode + """ + if command not in ('db_add', 'db_update'): + return [''] + + cols = [c + '=' for c in TLS_MGM_COLUMNS] + if not text: + return cols + + return [c for c in cols if c.startswith(text)] or [''] def __exclude__(self): return (not openssl_version, None) From b3e63a0227fc81da0c73edb2341ba22fa13fc736 Mon Sep 17 00:00:00 2001 From: Stefan Darius Date: Wed, 9 Sep 2026 15:58:24 +0300 Subject: [PATCH 2/7] tls: require the domain type in db_update and db_delete A tls_mgm row is identified by (domain, type), so defaulting the type when it is not given lets these two commands change a different domain than the intended one. Require it for the commands that modify an existing row; db_add and db_show keep falling back to tls_db_type. Also guard against read_param() returning None when there is no terminal to prompt on, which made the type default path raise an AttributeError instead of reporting the missing value. --- docs/modules/tls.md | 3 +++ opensipscli/modules/tls.py | 22 ++++++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/docs/modules/tls.md b/docs/modules/tls.md index c798d8c..48d8cc0 100644 --- a/docs/modules/tls.md +++ b/docs/modules/tls.md @@ -20,6 +20,9 @@ as paths to files. A TLS domain is identified by its name and its type ``` opensips-cli -x tls db_delete a.example.org server ``` +`db_add` and `db_show` fall back to the `tls_db_type` setting, and then to +`server`, when no type is given. `db_update` and `db_delete` always require +it, so that they cannot change a different domain than the intended one. `db_add` and `db_update` take the remaining `tls_mgm` columns as `column=value` arguments, in any order and after the domain and the type: diff --git a/opensipscli/modules/tls.py b/opensipscli/modules/tls.py index 8ffbf09..bc82714 100644 --- a/opensipscli/modules/tls.py +++ b/opensipscli/modules/tls.py @@ -408,9 +408,11 @@ def tls_db_connect(self): return db - def tls_db_domain(self, params): + def tls_db_domain(self, params, require_type=False): """ - resolves the (domain, type) pair identifying a tls_mgm row + resolves the (domain, type) pair identifying a tls_mgm row; commands + that change an existing row require the type, so that they cannot pick + a different row than the intended one """ if len(params) > 0: domain = params[0] @@ -423,9 +425,16 @@ def tls_db_domain(self, params): if len(params) > 1: dtype = params[1] + elif require_type: + logger.error("no TLS domain type specified: " + "expected 'server' or 'client'") + return None, None else: dtype = cfg.read_param("tls_db_type", "TLS domain type (server/client)", "server") + if not dtype: + logger.error("no TLS domain type specified!") + return None, None if dtype.lower() not in TLS_DOMAIN_TYPES: logger.error("invalid TLS domain type '%s': " @@ -442,13 +451,14 @@ def tls_db_reload(self): logger.warning("could not reload the TLS domains; " "OpenSIPS will load them at the next restart") - def tls_db_params(self, params): + def tls_db_params(self, params, require_type=False): """ splits the params into the (domain, type) pair identifying the row and the 'column=value' assignments; the value of a PEM column is the path of the file holding it """ - domain, dtype = self.tls_db_domain([p for p in params if '=' not in p]) + domain, dtype = self.tls_db_domain( + [p for p in params if '=' not in p], require_type) if not domain: return None, None, None @@ -511,7 +521,7 @@ def do_db_update(self, params=None, modifiers=None): """ changes the given columns of an existing TLS domain """ - domain, dtype, cols = self.tls_db_params(params or []) + domain, dtype, cols = self.tls_db_params(params or [], True) if not domain: return -1 @@ -619,7 +629,7 @@ def do_db_delete(self, params=None, modifiers=None): """ removes a TLS domain from the database """ - domain, dtype = self.tls_db_domain(params or []) + domain, dtype = self.tls_db_domain(params or [], True) if not domain: return -1 From fcff8112f61fe5a4002427d2846ac3b1ec1390a1 Mon Sep 17 00:00:00 2001 From: Stefan Darius Date: Wed, 9 Sep 2026 16:27:18 +0300 Subject: [PATCH 3/7] tls: drop the tls_db_type setting The type is part of the identity of a tls_mgm row, not an environment setting, so it does not belong in opensips-cli.cfg next to the database URL. Ask for it instead, defaulting to 'server' on empty input; the prompt already shows that default. db_update and db_delete keep requiring it as an argument. --- docs/modules/tls.md | 9 +++------ opensipscli/modules/tls.py | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/modules/tls.md b/docs/modules/tls.md index 48d8cc0..368cb44 100644 --- a/docs/modules/tls.md +++ b/docs/modules/tls.md @@ -20,9 +20,9 @@ as paths to files. A TLS domain is identified by its name and its type ``` opensips-cli -x tls db_delete a.example.org server ``` -`db_add` and `db_show` fall back to the `tls_db_type` setting, and then to -`server`, when no type is given. `db_update` and `db_delete` always require -it, so that they cannot change a different domain than the intended one. +`db_add` and `db_show` ask for the type when it is not given, defaulting to +`server`. `db_update` and `db_delete` always require it, so that they cannot +change a different domain than the intended one. `db_add` and `db_update` take the remaining `tls_mgm` columns as `column=value` arguments, in any order and after the domain and the type: @@ -95,8 +95,6 @@ List of `opensips-cli.cfg` settings for the `db_*` subcommands: * database_tls_url - URL of the database holding the `tls_mgm` table; falls back to `database_url` * database_tls_name - name of the database; falls back to `database_name` -* tls_db_type - the default TLS domain type ("server" or "client"), used when -no type is passed as an argument ## Examples @@ -167,7 +165,6 @@ Configuration file example for the `db_*` subcommands: [default] database_url: mysql://opensips:opensipsrw@localhost database_name: opensips -tls_db_type: server ``` To renew the certificate of a domain, or to change any of its other columns: diff --git a/opensipscli/modules/tls.py b/opensipscli/modules/tls.py index bc82714..f01d313 100644 --- a/opensipscli/modules/tls.py +++ b/opensipscli/modules/tls.py @@ -430,7 +430,7 @@ def tls_db_domain(self, params, require_type=False): "expected 'server' or 'client'") return None, None else: - dtype = cfg.read_param("tls_db_type", + dtype = cfg.read_param(None, "TLS domain type (server/client)", "server") if not dtype: logger.error("no TLS domain type specified!") From 8f5a057ca28758a3b54f2ed26f64052250446c8d Mon Sep 17 00:00:00 2001 From: Stefan Darius Date: Wed, 9 Sep 2026 16:31:50 +0300 Subject: [PATCH 4/7] tls: ask for the domain type instead of refusing it db_update and db_delete errored out when the type was missing, while the domain right next to it was asked for, which is confusing when running the commands interactively. Ask for the type as well, and drop the default for these two commands instead: read_param() keeps asking until a type is given, so they still cannot pick a different row than the intended one. --- docs/modules/tls.md | 7 ++++--- opensipscli/modules/tls.py | 12 ++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/modules/tls.md b/docs/modules/tls.md index 368cb44..4fe204d 100644 --- a/docs/modules/tls.md +++ b/docs/modules/tls.md @@ -20,9 +20,10 @@ as paths to files. A TLS domain is identified by its name and its type ``` opensips-cli -x tls db_delete a.example.org server ``` -`db_add` and `db_show` ask for the type when it is not given, defaulting to -`server`. `db_update` and `db_delete` always require it, so that they cannot -change a different domain than the intended one. +All of them ask for the type when it is not given. `db_add` and `db_show` +default to `server`, while `db_update` and `db_delete` have no default and keep +asking until one is given, so that they cannot change a different domain than +the intended one. `db_add` and `db_update` take the remaining `tls_mgm` columns as `column=value` arguments, in any order and after the domain and the type: diff --git a/opensipscli/modules/tls.py b/opensipscli/modules/tls.py index f01d313..e4b7bd6 100644 --- a/opensipscli/modules/tls.py +++ b/opensipscli/modules/tls.py @@ -411,8 +411,8 @@ def tls_db_connect(self): def tls_db_domain(self, params, require_type=False): """ resolves the (domain, type) pair identifying a tls_mgm row; commands - that change an existing row require the type, so that they cannot pick - a different row than the intended one + that change an existing row get no default type, so that they cannot + pick a different row than the intended one """ if len(params) > 0: domain = params[0] @@ -425,13 +425,9 @@ def tls_db_domain(self, params, require_type=False): if len(params) > 1: dtype = params[1] - elif require_type: - logger.error("no TLS domain type specified: " - "expected 'server' or 'client'") - return None, None else: - dtype = cfg.read_param(None, - "TLS domain type (server/client)", "server") + dtype = cfg.read_param(None, "TLS domain type (server/client)", + None if require_type else "server") if not dtype: logger.error("no TLS domain type specified!") return None, None From 920ca688c1f1923494516349c658d97a148757e1 Mon Sep 17 00:00:00 2001 From: Stefan Darius Date: Wed, 9 Sep 2026 16:40:05 +0300 Subject: [PATCH 5/7] tls: correct which db_* commands ask for the domain type db_list addresses no domain at all, so it never asks for a type. --- docs/modules/tls.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/modules/tls.md b/docs/modules/tls.md index 4fe204d..f0e8110 100644 --- a/docs/modules/tls.md +++ b/docs/modules/tls.md @@ -20,10 +20,10 @@ as paths to files. A TLS domain is identified by its name and its type ``` opensips-cli -x tls db_delete a.example.org server ``` -All of them ask for the type when it is not given. `db_add` and `db_show` -default to `server`, while `db_update` and `db_delete` have no default and keep -asking until one is given, so that they cannot change a different domain than -the intended one. +The commands addressing a single domain ask for the type when it is not given. +`db_add` and `db_show` default to `server`, while `db_update` and `db_delete` +have no default and keep asking until one is given, so that they cannot change +a different domain than the intended one. `db_add` and `db_update` take the remaining `tls_mgm` columns as `column=value` arguments, in any order and after the domain and the type: From 63af333682f3d34186178013ec0c64ed73cc94e1 Mon Sep 17 00:00:00 2001 From: Stefan Darius Date: Wed, 9 Sep 2026 16:57:22 +0300 Subject: [PATCH 6/7] tls: report the identity columns instead of calling them unknown 'db_add a.example.org type=client' reported "unknown tls_mgm column 'type'", which is not true: the column exists, it just identifies the row and is passed as an argument. Name the three identity columns and say so. Parse the columns before resolving the domain as well, so that such a command is rejected right away rather than after asking for the domain and its type. --- opensipscli/modules/tls.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/opensipscli/modules/tls.py b/opensipscli/modules/tls.py index e4b7bd6..9f21d92 100644 --- a/opensipscli/modules/tls.py +++ b/opensipscli/modules/tls.py @@ -48,6 +48,9 @@ # columns holding PEM content, which is read from the file they point to TLS_PEM_COLUMNS = [TLS_CERT_COL, TLS_PK_COL, TLS_CALIST_COL, TLS_DH_COL] +# columns identifying a row, which are not provisioned as 'column=value' +TLS_ID_COLUMNS = ["id", TLS_DOMAIN_COL, TLS_TYPE_COL] + # as defined by CLIENT_DOMAIN_TYPE/SERVER_DOMAIN_TYPE in tls_mgm/tls_domain.h TLS_DOMAIN_TYPES = {"client": 1, "server": 2} TLS_TYPE_NAMES = {v: k for k, v in TLS_DOMAIN_TYPES.items()} @@ -449,18 +452,20 @@ def tls_db_reload(self): def tls_db_params(self, params, require_type=False): """ - splits the params into the (domain, type) pair identifying the row and - the 'column=value' assignments; the value of a PEM column is the path - of the file holding it + splits the params into the 'column=value' assignments and the + (domain, type) pair identifying the row; the value of a PEM column is + the path of the file holding it. The columns are parsed first, so that + a bad one is reported without asking for the domain beforehand """ - domain, dtype = self.tls_db_domain( - [p for p in params if '=' not in p], require_type) - if not domain: - return None, None, None - cols = {} for param in [p for p in params if '=' in p]: col, val = param.split('=', 1) + if col in TLS_ID_COLUMNS: + logger.error("column '%s' identifies the row and cannot be " + "provisioned; the domain and its type are passed as " + "arguments", col) + return None, None, None + if col not in TLS_MGM_COLUMNS: logger.error("unknown %s column '%s'", TLS_MGM_TABLE, col) return None, None, None @@ -481,6 +486,11 @@ def tls_db_params(self, params, require_type=False): cols[col] = val + domain, dtype = self.tls_db_domain( + [p for p in params if '=' not in p], require_type) + if not domain: + return None, None, None + return domain, dtype, cols def do_db_add(self, params=None, modifiers=None): From d7eb6dd62ec65c11faa9f7645222fa014d8dbe58 Mon Sep 17 00:00:00 2001 From: Stefan Darius Date: Wed, 9 Sep 2026 17:04:32 +0300 Subject: [PATCH 7/7] tls: accept the domain and its type as named parameters too Rejecting 'domain=' and 'type=' meant the identity of a row was the one thing that could not be written the way every other column is, which is hard to justify to someone who just read the column list. Take everything as 'column=value', keep the first two arguments as a shorthand for the domain and its type, and refuse only the combination of the two, which is the single ambiguous case. Whatever is left out is then asked for, in one place instead of one per spelling. This folds tls_db_domain() into tls_db_params(), so db_show and db_delete go through the same parser and now report the columns they do not take. 'id' stays refused: the database generates it. --- docs/modules/tls.md | 14 +++-- opensipscli/modules/tls.py | 111 +++++++++++++++++++++---------------- 2 files changed, 72 insertions(+), 53 deletions(-) diff --git a/docs/modules/tls.md b/docs/modules/tls.md index f0e8110..178df76 100644 --- a/docs/modules/tls.md +++ b/docs/modules/tls.md @@ -20,10 +20,16 @@ as paths to files. A TLS domain is identified by its name and its type ``` opensips-cli -x tls db_delete a.example.org server ``` -The commands addressing a single domain ask for the type when it is not given. -`db_add` and `db_show` default to `server`, while `db_update` and `db_delete` -have no default and keep asking until one is given, so that they cannot change -a different domain than the intended one. +The domain and its type may also be given by name, in which case they can +appear anywhere among the other columns: +``` +opensips-cli -x tls db_delete domain=a.example.org type=server +``` +Giving one of them both ways at once is an error. The commands addressing a +single domain ask for whatever is left out; `db_add` and `db_show` default the +type to `server`, while `db_update` and `db_delete` have no default and keep +asking until one is given, so that they cannot change a different domain than +the intended one. `db_add` and `db_update` take the remaining `tls_mgm` columns as `column=value` arguments, in any order and after the domain and the type: diff --git a/opensipscli/modules/tls.py b/opensipscli/modules/tls.py index 9f21d92..717aed5 100644 --- a/opensipscli/modules/tls.py +++ b/opensipscli/modules/tls.py @@ -38,8 +38,7 @@ TLS_CALIST_COL = "ca_list" TLS_DH_COL = "dh_params" -# the tls_mgm columns that can be provisioned; 'id' is auto-incremented, while -# 'domain' and 'type' identify the row and are passed as arguments +# the tls_mgm columns that can be provisioned TLS_MGM_COLUMNS = ["match_ip_address", "match_sip_domain", "method", "verify_cert", "require_cert", TLS_CERT_COL, TLS_PK_COL, "crl_check_all", "crl_dir", TLS_CALIST_COL, "ca_dir", "cipher_list", @@ -48,8 +47,11 @@ # columns holding PEM content, which is read from the file they point to TLS_PEM_COLUMNS = [TLS_CERT_COL, TLS_PK_COL, TLS_CALIST_COL, TLS_DH_COL] -# columns identifying a row, which are not provisioned as 'column=value' -TLS_ID_COLUMNS = ["id", TLS_DOMAIN_COL, TLS_TYPE_COL] +# columns identifying a row, in the order they are accepted as arguments +TLS_KEY_COLUMNS = [TLS_DOMAIN_COL, TLS_TYPE_COL] + +# generated by the database, never provisioned +TLS_ID_COL = "id" # as defined by CLIENT_DOMAIN_TYPE/SERVER_DOMAIN_TYPE in tls_mgm/tls_domain.h TLS_DOMAIN_TYPES = {"client": 1, "server": 2} @@ -411,37 +413,6 @@ def tls_db_connect(self): return db - def tls_db_domain(self, params, require_type=False): - """ - resolves the (domain, type) pair identifying a tls_mgm row; commands - that change an existing row get no default type, so that they cannot - pick a different row than the intended one - """ - if len(params) > 0: - domain = params[0] - else: - domain = cfg.read_param(None, - "Please provide the name of the TLS domain") - if not domain: - logger.error("no TLS domain specified!") - return None, None - - if len(params) > 1: - dtype = params[1] - else: - dtype = cfg.read_param(None, "TLS domain type (server/client)", - None if require_type else "server") - if not dtype: - logger.error("no TLS domain type specified!") - return None, None - - if dtype.lower() not in TLS_DOMAIN_TYPES: - logger.error("invalid TLS domain type '%s': " - "expected 'server' or 'client'", dtype) - return None, None - - return domain, TLS_DOMAIN_TYPES[dtype.lower()] - def tls_db_reload(self): """ makes a running OpenSIPS pick up the tls_mgm changes @@ -452,21 +423,21 @@ def tls_db_reload(self): def tls_db_params(self, params, require_type=False): """ - splits the params into the 'column=value' assignments and the - (domain, type) pair identifying the row; the value of a PEM column is - the path of the file holding it. The columns are parsed first, so that - a bad one is reported without asking for the domain beforehand + resolves the (domain, type) pair identifying a tls_mgm row, along with + the columns to provision. Everything is given as 'column=value', with + the domain and its type also accepted as the first two arguments; what + is left out is asked for. The value of a PEM column is the path of the + file holding it """ cols = {} for param in [p for p in params if '=' in p]: col, val = param.split('=', 1) - if col in TLS_ID_COLUMNS: - logger.error("column '%s' identifies the row and cannot be " - "provisioned; the domain and its type are passed as " - "arguments", col) + if col == TLS_ID_COL: + logger.error("column '%s' is generated by the database", + TLS_ID_COL) return None, None, None - if col not in TLS_MGM_COLUMNS: + if col not in TLS_MGM_COLUMNS and col not in TLS_KEY_COLUMNS: logger.error("unknown %s column '%s'", TLS_MGM_TABLE, col) return None, None, None @@ -486,12 +457,48 @@ def tls_db_params(self, params, require_type=False): cols[col] = val - domain, dtype = self.tls_db_domain( - [p for p in params if '=' not in p], require_type) + # the domain and its type identify the row, they are not provisioned + args = [p for p in params if '=' not in p] + if len(args) > len(TLS_KEY_COLUMNS): + logger.error("too many arguments: expected at most a domain and " + "its type") + return None, None, None + + key = {} + for i, col in enumerate(TLS_KEY_COLUMNS): + if i < len(args): + if col in cols: + logger.error("'%s' given both as an argument and as " + "'%s='", col, col) + return None, None, None + key[col] = args[i] + else: + key[col] = cols.pop(col, None) + + domain = key[TLS_DOMAIN_COL] if not domain: + domain = cfg.read_param(None, + "Please provide the name of the TLS domain") + if not domain: + logger.error("no TLS domain specified!") + return None, None, None + + dtype = key[TLS_TYPE_COL] + if not dtype: + # commands changing an existing row get no default, so that they + # cannot pick a different row than the intended one + dtype = cfg.read_param(None, "TLS domain type (server/client)", + None if require_type else "server") + if not dtype: + logger.error("no TLS domain type specified!") + return None, None, None + + if dtype.lower() not in TLS_DOMAIN_TYPES: + logger.error("invalid TLS domain type '%s': " + "expected 'server' or 'client'", dtype) return None, None, None - return domain, dtype, cols + return domain, TLS_DOMAIN_TYPES[dtype.lower()], cols def do_db_add(self, params=None, modifiers=None): """ @@ -590,7 +597,10 @@ def do_db_show(self, params=None, modifiers=None): """ prints the columns of a TLS domain """ - domain, dtype = self.tls_db_domain(params or []) + domain, dtype, cols = self.tls_db_params(params or []) + if domain and cols: + logger.error("db_show takes no column: '%s'", list(cols)[0]) + return -1 if not domain: return -1 @@ -635,7 +645,10 @@ def do_db_delete(self, params=None, modifiers=None): """ removes a TLS domain from the database """ - domain, dtype = self.tls_db_domain(params or [], True) + domain, dtype, cols = self.tls_db_params(params or [], True) + if domain and cols: + logger.error("db_delete takes no column: '%s'", list(cols)[0]) + return -1 if not domain: return -1