PageRenderTime 51ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/gateone/core/configuration.py

http://github.com/liftoff/GateOne
Python | 1394 lines | 1367 code | 3 blank | 24 comment | 64 complexity | 2d818ebe469a0022dce1b6b48a537c79 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright 2013 Liftoff Software Corporation
  4. # Meta
  5. __license__ = "AGPLv3 or Proprietary (see LICENSE.txt)"
  6. __doc__ = """
  7. .. _settings.py:
  8. Settings Module for Gate One
  9. ============================
  10. This module contains functions that deal with Gate One's options/settings
  11. """
  12. import os, sys, io, re, socket, tempfile, logging
  13. from pkg_resources import resource_filename, resource_listdir, resource_string
  14. from .log import FACILITIES
  15. from gateone.core.log import go_logger
  16. from tornado import locale
  17. from tornado.escape import json_decode
  18. from tornado.options import define, options, Error
  19. # Locale stuff (can't use .locale since .locale uses this module)
  20. # Default to using the environment's locale with en_US fallback
  21. temp_locale = locale.get(os.environ.get('LANG', 'en_US').split('.')[0])
  22. _ = temp_locale.translate
  23. del temp_locale
  24. logger = go_logger(None)
  25. comments_re = re.compile(
  26. r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"',
  27. re.DOTALL | re.MULTILINE
  28. )
  29. trailing_commas_re = re.compile(
  30. r'(,)\s*}(?=([^"\\]*(\\.|"([^"\\]*\\.)*[^"\\]*"))*[^"]*$)')
  31. class SettingsError(Exception):
  32. """
  33. Raised when we encounter an error parsing .conf files in the settings dir.
  34. """
  35. pass
  36. class RUDict(dict):
  37. """
  38. A dict that will recursively update keys and values in a safe manner so that
  39. sub-dicts will be merged without one clobbering the other.
  40. .. note::
  41. This class (mostly) taken from `here
  42. <http://stackoverflow.com/questions/6256183/combine-two-dictionaries-of-dictionaries-python>`_
  43. """
  44. def __init__(self, *args, **kw):
  45. super(RUDict,self).__init__(*args, **kw)
  46. def update(self, E=None, **F):
  47. if E is not None:
  48. if 'keys' in dir(E) and callable(getattr(E, 'keys')):
  49. for k in E:
  50. if k in self: # Existing ...must recurse into both sides
  51. self.r_update(k, E)
  52. else: # Doesn't currently exist, just update
  53. self[k] = E[k]
  54. else:
  55. for (k, v) in E:
  56. self.r_update(k, {k:v})
  57. for k in F:
  58. self.r_update(k, {k:F[k]})
  59. def r_update(self, key, other_dict):
  60. if isinstance(self[key], dict) and isinstance(other_dict[key], dict):
  61. od = RUDict(self[key])
  62. nd = other_dict[key]
  63. od.update(nd)
  64. self[key] = od
  65. else:
  66. self[key] = other_dict[key]
  67. def __repr__(self):
  68. """
  69. Returns the `RUDict` as indented json to better resemble how it looks in
  70. a .conf file.
  71. """
  72. import json # Tornado's json_encode doesn't do indentation
  73. return json.dumps(self, indent=4)
  74. def __str__(self):
  75. """
  76. Just returns `self.__repr__()` with an extra newline at the end.
  77. """
  78. return self.__repr__() + "\n"
  79. # Utility functions (copied from utils.py so we don't have an import paradox)
  80. def generate_session_id():
  81. """
  82. Returns a random, 45-character session ID. Example:
  83. .. code-block:: python
  84. >>> generate_session_id()
  85. "NzY4YzFmNDdhMTM1NDg3Y2FkZmZkMWJmYjYzNjBjM2Y5O"
  86. >>>
  87. """
  88. import base64, uuid
  89. from tornado.escape import utf8
  90. session_id = base64.b64encode(
  91. utf8(uuid.uuid4().hex + uuid.uuid4().hex))[:45]
  92. if bytes != str: # Python 3
  93. return str(session_id, 'UTF-8')
  94. return session_id
  95. def mkdir_p(path):
  96. """
  97. Pythonic version of "mkdir -p". Example equivalents::
  98. >>> mkdir_p('/tmp/test/testing') # Does the same thing as...
  99. >>> from subprocess import call
  100. >>> call('mkdir -p /tmp/test/testing')
  101. .. note:: This doesn't actually call any external commands.
  102. """
  103. import errno
  104. try:
  105. os.makedirs(path)
  106. except OSError as exc:
  107. if exc.errno == errno.EEXIST:
  108. pass
  109. else: raise
  110. # Settings and options-related functions
  111. # NOTE: "options" refer to command line arguments (for the most part) while
  112. # "settings" refers to the .conf files. "commands" are CLI commmands specified
  113. # via apps and plugins (for the most part). e.g. termlog, install_license, etc
  114. def print_help(commands):
  115. """
  116. Tornado's options.print_help() function with a few minor changes:
  117. * Help text is not hard wrapped (why did the Tornado devs do that? Ugh).
  118. * It includes information about Gate One 'commands'.
  119. * It only prints to stdout.
  120. """
  121. import textwrap, fcntl, termios, struct
  122. renditions = False
  123. try:
  124. import curses
  125. if hasattr(sys.stderr, 'isatty') and sys.stderr.isatty():
  126. try:
  127. curses.setupterm()
  128. if curses.tigetnum("colors") > 0:
  129. renditions = True
  130. except Exception:
  131. renditions = False
  132. except ImportError:
  133. pass
  134. def bold(text):
  135. if renditions:
  136. return "\x1b[1m%s\x1b[0m" % text
  137. return text
  138. print("Usage: %s [OPTIONS]" % sys.argv[0])
  139. print(bold("\nOptions:\n"))
  140. rows, columns, hp, wp = struct.unpack('HHHH', fcntl.ioctl(
  141. 0, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))
  142. by_group = {}
  143. for option in options._options.values():
  144. by_group.setdefault(option.group_name, []).append(option)
  145. for filename, o in sorted(by_group.items()):
  146. if filename:
  147. print(bold("\n%s options:\n" % os.path.normpath(filename)))
  148. o.sort(key=lambda option: option.name)
  149. for option in o:
  150. prefix = option.name
  151. if option.metavar:
  152. prefix += "=" + option.metavar
  153. description = option.help or ""
  154. if option.default is not None and option.default != '':
  155. description += " (default %s)" % option.default
  156. lines = textwrap.wrap(description, columns - 35)
  157. if len(prefix) > 30 or len(lines) == 0:
  158. lines.insert(0, '')
  159. print(" --%-30s %s" % (prefix, lines[0]))
  160. for line in lines[1:]:
  161. print("%-34s %s" % (' ', line))
  162. print(bold("\nCommands:\n"))
  163. print(" Usage: %s <command> [OPTIONS]\n" % sys.argv[0])
  164. commands_description = _(
  165. "GateOne supports many different CLI 'commands' which can be used "
  166. "to invoke special functionality provided by plugins and applications "
  167. "(and application's plugins). Each command can have it's own options "
  168. "and most will have a --help function of their own.")
  169. lines = textwrap.wrap(commands_description, columns)
  170. for line in lines:
  171. print("%s %s" % (' ', line))
  172. print("")
  173. for module, command_dict in commands.items():
  174. print(bold("Commands provided by '%s':\n" % module))
  175. for command, details in sorted(command_dict.items()):
  176. print(" %-32s %s" % (command, details['description']))
  177. print("")
  178. print(bold("Example command usage:\n"))
  179. print(" %s termlog --help" % sys.argv[0])
  180. print("") # The oh-so-important whitespace before the prompt
  181. sys.exit(1)
  182. def define_options(installed=True, cli_commands=None):
  183. """
  184. Calls `tornado.options.define` for all of Gate One's command-line options.
  185. If *installed* is ``False`` the defaults will be set under the assumption
  186. that the user is non-root and running Gate One out of a download/cloned
  187. directory.
  188. """
  189. # NOTE: To test this function interactively you must import tornado.options
  190. # and call tornado.options.parse_config_file(*some_config_path*). After you
  191. # do that the options will wind up in tornado.options.options
  192. global user_locale
  193. # Default to using the shell's LANG variable as the locale
  194. try:
  195. default_locale = os.environ['LANG'].split('.')[0]
  196. except KeyError: # $LANG isn't set
  197. default_locale = "en_US"
  198. user_locale = locale.get(default_locale)
  199. # NOTE: The locale setting above is only for the --help messages.
  200. # Simplify the auth option help message
  201. auths = "none, api, cas, google, ssl"
  202. from gateone.auth.authentication import PAMAuthHandler, KerberosAuthHandler
  203. if KerberosAuthHandler:
  204. auths += ", kerberos"
  205. if PAMAuthHandler:
  206. auths += ", pam"
  207. # Simplify the syslog_facility option help message
  208. facilities = list(FACILITIES.keys())
  209. facilities.sort()
  210. # Figure out the default origins
  211. default_origins = [
  212. 'localhost',
  213. '127.0.0.1',
  214. ]
  215. # Used both http and https above to demonstrate that both are acceptable
  216. try:
  217. additional_origins = socket.gethostbyname_ex(socket.gethostname())
  218. except socket.gaierror:
  219. # Couldn't get any IPs from the hostname
  220. additional_origins = []
  221. for host in additional_origins:
  222. if isinstance(host, str):
  223. default_origins.append('%s' % host)
  224. else: # It's a list
  225. for _host in host:
  226. default_origins.append('%s' % _host)
  227. default_origins = ";".join(default_origins)
  228. config_default = os.path.join(os.path.sep, "opt", "gateone", "server.conf")
  229. # NOTE: --settings_dir deprecates --config
  230. settings_base = os.path.join(os.path.sep, 'etc', 'gateone')
  231. settings_default = os.path.join(settings_base, 'conf.d')
  232. port_default = 443
  233. log_default = os.path.join(
  234. os.path.sep, "var", "log", 'gateone', 'gateone.log')
  235. user_dir_default = os.path.join(
  236. os.path.sep, "var", "lib", "gateone", "users")
  237. pid_default = os.path.join(os.path.sep, "var", "run", 'gateone.pid')
  238. session_dir_default = os.path.join(tempfile.gettempdir(), 'gateone')
  239. cache_dir_default = os.path.join(tempfile.gettempdir(), 'gateone_cache')
  240. if os.getuid() != 0: # Not root? Use $HOME/.gateone/ for everything
  241. home = os.path.expanduser('~')
  242. user_dir_default = os.path.join(home, '.gateone')
  243. settings_default = os.path.join(user_dir_default, 'conf.d')
  244. port_default = 10443
  245. log_default = os.path.join(user_dir_default, 'logs', 'gateone.log')
  246. pid_default = os.path.join(user_dir_default, 'gateone.pid')
  247. session_dir_default = os.path.join(user_dir_default, 'sessions')
  248. cache_dir_default = os.path.join(user_dir_default, 'cache')
  249. if not installed:
  250. # Running inside the download directory? Change various defaults to
  251. # work inside of this directory
  252. here = os.path.dirname(os.path.abspath(__file__))
  253. settings_base = os.path.normpath(os.path.join(here, '..', '..'))
  254. settings_default = os.path.join(settings_base, 'conf.d')
  255. port_default = 10443
  256. log_default = os.path.join(settings_base, 'logs', 'gateone.log')
  257. user_dir_default = os.path.join(settings_base, 'users')
  258. pid_default = os.path.join(settings_base, 'gateone.pid')
  259. session_dir_default = os.path.join(settings_base, 'sessions')
  260. cache_dir_default = os.path.join(settings_base, 'cache')
  261. options.log_file_prefix = log_default
  262. ssl_dir = os.path.join(settings_base, 'ssl')
  263. # Override Tornado's help so we can print CLI 'commands'
  264. del options._options['help']
  265. define("help",
  266. type=bool,
  267. help="Show this help information")
  268. define("version",
  269. type=bool,
  270. group='gateone',
  271. help=_("Display version information."),
  272. )
  273. define("config",
  274. default=config_default,
  275. group='gateone',
  276. help=_("DEPRECATED. Use --settings_dir."),
  277. type=basestring,
  278. )
  279. define("settings_dir",
  280. default=settings_default,
  281. group='gateone',
  282. help=_("Path to the settings directory."),
  283. type=basestring
  284. )
  285. define(
  286. "cache_dir",
  287. default=cache_dir_default,
  288. group='gateone',
  289. help=_(
  290. "Path where Gate One should store temporary global files (e.g. "
  291. "rendered templates, CSS, JS, etc)."),
  292. type=basestring
  293. )
  294. define(
  295. "debug",
  296. default=False,
  297. group='gateone',
  298. help=_("Enable debugging features such as auto-restarting when files "
  299. "are modified.")
  300. )
  301. define("cookie_secret", # 45 chars is, "Good enough for me" (cookie joke =)
  302. default=None,
  303. group='gateone',
  304. help=_("Use the given 45-character string for cookie encryption."),
  305. type=basestring
  306. )
  307. define("command",
  308. default=None,
  309. group='gateone',
  310. help=_(
  311. "DEPRECATED: Use the 'commands' option in the terminal settings."),
  312. type=basestring
  313. )
  314. define("address",
  315. default="",
  316. group='gateone',
  317. help=_("Run on the given address. Default is all addresses (IPv6 "
  318. "included). Multiple address can be specified using a semicolon"
  319. " as a separator (e.g. '127.0.0.1;::1;10.1.1.100')."),
  320. type=basestring)
  321. define("port",
  322. default=port_default,
  323. group='gateone',
  324. help=_("Run on the given port."),
  325. type=int)
  326. define(
  327. "enable_unix_socket",
  328. default=False,
  329. group='gateone',
  330. help=_("Enable Unix socket support."),
  331. type=bool)
  332. define(
  333. "unix_socket_path",
  334. default="/tmp/gateone.sock",
  335. group='gateone',
  336. help=_("Path to the Unix socket (if --enable_unix_socket=True)."),
  337. type=basestring)
  338. define(
  339. "unix_socket_mode",
  340. default="0600",
  341. group='gateone',
  342. help=_("Unix socket mode (if --enable_unix_socket=True)."),
  343. type=basestring)
  344. # Please only use this if Gate One is running behind something with SSL:
  345. define(
  346. "disable_ssl",
  347. default=False,
  348. group='gateone',
  349. help=_("If enabled Gate One will run without SSL (generally not a "
  350. "good idea).")
  351. )
  352. define(
  353. "certificate",
  354. default=os.path.join(ssl_dir, "certificate.pem"),
  355. group='gateone',
  356. help=_(
  357. "Path to the SSL certificate. Will be auto-generated if not "
  358. "found."),
  359. type=basestring
  360. )
  361. define(
  362. "keyfile",
  363. default=os.path.join(ssl_dir, "keyfile.pem"),
  364. group='gateone',
  365. help=_("Path to the SSL keyfile. Will be auto-generated if none is"
  366. " provided."),
  367. type=basestring
  368. )
  369. define(
  370. "ca_certs",
  371. default=None,
  372. group='gateone',
  373. help=_("Path to a file containing any number of concatenated CA "
  374. "certificates in PEM format. They will be used to authenticate "
  375. "clients if the 'ssl_auth' option is set to 'optional' or "
  376. "'required'."),
  377. type=basestring
  378. )
  379. define(
  380. "ssl_auth",
  381. default='none',
  382. group='gateone',
  383. help=_("Enable the use of client SSL (X.509) certificates as a "
  384. "secondary authentication factor (the configured 'auth' type "
  385. "will come after SSL auth). May be one of 'none', 'optional', "
  386. "or 'required'. NOTE: Only works if the 'ca_certs' option is "
  387. "configured."),
  388. type=basestring
  389. )
  390. define(
  391. "user_dir",
  392. default=user_dir_default,
  393. group='gateone',
  394. help=_("Path to the location where user files will be stored."),
  395. type=basestring
  396. )
  397. define(
  398. "user_logs_max_age",
  399. default="30d",
  400. group='gateone',
  401. help=_(
  402. "Maximum length of time to keep any given user log before it is "
  403. "automatically removed."),
  404. type=basestring
  405. )
  406. define(
  407. "session_dir",
  408. default=session_dir_default,
  409. group='gateone',
  410. help=_(
  411. "Path to the location where session information will be stored."),
  412. type=basestring
  413. )
  414. define(
  415. "syslog_facility",
  416. default="daemon",
  417. group='gateone',
  418. help=_("Syslog facility to use when logging to syslog (if "
  419. "syslog_session_logging is enabled). Must be one of: %s."
  420. % ", ".join(facilities)),
  421. type=basestring
  422. )
  423. define(
  424. "session_timeout",
  425. default="5d",
  426. group='gateone',
  427. help=_("Amount of time that a session is allowed to idle before it is "
  428. "killed. Accepts <num>X where X could be one of s, m, h, or d for "
  429. "seconds, minutes, hours, and days. Set to '0' to disable the ability "
  430. "to resume sessions."),
  431. type=basestring
  432. )
  433. define(
  434. "new_api_key",
  435. default=False,
  436. group='gateone',
  437. help=_("Generate a new API key that an external application can use to "
  438. "embed Gate One."),
  439. )
  440. define(
  441. "auth",
  442. default="none",
  443. group='gateone',
  444. help=_("Authentication method to use. Valid options are: %s" % auths),
  445. type=basestring
  446. )
  447. # This is to prevent replay attacks. Gate One only keeps a "working memory"
  448. # of API auth objects for this amount of time. So if the Gate One server is
  449. # restarted we don't have to write them to disk as anything older than this
  450. # setting will be invalid (no need to check if it has already been used).
  451. define(
  452. "api_timestamp_window",
  453. default="30s", # 30 seconds
  454. group='gateone',
  455. help=_(
  456. "How long before an API authentication object becomes invalid."),
  457. type=basestring
  458. )
  459. define(
  460. "sso_realm",
  461. default=None,
  462. group='gateone',
  463. help=_("Kerberos REALM (aka DOMAIN) to use when authenticating clients."
  464. " Only relevant if Kerberos authentication is enabled."),
  465. type=basestring
  466. )
  467. define(
  468. "sso_service",
  469. default='HTTP',
  470. group='gateone',
  471. help=_("Kerberos service (aka application) to use. "
  472. "Only relevant if Kerberos authentication is enabled."),
  473. type=basestring
  474. )
  475. define(
  476. "pam_realm",
  477. default=os.uname()[1],
  478. group='gateone',
  479. help=_("Basic auth REALM to display when authenticating clients. "
  480. "Default: hostname. "
  481. "Only relevant if PAM authentication is enabled."),
  482. # NOTE: This is only used to show the user a REALM at the basic auth
  483. # prompt and as the name in the 'user_dir/<user>' directory
  484. type=basestring
  485. )
  486. define(
  487. "pam_service",
  488. default='login',
  489. group='gateone',
  490. help=_("PAM service to use. Defaults to 'login'. "
  491. "Only relevant if PAM authentication is enabled."),
  492. type=basestring
  493. )
  494. define(
  495. "embedded",
  496. default=False,
  497. group='gateone',
  498. help=_(
  499. "When embedding Gate One this option is available to plugins, "
  500. "applications, and templates so they know they're running in "
  501. "embedded mode and can change behavior (if necessary).")
  502. )
  503. define(
  504. "locale",
  505. default=default_locale,
  506. group='gateone',
  507. help=_("The locale (e.g. pt_PT) Gate One should use for translations."
  508. " If not provided, will default to $LANG (which is '%s' in your "
  509. "current shell)."
  510. % os.environ.get('LANG', 'not set').split('.')[0]),
  511. type=basestring
  512. )
  513. define("js_init",
  514. default="",
  515. group='gateone',
  516. help=_("A JavaScript object (string) that will be used when running "
  517. "GateOne.init() inside index.html. "
  518. "Example: --js_init=\"{theme: 'white'}\" would result in "
  519. "GateOne.init({theme: 'white'})"),
  520. type=basestring
  521. )
  522. define(
  523. "https_redirect",
  524. default=False,
  525. group='gateone',
  526. help=_("If enabled a separate listener will be started on port 80 that"
  527. " redirects users to the configured port using HTTPS.")
  528. )
  529. define(
  530. "url_prefix",
  531. default="/",
  532. group='gateone',
  533. help=_("An optional prefix to place before all Gate One URLs. e.g. "
  534. "'/gateone/'. Use this if Gate One will be running behind a "
  535. "reverse proxy where you want it to be located at some sub-"
  536. "URL path."),
  537. type=basestring
  538. )
  539. define(
  540. "origins",
  541. default=default_origins,
  542. group='gateone',
  543. help=_("A semicolon-separated list of origins you wish to allow access "
  544. "to your Gate One server over the WebSocket. This value may "
  545. "contain hostnames/FQDNs (e.g. foo;foo.bar;) and IP addresses. "
  546. "This value must contain all the hostnames/IPs that users will "
  547. "use to connect to Gate One. "
  548. "Alternatively, '*' may be specified to allow access from "
  549. "anywhere. NOTE: Using a '*' is only a good idea if you've "
  550. "configured Gate One to use API authentication."),
  551. type=basestring
  552. )
  553. define(
  554. "pid_file",
  555. default=pid_default,
  556. group='gateone',
  557. help=_("Define the path to the pid file."),
  558. type=basestring
  559. )
  560. define(
  561. "uid",
  562. default=str(os.getuid()),
  563. group='gateone',
  564. help=_("Drop privileges and run Gate One as this user/uid."),
  565. type=basestring
  566. )
  567. define(
  568. "gid",
  569. default=str(os.getgid()),
  570. group='gateone',
  571. help=_("Drop privileges and run Gate One as this group/gid."),
  572. type=basestring
  573. )
  574. define(
  575. "api_keys",
  576. default="",
  577. group='gateone',
  578. help=_("The 'key:secret,...' API key pairs you wish to use (only "
  579. "applies if using API authentication)"),
  580. type=basestring
  581. )
  582. define(
  583. "combine_js",
  584. default="",
  585. group='gateone',
  586. help=_(
  587. "Combines all of Gate One's JavaScript files into one big file and "
  588. "saves it to the given path (e.g. ./gateone.py "
  589. "--combine_js=/tmp/gateone.js)"),
  590. type=basestring
  591. )
  592. define(
  593. "combine_css",
  594. default="",
  595. group='gateone',
  596. help=_(
  597. "Combines all of Gate One's CSS Template files into one big file "
  598. "and saves it to the given path (e.g. ./gateone.py "
  599. "--combine_css=/tmp/gateone.css)."),
  600. type=basestring
  601. )
  602. define(
  603. "combine_css_container",
  604. default="gateone",
  605. group='gateone',
  606. help=_(
  607. "Use this setting in conjunction with --combine_css if the <div> "
  608. "where Gate One lives is named something other than #gateone"),
  609. type=basestring
  610. )
  611. define(
  612. "multiprocessing_workers",
  613. default=None,
  614. group='gateone',
  615. help=_(
  616. "The number of processes to spawn use when using multiprocessing. "
  617. "Default is: <number of cores> + 1. Set to 0 to disable "
  618. "multiprocessing."),
  619. type=int
  620. )
  621. define(
  622. "configure",
  623. default=False,
  624. group='gateone',
  625. help=_(
  626. "Only configure Gate One (create SSL certs, conf.d, etc). Do not "
  627. "start any Gate One processes."),
  628. )
  629. def settings_template(path, **kwargs):
  630. """
  631. Renders and returns the Tornado template at *path* using the given *kwargs*.
  632. .. note:: Any blank lines in the rendered template will be removed.
  633. """
  634. from tornado.template import Template
  635. with io.open(path, mode='r', encoding='utf-8') as f:
  636. template_data = f.read()
  637. t = Template(template_data)
  638. # NOTE: Tornado returns templates as bytes, not unicode. That's why we need
  639. # the decode() below...
  640. rendered = t.generate(**kwargs).decode('utf-8')
  641. out = ""
  642. for line in rendered.splitlines():
  643. if line.strip():
  644. out += line + "\n"
  645. return out
  646. def parse_commands(commands):
  647. """
  648. Given a list of *commands* (which can include arguments) such as::
  649. ['ls', '--color="always"', '-lh', 'ps', '--context', '-ef']
  650. Returns an `OrderedDict` like so::
  651. OrderedDict([
  652. ('ls', ['--color="always"', '-ltrh']),
  653. ('ps', ['--context', '-ef'])
  654. ])
  655. """
  656. try:
  657. from collections import OrderedDict
  658. except ImportError: # Python <2.7 didn't have OrderedDict in collections
  659. from ordereddict import OrderedDict
  660. out = OrderedDict()
  661. command = OrderedDict()
  662. for item in commands:
  663. if item.startswith('-') or ' ' in item:
  664. out[command].append(item)
  665. else:
  666. command = item
  667. out[command] = []
  668. return out
  669. def generate_server_conf(installed=True):
  670. """
  671. Generates a fresh settings/10server.conf file using the arguments provided
  672. on the command line to override defaults.
  673. If *installed* is ``False`` the defaults will be set under the assumption
  674. that the user is non-root and running Gate One out of a download/cloned
  675. directory.
  676. """
  677. logger.info(_(
  678. u"Gate One settings are incomplete. A new <settings_dir>/10server.conf"
  679. u" will be generated."))
  680. auth_settings = {} # Auth stuff goes in 20authentication.conf
  681. all_setttings = options_to_settings(options) # NOTE: options is global
  682. settings_path = options.settings_dir
  683. server_conf_path = os.path.join(settings_path, '10server.conf')
  684. if os.path.exists(server_conf_path):
  685. logger.error(_(
  686. "You have a 10server.conf but it is either invalid (syntax "
  687. "error) or missing essential settings."))
  688. sys.exit(1)
  689. config_defaults = all_setttings['*']['gateone']
  690. # Don't need this in the actual settings file:
  691. del config_defaults['settings_dir']
  692. non_options = [
  693. # These are things that don't really belong in settings
  694. 'new_api_key', 'help', 'kill', 'config', 'version', 'combine_css',
  695. 'combine_js', 'combine_css_container', 'configure'
  696. ]
  697. # Don't need non-options in there either:
  698. for non_option in non_options:
  699. if non_option in config_defaults:
  700. del config_defaults[non_option]
  701. # Generate a new cookie_secret
  702. config_defaults['cookie_secret'] = generate_session_id()
  703. # Separate out the authentication settings
  704. authentication_options = [
  705. # These are here only for logical separation in the .conf files
  706. 'api_timestamp_window', 'auth', 'pam_realm', 'pam_service',
  707. 'sso_keytab', 'sso_realm', 'sso_service', 'ssl_auth'
  708. ]
  709. # Provide some kerberos (sso) defaults
  710. auth_settings['sso_realm'] = "EXAMPLE.COM"
  711. auth_settings['sso_keytab'] = None # Allow /etc/krb5.conf to control it
  712. for key, value in list(config_defaults.items()):
  713. if key in authentication_options:
  714. auth_settings.update({key: value})
  715. del config_defaults[key]
  716. if key == 'origins':
  717. # As a convenience to the user, add any --port to the origins
  718. if config_defaults['port'] not in [80, 443]:
  719. for i, origin in enumerate(list(value)):
  720. value[i] = "{origin}:{port}".format(
  721. origin=origin, port=config_defaults['port'])
  722. # Make sure we have a valid log_file_prefix
  723. if config_defaults['log_file_prefix'] == None:
  724. web_log_dir = os.path.join(os.path.sep, "var", "log", "gateone")
  725. if installed:
  726. here = os.path.dirname(os.path.abspath(__file__))
  727. web_log_dir = os.path.normpath(
  728. os.path.join(here, '..', '..', 'logs'))
  729. web_log_path = os.path.join(web_log_dir, 'gateone.log')
  730. config_defaults['log_file_prefix'] = web_log_path
  731. else:
  732. web_log_dir = os.path.split(config_defaults['log_file_prefix'])[0]
  733. if not os.path.exists(web_log_dir):
  734. # Make sure the directory exists
  735. mkdir_p(web_log_dir)
  736. if not os.path.exists(config_defaults['log_file_prefix']):
  737. # Make sure the file is present
  738. io.open(
  739. config_defaults['log_file_prefix'],
  740. mode='w', encoding='utf-8').write(u'')
  741. auth_conf_path = os.path.join(settings_path, '20authentication.conf')
  742. template_path = resource_filename(
  743. 'gateone', '/templates/settings/generic.conf')
  744. new_settings = settings_template(
  745. template_path, settings=config_defaults)
  746. with io.open(server_conf_path, mode='w') as s:
  747. s.write(u"// This is Gate One's main settings file.\n")
  748. s.write(new_settings)
  749. new_auth_settings = settings_template(
  750. template_path, settings=auth_settings)
  751. with io.open(auth_conf_path, mode='w') as s:
  752. s.write(u"// This is Gate One's authentication settings file.\n")
  753. s.write(new_auth_settings)
  754. # NOTE: After Gate One 1.2 is officially released this function will be removed:
  755. def convert_old_server_conf():
  756. """
  757. Converts old-style server.conf files to the new settings/10server.conf
  758. format.
  759. """
  760. settings = RUDict()
  761. auth_settings = RUDict()
  762. terminal_settings = RUDict()
  763. api_keys = RUDict({"*": {"gateone": {"api_keys": {}}}})
  764. terminal_options = [ # These are now terminal-app-specific setttings
  765. 'command', 'dtach', 'session_logging', 'session_logs_max_age',
  766. 'syslog_session_logging'
  767. ]
  768. authentication_options = [
  769. # These are here only for logical separation in the .conf files
  770. 'api_timestamp_window', 'auth', 'pam_realm', 'pam_service',
  771. 'sso_realm', 'sso_service', 'ssl_auth'
  772. ]
  773. with io.open(options.config) as f:
  774. # Regular server-wide settings will go in 10server.conf by default.
  775. # These settings can actually be spread out into any number of .conf
  776. # files in the settings directory using whatever naming convention
  777. # you want.
  778. settings_path = options.settings_dir
  779. server_conf_path = os.path.join(settings_path, '10server.conf')
  780. # Using 20authentication.conf for authentication settings
  781. auth_conf_path = os.path.join(
  782. settings_path, '20authentication.conf')
  783. terminal_conf_path = os.path.join(settings_path, '50terminal.conf')
  784. api_keys_conf = os.path.join(settings_path, '30api_keys.conf')
  785. # NOTE: Using a separate file for authentication stuff for no other
  786. # reason than it seems like a good idea. Don't want one
  787. # gigantic config file for everything (by default, anyway).
  788. logger.info(_(
  789. "Old server.conf file found. Converting to the new format as "
  790. "%s, %s, and %s" % (
  791. server_conf_path, auth_conf_path, terminal_conf_path)))
  792. for line in f:
  793. if line.startswith('#'):
  794. continue
  795. key = line.split('=', 1)[0].strip()
  796. value = eval(line.split('=', 1)[1].strip())
  797. if key in terminal_options:
  798. if key == 'command':
  799. # Fix the path to ssh_connect.py if present
  800. if 'ssh_connect.py' in value:
  801. value = value.replace(
  802. '/plugins/', '/applications/terminal/plugins/')
  803. if key == 'session_logs_max_age':
  804. # This is now user_logs_max_age. Put it in 'gateone'
  805. settings.update({'user_logs_max_age': value})
  806. terminal_settings.update({key: value})
  807. elif key in authentication_options:
  808. auth_settings.update({key: value})
  809. elif key == 'origins':
  810. # Convert to the new format (a list with no http://)
  811. origins = value.split(';')
  812. converted_origins = []
  813. for origin in origins:
  814. # The new format doesn't bother with http:// or https://
  815. if origin == '*':
  816. converted_origins.append(origin)
  817. continue
  818. origin = origin.split('://')[1]
  819. if origin not in converted_origins:
  820. converted_origins.append(origin)
  821. settings.update({key: converted_origins})
  822. elif key == 'api_keys':
  823. # Move these to the new location/format (30api_keys.conf)
  824. for pair in value.split(','):
  825. api_key, secret = pair.split(':')
  826. if bytes == str:
  827. api_key = api_key.decode('UTF-8')
  828. secret = secret.decode('UTF-8')
  829. api_keys['*']['gateone']['api_keys'].update(
  830. {api_key: secret})
  831. # API keys can be written right away
  832. with io.open(api_keys_conf, 'w') as conf:
  833. msg = _(
  834. u"// This file contains the key and secret pairs "
  835. u"used by Gate One's API authentication method.\n")
  836. conf.write(msg)
  837. conf.write(unicode(api_keys))
  838. else:
  839. settings.update({key: value})
  840. template_path = resource_filename(
  841. 'gateone', '/templates/settings/generic.conf')
  842. new_settings = settings_template(template_path, settings=settings)
  843. if not os.path.exists(server_conf_path):
  844. with io.open(server_conf_path, 'w') as s:
  845. s.write(_(u"// This is Gate One's main settings file.\n"))
  846. s.write(new_settings)
  847. new_auth_settings = settings_template(
  848. template_path, settings=auth_settings)
  849. if not os.path.exists(auth_conf_path):
  850. with io.open(auth_conf_path, 'w') as s:
  851. s.write(_(
  852. u"// This is Gate One's authentication settings file.\n"))
  853. s.write(new_auth_settings)
  854. # Terminal uses a slightly different template; it converts 'command'
  855. # to the new 'commands' format.
  856. template_path = resource_filename(
  857. 'gateone', '/templates/settings/50terminal.conf')
  858. new_term_settings = settings_template(
  859. template_path, settings=terminal_settings)
  860. if not os.path.exists(terminal_conf_path):
  861. with io.open(terminal_conf_path, 'w') as s:
  862. s.write(_(
  863. u"// This is Gate One's Terminal application settings "
  864. u"file.\n"))
  865. s.write(new_term_settings)
  866. # Rename the old server.conf so this logic doesn't happen again
  867. os.rename(options.config, "%s.old" % options.config)
  868. def apply_cli_overrides(go_settings):
  869. """
  870. Updates *go_settings* in-place with values given on the command line.
  871. """
  872. # Figure out which options are being overridden on the command line
  873. arguments = []
  874. non_options = [
  875. # These are things that don't really belong in settings
  876. 'new_api_key', 'help', 'kill', 'config', 'combine_js', 'combine_css',
  877. 'combine_css_container', 'version', 'configure'
  878. ]
  879. for arg in list(sys.argv)[1:]:
  880. if not arg.startswith('-'):
  881. break
  882. else:
  883. arguments.append(arg.lstrip('-').split('=', 1)[0])
  884. go_settings['cli_overrides'] = arguments
  885. for argument in arguments:
  886. if argument in non_options:
  887. continue
  888. if argument in list(options):
  889. go_settings[argument] = options[argument]
  890. # Update Tornado's options from our settings.
  891. # NOTE: For options given on the command line this step should be redundant.
  892. for key, value in go_settings.items():
  893. if key in non_options:
  894. continue
  895. if key in list(options):
  896. if key in ('origins', 'api_keys'):
  897. # These two settings are special and taken care of elsewhere
  898. continue
  899. try:
  900. setattr(options, key, value)
  901. except Error:
  902. if isinstance(value, str):
  903. if str == bytes: # Python 2
  904. setattr(options, key, unicode(value))
  905. else:
  906. setattr(options, key, str(value))
  907. def remove_comments(json_like):
  908. """
  909. Removes C-style comments from *json_like* and returns the result.
  910. """
  911. def replacer(match):
  912. s = match.group(0)
  913. if s[0] == '/': return ""
  914. return s
  915. return comments_re.sub(replacer, json_like)
  916. def remove_trailing_commas(json_like):
  917. """
  918. Removes trailing commas from *json_like* and returns the result.
  919. """
  920. return trailing_commas_re.sub("}", json_like)
  921. def get_settings(path, add_default=True):
  922. """
  923. Reads any and all *.conf files containing JSON (JS-style comments are OK)
  924. inside *path* and returns them as an :class:`RUDict`. Optionally, *path*
  925. may be a specific file (as opposed to just a directory).
  926. By default, all returned :class:`RUDict` objects will include a '*' dict
  927. which indicates "all users". This behavior can be skipped by setting the
  928. *add_default* keyword argument to `False`.
  929. """
  930. settings = RUDict()
  931. if add_default:
  932. settings['*'] = {}
  933. # Using an RUDict so that subsequent .conf files can safely override
  934. # settings way down the chain without clobbering parent keys/dicts.
  935. if os.path.isdir(path):
  936. settings_files = [a for a in os.listdir(path) if a.endswith('.conf')]
  937. settings_files.sort()
  938. else:
  939. if not os.path.exists(path):
  940. raise IOError(_("%s does not exist" % path))
  941. settings_files = [path]
  942. for fname in settings_files:
  943. # Use this file to update settings
  944. if os.path.isdir(path):
  945. filepath = os.path.join(path, fname)
  946. else:
  947. filepath = path
  948. with io.open(filepath, encoding='utf-8') as f:
  949. # Remove comments
  950. almost_json = remove_comments(f.read())
  951. proper_json = remove_trailing_commas(almost_json)
  952. # Remove blank/empty lines
  953. proper_json = os.linesep.join([
  954. s for s in proper_json.splitlines() if s.strip()])
  955. try:
  956. settings.update(json_decode(proper_json))
  957. except ValueError as e:
  958. # Something was wrong with the JSON (syntax error, usually)
  959. logging.error(
  960. "Error decoding JSON in settings file: %s"
  961. % os.path.join(path, fname))
  962. logging.error(e)
  963. # Let's try to be as user-friendly as possible by pointing out
  964. # *precisely* where the error occurred (if possible)...
  965. try:
  966. line_no = int(str(e).split(': line ', 1)[1].split()[0])
  967. column = int(str(e).split(': line ', 1)[1].split()[2])
  968. for i, line in enumerate(proper_json.splitlines()):
  969. if i == line_no-1:
  970. print(
  971. line[:column] +
  972. _(" <-- Something went wrong right here (or "
  973. "right above it)")
  974. )
  975. break
  976. else:
  977. print(line)
  978. raise SettingsError()
  979. except (ValueError, IndexError):
  980. print(_(
  981. "Got an exception trying to display precisely where "
  982. "the problem was. This usually happens when you've "
  983. "used single quotes (') instead of double quotes (\")."
  984. ))
  985. # Couldn't parse the exception message for line/column info
  986. pass # No big deal; the user will figure it out eventually
  987. return settings
  988. def options_to_settings(options):
  989. """
  990. Converts the given Tornado-style *options* to new-style settings. Returns
  991. an :class:`RUDict` containing all the settings.
  992. """
  993. settings = RUDict({'*': {'gateone': {}, 'terminal': {}}})
  994. # In the new settings format some options have moved to the terminal app.
  995. # These settings are below and will be placed in the 'terminal' sub-dict.
  996. terminal_options = [
  997. 'command', 'dtach', 'session_logging', 'session_logs_max_age',
  998. 'syslog_session_logging'
  999. ]
  1000. non_options = [
  1001. # These are things that don't really belong in settings
  1002. 'new_api_key', 'help', 'kill', 'config', 'version', 'configure'
  1003. ]
  1004. for key, value in options.items():
  1005. if key in terminal_options:
  1006. settings['*']['terminal'].update({key: value})
  1007. elif key in non_options:
  1008. continue
  1009. else:
  1010. if key == 'origins':
  1011. #if value == '*':
  1012. #continue
  1013. # Convert to the new format (a list with no http://)
  1014. origins = value.split(';')
  1015. converted_origins = []
  1016. for origin in origins:
  1017. if '://' in origin:
  1018. # The new format doesn't bother with http:// or https://
  1019. origin = origin.split('://')[1]
  1020. if origin not in converted_origins:
  1021. converted_origins.append(origin)
  1022. elif origin not in converted_origins:
  1023. converted_origins.append(origin)
  1024. settings['*']['gateone'].update({key: converted_origins})
  1025. elif key == 'api_keys':
  1026. if not value:
  1027. continue
  1028. # API keys/secrets are now a dict instead of a string
  1029. settings['*']['gateone']['api_keys'] = {}
  1030. for pair in value.split(','):
  1031. api_key, secret = pair.split(':', 1)
  1032. if bytes == str: # Python 2
  1033. api_key = api_key.decode('UTF-8')
  1034. secret = secret.decode('UTF-8')
  1035. settings['*']['gateone']['api_keys'].update(
  1036. {api_key: secret})
  1037. else:
  1038. settings['*']['gateone'].update({key: value})
  1039. return settings
  1040. def combine_javascript(path, settings_dir=None):
  1041. """
  1042. Combines all application and plugin .js files into one big one; saved to the
  1043. given *path*. If given, *settings_dir* will be used to determine which
  1044. applications and plugins should be included in the dump based on what is
  1045. enabled.
  1046. """
  1047. # A couple partials to save some space/typing
  1048. resource = lambda s: resource_string('gateone', s).decode('utf-8')
  1049. resource_fn = lambda s: resource_filename('gateone', s)
  1050. resource_dir = lambda s: resource_listdir('gateone', s)
  1051. if not settings_dir:
  1052. settings_dir = resource_filename('gateone', '/settings')
  1053. all_settings = get_settings(settings_dir)
  1054. enabled_plugins = []
  1055. enabled_applications = []
  1056. if 'gateone' in all_settings['*']:
  1057. # The check above will fail in first-run situations
  1058. enabled_plugins = all_settings['*']['gateone'].get(
  1059. 'enabled_plugins', [])
  1060. enabled_applications = all_settings['*']['gateone'].get(
  1061. 'enabled_applications', [])
  1062. plugins_dir = resource_fn('/plugins')
  1063. pluginslist = resource_dir('/plugins')
  1064. pluginslist.sort()
  1065. applications_dir = resource_fn('/applications')
  1066. appslist = resource_dir('/applications')
  1067. appslist.sort()
  1068. logger.info(_("Combining all Gate One JavaScript into a single file..."))
  1069. with io.open(path, 'w') as f:
  1070. # Start by adding Gate One's static JS files
  1071. go_static_files = [
  1072. a for a in resource_dir('/static') if a.endswith('.js')]
  1073. # gateone.js must always come first
  1074. go_first = [a for a in go_static_files if a.startswith('gateone')]
  1075. go_first.sort()
  1076. index = go_first.index('gateone.js')
  1077. if index: # Nonzero index means it's not first
  1078. go_first.insert(0, go_first.pop(index)) # Move it to the front
  1079. if 'gateone.min.js' in go_first: # Don't want two copies of gateone.js
  1080. go_first.remove('gateone.min.js')
  1081. go_last = [a for a in go_static_files if not a.startswith('gateone')]
  1082. go_static_files = go_first + go_last
  1083. for filename in go_static_files:
  1084. filepath = '/static/%s' % filename
  1085. logger.info(_("Concatenating: %s") % filepath)
  1086. f.write(resource(filepath) + u'\n')
  1087. # Gate One plugins
  1088. for plugin in pluginslist:
  1089. if enabled_plugins and plugin not in enabled_plugins:
  1090. continue
  1091. plugin_static_path = '/plugins/%s/static' % plugin
  1092. # NOTE: Using resource_filename() here so that it gets unpacked if
  1093. # necessary:
  1094. static_dir = resource_fn(plugin_static_path)
  1095. if os.path.isdir(static_dir):
  1096. filelist = resource_dir(plugin_static_path)
  1097. filelist.sort()
  1098. for filename in filelist:
  1099. filepath = os.path.join(plugin_static_path, filename)
  1100. if filename.endswith('.js'):
  1101. logger.info(_("Concatenating: %s") % filepath)
  1102. f.write(resource(filepath) + u'\n')
  1103. # Gate One applications
  1104. for application in appslist:
  1105. if enabled_applications:
  1106. # Only export JS of enabled apps
  1107. if application not in enabled_applications:
  1108. continue
  1109. app_static_path = '/applications/%s/static' % application
  1110. static_dir = resource_fn(app_static_path)
  1111. if os.path.isdir(static_dir):
  1112. filelist = resource_dir(app_static_path)
  1113. filelist.sort()
  1114. for filename in filelist:
  1115. filepath = os.path.join(app_static_path, filename)
  1116. if filename.endswith('.js'):
  1117. logger.info(_("Concatenating: %s") % filepath)
  1118. f.write(resource(filepath) + u'\n')
  1119. app_settings = all_settings['*'].get(application, None)
  1120. enabled_app_plugins = []
  1121. if app_settings:
  1122. enabled_app_plugins = app_settings.get('enabled_plugins', [])
  1123. app_plugins = '/applications/%s/plugins' % application
  1124. plugins_dir = resource_filename('gateone', app_plugins)
  1125. if os.path.isdir(plugins_dir):
  1126. pluginslist = resource_dir(app_plugins)
  1127. plugin_static_path = app_plugins + '/{plugin}/static'
  1128. pluginslist.sort()
  1129. # Gate One application plugins
  1130. for plugin in pluginslist:
  1131. # Only export JS of enabled app plugins
  1132. if enabled_app_plugins:
  1133. if plugin not in enabled_app_plugins:
  1134. continue
  1135. static_path = plugin_static_path.format(plugin=plugin)
  1136. static_dir = resource_fn(static_path)
  1137. if os.path.isdir(static_dir):
  1138. filelist = resource_dir(static_path)
  1139. filelist.sort()
  1140. for filename in filelist:
  1141. filepath = os.path.join(static_path, filename)
  1142. if filename.endswith('.js'):
  1143. logger.info(_("Concatenating: %s") % filepath)
  1144. f.write(resource(filepath) + u'\n')
  1145. f.flush()
  1146. logger.info(_("JavaScript concatenation completed: %s") % path)
  1147. logger.info(_(
  1148. "Don't forget to set '\"send_js\": false' in your 10server.conf to "
  1149. "disable sending of JavaScript"))
  1150. def combine_css(path, container, settings_dir=None):
  1151. """
  1152. Combines all application and plugin .css template files into one big one;
  1153. saved to the given *path*. Templates will be rendered using the given
  1154. *container* as the replacement for templates use of '#{{container}}'.
  1155. If given, *settings_dir* will be used to determine which applications and
  1156. plugins should be included in the dump based on what is enabled.
  1157. """
  1158. # A couple partials to save some space/typing
  1159. resource = lambda s: resource_string('gateone', s).decode('utf-8')
  1160. resource_fn = lambda s: resource_filename('gateone', s)
  1161. resource_dir = lambda s: resource_listdir('gateone', s)
  1162. if container.startswith('#'): # This is just in case (don't want ##gateone)
  1163. container = container.lstrip('#')
  1164. if not settings_dir:
  1165. settings_dir = resource_filename('gateone', '/settings')
  1166. all_settings = get_settings(settings_dir)
  1167. enabled_plugins = []
  1168. enabled_applications = []
  1169. embedded = False
  1170. url_prefix = '/'
  1171. if 'gateone' in all_settings['*']:
  1172. # The check above will fail in first-run situations
  1173. enabled_plugins = all_settings['*']['gateone'].get(
  1174. 'enabled_plugins', [])
  1175. enabled_applications = all_settings['*']['gateone'].get(
  1176. 'enabled_applications', [])
  1177. embedded = all_settings['*']['gateone'].get('embedded', False)
  1178. url_prefix = all_settings['*']['gateone'].get('url_prefix', False)
  1179. plugins_dir = resource_fn('/plugins')
  1180. pluginslist = resource_dir('/plugins')
  1181. pluginslist.sort()
  1182. applications_dir = resource_fn('/applications')
  1183. appslist = resource_dir('/applications')
  1184. appslist.sort()
  1185. logger.info(_("Combining all Gate One CSS into a single file..."))
  1186. global_themes_dir = resource_fn('/templates/themes')
  1187. themes = resource_dir('/templates/themes')
  1188. theme_writers = {}
  1189. for theme in themes:
  1190. combined_theme_path = "%s_theme_%s" % (
  1191. path.split('.css')[0], theme)
  1192. theme_writers[theme] = io.open(combined_theme_path, 'w')
  1193. theme_relpath = '/templates/themes/' + theme
  1194. themepath = resource_fn(th

Large files files are truncated, but you can click here to view the full file