| 1 | """ |
|---|
| 2 | This module contains classes and functions to implement and manage |
|---|
| 3 | a node for Tahoe-LAFS. |
|---|
| 4 | |
|---|
| 5 | Ported to Python 3. |
|---|
| 6 | """ |
|---|
| 7 | from __future__ import annotations |
|---|
| 8 | |
|---|
| 9 | from six import ensure_str, ensure_text |
|---|
| 10 | |
|---|
| 11 | import json |
|---|
| 12 | import datetime |
|---|
| 13 | import os.path |
|---|
| 14 | import re |
|---|
| 15 | import types |
|---|
| 16 | import errno |
|---|
| 17 | from base64 import b32decode, b32encode |
|---|
| 18 | from errno import ENOENT, EPERM |
|---|
| 19 | from warnings import warn |
|---|
| 20 | from typing import Union, Iterable |
|---|
| 21 | |
|---|
| 22 | import attr |
|---|
| 23 | |
|---|
| 24 | # On Python 2 this will be the backported package. |
|---|
| 25 | import configparser |
|---|
| 26 | |
|---|
| 27 | from twisted.python.filepath import ( |
|---|
| 28 | FilePath, |
|---|
| 29 | ) |
|---|
| 30 | from twisted.python import log as twlog |
|---|
| 31 | from twisted.application import service |
|---|
| 32 | from twisted.python.failure import Failure |
|---|
| 33 | from foolscap.api import Tub |
|---|
| 34 | |
|---|
| 35 | import foolscap.logging.log |
|---|
| 36 | |
|---|
| 37 | from allmydata.util import log |
|---|
| 38 | from allmydata.util import fileutil, iputil |
|---|
| 39 | from allmydata.util.fileutil import abspath_expanduser_unicode |
|---|
| 40 | from allmydata.util.encodingutil import get_filesystem_encoding, quote_output |
|---|
| 41 | from allmydata.util import configutil |
|---|
| 42 | from allmydata.util.yamlutil import ( |
|---|
| 43 | safe_load, |
|---|
| 44 | ) |
|---|
| 45 | |
|---|
| 46 | from . import ( |
|---|
| 47 | __full_version__, |
|---|
| 48 | ) |
|---|
| 49 | from .protocol_switch import create_tub_with_https_support |
|---|
| 50 | |
|---|
| 51 | |
|---|
| 52 | def _common_valid_config(): |
|---|
| 53 | return configutil.ValidConfiguration({ |
|---|
| 54 | "connections": ( |
|---|
| 55 | "tcp", |
|---|
| 56 | ), |
|---|
| 57 | "node": ( |
|---|
| 58 | "log_gatherer.furl", |
|---|
| 59 | "nickname", |
|---|
| 60 | "reveal-ip-address", |
|---|
| 61 | "tempdir", |
|---|
| 62 | "timeout.disconnect", |
|---|
| 63 | "timeout.keepalive", |
|---|
| 64 | "tub.location", |
|---|
| 65 | "tub.port", |
|---|
| 66 | "web.port", |
|---|
| 67 | "web.static", |
|---|
| 68 | ), |
|---|
| 69 | "i2p": ( |
|---|
| 70 | "enabled", |
|---|
| 71 | "i2p.configdir", |
|---|
| 72 | "i2p.executable", |
|---|
| 73 | "launch", |
|---|
| 74 | "sam.port", |
|---|
| 75 | "dest", |
|---|
| 76 | "dest.port", |
|---|
| 77 | "dest.private_key_file", |
|---|
| 78 | ), |
|---|
| 79 | "tor": ( |
|---|
| 80 | "control.port", |
|---|
| 81 | "enabled", |
|---|
| 82 | "launch", |
|---|
| 83 | "socks.port", |
|---|
| 84 | "tor.executable", |
|---|
| 85 | "onion", |
|---|
| 86 | "onion.local_port", |
|---|
| 87 | "onion.external_port", |
|---|
| 88 | "onion.private_key_file", |
|---|
| 89 | ), |
|---|
| 90 | }) |
|---|
| 91 | |
|---|
| 92 | # group 1 will be addr (dotted quad string), group 3 if any will be portnum (string) |
|---|
| 93 | ADDR_RE = re.compile("^([1-9][0-9]*\.[1-9][0-9]*\.[1-9][0-9]*\.[1-9][0-9]*)(:([1-9][0-9]*))?$") |
|---|
| 94 | |
|---|
| 95 | # this is put into README in new node-directories (for client and introducers) |
|---|
| 96 | PRIV_README = """ |
|---|
| 97 | This directory contains files which contain private data for the Tahoe node, |
|---|
| 98 | such as private keys. On Unix-like systems, the permissions on this directory |
|---|
| 99 | are set to disallow users other than its owner from reading the contents of |
|---|
| 100 | the files. See the 'configuration.rst' documentation file for details. |
|---|
| 101 | """ |
|---|
| 102 | |
|---|
| 103 | |
|---|
| 104 | def formatTimeTahoeStyle(self, when): |
|---|
| 105 | """ |
|---|
| 106 | Format the given (UTC) timestamp in the way Tahoe-LAFS expects it, |
|---|
| 107 | for example: 2007-10-12 00:26:28.566Z |
|---|
| 108 | |
|---|
| 109 | :param when: UTC POSIX timestamp |
|---|
| 110 | :type when: float |
|---|
| 111 | :returns: datetime.datetime |
|---|
| 112 | """ |
|---|
| 113 | d = datetime.datetime.utcfromtimestamp(when) |
|---|
| 114 | if d.microsecond: |
|---|
| 115 | return d.isoformat(" ")[:-3]+"Z" |
|---|
| 116 | return d.isoformat(" ") + ".000Z" |
|---|
| 117 | |
|---|
| 118 | PRIV_README = """ |
|---|
| 119 | This directory contains files which contain private data for the Tahoe node, |
|---|
| 120 | such as private keys. On Unix-like systems, the permissions on this directory |
|---|
| 121 | are set to disallow users other than its owner from reading the contents of |
|---|
| 122 | the files. See the 'configuration.rst' documentation file for details.""" |
|---|
| 123 | |
|---|
| 124 | class _None(object): |
|---|
| 125 | """ |
|---|
| 126 | This class is to be used as a marker in get_config() |
|---|
| 127 | """ |
|---|
| 128 | pass |
|---|
| 129 | |
|---|
| 130 | class MissingConfigEntry(Exception): |
|---|
| 131 | """ A required config entry was not found. """ |
|---|
| 132 | |
|---|
| 133 | class OldConfigError(Exception): |
|---|
| 134 | """ An obsolete config file was found. See |
|---|
| 135 | docs/historical/configuration.rst. """ |
|---|
| 136 | def __str__(self): |
|---|
| 137 | return ("Found pre-Tahoe-LAFS-v1.3 configuration file(s):\n" |
|---|
| 138 | "%s\n" |
|---|
| 139 | "See docs/historical/configuration.rst." |
|---|
| 140 | % "\n".join([quote_output(fname) for fname in self.args[0]])) |
|---|
| 141 | |
|---|
| 142 | class OldConfigOptionError(Exception): |
|---|
| 143 | """Indicate that outdated configuration options are being used.""" |
|---|
| 144 | pass |
|---|
| 145 | |
|---|
| 146 | class UnescapedHashError(Exception): |
|---|
| 147 | """Indicate that a configuration entry contains an unescaped '#' character.""" |
|---|
| 148 | def __str__(self): |
|---|
| 149 | return ("The configuration entry %s contained an unescaped '#' character." |
|---|
| 150 | % quote_output("[%s]%s = %s" % self.args)) |
|---|
| 151 | |
|---|
| 152 | class PrivacyError(Exception): |
|---|
| 153 | """reveal-IP-address = false, but the node is configured in such a way |
|---|
| 154 | that the IP address could be revealed""" |
|---|
| 155 | |
|---|
| 156 | |
|---|
| 157 | def create_node_dir(basedir, readme_text): |
|---|
| 158 | """ |
|---|
| 159 | Create new new 'node directory' at 'basedir'. This includes a |
|---|
| 160 | 'private' subdirectory. If basedir (and privdir) already exists, |
|---|
| 161 | nothing is done. |
|---|
| 162 | |
|---|
| 163 | :param readme_text: text to put in <basedir>/private/README |
|---|
| 164 | """ |
|---|
| 165 | if not os.path.exists(basedir): |
|---|
| 166 | fileutil.make_dirs(basedir) |
|---|
| 167 | privdir = os.path.join(basedir, "private") |
|---|
| 168 | if not os.path.exists(privdir): |
|---|
| 169 | fileutil.make_dirs(privdir, 0o700) |
|---|
| 170 | readme_text = ensure_text(readme_text) |
|---|
| 171 | with open(os.path.join(privdir, 'README'), 'w') as f: |
|---|
| 172 | f.write(readme_text) |
|---|
| 173 | |
|---|
| 174 | |
|---|
| 175 | def read_config(basedir, portnumfile, generated_files: Iterable = (), _valid_config=None): |
|---|
| 176 | """ |
|---|
| 177 | Read and validate configuration. |
|---|
| 178 | |
|---|
| 179 | :param unicode basedir: directory where configuration data begins |
|---|
| 180 | |
|---|
| 181 | :param unicode portnumfile: filename fragment for "port number" files |
|---|
| 182 | |
|---|
| 183 | :param list generated_files: a list of automatically-generated |
|---|
| 184 | configuration files. |
|---|
| 185 | |
|---|
| 186 | :param ValidConfiguration _valid_config: (internal use, optional) a |
|---|
| 187 | structure defining valid configuration sections and keys |
|---|
| 188 | |
|---|
| 189 | :returns: :class:`allmydata.node._Config` instance |
|---|
| 190 | """ |
|---|
| 191 | basedir = abspath_expanduser_unicode(ensure_text(basedir)) |
|---|
| 192 | if _valid_config is None: |
|---|
| 193 | _valid_config = _common_valid_config() |
|---|
| 194 | |
|---|
| 195 | # complain if there's bad stuff in the config dir |
|---|
| 196 | _error_about_old_config_files(basedir, generated_files) |
|---|
| 197 | |
|---|
| 198 | # canonicalize the portnum file |
|---|
| 199 | portnumfile = os.path.join(basedir, portnumfile) |
|---|
| 200 | |
|---|
| 201 | config_path = FilePath(basedir).child("tahoe.cfg") |
|---|
| 202 | try: |
|---|
| 203 | config_bytes = config_path.getContent() |
|---|
| 204 | except EnvironmentError as e: |
|---|
| 205 | if e.errno != errno.ENOENT: |
|---|
| 206 | raise |
|---|
| 207 | # The file is missing, just create empty ConfigParser. |
|---|
| 208 | config_str = u"" |
|---|
| 209 | else: |
|---|
| 210 | config_str = config_bytes.decode("utf-8-sig") |
|---|
| 211 | |
|---|
| 212 | return config_from_string( |
|---|
| 213 | basedir, |
|---|
| 214 | portnumfile, |
|---|
| 215 | config_str, |
|---|
| 216 | _valid_config, |
|---|
| 217 | config_path, |
|---|
| 218 | ) |
|---|
| 219 | |
|---|
| 220 | |
|---|
| 221 | def config_from_string(basedir, portnumfile, config_str, _valid_config=None, fpath=None): |
|---|
| 222 | """ |
|---|
| 223 | load and validate configuration from in-memory string |
|---|
| 224 | """ |
|---|
| 225 | if _valid_config is None: |
|---|
| 226 | _valid_config = _common_valid_config() |
|---|
| 227 | |
|---|
| 228 | if isinstance(config_str, bytes): |
|---|
| 229 | config_str = config_str.decode("utf-8") |
|---|
| 230 | |
|---|
| 231 | # load configuration from in-memory string |
|---|
| 232 | parser = configutil.get_config_from_string(config_str) |
|---|
| 233 | |
|---|
| 234 | configutil.validate_config( |
|---|
| 235 | "<string>" if fpath is None else fpath.path, |
|---|
| 236 | parser, |
|---|
| 237 | _valid_config, |
|---|
| 238 | ) |
|---|
| 239 | |
|---|
| 240 | return _Config( |
|---|
| 241 | parser, |
|---|
| 242 | portnumfile, |
|---|
| 243 | basedir, |
|---|
| 244 | fpath, |
|---|
| 245 | _valid_config, |
|---|
| 246 | ) |
|---|
| 247 | |
|---|
| 248 | |
|---|
| 249 | def _error_about_old_config_files(basedir, generated_files): |
|---|
| 250 | """ |
|---|
| 251 | If any old configuration files are detected, raise |
|---|
| 252 | OldConfigError. |
|---|
| 253 | """ |
|---|
| 254 | oldfnames = set() |
|---|
| 255 | old_names = [ |
|---|
| 256 | 'nickname', 'webport', 'keepalive_timeout', 'log_gatherer.furl', |
|---|
| 257 | 'disconnect_timeout', 'advertised_ip_addresses', 'introducer.furl', |
|---|
| 258 | 'helper.furl', 'key_generator.furl', 'stats_gatherer.furl', |
|---|
| 259 | 'no_storage', 'readonly_storage', 'sizelimit', |
|---|
| 260 | 'debug_discard_storage', 'run_helper' |
|---|
| 261 | ] |
|---|
| 262 | for fn in generated_files: |
|---|
| 263 | old_names.remove(fn) |
|---|
| 264 | for name in old_names: |
|---|
| 265 | fullfname = os.path.join(basedir, name) |
|---|
| 266 | if os.path.exists(fullfname): |
|---|
| 267 | oldfnames.add(fullfname) |
|---|
| 268 | if oldfnames: |
|---|
| 269 | e = OldConfigError(oldfnames) |
|---|
| 270 | twlog.msg(e) |
|---|
| 271 | raise e |
|---|
| 272 | |
|---|
| 273 | |
|---|
| 274 | def ensure_text_and_abspath_expanduser_unicode(basedir: Union[bytes, str]) -> str: |
|---|
| 275 | return abspath_expanduser_unicode(ensure_text(basedir)) |
|---|
| 276 | |
|---|
| 277 | |
|---|
| 278 | @attr.s |
|---|
| 279 | class _Config(object): |
|---|
| 280 | """ |
|---|
| 281 | Manages configuration of a Tahoe 'node directory'. |
|---|
| 282 | |
|---|
| 283 | Note: all this code and functionality was formerly in the Node |
|---|
| 284 | class; names and funtionality have been kept the same while moving |
|---|
| 285 | the code. It probably makes sense for several of these APIs to |
|---|
| 286 | have better names. |
|---|
| 287 | |
|---|
| 288 | :ivar ConfigParser config: The actual configuration values. |
|---|
| 289 | |
|---|
| 290 | :ivar str portnum_fname: filename to use for the port-number file (a |
|---|
| 291 | relative path inside basedir). |
|---|
| 292 | |
|---|
| 293 | :ivar str _basedir: path to our "node directory", inside which all |
|---|
| 294 | configuration is managed. |
|---|
| 295 | |
|---|
| 296 | :ivar (FilePath|NoneType) config_path: The path actually used to create |
|---|
| 297 | the configparser (might be ``None`` if using in-memory data). |
|---|
| 298 | |
|---|
| 299 | :ivar ValidConfiguration valid_config_sections: The validator for the |
|---|
| 300 | values in this configuration. |
|---|
| 301 | """ |
|---|
| 302 | config = attr.ib(validator=attr.validators.instance_of(configparser.ConfigParser)) |
|---|
| 303 | portnum_fname = attr.ib() |
|---|
| 304 | _basedir = attr.ib( |
|---|
| 305 | converter=ensure_text_and_abspath_expanduser_unicode, |
|---|
| 306 | ) # type: str |
|---|
| 307 | config_path = attr.ib( |
|---|
| 308 | validator=attr.validators.optional( |
|---|
| 309 | attr.validators.instance_of(FilePath), |
|---|
| 310 | ), |
|---|
| 311 | ) |
|---|
| 312 | valid_config_sections = attr.ib( |
|---|
| 313 | default=configutil.ValidConfiguration.everything(), |
|---|
| 314 | validator=attr.validators.instance_of(configutil.ValidConfiguration), |
|---|
| 315 | ) |
|---|
| 316 | |
|---|
| 317 | @property |
|---|
| 318 | def nickname(self): |
|---|
| 319 | nickname = self.get_config("node", "nickname", u"<unspecified>") |
|---|
| 320 | assert isinstance(nickname, str) |
|---|
| 321 | return nickname |
|---|
| 322 | |
|---|
| 323 | @property |
|---|
| 324 | def _config_fname(self): |
|---|
| 325 | if self.config_path is None: |
|---|
| 326 | return "<string>" |
|---|
| 327 | return self.config_path.path |
|---|
| 328 | |
|---|
| 329 | def write_config_file(self, name, value, mode="w"): |
|---|
| 330 | """ |
|---|
| 331 | writes the given 'value' into a file called 'name' in the config |
|---|
| 332 | directory |
|---|
| 333 | """ |
|---|
| 334 | fn = os.path.join(self._basedir, name) |
|---|
| 335 | try: |
|---|
| 336 | fileutil.write(fn, value, mode) |
|---|
| 337 | except EnvironmentError: |
|---|
| 338 | log.err( |
|---|
| 339 | Failure(), |
|---|
| 340 | "Unable to write config file '{}'".format(fn), |
|---|
| 341 | ) |
|---|
| 342 | |
|---|
| 343 | def enumerate_section(self, section): |
|---|
| 344 | """ |
|---|
| 345 | returns a dict containing all items in a configuration section. an |
|---|
| 346 | empty dict is returned if the section doesn't exist. |
|---|
| 347 | """ |
|---|
| 348 | answer = dict() |
|---|
| 349 | try: |
|---|
| 350 | for k in self.config.options(section): |
|---|
| 351 | answer[k] = self.config.get(section, k) |
|---|
| 352 | except configparser.NoSectionError: |
|---|
| 353 | pass |
|---|
| 354 | return answer |
|---|
| 355 | |
|---|
| 356 | def items(self, section, default=_None): |
|---|
| 357 | try: |
|---|
| 358 | return self.config.items(section) |
|---|
| 359 | except configparser.NoSectionError: |
|---|
| 360 | if default is _None: |
|---|
| 361 | raise |
|---|
| 362 | return default |
|---|
| 363 | |
|---|
| 364 | def get_config(self, section, option, default=_None, boolean=False): |
|---|
| 365 | try: |
|---|
| 366 | if boolean: |
|---|
| 367 | return self.config.getboolean(section, option) |
|---|
| 368 | |
|---|
| 369 | item = self.config.get(section, option) |
|---|
| 370 | if option.endswith(".furl") and '#' in item: |
|---|
| 371 | raise UnescapedHashError(section, option, item) |
|---|
| 372 | |
|---|
| 373 | return item |
|---|
| 374 | except (configparser.NoOptionError, configparser.NoSectionError): |
|---|
| 375 | if default is _None: |
|---|
| 376 | raise MissingConfigEntry( |
|---|
| 377 | "{} is missing the [{}]{} entry".format( |
|---|
| 378 | quote_output(self._config_fname), |
|---|
| 379 | section, |
|---|
| 380 | option, |
|---|
| 381 | ) |
|---|
| 382 | ) |
|---|
| 383 | return default |
|---|
| 384 | |
|---|
| 385 | def set_config(self, section, option, value): |
|---|
| 386 | """ |
|---|
| 387 | Set a config option in a section and re-write the tahoe.cfg file |
|---|
| 388 | |
|---|
| 389 | :param str section: The name of the section in which to set the |
|---|
| 390 | option. |
|---|
| 391 | |
|---|
| 392 | :param str option: The name of the option to set. |
|---|
| 393 | |
|---|
| 394 | :param str value: The value of the option. |
|---|
| 395 | |
|---|
| 396 | :raise UnescapedHashError: If the option holds a fURL and there is a |
|---|
| 397 | ``#`` in the value. |
|---|
| 398 | """ |
|---|
| 399 | if option.endswith(".furl") and "#" in value: |
|---|
| 400 | raise UnescapedHashError(section, option, value) |
|---|
| 401 | |
|---|
| 402 | copied_config = configutil.copy_config(self.config) |
|---|
| 403 | configutil.set_config(copied_config, section, option, value) |
|---|
| 404 | configutil.validate_config( |
|---|
| 405 | self._config_fname, |
|---|
| 406 | copied_config, |
|---|
| 407 | self.valid_config_sections, |
|---|
| 408 | ) |
|---|
| 409 | if self.config_path is not None: |
|---|
| 410 | configutil.write_config(self.config_path, copied_config) |
|---|
| 411 | self.config = copied_config |
|---|
| 412 | |
|---|
| 413 | def get_config_from_file(self, name, required=False): |
|---|
| 414 | """Get the (string) contents of a config file, or None if the file |
|---|
| 415 | did not exist. If required=True, raise an exception rather than |
|---|
| 416 | returning None. Any leading or trailing whitespace will be stripped |
|---|
| 417 | from the data.""" |
|---|
| 418 | fn = os.path.join(self._basedir, name) |
|---|
| 419 | try: |
|---|
| 420 | return fileutil.read(fn).strip() |
|---|
| 421 | except EnvironmentError as e: |
|---|
| 422 | if e.errno != errno.ENOENT: |
|---|
| 423 | raise # we only care about "file doesn't exist" |
|---|
| 424 | if not required: |
|---|
| 425 | return None |
|---|
| 426 | raise |
|---|
| 427 | |
|---|
| 428 | def get_or_create_private_config(self, name, default=_None): |
|---|
| 429 | """Try to get the (string) contents of a private config file (which |
|---|
| 430 | is a config file that resides within the subdirectory named |
|---|
| 431 | 'private'), and return it. Any leading or trailing whitespace will be |
|---|
| 432 | stripped from the data. |
|---|
| 433 | |
|---|
| 434 | If the file does not exist, and default is not given, report an error. |
|---|
| 435 | If the file does not exist and a default is specified, try to create |
|---|
| 436 | it using that default, and then return the value that was written. |
|---|
| 437 | If 'default' is a string, use it as a default value. If not, treat it |
|---|
| 438 | as a zero-argument callable that is expected to return a string. |
|---|
| 439 | """ |
|---|
| 440 | privname = os.path.join(self._basedir, "private", name) |
|---|
| 441 | try: |
|---|
| 442 | value = fileutil.read(privname, mode="r") |
|---|
| 443 | except EnvironmentError as e: |
|---|
| 444 | if e.errno != errno.ENOENT: |
|---|
| 445 | raise # we only care about "file doesn't exist" |
|---|
| 446 | if default is _None: |
|---|
| 447 | raise MissingConfigEntry("The required configuration file %s is missing." |
|---|
| 448 | % (quote_output(privname),)) |
|---|
| 449 | if isinstance(default, bytes): |
|---|
| 450 | default = str(default, "utf-8") |
|---|
| 451 | if isinstance(default, str): |
|---|
| 452 | value = default |
|---|
| 453 | else: |
|---|
| 454 | value = default() |
|---|
| 455 | fileutil.write(privname, value) |
|---|
| 456 | return value.strip() |
|---|
| 457 | |
|---|
| 458 | def write_private_config(self, name, value): |
|---|
| 459 | """Write the (string) contents of a private config file (which is a |
|---|
| 460 | config file that resides within the subdirectory named 'private'), and |
|---|
| 461 | return it. |
|---|
| 462 | """ |
|---|
| 463 | if isinstance(value, str): |
|---|
| 464 | value = value.encode("utf-8") |
|---|
| 465 | privname = os.path.join(self._basedir, "private", name) |
|---|
| 466 | with open(privname, "wb") as f: |
|---|
| 467 | f.write(value) |
|---|
| 468 | |
|---|
| 469 | def get_private_config(self, name, default=_None): |
|---|
| 470 | """Read the (native string) contents of a private config file (a |
|---|
| 471 | config file that resides within the subdirectory named 'private'), |
|---|
| 472 | and return it. Return a default, or raise an error if one was not |
|---|
| 473 | given. |
|---|
| 474 | """ |
|---|
| 475 | privname = os.path.join(self._basedir, "private", name) |
|---|
| 476 | try: |
|---|
| 477 | return fileutil.read(privname, mode="r").strip() |
|---|
| 478 | except EnvironmentError as e: |
|---|
| 479 | if e.errno != errno.ENOENT: |
|---|
| 480 | raise # we only care about "file doesn't exist" |
|---|
| 481 | if default is _None: |
|---|
| 482 | raise MissingConfigEntry("The required configuration file %s is missing." |
|---|
| 483 | % (quote_output(privname),)) |
|---|
| 484 | return default |
|---|
| 485 | |
|---|
| 486 | def get_private_path(self, *args): |
|---|
| 487 | """ |
|---|
| 488 | returns an absolute path inside the 'private' directory with any |
|---|
| 489 | extra args join()-ed |
|---|
| 490 | |
|---|
| 491 | This exists for historical reasons. New code should ideally |
|---|
| 492 | not call this because it makes it harder for e.g. a SQL-based |
|---|
| 493 | _Config object to exist. Code that needs to call this method |
|---|
| 494 | should probably be a _Config method itself. See |
|---|
| 495 | e.g. get_grid_manager_certificates() |
|---|
| 496 | """ |
|---|
| 497 | return os.path.join(self._basedir, "private", *args) |
|---|
| 498 | |
|---|
| 499 | def get_config_path(self, *args): |
|---|
| 500 | """ |
|---|
| 501 | returns an absolute path inside the config directory with any |
|---|
| 502 | extra args join()-ed |
|---|
| 503 | |
|---|
| 504 | This exists for historical reasons. New code should ideally |
|---|
| 505 | not call this because it makes it harder for e.g. a SQL-based |
|---|
| 506 | _Config object to exist. Code that needs to call this method |
|---|
| 507 | should probably be a _Config method itself. See |
|---|
| 508 | e.g. get_grid_manager_certificates() |
|---|
| 509 | """ |
|---|
| 510 | # note: we re-expand here (_basedir already went through this |
|---|
| 511 | # expanduser function) in case the path we're being asked for |
|---|
| 512 | # has embedded ".."'s in it |
|---|
| 513 | return abspath_expanduser_unicode( |
|---|
| 514 | os.path.join(self._basedir, *args) |
|---|
| 515 | ) |
|---|
| 516 | |
|---|
| 517 | def get_grid_manager_certificates(self): |
|---|
| 518 | """ |
|---|
| 519 | Load all Grid Manager certificates in the config. |
|---|
| 520 | |
|---|
| 521 | :returns: A list of all certificates. An empty list is |
|---|
| 522 | returned if there are none. |
|---|
| 523 | """ |
|---|
| 524 | grid_manager_certificates = [] |
|---|
| 525 | |
|---|
| 526 | cert_fnames = list(self.enumerate_section("grid_manager_certificates").values()) |
|---|
| 527 | for fname in cert_fnames: |
|---|
| 528 | fname = self.get_config_path(fname) |
|---|
| 529 | if not os.path.exists(fname): |
|---|
| 530 | raise ValueError( |
|---|
| 531 | "Grid Manager certificate file '{}' doesn't exist".format( |
|---|
| 532 | fname |
|---|
| 533 | ) |
|---|
| 534 | ) |
|---|
| 535 | with open(fname, 'r') as f: |
|---|
| 536 | cert = json.load(f) |
|---|
| 537 | if set(cert.keys()) != {"certificate", "signature"}: |
|---|
| 538 | raise ValueError( |
|---|
| 539 | "Unknown key in Grid Manager certificate '{}'".format( |
|---|
| 540 | fname |
|---|
| 541 | ) |
|---|
| 542 | ) |
|---|
| 543 | grid_manager_certificates.append(cert) |
|---|
| 544 | return grid_manager_certificates |
|---|
| 545 | |
|---|
| 546 | def get_introducer_configuration(self): |
|---|
| 547 | """ |
|---|
| 548 | Get configuration for introducers. |
|---|
| 549 | |
|---|
| 550 | :return {unicode: (unicode, FilePath)}: A mapping from introducer |
|---|
| 551 | petname to a tuple of the introducer's fURL and local cache path. |
|---|
| 552 | """ |
|---|
| 553 | introducers_yaml_filename = self.get_private_path("introducers.yaml") |
|---|
| 554 | introducers_filepath = FilePath(introducers_yaml_filename) |
|---|
| 555 | |
|---|
| 556 | def get_cache_filepath(petname): |
|---|
| 557 | return FilePath( |
|---|
| 558 | self.get_private_path("introducer_{}_cache.yaml".format(petname)), |
|---|
| 559 | ) |
|---|
| 560 | |
|---|
| 561 | try: |
|---|
| 562 | with introducers_filepath.open() as f: |
|---|
| 563 | introducers_yaml = safe_load(f) |
|---|
| 564 | if introducers_yaml is None: |
|---|
| 565 | raise EnvironmentError( |
|---|
| 566 | EPERM, |
|---|
| 567 | "Can't read '{}'".format(introducers_yaml_filename), |
|---|
| 568 | introducers_yaml_filename, |
|---|
| 569 | ) |
|---|
| 570 | introducers = { |
|---|
| 571 | petname: config["furl"] |
|---|
| 572 | for petname, config |
|---|
| 573 | in introducers_yaml.get("introducers", {}).items() |
|---|
| 574 | } |
|---|
| 575 | non_strs = list( |
|---|
| 576 | k |
|---|
| 577 | for k |
|---|
| 578 | in introducers.keys() |
|---|
| 579 | if not isinstance(k, str) |
|---|
| 580 | ) |
|---|
| 581 | if non_strs: |
|---|
| 582 | raise TypeError( |
|---|
| 583 | "Introducer petnames {!r} should have been str".format( |
|---|
| 584 | non_strs, |
|---|
| 585 | ), |
|---|
| 586 | ) |
|---|
| 587 | non_strs = list( |
|---|
| 588 | v |
|---|
| 589 | for v |
|---|
| 590 | in introducers.values() |
|---|
| 591 | if not isinstance(v, str) |
|---|
| 592 | ) |
|---|
| 593 | if non_strs: |
|---|
| 594 | raise TypeError( |
|---|
| 595 | "Introducer fURLs {!r} should have been str".format( |
|---|
| 596 | non_strs, |
|---|
| 597 | ), |
|---|
| 598 | ) |
|---|
| 599 | log.msg( |
|---|
| 600 | "found {} introducers in {!r}".format( |
|---|
| 601 | len(introducers), |
|---|
| 602 | introducers_yaml_filename, |
|---|
| 603 | ) |
|---|
| 604 | ) |
|---|
| 605 | except EnvironmentError as e: |
|---|
| 606 | if e.errno != ENOENT: |
|---|
| 607 | raise |
|---|
| 608 | introducers = {} |
|---|
| 609 | |
|---|
| 610 | # supported the deprecated [client]introducer.furl item in tahoe.cfg |
|---|
| 611 | tahoe_cfg_introducer_furl = self.get_config("client", "introducer.furl", None) |
|---|
| 612 | if tahoe_cfg_introducer_furl == "None": |
|---|
| 613 | raise ValueError( |
|---|
| 614 | "tahoe.cfg has invalid 'introducer.furl = None':" |
|---|
| 615 | " to disable it omit the key entirely" |
|---|
| 616 | ) |
|---|
| 617 | if tahoe_cfg_introducer_furl: |
|---|
| 618 | warn( |
|---|
| 619 | "tahoe.cfg [client]introducer.furl is deprecated; " |
|---|
| 620 | "use private/introducers.yaml instead.", |
|---|
| 621 | category=DeprecationWarning, |
|---|
| 622 | stacklevel=-1, |
|---|
| 623 | ) |
|---|
| 624 | if "default" in introducers: |
|---|
| 625 | raise ValueError( |
|---|
| 626 | "'default' introducer furl cannot be specified in tahoe.cfg and introducers.yaml;" |
|---|
| 627 | " please fix impossible configuration." |
|---|
| 628 | ) |
|---|
| 629 | introducers['default'] = tahoe_cfg_introducer_furl |
|---|
| 630 | |
|---|
| 631 | return { |
|---|
| 632 | petname: (furl, get_cache_filepath(petname)) |
|---|
| 633 | for (petname, furl) |
|---|
| 634 | in introducers.items() |
|---|
| 635 | } |
|---|
| 636 | |
|---|
| 637 | |
|---|
| 638 | def create_tub_options(config): |
|---|
| 639 | """ |
|---|
| 640 | :param config: a _Config instance |
|---|
| 641 | |
|---|
| 642 | :returns: dict containing all Foolscap Tub-related options, |
|---|
| 643 | overriding defaults with appropriate config from `config` |
|---|
| 644 | instance. |
|---|
| 645 | """ |
|---|
| 646 | # We can't unify the camelCase vs. dashed-name divide here, |
|---|
| 647 | # because these are options for Foolscap |
|---|
| 648 | tub_options = { |
|---|
| 649 | "logLocalFailures": True, |
|---|
| 650 | "logRemoteFailures": True, |
|---|
| 651 | "expose-remote-exception-types": False, |
|---|
| 652 | "accept-gifts": False, |
|---|
| 653 | } |
|---|
| 654 | |
|---|
| 655 | # see #521 for a discussion of how to pick these timeout values. |
|---|
| 656 | keepalive_timeout_s = config.get_config("node", "timeout.keepalive", "") |
|---|
| 657 | if keepalive_timeout_s: |
|---|
| 658 | tub_options["keepaliveTimeout"] = int(keepalive_timeout_s) |
|---|
| 659 | disconnect_timeout_s = config.get_config("node", "timeout.disconnect", "") |
|---|
| 660 | if disconnect_timeout_s: |
|---|
| 661 | # N.B.: this is in seconds, so use "1800" to get 30min |
|---|
| 662 | tub_options["disconnectTimeout"] = int(disconnect_timeout_s) |
|---|
| 663 | return tub_options |
|---|
| 664 | |
|---|
| 665 | |
|---|
| 666 | def _make_tcp_handler(): |
|---|
| 667 | """ |
|---|
| 668 | :returns: a Foolscap default TCP handler |
|---|
| 669 | """ |
|---|
| 670 | # this is always available |
|---|
| 671 | from foolscap.connections.tcp import default |
|---|
| 672 | return default() |
|---|
| 673 | |
|---|
| 674 | |
|---|
| 675 | def create_default_connection_handlers(config, handlers): |
|---|
| 676 | """ |
|---|
| 677 | :return: A dictionary giving the default connection handlers. The keys |
|---|
| 678 | are strings like "tcp" and the values are strings like "tor" or |
|---|
| 679 | ``None``. |
|---|
| 680 | """ |
|---|
| 681 | reveal_ip = config.get_config("node", "reveal-IP-address", True, boolean=True) |
|---|
| 682 | |
|---|
| 683 | # Remember the default mappings from tahoe.cfg |
|---|
| 684 | default_connection_handlers = { |
|---|
| 685 | name: name |
|---|
| 686 | for name |
|---|
| 687 | in handlers |
|---|
| 688 | } |
|---|
| 689 | tcp_handler_name = config.get_config("connections", "tcp", "tcp").lower() |
|---|
| 690 | if tcp_handler_name == "disabled": |
|---|
| 691 | default_connection_handlers["tcp"] = None |
|---|
| 692 | else: |
|---|
| 693 | if tcp_handler_name not in handlers: |
|---|
| 694 | raise ValueError( |
|---|
| 695 | "'tahoe.cfg [connections] tcp=' uses " |
|---|
| 696 | "unknown handler type '{}'".format( |
|---|
| 697 | tcp_handler_name |
|---|
| 698 | ) |
|---|
| 699 | ) |
|---|
| 700 | if not handlers[tcp_handler_name]: |
|---|
| 701 | raise ValueError( |
|---|
| 702 | "'tahoe.cfg [connections] tcp=' uses " |
|---|
| 703 | "unavailable/unimportable handler type '{}'. " |
|---|
| 704 | "Please pip install tahoe-lafs[{}] to fix.".format( |
|---|
| 705 | tcp_handler_name, |
|---|
| 706 | tcp_handler_name, |
|---|
| 707 | ) |
|---|
| 708 | ) |
|---|
| 709 | default_connection_handlers["tcp"] = tcp_handler_name |
|---|
| 710 | |
|---|
| 711 | if not reveal_ip: |
|---|
| 712 | if default_connection_handlers.get("tcp") == "tcp": |
|---|
| 713 | raise PrivacyError( |
|---|
| 714 | "Privacy requested with `reveal-IP-address = false` " |
|---|
| 715 | "but `tcp = tcp` conflicts with this.", |
|---|
| 716 | ) |
|---|
| 717 | return default_connection_handlers |
|---|
| 718 | |
|---|
| 719 | |
|---|
| 720 | def create_connection_handlers(config, i2p_provider, tor_provider): |
|---|
| 721 | """ |
|---|
| 722 | :returns: 2-tuple of default_connection_handlers, foolscap_connection_handlers |
|---|
| 723 | """ |
|---|
| 724 | # We store handlers for everything. None means we were unable to |
|---|
| 725 | # create that handler, so hints which want it will be ignored. |
|---|
| 726 | handlers = { |
|---|
| 727 | "tcp": _make_tcp_handler(), |
|---|
| 728 | "tor": tor_provider.get_client_endpoint(), |
|---|
| 729 | "i2p": i2p_provider.get_client_endpoint(), |
|---|
| 730 | } |
|---|
| 731 | log.msg( |
|---|
| 732 | format="built Foolscap connection handlers for: %(known_handlers)s", |
|---|
| 733 | known_handlers=sorted([k for k,v in handlers.items() if v]), |
|---|
| 734 | facility="tahoe.node", |
|---|
| 735 | umid="PuLh8g", |
|---|
| 736 | ) |
|---|
| 737 | return create_default_connection_handlers( |
|---|
| 738 | config, |
|---|
| 739 | handlers, |
|---|
| 740 | ), handlers |
|---|
| 741 | |
|---|
| 742 | |
|---|
| 743 | def create_tub(tub_options, default_connection_handlers, foolscap_connection_handlers, |
|---|
| 744 | handler_overrides=None, force_foolscap=False, **kwargs): |
|---|
| 745 | """ |
|---|
| 746 | Create a Tub with the right options and handlers. It will be |
|---|
| 747 | ephemeral unless the caller provides certFile= in kwargs |
|---|
| 748 | |
|---|
| 749 | :param handler_overrides: anything in this will override anything |
|---|
| 750 | in `default_connection_handlers` for just this call. |
|---|
| 751 | |
|---|
| 752 | :param dict tub_options: every key-value pair in here will be set in |
|---|
| 753 | the new Tub via `Tub.setOption` |
|---|
| 754 | |
|---|
| 755 | :param bool force_foolscap: If True, only allow Foolscap, not just HTTPS |
|---|
| 756 | storage protocol. |
|---|
| 757 | """ |
|---|
| 758 | if handler_overrides is None: |
|---|
| 759 | handler_overrides = {} |
|---|
| 760 | # We listen simultaneously for both Foolscap and HTTPS on the same port, |
|---|
| 761 | # so we have to create a special Foolscap Tub for that to work: |
|---|
| 762 | if force_foolscap: |
|---|
| 763 | tub = Tub(**kwargs) |
|---|
| 764 | else: |
|---|
| 765 | tub = create_tub_with_https_support(**kwargs) |
|---|
| 766 | |
|---|
| 767 | for (name, value) in list(tub_options.items()): |
|---|
| 768 | tub.setOption(name, value) |
|---|
| 769 | handlers = default_connection_handlers.copy() |
|---|
| 770 | handlers.update(handler_overrides) |
|---|
| 771 | tub.removeAllConnectionHintHandlers() |
|---|
| 772 | for hint_type, handler_name in list(handlers.items()): |
|---|
| 773 | handler = foolscap_connection_handlers.get(handler_name) |
|---|
| 774 | if handler: |
|---|
| 775 | tub.addConnectionHintHandler(hint_type, handler) |
|---|
| 776 | return tub |
|---|
| 777 | |
|---|
| 778 | |
|---|
| 779 | def _convert_tub_port(s): |
|---|
| 780 | """ |
|---|
| 781 | :returns: a proper Twisted endpoint string like (`tcp:X`) is `s` |
|---|
| 782 | is a bare number, or returns `s` as-is |
|---|
| 783 | """ |
|---|
| 784 | us = s |
|---|
| 785 | if isinstance(s, bytes): |
|---|
| 786 | us = s.decode("utf-8") |
|---|
| 787 | if re.search(r'^\d+$', us): |
|---|
| 788 | return "tcp:{}".format(int(us)) |
|---|
| 789 | return us |
|---|
| 790 | |
|---|
| 791 | |
|---|
| 792 | class PortAssignmentRequired(Exception): |
|---|
| 793 | """ |
|---|
| 794 | A Tub port number was configured to be 0 where this is not allowed. |
|---|
| 795 | """ |
|---|
| 796 | |
|---|
| 797 | |
|---|
| 798 | def _tub_portlocation(config, get_local_addresses_sync, allocate_tcp_port): |
|---|
| 799 | """ |
|---|
| 800 | Figure out the network location of the main tub for some configuration. |
|---|
| 801 | |
|---|
| 802 | :param get_local_addresses_sync: A function like |
|---|
| 803 | ``iputil.get_local_addresses_sync``. |
|---|
| 804 | |
|---|
| 805 | :param allocate_tcp_port: A function like ``iputil.allocate_tcp_port``. |
|---|
| 806 | |
|---|
| 807 | :returns: None or tuple of (port, location) for the main tub based |
|---|
| 808 | on the given configuration. May raise ValueError or PrivacyError |
|---|
| 809 | if there are problems with the config |
|---|
| 810 | """ |
|---|
| 811 | cfg_tubport = config.get_config("node", "tub.port", None) |
|---|
| 812 | cfg_location = config.get_config("node", "tub.location", None) |
|---|
| 813 | reveal_ip = config.get_config("node", "reveal-IP-address", True, boolean=True) |
|---|
| 814 | tubport_disabled = False |
|---|
| 815 | |
|---|
| 816 | if cfg_tubport is not None: |
|---|
| 817 | cfg_tubport = cfg_tubport.strip() |
|---|
| 818 | if cfg_tubport == "": |
|---|
| 819 | raise ValueError("tub.port must not be empty") |
|---|
| 820 | if cfg_tubport == "disabled": |
|---|
| 821 | tubport_disabled = True |
|---|
| 822 | |
|---|
| 823 | location_disabled = False |
|---|
| 824 | if cfg_location is not None: |
|---|
| 825 | cfg_location = cfg_location.strip() |
|---|
| 826 | if cfg_location == "": |
|---|
| 827 | raise ValueError("tub.location must not be empty") |
|---|
| 828 | if cfg_location == "disabled": |
|---|
| 829 | location_disabled = True |
|---|
| 830 | |
|---|
| 831 | if tubport_disabled and location_disabled: |
|---|
| 832 | return None |
|---|
| 833 | if tubport_disabled and not location_disabled: |
|---|
| 834 | raise ValueError("tub.port is disabled, but not tub.location") |
|---|
| 835 | if location_disabled and not tubport_disabled: |
|---|
| 836 | raise ValueError("tub.location is disabled, but not tub.port") |
|---|
| 837 | |
|---|
| 838 | if cfg_tubport is None: |
|---|
| 839 | # For 'tub.port', tahoe.cfg overrides the individual file on |
|---|
| 840 | # disk. So only read config.portnum_fname if tahoe.cfg doesn't |
|---|
| 841 | # provide a value. |
|---|
| 842 | if os.path.exists(config.portnum_fname): |
|---|
| 843 | file_tubport = fileutil.read(config.portnum_fname).strip() |
|---|
| 844 | tubport = _convert_tub_port(file_tubport) |
|---|
| 845 | else: |
|---|
| 846 | tubport = "tcp:%d" % (allocate_tcp_port(),) |
|---|
| 847 | fileutil.write_atomically(config.portnum_fname, tubport + "\n", |
|---|
| 848 | mode="") |
|---|
| 849 | else: |
|---|
| 850 | tubport = _convert_tub_port(cfg_tubport) |
|---|
| 851 | |
|---|
| 852 | for port in tubport.split(","): |
|---|
| 853 | if port in ("0", "tcp:0", "tcp:port=0", "tcp:0:interface=127.0.0.1"): |
|---|
| 854 | raise PortAssignmentRequired() |
|---|
| 855 | |
|---|
| 856 | if cfg_location is None: |
|---|
| 857 | cfg_location = "AUTO" |
|---|
| 858 | |
|---|
| 859 | local_portnum = None # needed to hush lgtm.com static analyzer |
|---|
| 860 | # Replace the location "AUTO", if present, with the detected local |
|---|
| 861 | # addresses. Don't probe for local addresses unless necessary. |
|---|
| 862 | split_location = cfg_location.split(",") |
|---|
| 863 | if "AUTO" in split_location: |
|---|
| 864 | if not reveal_ip: |
|---|
| 865 | raise PrivacyError("tub.location uses AUTO") |
|---|
| 866 | local_addresses = get_local_addresses_sync() |
|---|
| 867 | # tubport must be like "tcp:12345" or "tcp:12345:morestuff" |
|---|
| 868 | local_portnum = int(tubport.split(":")[1]) |
|---|
| 869 | new_locations = [] |
|---|
| 870 | for loc in split_location: |
|---|
| 871 | if loc == "AUTO": |
|---|
| 872 | new_locations.extend(["tcp:%s:%d" % (ip, local_portnum) |
|---|
| 873 | for ip in local_addresses]) |
|---|
| 874 | else: |
|---|
| 875 | if not reveal_ip: |
|---|
| 876 | # Legacy hints are "host:port". We use Foolscap's utility |
|---|
| 877 | # function to convert all hints into the modern format |
|---|
| 878 | # ("tcp:host:port") because that's what the receiving |
|---|
| 879 | # client will probably do. We test the converted hint for |
|---|
| 880 | # TCP-ness, but publish the original hint because that |
|---|
| 881 | # was the user's intent. |
|---|
| 882 | from foolscap.connections.tcp import convert_legacy_hint |
|---|
| 883 | converted_hint = convert_legacy_hint(loc) |
|---|
| 884 | hint_type = converted_hint.split(":")[0] |
|---|
| 885 | if hint_type == "tcp": |
|---|
| 886 | raise PrivacyError("tub.location includes tcp: hint") |
|---|
| 887 | new_locations.append(loc) |
|---|
| 888 | location = ",".join(new_locations) |
|---|
| 889 | |
|---|
| 890 | # Lacking this, Python 2 blows up in Foolscap when it is confused by a |
|---|
| 891 | # Unicode FURL. |
|---|
| 892 | location = location.encode("utf-8") |
|---|
| 893 | |
|---|
| 894 | return tubport, location |
|---|
| 895 | |
|---|
| 896 | |
|---|
| 897 | def tub_listen_on(i2p_provider, tor_provider, tub, tubport, location): |
|---|
| 898 | """ |
|---|
| 899 | Assign a Tub its listener locations. |
|---|
| 900 | |
|---|
| 901 | :param i2p_provider: See ``allmydata.util.i2p_provider.create``. |
|---|
| 902 | :param tor_provider: See ``allmydata.util.tor_provider.create``. |
|---|
| 903 | """ |
|---|
| 904 | for port in tubport.split(","): |
|---|
| 905 | if port == "listen:i2p": |
|---|
| 906 | # the I2P provider will read its section of tahoe.cfg and |
|---|
| 907 | # return either a fully-formed Endpoint, or a descriptor |
|---|
| 908 | # that will create one, so we don't have to stuff all the |
|---|
| 909 | # options into the tub.port string (which would need a lot |
|---|
| 910 | # of escaping) |
|---|
| 911 | port_or_endpoint = i2p_provider.get_listener() |
|---|
| 912 | elif port == "listen:tor": |
|---|
| 913 | port_or_endpoint = tor_provider.get_listener() |
|---|
| 914 | else: |
|---|
| 915 | port_or_endpoint = port |
|---|
| 916 | # Foolscap requires native strings: |
|---|
| 917 | if isinstance(port_or_endpoint, (bytes, str)): |
|---|
| 918 | port_or_endpoint = ensure_str(port_or_endpoint) |
|---|
| 919 | tub.listenOn(port_or_endpoint) |
|---|
| 920 | # This last step makes the Tub is ready for tub.registerReference() |
|---|
| 921 | tub.setLocation(location) |
|---|
| 922 | |
|---|
| 923 | |
|---|
| 924 | def create_main_tub(config, tub_options, |
|---|
| 925 | default_connection_handlers, foolscap_connection_handlers, |
|---|
| 926 | i2p_provider, tor_provider, |
|---|
| 927 | handler_overrides=None, cert_filename="node.pem"): |
|---|
| 928 | """ |
|---|
| 929 | Creates a 'main' Foolscap Tub, typically for use as the top-level |
|---|
| 930 | access point for a running Node. |
|---|
| 931 | |
|---|
| 932 | :param Config: a `_Config` instance |
|---|
| 933 | |
|---|
| 934 | :param dict tub_options: any options to change in the tub |
|---|
| 935 | |
|---|
| 936 | :param default_connection_handlers: default Foolscap connection |
|---|
| 937 | handlers |
|---|
| 938 | |
|---|
| 939 | :param foolscap_connection_handlers: Foolscap connection |
|---|
| 940 | handlers for this tub |
|---|
| 941 | |
|---|
| 942 | :param i2p_provider: None, or a _Provider instance if I2P is |
|---|
| 943 | installed. |
|---|
| 944 | |
|---|
| 945 | :param tor_provider: None, or a _Provider instance if txtorcon + |
|---|
| 946 | Tor are installed. |
|---|
| 947 | """ |
|---|
| 948 | if handler_overrides is None: |
|---|
| 949 | handler_overrides = {} |
|---|
| 950 | portlocation = _tub_portlocation( |
|---|
| 951 | config, |
|---|
| 952 | iputil.get_local_addresses_sync, |
|---|
| 953 | iputil.allocate_tcp_port, |
|---|
| 954 | ) |
|---|
| 955 | |
|---|
| 956 | # FIXME? "node.pem" was the CERTFILE option/thing |
|---|
| 957 | certfile = config.get_private_path("node.pem") |
|---|
| 958 | tub = create_tub( |
|---|
| 959 | tub_options, |
|---|
| 960 | default_connection_handlers, |
|---|
| 961 | foolscap_connection_handlers, |
|---|
| 962 | force_foolscap=config.get_config( |
|---|
| 963 | "storage", "force_foolscap", default=False, boolean=True |
|---|
| 964 | ), |
|---|
| 965 | handler_overrides=handler_overrides, |
|---|
| 966 | certFile=certfile, |
|---|
| 967 | ) |
|---|
| 968 | |
|---|
| 969 | if portlocation is None: |
|---|
| 970 | log.msg("Tub is not listening") |
|---|
| 971 | else: |
|---|
| 972 | tubport, location = portlocation |
|---|
| 973 | tub_listen_on( |
|---|
| 974 | i2p_provider, |
|---|
| 975 | tor_provider, |
|---|
| 976 | tub, |
|---|
| 977 | tubport, |
|---|
| 978 | location, |
|---|
| 979 | ) |
|---|
| 980 | log.msg("Tub location set to %r" % (location,)) |
|---|
| 981 | return tub |
|---|
| 982 | |
|---|
| 983 | |
|---|
| 984 | class Node(service.MultiService): |
|---|
| 985 | """ |
|---|
| 986 | This class implements common functionality of both Client nodes and Introducer nodes. |
|---|
| 987 | """ |
|---|
| 988 | NODETYPE = "unknown NODETYPE" |
|---|
| 989 | CERTFILE = "node.pem" |
|---|
| 990 | |
|---|
| 991 | def __init__(self, config, main_tub, i2p_provider, tor_provider): |
|---|
| 992 | """ |
|---|
| 993 | Initialize the node with the given configuration. Its base directory |
|---|
| 994 | is the current directory by default. |
|---|
| 995 | """ |
|---|
| 996 | service.MultiService.__init__(self) |
|---|
| 997 | |
|---|
| 998 | self.config = config |
|---|
| 999 | self.get_config = config.get_config # XXX stopgap |
|---|
| 1000 | self.nickname = config.nickname # XXX stopgap |
|---|
| 1001 | |
|---|
| 1002 | # this can go away once Client.init_client_storage_broker is moved into create_client() |
|---|
| 1003 | # (tests sometimes have None here) |
|---|
| 1004 | self._i2p_provider = i2p_provider |
|---|
| 1005 | self._tor_provider = tor_provider |
|---|
| 1006 | |
|---|
| 1007 | self.create_log_tub() |
|---|
| 1008 | self.logSource = "Node" |
|---|
| 1009 | self.setup_logging() |
|---|
| 1010 | |
|---|
| 1011 | self.tub = main_tub |
|---|
| 1012 | if self.tub is not None: |
|---|
| 1013 | self.nodeid = b32decode(self.tub.tubID.upper()) # binary format |
|---|
| 1014 | self.short_nodeid = b32encode(self.nodeid).lower()[:8] # for printing |
|---|
| 1015 | self.config.write_config_file("my_nodeid", b32encode(self.nodeid).lower() + b"\n", mode="wb") |
|---|
| 1016 | self.tub.setServiceParent(self) |
|---|
| 1017 | else: |
|---|
| 1018 | self.nodeid = self.short_nodeid = None |
|---|
| 1019 | |
|---|
| 1020 | self.log("Node constructed. " + __full_version__) |
|---|
| 1021 | iputil.increase_rlimits() |
|---|
| 1022 | |
|---|
| 1023 | def _is_tub_listening(self): |
|---|
| 1024 | """ |
|---|
| 1025 | :returns: True if the main tub is listening |
|---|
| 1026 | """ |
|---|
| 1027 | return len(self.tub.getListeners()) > 0 |
|---|
| 1028 | |
|---|
| 1029 | # pull this outside of Node's __init__ too, see: |
|---|
| 1030 | # https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2948 |
|---|
| 1031 | def create_log_tub(self): |
|---|
| 1032 | # The logport uses a localhost-only ephemeral Tub, with no control |
|---|
| 1033 | # over the listening port or location. This might change if we |
|---|
| 1034 | # discover a compelling reason for it in the future (e.g. being able |
|---|
| 1035 | # to use "flogtool tail" against a remote server), but for now I |
|---|
| 1036 | # think we can live without it. |
|---|
| 1037 | self.log_tub = Tub() |
|---|
| 1038 | portnum = iputil.listenOnUnused(self.log_tub) |
|---|
| 1039 | self.log("Log Tub location set to 127.0.0.1:%s" % (portnum,)) |
|---|
| 1040 | self.log_tub.setServiceParent(self) |
|---|
| 1041 | |
|---|
| 1042 | def startService(self): |
|---|
| 1043 | # Note: this class can be started and stopped at most once. |
|---|
| 1044 | self.log("Node.startService") |
|---|
| 1045 | # Record the process id in the twisted log, after startService() |
|---|
| 1046 | # (__init__ is called before fork(), but startService is called |
|---|
| 1047 | # after). Note that Foolscap logs handle pid-logging by itself, no |
|---|
| 1048 | # need to send a pid to the foolscap log here. |
|---|
| 1049 | twlog.msg("My pid: %s" % os.getpid()) |
|---|
| 1050 | try: |
|---|
| 1051 | os.chmod("twistd.pid", 0o644) |
|---|
| 1052 | except EnvironmentError: |
|---|
| 1053 | pass |
|---|
| 1054 | |
|---|
| 1055 | service.MultiService.startService(self) |
|---|
| 1056 | self.log("%s running" % self.NODETYPE) |
|---|
| 1057 | twlog.msg("%s running" % self.NODETYPE) |
|---|
| 1058 | |
|---|
| 1059 | def stopService(self): |
|---|
| 1060 | self.log("Node.stopService") |
|---|
| 1061 | return service.MultiService.stopService(self) |
|---|
| 1062 | |
|---|
| 1063 | def shutdown(self): |
|---|
| 1064 | """Shut down the node. Returns a Deferred that fires (with None) when |
|---|
| 1065 | it finally stops kicking.""" |
|---|
| 1066 | self.log("Node.shutdown") |
|---|
| 1067 | return self.stopService() |
|---|
| 1068 | |
|---|
| 1069 | def setup_logging(self): |
|---|
| 1070 | # we replace the formatTime() method of the log observer that |
|---|
| 1071 | # twistd set up for us, with a method that uses our preferred |
|---|
| 1072 | # timestamp format. |
|---|
| 1073 | for o in twlog.theLogPublisher.observers: |
|---|
| 1074 | # o might be a FileLogObserver's .emit method |
|---|
| 1075 | if type(o) is type(self.setup_logging): # bound method |
|---|
| 1076 | ob = o.__self__ |
|---|
| 1077 | if isinstance(ob, twlog.FileLogObserver): |
|---|
| 1078 | newmeth = types.MethodType(formatTimeTahoeStyle, ob) |
|---|
| 1079 | ob.formatTime = newmeth |
|---|
| 1080 | # TODO: twisted >2.5.0 offers maxRotatedFiles=50 |
|---|
| 1081 | |
|---|
| 1082 | lgfurl_file = self.config.get_private_path("logport.furl").encode(get_filesystem_encoding()) |
|---|
| 1083 | if os.path.exists(lgfurl_file): |
|---|
| 1084 | os.remove(lgfurl_file) |
|---|
| 1085 | self.log_tub.setOption("logport-furlfile", lgfurl_file) |
|---|
| 1086 | lgfurl = self.config.get_config("node", "log_gatherer.furl", "") |
|---|
| 1087 | if lgfurl: |
|---|
| 1088 | # this is in addition to the contents of log-gatherer-furlfile |
|---|
| 1089 | lgfurl = lgfurl.encode("utf-8") |
|---|
| 1090 | self.log_tub.setOption("log-gatherer-furl", lgfurl) |
|---|
| 1091 | self.log_tub.setOption("log-gatherer-furlfile", |
|---|
| 1092 | self.config.get_config_path("log_gatherer.furl")) |
|---|
| 1093 | |
|---|
| 1094 | incident_dir = self.config.get_config_path("logs", "incidents") |
|---|
| 1095 | foolscap.logging.log.setLogDir(incident_dir) |
|---|
| 1096 | twlog.msg("Foolscap logging initialized") |
|---|
| 1097 | twlog.msg("Note to developers: twistd.log does not receive very much.") |
|---|
| 1098 | twlog.msg("Use 'flogtool tail -c NODEDIR/private/logport.furl' instead") |
|---|
| 1099 | twlog.msg("and read docs/logging.rst") |
|---|
| 1100 | |
|---|
| 1101 | def log(self, *args, **kwargs): |
|---|
| 1102 | return log.msg(*args, **kwargs) |
|---|