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 @@
+
+
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,