def create(self, files): if self.cleaned_data['generate_key']: try: SSHClient().generate_user_key() except IOError as e: self.errors['generate_key'] = forms.util.ErrorList([ _('Unable to write SSH key file: %s') % e ]) raise except Exception as e: self.errors['generate_key'] = forms.util.ErrorList([ _('Error generating SSH key: %s') % e ]) raise elif self.cleaned_data['keyfile']: try: SSHClient().import_user_key(files['keyfile']) except IOError as e: self.errors['keyfile'] = forms.util.ErrorList([ _('Unable to write SSH key file: %s') % e ]) raise except Exception as e: self.errors['keyfile'] = forms.util.ErrorList([ _('Error uploading SSH key: %s') % e ]) raise
def test_import_user_key(self, namespace=None): """Testing SSHClient.import_user_key""" self._set_home(self.tempdir) client = SSHClient(namespace=namespace) client.import_user_key(test_keys.rsa_key) self.assertEqual(client.get_user_key(), test_keys.rsa_key)
def check_host(hostname, username=None, password=None, namespace=None): """ Checks if we can connect to a host with a known key. This will raise an exception if we cannot connect to the host. The exception will be one of BadHostKeyError, UnknownHostKeyError, or SCMError. """ from django.conf import settings client = SSHClient(namespace=namespace) client.set_missing_host_key_policy(RaiseUnknownHostKeyPolicy()) kwargs = {} # We normally want to notify on unknown host keys, but not when running # unit tests. if getattr(settings, 'RUNNING_TEST', False): client.set_missing_host_key_policy(paramiko.WarningPolicy()) kwargs['allow_agent'] = False try: client.connect(hostname, username=username, password=password, pkey=client.get_user_key(), **kwargs) except paramiko.BadHostKeyException, e: raise BadHostKeyError(e.hostname, e.key, e.expected_key)
def ssh_settings(request, template_name='admin/ssh_settings.html'): client = SSHClient() key = client.get_user_key() if request.method == 'POST': form = SSHSettingsForm(request.POST, request.FILES) if form.is_valid(): if form.did_request_delete() and client.get_user_key() is not None: try: form.delete() return HttpResponseRedirect('.') except Exception as e: logging.error('Deleting SSH key failed: %s' % e) else: try: form.create(request.FILES) return HttpResponseRedirect('.') except Exception as e: # Fall through. It will be reported inline and in the log. logging.error('Uploading SSH key failed: %s' % e) else: form = SSHSettingsForm() if key: fingerprint = humanize_key(key) else: fingerprint = None return render_to_response(template_name, RequestContext(request, { 'key': key, 'fingerprint': fingerprint, 'public_key': client.get_public_key(key), 'form': form, }))
def create(self, files): """Generate or import an SSH key. This will generate a new SSH key if :py:attr:`generate_key` was set to ``True``. Otherwise, a if :py:attr:`keyfile` was provided, its corresponding file upload will be used as the new key. In either case, the key will be validated, and if validation fails, an error will be set for the appropriate field. Args: files (django.utils.datastructures.MultiValueDict): The files uploaded in the request. This may contain a ``keyfile`` entry representing a key to upload. Raises: Exception: There was an error generating or importing a key. The form will have a suitable error for the field triggering the error. """ if self.cleaned_data['generate_key']: try: SSHClient().generate_user_key() except IOError as e: self.add_error( 'generate_key', ugettext('Unable to write SSH key file: %s') % e) raise except Exception as e: self.add_error( 'generate_key', ugettext('Error generating SSH key: %s') % e) raise elif self.cleaned_data['upload_key']: try: SSHClient().import_user_key(files['keyfile']) except IOError as e: self.add_error( 'keyfile', ugettext('Unable to write SSH key file: %s') % e) raise except Exception as e: self.add_error( 'keyfile', ugettext('Error uploading SSH key: %s') % e) raise
def check_host(netloc, username=None, password=None, namespace=None): """ Checks if we can connect to a host with a known key. This will raise an exception if we cannot connect to the host. The exception will be one of BadHostKeyError, UnknownHostKeyError, or SCMError. """ from django.conf import settings client = SSHClient(namespace=namespace) client.set_missing_host_key_policy(RaiseUnknownHostKeyPolicy()) kwargs = {} if ':' in netloc: hostname, port = netloc.split(':') try: port = int(port) except ValueError: raise SSHInvalidPortError(port) else: hostname = netloc port = SSH_PORT # We normally want to notify on unknown host keys, but not when running # unit tests. if getattr(settings, 'RUNNING_TEST', False): client.set_missing_host_key_policy(paramiko.WarningPolicy()) kwargs['allow_agent'] = False try: client.connect(hostname, port, username=username, password=password, pkey=client.get_user_key(), **kwargs) except paramiko.BadHostKeyException as e: raise BadHostKeyError(e.hostname, e.key, e.expected_key) except paramiko.AuthenticationException as e: # Some AuthenticationException instances have allowed_types set, # and some don't. allowed_types = getattr(e, 'allowed_types', []) if 'publickey' in allowed_types: key = client.get_user_key() else: key = None raise SSHAuthenticationError(allowed_types=allowed_types, user_key=key) except paramiko.SSHException as e: msg = six.text_type(e) if msg == 'No authentication methods available': raise SSHAuthenticationError else: raise SSHError(msg)
def test_generate_user_key(self, namespace=None): """Testing SSHClient.generate_user_key""" self._set_home(self.tempdir) client = SSHClient(namespace=namespace) key = client.generate_user_key(bits=1024) key_file = os.path.join(client.storage.get_ssh_dir(), 'id_rsa') self.assertTrue(os.path.exists(key_file)) self.assertEqual(client.get_user_key(), key)
def delete(self): """Try to delete the user SSH key upon request.""" if self.cleaned_data['delete_key']: try: SSHClient().delete_user_key() except Exception as e: self.errors['delete_key'] = forms.util.ErrorList( [_('Unable to delete SSH key file: %s') % e]) raise
def setUp(self): super(SSHSettingsFormTestCase, self).setUp() # Setup temp directory to prevent the original ssh related # configurations been overwritten. self.old_home = os.getenv('HOME') self.tempdir = tempfile.mkdtemp(prefix='rb-tests-home-') os.environ['RBSSH_ALLOW_AGENT'] = '0' self._set_home(self.tempdir) self.ssh_client = SSHClient()
def setUp(self): # Setup temp directory to prevent the original ssh related # configurations been overwritten. self.old_home = os.getenv('HOME') self.tempdir = tempfile.mkdtemp(prefix='rb-tests-home-') os.environ['RBSSH_ALLOW_AGENT'] = '0' self._set_home(self.tempdir) # Init client for http request, ssh_client for ssh config manipulation. self.client = Client() self.ssh_client = SSHClient()
def test_delete_user_key(self, namespace=None): """Testing SSHClient.delete_user_key""" self._set_home(self.tempdir) client = SSHClient(namespace=namespace) client.import_user_key(test_keys.rsa_key) key_file = os.path.join(client.storage.get_ssh_dir(), 'id_rsa') self.assertTrue(os.path.exists(key_file)) self.assertEqual(client.get_user_key(), test_keys.rsa_key) client.delete_user_key() self.assertFalse(os.path.exists(key_file))
def ssh_settings(request, template_name='admin/ssh_settings.html'): client = SSHClient() key = client.get_user_key() if request.method == 'POST': form = SSHSettingsForm(request.POST, request.FILES) if form.is_valid(): try: form.create(request.FILES) return HttpResponseRedirect('.') except Exception, e: # Fall through. It will be reported inline and in the log. logging.error('Uploading SSH key failed: %s' % e)
def test_add_host_key(self, namespace=None): """Testing SSHClient.add_host_key""" self._set_home(self.tempdir) client = SSHClient(namespace=namespace) client.add_host_key('example.com', self.key1) known_hosts_file = client.storage.get_host_keys_filename() self.assertTrue(os.path.exists(known_hosts_file)) with open(known_hosts_file, 'r') as f: lines = f.readlines() self.assertEqual(len(lines), 1) self.assertEqual(lines[0].split(), ['example.com', self.key1.get_name(), self.key1_b64])
def delete(self): """Delete the configured SSH user key. This will only delete the key if :py:attr:`delete_key` was set. Raises: Exception: There was an unexpected error deleting the key. A validation error will be set for the ``delete_key`` field. """ if self.did_request_delete(): try: SSHClient().delete_user_key() except Exception as e: self.errors['delete_key'] = forms.util.ErrorList( [ugettext('Unable to delete SSH key file: %s') % e]) raise
def _check_can_test_ssh(self): """Check whether SSH-based tests can be run. This will check if the user's SSH keys is authorized by the local machine, for authentication. If so, SSH-based tests can be attempted. If SSH-based tests cannot be run, the current test will be flagged as skipped. """ if SCMTestCase._can_test_ssh is None: SCMTestCase.ssh_client = SSHClient() key = self.ssh_client.get_user_key() SCMTestCase._can_test_ssh = \ key is not None and self.ssh_client.is_key_authorized(key) if not SCMTestCase._can_test_ssh: raise nose.SkipTest( "Cannot perform SSH access tests. The local user's SSH " "public key must be in the %s file and SSH must be enabled." % os.path.join(self.ssh_client.storage.get_ssh_dir(), 'authorized_keys'))
def ssh_settings(request, template_name='admin/ssh_settings.html'): """Render the SSH settings page.""" client = SSHClient() key = client.get_user_key() if request.method == 'POST': form = SSHSettingsForm(request.POST, request.FILES) if form.is_valid(): if form.did_request_delete() and client.get_user_key() is not None: try: form.delete() return HttpResponseRedirect('.') except Exception as e: logger.error('Deleting SSH key failed: %s' % e) else: try: form.create(request.FILES) return HttpResponseRedirect('.') except Exception as e: # Fall through. It will be reported inline and in the log. logger.error('Uploading SSH key failed: %s' % e) else: form = SSHSettingsForm() if key: fingerprint = humanize_key(key) else: fingerprint = None return render(request=request, template_name=template_name, context={ 'has_file_field': True, 'key': key, 'fingerprint': fingerprint, 'public_key': client.get_public_key(key).replace('\n', ''), 'form': form, })
def create(self, files): if self.cleaned_data['generate_key']: try: SSHClient().generate_user_key() except IOError, e: self.errors['generate_key'] = forms.util.ErrorList( [_('Unable to write SSH key file: %s') % e]) raise except Exception, e: self.errors['generate_key'] = forms.util.ErrorList( [_('Error generating SSH key: %s') % e]) raise elif self.cleaned_data['keyfile']: try: SSHClient().import_user_key(files['keyfile']) except IOError, e: self.errors['keyfile'] = forms.util.ErrorList( [_('Unable to write SSH key file: %s') % e]) raise except Exception, e: self.errors['keyfile'] = forms.util.ErrorList( [_('Error uploading SSH key: %s') % e]) raise class Meta: title = _('SSH Settings') class StorageSettingsForm(SiteSettingsForm): """File storage backend settings for Review Board."""
class RepositoryForm(forms.ModelForm): """A form for creating and updating repositories. This form provides an interface for creating and updating repositories, handling the association with hosting services, linking accounts, dealing with SSH keys and SSL certificates, and more. """ REPOSITORY_INFO_FIELDSET = _('Repository Information') BUG_TRACKER_FIELDSET = _('Bug Tracker') NO_HOSTING_SERVICE_ID = 'custom' NO_HOSTING_SERVICE_NAME = _('(None - Custom Repository)') NO_BUG_TRACKER_ID = 'none' NO_BUG_TRACKER_NAME = _('(None)') CUSTOM_BUG_TRACKER_ID = 'custom' CUSTOM_BUG_TRACKER_NAME = _('(Custom Bug Tracker)') IGNORED_SERVICE_IDS = ('none', 'custom') DEFAULT_PLAN_ID = 'default' DEFAULT_PLAN_NAME = _('Default') # Host trust state reedit_repository = forms.BooleanField(label=_("Re-edit repository"), required=False) trust_host = forms.BooleanField(label=_("I trust this host"), required=False) # Repository Hosting fields hosting_type = forms.ChoiceField(label=_("Hosting service"), required=True, initial=NO_HOSTING_SERVICE_ID) hosting_account = forms.ModelChoiceField( label=_('Account'), required=True, empty_label=_('<Link a new account>'), help_text=_("Link this repository to an account on the hosting " "service. This username may be used as part of the " "repository URL, depending on the hosting service and " "plan."), queryset=HostingServiceAccount.objects.none()) hosting_account_username = forms.CharField( label=_('Account username'), required=True, widget=forms.TextInput(attrs={ 'size': 30, 'autocomplete': 'off' })) hosting_account_password = forms.CharField( label=_('Account password'), required=True, widget=forms.PasswordInput(attrs={ 'size': 30, 'autocomplete': 'off' })) # Repository Information fields tool = forms.ModelChoiceField(label=_("Repository type"), required=True, empty_label=None, queryset=Tool.objects.all()) repository_plan = forms.ChoiceField( label=_('Repository plan'), required=True, help_text=_('The plan for your repository on this hosting service. ' 'This must match what is set for your repository.')) # Bug Tracker fields bug_tracker_use_hosting = forms.BooleanField( label=_("Use hosting service's bug tracker"), initial=False, required=False) bug_tracker_type = forms.ChoiceField(label=_("Type"), required=True, initial=NO_BUG_TRACKER_ID) bug_tracker_plan = forms.ChoiceField(label=_('Bug tracker plan'), required=True) bug_tracker_hosting_account_username = forms.CharField( label=_('Account username'), required=True, widget=forms.TextInput(attrs={ 'size': 30, 'autocomplete': 'off' })) bug_tracker = forms.CharField( label=_("Bug tracker URL"), max_length=256, required=False, widget=forms.TextInput(attrs={'size': '60'}), help_text=_( "The optional path to the bug tracker for this " "repository. The path should resemble: " "http://www.example.com/issues?id=%s, where %s will be the " "bug number."), validators=[validate_bug_tracker]) def __init__(self, *args, **kwargs): self.local_site_name = kwargs.pop('local_site_name', None) super(RepositoryForm, self).__init__(*args, **kwargs) self.hostkeyerror = None self.certerror = None self.userkeyerror = None self.hosting_account_linked = False self.local_site = None self.repository_forms = {} self.bug_tracker_forms = {} self.hosting_service_info = {} self.validate_repository = True self.cert = None # Determine the local_site that will be associated with any # repository coming from this form. # # We're careful to disregard any local_sites that are specified # from the form data. The caller needs to pass in a local_site_name # to ensure that it will be used. if self.local_site_name: self.local_site = LocalSite.objects.get(name=self.local_site_name) elif self.instance and self.instance.local_site: self.local_site = self.instance.local_site self.local_site_name = self.local_site.name elif self.fields['local_site'].initial: self.local_site = self.fields['local_site'].initial self.local_site_name = self.local_site.name # Grab the entire list of HostingServiceAccounts that can be # used by this form. When the form is actually being used by the # user, the listed accounts will consist only of the ones available # for the selected hosting service. hosting_accounts = HostingServiceAccount.objects.accessible( local_site=self.local_site) self.fields['hosting_account'].queryset = hosting_accounts # Standard forms don't support 'instance', so don't pass it through # to any created hosting service forms. if 'instance' in kwargs: kwargs.pop('instance') # Load the list of repository forms and hosting services. hosting_service_choices = [] bug_tracker_choices = [] for hosting_service_id, hosting_service in get_hosting_services(): if hosting_service.supports_repositories: hosting_service_choices.append( (hosting_service_id, hosting_service.name)) if hosting_service.supports_bug_trackers: bug_tracker_choices.append( (hosting_service_id, hosting_service.name)) self.bug_tracker_forms[hosting_service_id] = {} self.repository_forms[hosting_service_id] = {} self.hosting_service_info[hosting_service_id] = { 'scmtools': hosting_service.supported_scmtools, 'plans': [], 'planInfo': {}, 'needs_authorization': hosting_service.needs_authorization, 'supports_bug_trackers': hosting_service.supports_bug_trackers, 'accounts': [{ 'pk': account.pk, 'username': account.username, 'is_authorized': account.is_authorized, } for account in hosting_accounts if account.service_name == hosting_service_id], } try: if hosting_service.plans: for type_id, info in hosting_service.plans: form = info.get('form', None) if form: self._load_hosting_service(hosting_service_id, hosting_service, type_id, info['name'], form, *args, **kwargs) elif hosting_service.form: self._load_hosting_service(hosting_service_id, hosting_service, self.DEFAULT_PLAN_ID, self.DEFAULT_PLAN_NAME, hosting_service.form, *args, **kwargs) except Exception, e: logging.error('Error loading hosting service %s: %s' % (hosting_service_id, e), exc_info=1) # Build the list of hosting service choices, sorted, with # "None" being first. hosting_service_choices.sort(key=lambda x: x[1]) hosting_service_choices.insert( 0, (self.NO_HOSTING_SERVICE_ID, self.NO_HOSTING_SERVICE_NAME)) self.fields['hosting_type'].choices = hosting_service_choices # Now do the same for bug trackers, but have separate None and Custom # entries. bug_tracker_choices.sort(key=lambda x: x[1]) bug_tracker_choices.insert( 0, (self.NO_BUG_TRACKER_ID, self.NO_BUG_TRACKER_NAME)) bug_tracker_choices.insert( 1, (self.CUSTOM_BUG_TRACKER_ID, self.CUSTOM_BUG_TRACKER_NAME)) self.fields['bug_tracker_type'].choices = bug_tracker_choices # Get the current SSH public key that would be used for repositories, # if one has been created. self.ssh_client = SSHClient(namespace=self.local_site_name) self.public_key = self.ssh_client.get_public_key( self.ssh_client.get_user_key()) if self.instance: self._populate_hosting_service_fields() self._populate_bug_tracker_fields()
def main(): if DEBUG: pid = os.getpid() log_filename = 'rbssh-%s.log' % pid if DEBUG_LOGDIR: log_path = os.path.join(DEBUG_LOGDIR, log_filename) else: log_path = log_filename logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(name)-18s %(levelname)-8s ' '%(message)s', datefmt='%m-%d %H:%M', filename=log_path, filemode='w') logging.debug('%s' % sys.argv) logging.debug('PID %s' % pid) ch = logging.StreamHandler() ch.setLevel(logging.INFO) ch.setFormatter(logging.Formatter('%(message)s')) ch.addFilter(logging.Filter('root')) logging.getLogger('').addHandler(ch) path, command = parse_options(sys.argv[1:]) if '://' not in path: path = 'ssh://' + path username, hostname = SCMTool.get_auth_from_uri(path, options.username) if username is None: username = getpass.getuser() logging.debug('!!! %s, %s, %s' % (hostname, username, command)) client = SSHClient(namespace=options.local_site_name) client.set_missing_host_key_policy(paramiko.WarningPolicy()) attempts = 0 password = None key = client.get_user_key() while True: try: client.connect(hostname, username=username, password=password, pkey=key, allow_agent=options.allow_agent) break except paramiko.AuthenticationException, e: if attempts == 3 or not sys.stdin.isatty(): logging.error('Too many authentication failures for %s' % username) sys.exit(1) attempts += 1 password = getpass.getpass("%s@%s's password: " % (username, hostname)) except paramiko.SSHException, e: logging.error('Error connecting to server: %s' % e) sys.exit(1)
def _check_repository(self, scmtool_class, path, username, password, local_site, trust_host, ret_cert, request): if local_site: local_site_name = local_site.name else: local_site_name = None while 1: # Keep doing this until we have an error we don't want # to ignore, or it's successful. try: scmtool_class.check_repository(path, username, password, local_site_name) return None except RepositoryNotFoundError: return MISSING_REPOSITORY except BadHostKeyError as e: if trust_host: try: client = SSHClient(namespace=local_site_name) client.replace_host_key(e.hostname, e.raw_expected_key, e.raw_key) except IOError as e: return SERVER_CONFIG_ERROR, { 'reason': six.text_type(e), } else: return BAD_HOST_KEY, { 'hostname': e.hostname, 'expected_key': e.raw_expected_key.get_base64(), 'key': e.raw_key.get_base64(), } except UnknownHostKeyError as e: if trust_host: try: client = SSHClient(namespace=local_site_name) client.add_host_key(e.hostname, e.raw_key) except IOError as e: return SERVER_CONFIG_ERROR, { 'reason': six.text_type(e), } else: return UNVERIFIED_HOST_KEY, { 'hostname': e.hostname, 'key': e.raw_key.get_base64(), } except UnverifiedCertificateError as e: if trust_host: try: cert = scmtool_class.accept_certificate( path, local_site_name) if cert: ret_cert.update(cert) except IOError as e: return SERVER_CONFIG_ERROR, { 'reason': six.text_type(e), } else: return UNVERIFIED_HOST_CERT, { 'certificate': { 'failures': e.certificate.failures, 'fingerprint': e.certificate.fingerprint, 'hostname': e.certificate.hostname, 'issuer': e.certificate.issuer, 'valid': { 'from': e.certificate.valid_from, 'until': e.certificate.valid_until, }, }, } except AuthenticationError as e: if 'publickey' in e.allowed_types and e.user_key is None: return MISSING_USER_KEY else: return REPO_AUTHENTICATION_ERROR, { 'reason': six.text_type(e), } except SSHError as e: logging.error('Got unexpected SSHError when checking ' 'repository: %s' % e, exc_info=1, request=request) return REPO_INFO_ERROR, { 'error': six.text_type(e), } except SCMError as e: logging.error('Got unexpected SCMError when checking ' 'repository: %s' % e, exc_info=1, request=request) return REPO_INFO_ERROR, { 'error': six.text_type(e), } except Exception as e: logging.error('Unknown error in checking repository %s: %s', path, e, exc_info=1, request=request) # We should give something better, but I don't have anything. # This will at least give a HTTP 500. raise
def main(): """Run the application.""" # We don't want any warnings to end up impacting output. warnings.simplefilter('ignore') if DEBUG: pid = os.getpid() log_filename = 'rbssh-%s.log' % pid if DEBUG_LOGDIR: log_path = os.path.join(DEBUG_LOGDIR, log_filename) else: log_path = log_filename logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(name)-18s %(levelname)-8s ' '%(message)s', datefmt='%m-%d %H:%M', filename=log_path, filemode='w') debug('%s', sys.argv) debug('PID %s', pid) # Perform the bare minimum to initialize the Django/Review Board # environment. We're not calling Review Board's initialize() because # we want to completely minimize what we import and set up. if hasattr(django, 'setup'): django.setup() from reviewboard.scmtools.core import SCMTool from reviewboard.ssh.client import SSHClient ch = logging.StreamHandler() ch.setLevel(logging.INFO) ch.setFormatter(logging.Formatter('%(message)s')) ch.addFilter(logging.Filter('root')) logging.getLogger('').addHandler(ch) path, port, command = parse_options(sys.argv[1:]) if '://' not in path: path = 'ssh://' + path username, hostname = SCMTool.get_auth_from_uri(path, options.username) if username is None: username = getpass.getuser() client = SSHClient(namespace=options.local_site_name) client.set_missing_host_key_policy(paramiko.WarningPolicy()) if command: purpose = command else: purpose = 'interactive shell' debug('!!! SSH backend = %s', type(client.storage)) debug('!!! Preparing to connect to %s@%s for %s', username, hostname, purpose) attempts = 0 password = None key = client.get_user_key() while True: try: client.connect(hostname, port, username=username, password=password, pkey=key, allow_agent=options.allow_agent) break except paramiko.AuthenticationException as e: if attempts == 3 or not sys.stdin.isatty(): logging.error('Too many authentication failures for %s' % username) sys.exit(1) attempts += 1 password = getpass.getpass("%s@%s's password: " % (username, hostname)) except paramiko.SSHException as e: logging.error('Error connecting to server: %s' % e) sys.exit(1) except Exception as e: logging.error('Unknown exception during connect: %s (%s)' % (e, type(e))) sys.exit(1) transport = client.get_transport() channel = transport.open_session() if sys.platform in ('cygwin', 'win32'): debug('!!! Using WindowsHandler') handler = WindowsHandler(channel) else: debug('!!! Using PosixHandler') handler = PosixHandler(channel) if options.subsystem == 'sftp': debug('!!! Invoking sftp subsystem') channel.invoke_subsystem('sftp') handler.transfer() elif command: debug('!!! Sending command %s', command) channel.exec_command(' '.join(command)) handler.transfer() else: debug('!!! Opening shell') channel.get_pty() channel.invoke_shell() handler.shell() debug('!!! Done') status = channel.recv_exit_status() client.close() return status
def main(): """Run the application.""" os.environ.setdefault(str('DJANGO_SETTINGS_MODULE'), str('reviewboard.settings')) if DEBUG: pid = os.getpid() log_filename = 'rbssh-%s.log' % pid if DEBUG_LOGDIR: log_path = os.path.join(DEBUG_LOGDIR, log_filename) else: log_path = log_filename logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(name)-18s %(levelname)-8s ' '%(message)s', datefmt='%m-%d %H:%M', filename=log_path, filemode='w') logging.debug('%s' % sys.argv) logging.debug('PID %s' % pid) initialize() ch = logging.StreamHandler() ch.setLevel(logging.INFO) ch.setFormatter(logging.Formatter('%(message)s')) ch.addFilter(logging.Filter('root')) logging.getLogger('').addHandler(ch) path, port, command = parse_options(sys.argv[1:]) if '://' not in path: path = 'ssh://' + path username, hostname = SCMTool.get_auth_from_uri(path, options.username) if username is None: username = getpass.getuser() logging.debug('!!! %s, %s, %s' % (hostname, username, command)) client = SSHClient(namespace=options.local_site_name) client.set_missing_host_key_policy(paramiko.WarningPolicy()) attempts = 0 password = None key = client.get_user_key() while True: try: client.connect(hostname, port, username=username, password=password, pkey=key, allow_agent=options.allow_agent) break except paramiko.AuthenticationException as e: if attempts == 3 or not sys.stdin.isatty(): logging.error('Too many authentication failures for %s' % username) sys.exit(1) attempts += 1 password = getpass.getpass("%s@%s's password: " % (username, hostname)) except paramiko.SSHException as e: logging.error('Error connecting to server: %s' % e) sys.exit(1) except Exception as e: logging.error('Unknown exception during connect: %s (%s)' % (e, type(e))) sys.exit(1) transport = client.get_transport() channel = transport.open_session() if sys.platform in ('cygwin', 'win32'): logging.debug('!!! Using WindowsHandler') handler = WindowsHandler(channel) else: logging.debug('!!! Using PosixHandler') handler = PosixHandler(channel) if options.subsystem == 'sftp': logging.debug('!!! Invoking sftp subsystem') channel.invoke_subsystem('sftp') handler.transfer() elif command: logging.debug('!!! Sending command %s' % command) channel.exec_command(' '.join(command)) handler.transfer() else: logging.debug('!!! Opening shell') channel.get_pty() channel.invoke_shell() handler.shell() logging.debug('!!! Done') status = channel.recv_exit_status() client.close() return status
def __init__(self, *args, **kwargs): self.local_site_name = kwargs.pop('local_site_name', None) super(RepositoryForm, self).__init__(*args, **kwargs) self.hostkeyerror = None self.certerror = None self.userkeyerror = None self.hosting_account_linked = False self.local_site = None self.repository_forms = {} self.bug_tracker_forms = {} self.hosting_service_info = {} self.validate_repository = True self.cert = None # Determine the local_site that will be associated with any # repository coming from this form. # # We're careful to disregard any local_sites that are specified # from the form data. The caller needs to pass in a local_site_name # to ensure that it will be used. if self.local_site_name: self.local_site = LocalSite.objects.get(name=self.local_site_name) elif self.instance and self.instance.local_site: self.local_site = self.instance.local_site self.local_site_name = self.local_site.name elif self.fields['local_site'].initial: self.local_site = self.fields['local_site'].initial self.local_site_name = self.local_site.name # Grab the entire list of HostingServiceAccounts that can be # used by this form. When the form is actually being used by the # user, the listed accounts will consist only of the ones available # for the selected hosting service. hosting_accounts = HostingServiceAccount.objects.accessible( local_site=self.local_site) self.fields['hosting_account'].queryset = hosting_accounts # Standard forms don't support 'instance', so don't pass it through # to any created hosting service forms. if 'instance' in kwargs: kwargs.pop('instance') # Load the list of repository forms and hosting services. hosting_service_choices = [] bug_tracker_choices = [] for hosting_service_id, hosting_service in get_hosting_services(): if hosting_service.supports_repositories: hosting_service_choices.append((hosting_service_id, hosting_service.name)) if hosting_service.supports_bug_trackers: bug_tracker_choices.append((hosting_service_id, hosting_service.name)) self.bug_tracker_forms[hosting_service_id] = {} self.repository_forms[hosting_service_id] = {} self.hosting_service_info[hosting_service_id] = { 'scmtools': hosting_service.supported_scmtools, 'plans': [], 'planInfo': {}, 'self_hosted': hosting_service.self_hosted, 'needs_authorization': hosting_service.needs_authorization, 'supports_bug_trackers': hosting_service.supports_bug_trackers, 'supports_ssh_key_association': hosting_service.supports_ssh_key_association, 'supports_two_factor_auth': hosting_service.supports_two_factor_auth, 'needs_two_factor_auth_code': False, 'accounts': [ { 'pk': account.pk, 'hosting_url': account.hosting_url, 'username': account.username, 'is_authorized': account.is_authorized, } for account in hosting_accounts if account.service_name == hosting_service_id ], } try: if hosting_service.plans: for type_id, info in hosting_service.plans: form = info.get('form', None) if form: self._load_hosting_service(hosting_service_id, hosting_service, type_id, info['name'], form, *args, **kwargs) elif hosting_service.form: self._load_hosting_service(hosting_service_id, hosting_service, self.DEFAULT_PLAN_ID, self.DEFAULT_PLAN_NAME, hosting_service.form, *args, **kwargs) except Exception as e: logging.error('Error loading hosting service %s: %s' % (hosting_service_id, e), exc_info=1) # Build the list of hosting service choices, sorted, with # "None" being first. hosting_service_choices.sort(key=lambda x: x[1]) hosting_service_choices.insert(0, (self.NO_HOSTING_SERVICE_ID, self.NO_HOSTING_SERVICE_NAME)) self.fields['hosting_type'].choices = hosting_service_choices # Now do the same for bug trackers, but have separate None and Custom # entries. bug_tracker_choices.sort(key=lambda x: x[1]) bug_tracker_choices.insert(0, (self.NO_BUG_TRACKER_ID, self.NO_BUG_TRACKER_NAME)) bug_tracker_choices.insert(1, (self.CUSTOM_BUG_TRACKER_ID, self.CUSTOM_BUG_TRACKER_NAME)) self.fields['bug_tracker_type'].choices = bug_tracker_choices # Get the current SSH public key that would be used for repositories, # if one has been created. self.ssh_client = SSHClient(namespace=self.local_site_name) ssh_key = self.ssh_client.get_user_key() if ssh_key: self.public_key = self.ssh_client.get_public_key(ssh_key) self.public_key_str = '%s %s' % ( ssh_key.get_name(), ''.join(six.text_type(self.public_key).splitlines()) ) else: self.public_key = None self.public_key_str = '' # If no SSH key has been created, disable the key association field. if not self.public_key: self.fields['associate_ssh_key'].help_text = \ self.NO_KEY_HELP_FMT % local_site_reverse( 'settings-ssh', local_site_name=self.local_site_name) self.fields['associate_ssh_key'].widget.attrs['disabled'] = \ 'disabled' if self.instance: self._populate_repository_info_fields() self._populate_hosting_service_fields() self._populate_bug_tracker_fields()
def _test_ssh_with_site(self, repo_path, filename=None): """Helper for testing an SSH connection and using a Local Site. This will attempt to SSH into the local machine and connect to the given repository, using an SSH key and repository based on a Local Site. It will check the repository for validity and optionally fetch a file. If this is unable to connect to the local machine, the test will be flagged as skipped. Args: repo_path (unicode): The repository path to check. filename (unicode, optional): The optional file in the repository to fetch. """ self._check_can_test_ssh() # Get the user's .ssh key, for use in the tests user_key = self.ssh_client.get_user_key() self.assertNotEqual(user_key, None) # Switch to a new SSH directory. self.tempdir = mkdtemp(prefix='rb-tests-home-') sshdir = os.path.join(self.tempdir, '.ssh') self._set_home(self.tempdir) self.assertEqual(sshdir, self.ssh_client.storage.get_ssh_dir()) self.assertFalse(os.path.exists(os.path.join(sshdir, 'id_rsa'))) self.assertFalse(os.path.exists(os.path.join(sshdir, 'id_dsa'))) self.assertEqual(self.ssh_client.get_user_key(), None) tool_class = self.repository.tool # Make sure we aren't using the old SSH key. We want auth errors. repo = Repository(name='SSH Test', path=repo_path, tool=tool_class) tool = repo.get_scmtool() self.assertRaises(AuthenticationError, lambda: tool.check_repository(repo_path)) if filename: self.assertRaises(SCMError, lambda: tool.get_file(filename, HEAD)) for local_site_name in ('site-1',): local_site = LocalSite(name=local_site_name) local_site.save() repo = Repository(name='SSH Test', path=repo_path, tool=tool_class, local_site=local_site) tool = repo.get_scmtool() ssh_client = SSHClient(namespace=local_site_name) self.assertEqual(ssh_client.storage.get_ssh_dir(), os.path.join(sshdir, local_site_name)) ssh_client.import_user_key(user_key) self.assertEqual(ssh_client.get_user_key(), user_key) # Make sure we can verify the repository and access files. tool.check_repository(repo_path, local_site_name=local_site_name) if filename: self.assertNotEqual(tool.get_file(filename, HEAD), None)
def _check_can_test_ssh(self): """Check whether SSH-based tests can be run. This will check if the user's SSH keys are authorized by the local machine for authentication, and whether any system-wide tools are available. If SSH-based tests cannot be run, the current test will be flagged as skipped. """ # These tests are global across all unit tests using this class. if SCMTestCase._can_test_ssh is None: SCMTestCase.ssh_client = SSHClient() key = self.ssh_client.get_user_key() SCMTestCase._can_test_ssh = ( key is not None and self.ssh_client.is_key_authorized(key)) if not SCMTestCase._can_test_ssh: raise SkipTest( "Cannot perform SSH access tests. The local user's SSH " "public key must be in the %s file and SSH must be enabled." % os.path.join(self.ssh_client.storage.get_ssh_dir(), 'authorized_keys')) # These tests are local to all unit tests using the same executable. system_exes = self.ssh_required_system_exes if system_exes: user_key = SCMTestCase.ssh_client.get_user_key() exes_to_check = (set(system_exes) - set(SCMTestCase._ssh_system_exe_status.keys())) for system_exe in exes_to_check: # For safety, we'll do one connection per check, to avoid # one check impacting another. client = SSHClient() client.connect('localhost', pkey=user_key) try: stdout, stderr = client.exec_command('which %s' % system_exe)[1:] # It's important to read all stdout/stderr data before # waiting for status. stdout.read() stderr.read() code = stdout.channel.recv_exit_status() status = (code == 0) except Exception as e: logger.error( 'Unexpected error running `which %s` on ' 'localhost for SSH test: %s', system_exe, e) status = False finally: client.close() SCMTestCase._ssh_system_exe_status[system_exe] = status missing_exes = ', '.join( '"%s"' % _system_exe for _system_exe in system_exes if not SCMTestCase._ssh_system_exe_status[_system_exe]) if missing_exes: raise SkipTest( 'Cannot perform SSH access tests. %s must be ' 'available in the system path when executing ' 'commands locally over SSH. You may need to install the ' 'tool or make sure that the correct directory is in ' '~/.zshenv, ~/.profile, or another suitable file used ' 'in non-interactive sessions.' % missing_exes)