diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html index 8cbc13b02..2c38f3766 100644 --- a/static/directives/config/config-setup-tool.html +++ b/static/directives/config/config-setup-tool.html @@ -340,6 +340,37 @@ + +
+
+ BitTorrent-based download +
+
+
+

If enabled, all images in the registry can be downloaded using the quayctl tool via the BitTorrent protocol. A JWT-compatible BitTorrent tracker such as Chihaya must be run. +

+ +
+ Enable BitTorrent downloads +
+ + + + + + +
Announce URL: + +
+ The HTTP URL at which the torrents should be announced. A JWT-compatible tracker such as Chihaya must be run to ensure proper security. Documentation on running Chihaya with + this support can be found at Running Chihaya for Quay Enterprise. +
+
+
+
+
diff --git a/static/js/core-config-setup.js b/static/js/core-config-setup.js index 4d9f5b138..dbed56e76 100644 --- a/static/js/core-config-setup.js +++ b/static/js/core-config-setup.js @@ -67,6 +67,10 @@ angular.module("core-config-setup", ['angularFileUpload']) {'id': 'security-scanner', 'title': 'Quay Security Scanner', 'condition': function(config) { return config.FEATURE_SECURITY_SCANNER; + }}, + + {'id': 'bittorrent', 'title': 'BitTorrent downloads', 'condition': function(config) { + return config.FEATURE_BITTORRENT; }} ]; diff --git a/util/config/configutil.py b/util/config/configutil.py index f54574193..c52d22928 100644 --- a/util/config/configutil.py +++ b/util/config/configutil.py @@ -1,4 +1,5 @@ from random import SystemRandom +from uuid import uuid4 def generate_secret_key(): cryptogen = SystemRandom() @@ -52,6 +53,10 @@ def add_enterprise_config_defaults(config_obj, current_secret_key, hostname): if not 'SECRET_KEY' in config_obj: config_obj['SECRET_KEY'] = current_secret_key + # Default torrent pepper. + if not 'BITTORRENT_FILENAME_PEPPER' in config_obj: + config_obj['BITTORRENT_FILENAME_PEPPER'] = str(uuid4()) + # Default storage configuration. if not 'DISTRIBUTED_STORAGE_CONFIG' in config_obj: config_obj['DISTRIBUTED_STORAGE_PREFERENCE'] = ['default'] diff --git a/util/config/validator.py b/util/config/validator.py index 73696aa26..1216047b6 100644 --- a/util/config/validator.py +++ b/util/config/validator.py @@ -12,6 +12,7 @@ import redis from flask import Flask from flask_mail import Mail, Message +from hashlib import sha1 from app import app, config_provider, get_app_url, OVERRIDE_CONFIG_DIRECTORY from auth.auth_context import get_authenticated_user @@ -25,6 +26,7 @@ from data.users.keystone import KeystoneUsers from storage import get_storage_driver from util.config.oauth import GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig from util.secscan.api import SecurityScannerAPI +from util.registry.torrent import torrent_jwt from util.security.signing import SIGNING_ENGINES @@ -452,6 +454,42 @@ def _validate_security_scanner(config, _): raise Exception('Could not ping security scanner: %s' % message) +def _validate_bittorrent(config, _): + """ Validates the configuration for using BitTorrent for downloads. """ + + # Ensure that the tracker is reachable and accepts requests signed with a registry key. + client = app.config['HTTPCLIENT'] + + params = { + 'info_hash': sha1('somedata').digest(), + 'peer_id': '-QUAY00-6wfG2wk6wWLc', + 'uploaded': 0, + 'downloaded': 0, + 'left': 0, + 'numwant': 0, + 'port': 80, + } + + encoded_jwt = torrent_jwt(params) + params['jwt'] = encoded_jwt + + resp = client.get(config['BITTORRENT_ANNOUNCE_URL'], timeout=5, params=params) + logger.debug('Got tracker response: %s: %s', resp.status_code, resp.text) + + if resp.status_code == 404: + raise Exception('Announce path not found; did you forget `/announce`?') + + if resp.status_code == 500: + raise Exception('Did not get expected response from Tracker; please check your settings') + + if resp.status_code == 200: + if 'invalid jwt' in resp.text: + raise Exception('Could not authorize to Tracker; is your Tracker properly configured?') + + if 'failure reason' in resp.text: + raise Exception('Could not validate signed announce request: ' + resp.text) + + _VALIDATORS = { 'database': _validate_database, 'redis': _validate_redis, @@ -468,4 +506,5 @@ _VALIDATORS = { 'keystone': _validate_keystone, 'signer': _validate_signer, 'security-scanner': _validate_security_scanner, + 'bittorrent': _validate_bittorrent, } diff --git a/util/registry/torrent.py b/util/registry/torrent.py index ec93e1405..74db8dd70 100644 --- a/util/registry/torrent.py +++ b/util/registry/torrent.py @@ -21,7 +21,10 @@ def _load_private_key(private_key_file_path): with open(private_key_file_path) as private_key_file: return private_key_file.read() -def _torrent_jwt(info_dict): +def torrent_jwt(info_dict): + """ Returns an encoded JWT for the given information dictionary, signed by the local instance's + private key. + """ token_data = { 'iss': instance_keys.service_name, 'aud': ANNOUNCE_URL, @@ -45,7 +48,7 @@ def make_torrent(name, webseed, length, piece_length, pieces): } return bencode.bencode({ - 'announce': ANNOUNCE_URL + "?jwt=" + _torrent_jwt(info_dict), + 'announce': ANNOUNCE_URL + "?jwt=" + torrent_jwt(info_dict), 'url-list': webseed, 'encoding': 'UTF-8', 'created by': REGISTRY_TITLE,