diff --git a/RNS/Utilities/rngit/client.py b/RNS/Utilities/rngit/client.py index 0c911e96..1cf8f439 100644 --- a/RNS/Utilities/rngit/client.py +++ b/RNS/Utilities/rngit/client.py @@ -101,6 +101,7 @@ class ReticulumGitClient(): self.config = None self.ready = False + self.destination_aliases = {} self.remote_identity = None self.destination = None self.link = None @@ -170,6 +171,18 @@ class ReticulumGitClient(): section = self.config["client"] if "ref_batch_size" in section: self.ref_batch_size = max(0, min(1024, section.as_int("ref_batch_size"))) + if "aliases" in self.config: + section = self.config["aliases"] + for alias in section: + alias_hexhash = section[alias] + len_ok = len(alias_hexhash) == RNS.Identity.TRUNCATED_HASHLENGTH//8*2 + try: alias_hash = bytes.fromhex(alias_hexhash) + except: alias_hash = None + alias_exists = alias in self.destination_aliases + if not len_ok or not alias_hash: continue + if alias_exists: continue + self.destination_aliases[alias] = RNS.hexrep(alias_hash, delimit=False) + if not os.path.isfile(self.identitypath): identity = RNS.Identity() identity.to_file(self.identitypath) @@ -185,6 +198,19 @@ class ReticulumGitClient(): else: self.identity = identity + self.destination_hexhash = self.__resolve_destination_alias(self.destination_hexhash) + + def __resolve_destination_alias(self, alias): + def resolve(alias): + len_match = len(alias) == RNS.Identity.TRUNCATED_HASHLENGTH//8*2 + try: hash_bytes = bytes.fromhex(alias) + except: hash_bytes = None + if len_match and hash_bytes: return alias + else: return self.destination_aliases[alias] if alias in self.destination_aliases else alias + + resolved = resolve(alias) + return resolved + def abort(self, reason=None, code=255): if not reason: reason = "Unknown reason" print(f"git-remote-rns failed: {reason}", file=sys.stderr) @@ -656,6 +682,21 @@ __default_rngit_config__ = '''# This is the default rngit client config file. ref_batch_size = 25 + +[aliases] + +# You can define aliases for commonly used destination +# hashes in this section. Each line must be in the format +# aliased_name = DESTINATION_HASH +# +# These hashes are used for resolving remote destinations. +# For rngit node permissions and identity resolution, +# aliases must be defined in ~/.rngit/config. + +# my_node = 063d38912bffc850af4a1b8a270a9d85 +# bobs_node = 714981d03e41deda0e4468cb274414cc + + [logging] # Valid log levels are 0 through 7: # 0: Log only critical information diff --git a/RNS/Utilities/rngit/server.py b/RNS/Utilities/rngit/server.py index 57caad35..5783d7fd 100644 --- a/RNS/Utilities/rngit/server.py +++ b/RNS/Utilities/rngit/server.py @@ -48,9 +48,7 @@ from RNS.Utilities.rngit.util import san_ref, san_refs, san_sha from RNS.vendor.configobj import ConfigObj from RNS.vendor import umsgpack as mp -def program_setup(configdir, rnsconfigdir=None, verbosity=0, quietness=0, service=False, interactive=False, - print_identity=False, task=None, identity=None): - +def program_setup(configdir, rnsconfigdir=None, verbosity=0, quietness=0, service=False, interactive=False, print_identity=False, task=None, identity=None): targetverbosity = verbosity-quietness if service: @@ -275,6 +273,7 @@ class ReticulumGitClient(): self.identity = None self.userdir = os.path.expanduser("~") self.config = None + self.destination_aliases = {} self.verbosity = verbosity or 0 self.path_timeout = self.PATH_TIMEOUT self.link_timeout = self.LINK_TIMEOUT @@ -320,15 +319,66 @@ class ReticulumGitClient(): if not identity: self.abort("Could not initialize client identity") else: self.identity = identity + if os.path.isfile(self.configpath): + try: self.config = ConfigObj(self.configpath) + except Exception as e: + RNS.log("Could not parse the configuration at "+self.configpath, RNS.LOG_ERROR) + RNS.log("Check your configuration file for errors!", RNS.LOG_ERROR) + RNS.panic() + else: + RNS.log("Could not load config file, creating default configuration file...") + self.__create_default_config() + RNS.log("Default config file created. Make any necessary changes in "+self.configdir+"/config and restart rngit.") + RNS.log("Exiting now") + exit(1) + + self.__apply_config() + + def __create_default_config(self): + from RNS.Utilities.rngit.client import __default_rngit_config__ as __default_rngit_client_config__ + self.config = ConfigObj(__default_rngit_client_config__) + self.config.filename = self.configpath + if not os.path.isdir(self.configdir): os.makedirs(self.configdir) + self.config.write() + + def __apply_config(self): + if "logging" in self.config: + section = self.config["logging"] + if "loglevel" in section: RNS.loglevel = max(RNS.LOG_NONE, min(RNS.LOG_EXTREME, section.as_int("loglevel"))) + + if "aliases" in self.config: + section = self.config["aliases"] + for alias in section: + alias_hexhash = section[alias] + len_ok = len(alias_hexhash) == RNS.Identity.TRUNCATED_HASHLENGTH//8*2 + try: alias_hash = bytes.fromhex(alias_hexhash) + except: alias_hash = None + alias_exists = alias in self.destination_aliases + if not len_ok or not alias_hash: continue + if alias_exists: continue + self.destination_aliases[alias] = RNS.hexrep(alias_hash, delimit=False) + + def __resolve_destination_alias(self, alias): + def resolve(alias): + len_match = len(alias) == RNS.Identity.TRUNCATED_HASHLENGTH//8*2 + try: hash_bytes = bytes.fromhex(alias) + except: hash_bytes = None + if len_match and hash_bytes: return alias + else: return self.destination_aliases[alias] if alias in self.destination_aliases else alias + + resolved = resolve(alias) + return resolved + def abort(self, msg): print(msg); exit(1) def parse_remote_url(self, remote): if not remote.lower().startswith(self.PROTO_SPEC): self.abort("Invalid protocol in remote URL") components = remote[len(self.PROTO_SPEC):].split("/") + destination_hexhash = self.__resolve_destination_alias(components[0]) if not len(components) == 3: self.abort("Invalid number of URL components") - if not len(components[0]) == RNS.Identity.TRUNCATED_HASHLENGTH//8*2: self.abort("Invalid destination hash length") - try: destination_hash = bytes.fromhex(components[0]) + if not len(destination_hexhash) == RNS.Identity.TRUNCATED_HASHLENGTH//8*2: self.abort("Invalid destination hash length") + try: destination_hash = bytes.fromhex(destination_hexhash) except Exception as e: self.abort(f"Invalid destination hash: {e}") return destination_hash, components[1], components[2] @@ -472,7 +522,16 @@ class ReticulumGitClient(): if not target: print(f"No target specified"); exit(1) self._remote_clone_operation(source, target, self.PATH_MIRROR, "mirror") + def _resolve_aliased_url(self, url): + if url.lower().startswith("rns://"): + destination_hash, group, repo = self.parse_remote_url(url) + if not destination_hash or not group or not repo: self.abort("Invalid source URL") + url = f"rns://{RNS.hexrep(destination_hash, delimit=False)}/{group}/{repo}" + + return url + def _remote_clone_operation(self, source, target, path, operation_name): + source = self._resolve_aliased_url(source) self.connect_remote(target) timeout = self.link_timeout @@ -1510,6 +1569,7 @@ class ReticulumGitNode(): TGT_ALL = 0x02 TGT_NONE_SMPHR = ["n", "none", "nobody"] TGT_ALL_SMPHR = ["a", "all", "everyone"] + ALL_TGTS = TGT_NONE_SMPHR+TGT_ALL_SMPHR PATH_LIST = "/git/list" PATH_FETCH = "/git/fetch" @@ -1533,10 +1593,13 @@ class ReticulumGitNode(): WORK_DOC_LIMIT = 256*1024 + CLONE_PROTOS = ["rns", "http", "https", "ssh"] + def __init__(self, configdir=None, verbosity=None, print_identity=False): self.identity = None self.userdir = os.path.expanduser("~") self.global_allow = RNS.Destination.ALLOW_ALL + self.identity_aliases = {} self.groups = {} self.active_links = {} self.page_servers = {} @@ -1728,6 +1791,20 @@ class ReticulumGitNode(): else: self.identity = identity + if "aliases" in self.config: + section = self.config["aliases"] + for alias in section: + alias_hexhash = section[alias] + name_ok = not alias in self.ALL_TGTS + len_ok = len(alias_hexhash) == RNS.Identity.TRUNCATED_HASHLENGTH//8*2 + try: alias_hash = bytes.fromhex(alias_hexhash) + except: alias_hash = None + alias_exists = alias in self.identity_aliases + if not len_ok or not alias_hash: RNS.log(f"Invalid identity hash for alias {alias} in configuration file, ignoring", RNS.LOG_WARNING); continue + if not name_ok: RNS.log(f"Invalid alias {alias} in configuration file, ignoring", RNS.LOG_WARNING); continue + if alias_exists: RNS.log(f"Duplicate alias {alias} in configuration file, ignoring", RNS.LOG_WARNING); continue + self.identity_aliases[alias] = RNS.hexrep(alias_hash, delimit=False) + if "rngit" in self.config: section = self.config["rngit"] if "node_name" in section: self.node_name = section["node_name"] @@ -1737,6 +1814,7 @@ class ReticulumGitNode(): if "stats_ignore_identities" in section: ignored = section.as_list("stats_ignore_identities") for identhexhash in ignored: + identhexhash = self.__resolve_identity_alias(identhexhash) if not len(identhexhash) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2: continue else: try: self.stats_ignored[bytes.fromhex(identhexhash)] = True @@ -1758,11 +1836,25 @@ class ReticulumGitNode(): if not os.path.isdir(group_path): RNS.log(f"The path \"{group_path}\" specified for repository group \"{group_name}\" does not exist, skipping.", RNS.LOG_ERROR) else: self.load_repository_group(group_name, group_path) + def __resolve_identity_alias(self, alias): + def resolve(alias): + if alias.lower() in self.ALL_TGTS: return alias + len_match = len(alias) == RNS.Identity.TRUNCATED_HASHLENGTH//8*2 + try: hash_bytes = bytes.fromhex(alias) + except: hash_bytes = None + if len_match and hash_bytes: return alias + else: return self.identity_aliases[alias] if alias in self.identity_aliases else alias + + resolved = resolve(alias) + return resolved + def parse_permission(self, permission_string): comps = permission_string.split(":") if not len(comps) == 2: return None, None else: perm = comps[0].lower(); target = comps[1] + target = self.__resolve_identity_alias(target) + if perm in self.PERM_R_SMPHR: perm = self.PERM_READ elif perm in self.PERM_W_SMPHR: perm = self.PERM_WRITE elif perm in self.PERM_RW_SMPHR: perm = self.PERM_READWRITE @@ -2621,6 +2713,8 @@ class ReticulumGitNode(): source_url = data.get("source", "") if not source_url: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No source specified" + if not type(source_url) == str: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid source URL" + if not source_url.lower().split("://")[0] in self.CLONE_PROTOS: return self.RES_DISALLOWED.to_bytes(1, "big") + b"Prohibited source URL" group_name, repository_name = self.parse_request_repository_path(data[self.IDX_REPOSITORY]) if not group_name or not repository_name: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid request" @@ -3747,7 +3841,7 @@ class ReticulumGitNode(): if not stripped or stripped.startswith("#"): continue perm, target = self.parse_permission(stripped) - if perm is None or target is None: + if not perm or not target: valid = False error_line = line_num invalid_perm = stripped @@ -4088,6 +4182,19 @@ showcase = /another/path/to/directory/with/git/repositories # mirror_interval = 24 +[aliases] + +# You can define aliases for commonly used identity hashes +# in this section. Each line must be in the format +# aliased_name = IDENTITY_HASH +# +# These hashes are used for the permissions system and +# identity resolution. For rngit CLI client operations, +# aliases must be defined in ~/.rngit/client_config. + +# alice = d09285e660cfe27cee6d9a0beb58b7e0 +# bob = ffcffb4e255e156e77f79b82c13086a6 + [access] # You can apply permissions for all repositories within