diff options
Diffstat (limited to 'src/leap/bitmask/services')
-rw-r--r-- | src/leap/bitmask/services/__init__.py | 7 | ||||
-rw-r--r-- | src/leap/bitmask/services/connections.py | 10 | ||||
-rw-r--r-- | src/leap/bitmask/services/eip/connection.py | 2 | ||||
-rw-r--r-- | src/leap/bitmask/services/eip/darwinvpnlauncher.py | 5 | ||||
-rw-r--r-- | src/leap/bitmask/services/eip/linuxvpnlauncher.py | 5 | ||||
-rw-r--r-- | src/leap/bitmask/services/eip/vpnlauncher.py | 3 | ||||
-rw-r--r-- | src/leap/bitmask/services/eip/vpnprocess.py | 18 | ||||
-rw-r--r-- | src/leap/bitmask/services/mail/conductor.py | 384 | ||||
-rw-r--r-- | src/leap/bitmask/services/mail/connection.py | 103 | ||||
-rw-r--r-- | src/leap/bitmask/services/mail/imap.py | 7 | ||||
-rw-r--r-- | src/leap/bitmask/services/soledad/soledadbootstrapper.py | 28 |
11 files changed, 539 insertions, 33 deletions
diff --git a/src/leap/bitmask/services/__init__.py b/src/leap/bitmask/services/__init__.py index f9456159..e62277b6 100644 --- a/src/leap/bitmask/services/__init__.py +++ b/src/leap/bitmask/services/__init__.py @@ -19,6 +19,7 @@ Services module. """ import logging import os +import sys from PySide import QtCore @@ -135,8 +136,12 @@ def download_service_config(provider_config, service_config, if token is not None: headers["Authorization"] = 'Token token="{0}"'.format(token) + verify = provider_config.get_ca_cert_path() + if verify: + verify = verify.encode(sys.getfilesystemencoding()) + res = session.get(config_uri, - verify=provider_config.get_ca_cert_path(), + verify=verify, headers=headers, timeout=REQUEST_TIMEOUT, cookies=cookies) diff --git a/src/leap/bitmask/services/connections.py b/src/leap/bitmask/services/connections.py index 8aeb4e0c..ecfd35ff 100644 --- a/src/leap/bitmask/services/connections.py +++ b/src/leap/bitmask/services/connections.py @@ -41,9 +41,6 @@ The different services should declare a ServiceConnection class that inherits from AbstractLEAPConnection, so an instance of such class can be used to inform the StateMachineBuilder of the particularities of the state transitions for each particular connection. - -In the future, we will extend this class to allow composites in connections, -so we can apply conditional logic to the transitions. """ @@ -79,12 +76,7 @@ class AbstractLEAPConnection(object): """ return self._qtsigs - # XXX for conditional transitions with composites, - # we might want to add - # a field with dependencies: what this connection - # needs for (ON) state. - # XXX Look also at child states in the state machine. - #depends = () + components = None # Signals that derived classes # have to implement. diff --git a/src/leap/bitmask/services/eip/connection.py b/src/leap/bitmask/services/eip/connection.py index 962d9cf2..8a35d550 100644 --- a/src/leap/bitmask/services/eip/connection.py +++ b/src/leap/bitmask/services/eip/connection.py @@ -46,5 +46,5 @@ class EIPConnectionSignals(QtCore.QObject): class EIPConnection(AbstractLEAPConnection): def __init__(self): - # XXX this should be public instead self._qtsigs = EIPConnectionSignals() + self._connection_name = "Encrypted Internet" diff --git a/src/leap/bitmask/services/eip/darwinvpnlauncher.py b/src/leap/bitmask/services/eip/darwinvpnlauncher.py index f3b6bfc8..fe3fe4c1 100644 --- a/src/leap/bitmask/services/eip/darwinvpnlauncher.py +++ b/src/leap/bitmask/services/eip/darwinvpnlauncher.py @@ -21,6 +21,7 @@ import commands import getpass import logging import os +import sys from leap.bitmask.services.eip.vpnlauncher import VPNLauncher from leap.bitmask.services.eip.vpnlauncher import VPNLauncherException @@ -185,6 +186,8 @@ class DarwinVPNLauncher(VPNLauncher): :rtype: dict """ + ld_library_path = os.path.join(get_path_prefix(), "..", "lib") + ld_library_path.encode(sys.getfilesystemencoding()) return { - "DYLD_LIBRARY_PATH": os.path.join(get_path_prefix(), "..", "lib") + "DYLD_LIBRARY_PATH": ld_library_path } diff --git a/src/leap/bitmask/services/eip/linuxvpnlauncher.py b/src/leap/bitmask/services/eip/linuxvpnlauncher.py index efb23285..d02f6f96 100644 --- a/src/leap/bitmask/services/eip/linuxvpnlauncher.py +++ b/src/leap/bitmask/services/eip/linuxvpnlauncher.py @@ -21,6 +21,7 @@ import commands import logging import os import subprocess +import sys import time from leap.bitmask.config import flags @@ -231,6 +232,8 @@ class LinuxVPNLauncher(VPNLauncher): :rtype: dict """ + ld_library_path = os.path.join(get_path_prefix(), "..", "lib") + ld_library_path.encode(sys.getfilesystemencoding()) return { - "LD_LIBRARY_PATH": os.path.join(get_path_prefix(), "..", "lib") + "LD_LIBRARY_PATH": ld_library_path } diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py index bce3599b..07497814 100644 --- a/src/leap/bitmask/services/eip/vpnlauncher.py +++ b/src/leap/bitmask/services/eip/vpnlauncher.py @@ -250,9 +250,6 @@ class VPNLauncher(object): '--ping-restart', '30'] command_and_args = [openvpn] + args - logger.debug("Running VPN with command:") - logger.debug(" ".join(command_and_args)) - return command_and_args @classmethod diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index 19e1aa7b..51f0f738 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -23,6 +23,7 @@ import psutil import psutil.error import shutil import socket +import sys from itertools import chain, repeat @@ -864,15 +865,26 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): """ Gets the vpn command from the aproppriate launcher. - Might throw: VPNLauncherException, OpenVPNNotFoundException. + Might throw: + VPNLauncherException, + OpenVPNNotFoundException. + + :rtype: list of str """ - cmd = self._launcher.get_vpn_command( + command = self._launcher.get_vpn_command( eipconfig=self._eipconfig, providerconfig=self._providerconfig, socket_host=self._socket_host, socket_port=self._socket_port, openvpn_verb=self._openvpn_verb) - return map(str, cmd) + + encoding = sys.getfilesystemencoding() + for i, c in enumerate(command): + if not isinstance(c, str): + command[i] = c.encode(encoding) + + logger.debug("Running VPN with command: {0}".format(command)) + return command # shutdown diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py new file mode 100644 index 00000000..c294381b --- /dev/null +++ b/src/leap/bitmask/services/mail/conductor.py @@ -0,0 +1,384 @@ +# -*- coding: utf-8 -*- +# conductor.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +Mail Services Conductor +""" +import logging +import os + +from PySide import QtCore +from zope.proxy import sameProxiedObjects + +from leap.bitmask.gui import statemachines +from leap.bitmask.services.mail import imap +from leap.bitmask.services.mail import connection as mail_connection +from leap.bitmask.services.mail.smtpbootstrapper import SMTPBootstrapper +from leap.bitmask.services.mail.smtpconfig import SMTPConfig +from leap.bitmask.util import is_file + +from leap.common.check import leap_assert + +from leap.common.events import register as leap_register +from leap.common.events import events_pb2 as leap_events + +logger = logging.getLogger(__name__) + + +class IMAPControl(object): + """ + Methods related to IMAP control. + """ + def __init__(self): + """ + Initializes smtp variables. + """ + self.imap_machine = None + self.imap_service = None + self.imap_port = None + self.imap_factory = None + self.imap_connection = None + + leap_register(signal=leap_events.IMAP_SERVICE_STARTED, + callback=self._handle_imap_events, + reqcbk=lambda req, resp: None) + leap_register(signal=leap_events.IMAP_SERVICE_FAILED_TO_START, + callback=self._handle_imap_events, + reqcbk=lambda req, resp: None) + + def set_imap_connection(self, imap_connection): + """ + Sets the imap connection to an initialized connection. + + :param imap_connection: an initialized imap connection + :type imap_connection: IMAPConnection instance. + """ + self.imap_connection = imap_connection + + def start_imap_service(self): + """ + Starts imap service. + """ + logger.debug('Starting imap service') + leap_assert(sameProxiedObjects(self._soledad, None) + is not True, + "We need a non-null soledad for initializing imap service") + leap_assert(sameProxiedObjects(self._keymanager, None) + is not True, + "We need a non-null keymanager for initializing imap " + "service") + + if self.imap_service is None: + # first time. + self.imap_service, \ + self.imap_port, \ + self.imap_factory = imap.start_imap_service( + self._soledad, + self._keymanager) + else: + # we have the fetcher. just start it. + self.imap_service.start_loop() + + def stop_imap_service(self): + """ + Stops imap service (fetcher, factory and port). + """ + self.imap_connection.qtsigs.disconnecting_signal.emit() + # TODO We should homogenize both services. + if self.imap_service is not None: + logger.debug('Stopping imap service.') + # Stop the loop call in the fetcher + self.imap_service.stop() + # Stop listening on the IMAP port + self.imap_port.stopListening() + # Stop the protocol + self.imap_factory.doStop() + + def fetch_incoming_mail(self): + """ + Fetches incoming mail. + """ + # TODO have a mutex over fetch operation. + if self.imap_service: + logger.debug('Client connected, fetching mail...') + self.imap_service.fetch() + + # handle events + + def _handle_imap_events(self, req): + """ + Callback handler for the IMAP events + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + if req.event == leap_events.IMAP_SERVICE_STARTED: + self.on_imap_connected() + elif req.event == leap_events.IMAP_SERVICE_FAILED_TO_START: + self.on_imap_failed() + + # emit connection signals + + def on_imap_connecting(self): + """ + Callback for IMAP connecting state. + """ + self.imap_connection.qtsigs.connecting_signal.emit() + + def on_imap_connected(self): + """ + Callback for IMAP connected state. + """ + self.imap_connection.qtsigs.connected_signal.emit() + + def on_imap_failed(self): + """ + Callback for IMAP failed state. + """ + self.imap_connection.qtsigs.connetion_aborted_signal.emit() + + +class SMTPControl(object): + + PORT_KEY = "port" + IP_KEY = "ip_address" + + def __init__(self): + """ + Initializes smtp variables. + """ + self.smtp_config = SMTPConfig() + self.smtp_connection = None + self.smtp_machine = None + self._smtp_service = None + self._smtp_port = None + + self.smtp_bootstrapper = SMTPBootstrapper() + self.smtp_bootstrapper.download_config.connect( + self.smtp_bootstrapped_stage) + + leap_register(signal=leap_events.SMTP_SERVICE_STARTED, + callback=self._handle_smtp_events, + reqcbk=lambda req, resp: None) + leap_register(signal=leap_events.SMTP_SERVICE_FAILED_TO_START, + callback=self._handle_smtp_events, + reqcbk=lambda req, resp: None) + + def set_smtp_connection(self, smtp_connection): + """ + Sets the smtp connection to an initialized connection. + :param smtp_connection: an initialized smtp connection + :type smtp_connection: SMTPConnection instance. + """ + self.smtp_connection = smtp_connection + + def start_smtp_service(self, host, port, cert): + """ + Starts the smtp service. + + :param host: the hostname of the remove SMTP server. + :type host: str + :param port: the port of the remote SMTP server + :type port: str + :param cert: the client certificate for authentication + :type cert: str + """ + # TODO Make the encrypted_only configurable + # TODO pick local smtp port in a better way + # TODO remove hard-coded port and let leap.mail set + # the specific default. + self.smtp_connection.qtsigs.connecting_signal.emit() + from leap.mail.smtp import setup_smtp_relay + self._smtp_service, self._smtp_port = setup_smtp_relay( + port=2013, + keymanager=self._keymanager, + smtp_host=host, + smtp_port=port, + smtp_cert=cert, + smtp_key=cert, + encrypted_only=False) + + def stop_smtp_service(self): + """ + Stops the smtp service (port and factory). + """ + self.smtp_connection.qtsigs.disconnecting_signal.emit() + # TODO We should homogenize both services. + if self._smtp_service is not None: + logger.debug('Stopping smtp service.') + self._smtp_port.stopListening() + self._smtp_service.doStop() + + @QtCore.Slot() + def smtp_bootstrapped_stage(self, data): + """ + SLOT + TRIGGERS: + self.smtp_bootstrapper.download_config + + If there was a problem, displays it, otherwise it does nothing. + This is used for intermediate bootstrapping stages, in case + they fail. + + :param data: result from the bootstrapping stage for Soledad + :type data: dict + """ + passed = data[self.smtp_bootstrapper.PASSED_KEY] + if not passed: + logger.error(data[self.smtp_bootstrapper.ERROR_KEY]) + return + logger.debug("Done bootstrapping SMTP") + self.check_smtp_config() + + def check_smtp_config(self): + """ + Checks smtp config and tries to download smtp client cert if needed. + Currently called when smtp_bootstrapped_stage has successfuly finished. + """ + logger.debug("Checking SMTP config...") + leap_assert(self.smtp_bootstrapper._provider_config, + "smtp bootstrapper does not have a provider_config") + + provider_config = self.smtp_bootstrapper._provider_config + smtp_config = self.smtp_config + hosts = smtp_config.get_hosts() + # TODO handle more than one host and define how to choose + if len(hosts) > 0: + hostname = hosts.keys()[0] + logger.debug("Using hostname %s for SMTP" % (hostname,)) + host = hosts[hostname][self.IP_KEY].encode("utf-8") + port = hosts[hostname][self.PORT_KEY] + + client_cert = smtp_config.get_client_cert_path( + provider_config, + about_to_download=True) + + # XXX change this logic! + # check_config should be called from within start_service, + # and not the other way around. + if not is_file(client_cert): + self.smtp_bootstrapper._download_client_certificates() + if os.path.isfile(client_cert): + self.start_smtp_service(host, port, client_cert) + else: + logger.warning("Tried to download email client " + "certificate, but could not find any") + + else: + logger.warning("No smtp hosts configured") + + # handle smtp events + + def _handle_smtp_events(self, req): + """ + Callback handler for the SMTP events. + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + if req.event == leap_events.SMTP_SERVICE_STARTED: + self.on_smtp_connected() + elif req.event == leap_events.SMTP_SERVICE_FAILED_TO_START: + self.on_smtp_failed() + + # emit connection signals + + def on_smtp_connecting(self): + """ + Callback for SMTP connecting state. + """ + self.smtp_connection.qtsigs.connecting_signal.emit() + + def on_smtp_connected(self): + """ + Callback for SMTP connected state. + """ + self.smtp_connection.qtsigs.connected_signal.emit() + + def on_smtp_failed(self): + """ + Callback for SMTP failed state. + """ + self.smtp_connection.qtsigs.connection_aborted_signal.emit() + + +class MailConductor(IMAPControl, SMTPControl): + """ + This class encapsulates everything related to the initialization and + process control for the mail services. + Currently, it initializes IMAPConnection and SMPTConnection. + """ + # XXX We could consider to use composition instead of inheritance here. + + def __init__(self, soledad, keymanager): + """ + Initializes the mail conductor. + + :param soledad: a transparent proxy that eventually will point to a + Soledad Instance. + :type soledad: zope.proxy.ProxyBase + + :param keymanager: a transparent proxy that eventually will point to a + Keymanager Instance. + :type soledad: zope.proxy.ProxyBase + """ + IMAPControl.__init__(self) + SMTPControl.__init__(self) + self._soledad = soledad + self._keymanager = keymanager + + self._mail_machine = None + + self._mail_connection = mail_connection.MailConnection() + + def start_mail_machine(self, **kwargs): + """ + Starts mail machine. + """ + logger.debug("Starting mail state machine...") + builder = statemachines.ConnectionMachineBuilder(self._mail_connection) + (mail, (imap, smtp)) = builder.make_machine(**kwargs) + + # we have instantiated the connections while building the composite + # machines, and we have to use the qtsigs instantiated there. + # XXX we could probably use a proxy here too to make the thing + # transparent. + self.set_imap_connection(imap.conn) + self.set_smtp_connection(smtp.conn) + + self._mail_machine = mail + # XXX ------------------- + # need to keep a reference? + #self._mail_events = mail.events + self._mail_machine.start() + + self._imap_machine = imap + self._imap_machine.start() + self._smtp_machine = smtp + self._smtp_machine.start() + + def connect_mail_signals(self, widget): + """ + Connects the mail signals to the mail_status widget slots. + + :param widget: the widget containing the slots. + :type widget: QtCore.QWidget + """ + qtsigs = self._mail_connection.qtsigs + qtsigs.connected_signal.connect(widget.mail_state_connected) + qtsigs.connecting_signal.connect(widget.mail_state_connecting) + qtsigs.disconnecting_signal.connect(widget.mail_state_disconnecting) + qtsigs.disconnected_signal.connect(widget.mail_state_disconnected) diff --git a/src/leap/bitmask/services/mail/connection.py b/src/leap/bitmask/services/mail/connection.py new file mode 100644 index 00000000..29378f62 --- /dev/null +++ b/src/leap/bitmask/services/mail/connection.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# connection.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +Email Connections +""" +from PySide import QtCore + +from leap.bitmask.services.connections import AbstractLEAPConnection + + +class IMAPConnectionSignals(QtCore.QObject): + """ + Qt Signals used by IMAPConnection + """ + # commands + do_connect_signal = QtCore.Signal() + do_disconnect_signal = QtCore.Signal() + + # intermediate stages + connecting_signal = QtCore.Signal() + disconnecting_signal = QtCore.Signal() + + connected_signal = QtCore.Signal() + disconnected_signal = QtCore.Signal() + + connection_died_signal = QtCore.Signal() + connection_aborted_signal = QtCore.Signal() + + +class IMAPConnection(AbstractLEAPConnection): + + _connection_name = "IMAP" + + def __init__(self): + self._qtsigs = IMAPConnectionSignals() + + +class SMTPConnectionSignals(QtCore.QObject): + """ + Qt Signals used by SMTPConnection + """ + # commands + do_connect_signal = QtCore.Signal() + do_disconnect_signal = QtCore.Signal() + + # intermediate stages + connecting_signal = QtCore.Signal() + disconnecting_signal = QtCore.Signal() + + connected_signal = QtCore.Signal() + disconnected_signal = QtCore.Signal() + + connection_died_signal = QtCore.Signal() + connection_aborted_signal = QtCore.Signal() + + +class SMTPConnection(AbstractLEAPConnection): + + _connection_name = "IMAP" + + def __init__(self): + self._qtsigs = SMTPConnectionSignals() + + +class MailConnectionSignals(QtCore.QObject): + """ + Qt Signals used by MailConnection + """ + # commands + do_connect_signal = QtCore.Signal() + do_disconnect_signal = QtCore.Signal() + + connecting_signal = QtCore.Signal() + disconnecting_signal = QtCore.Signal() + + connected_signal = QtCore.Signal() + disconnected_signal = QtCore.Signal() + + connection_died_signal = QtCore.Signal() + connection_aborted_signal = QtCore.Signal() + + +class MailConnection(AbstractLEAPConnection): + + components = IMAPConnection, SMTPConnection + _connection_name = "Mail" + + def __init__(self): + self._qtsigs = MailConnectionSignals() diff --git a/src/leap/bitmask/services/mail/imap.py b/src/leap/bitmask/services/mail/imap.py index 4828180e..2667f156 100644 --- a/src/leap/bitmask/services/mail/imap.py +++ b/src/leap/bitmask/services/mail/imap.py @@ -41,9 +41,10 @@ def get_mail_check_period(): try: period = int(period_str) except (ValueError, TypeError): - logger.warning("BAD value found for %s: %s" % ( - INCOMING_CHECK_PERIOD_ENV, - period_str)) + if period is not None: + logger.warning("BAD value found for %s: %s" % ( + INCOMING_CHECK_PERIOD_ENV, + period_str)) except Exception as exc: logger.warning("Unhandled error while getting %s: %r" % ( INCOMING_CHECK_PERIOD_ENV, diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index 4619ba80..54ef67eb 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -20,11 +20,13 @@ Soledad bootstrapping import logging import os import socket +import sys from ssl import SSLError from PySide import QtCore from u1db import errors as u1db_errors +from zope.proxy import sameProxiedObjects from leap.bitmask.config import flags from leap.bitmask.config.providerconfig import ProviderConfig @@ -39,7 +41,7 @@ from leap.common.check import leap_assert, leap_assert_type, leap_check from leap.common.files import which from leap.keymanager import KeyManager, openpgp from leap.keymanager.errors import KeyNotFound -from leap.soledad.client import Soledad +from leap.soledad.client import Soledad, BootstrapSequenceError logger = logging.getLogger(__name__) @@ -190,8 +192,8 @@ class SoledadBootstrapper(AbstractBootstrapper): # soledad-launcher in the gui. raise - leap_check(self._soledad is not None, - "Null soledad, error while initializing") + leap_assert(not sameProxiedObjects(self._soledad, None), + "Null soledad, error while initializing") # and now, let's sync sync_tries = self.MAX_SYNC_RETRIES @@ -239,14 +241,15 @@ class SoledadBootstrapper(AbstractBootstrapper): """ # TODO: If selected server fails, retry with another host # (issue #3309) + encoding = sys.getfilesystemencoding() try: self._soledad = Soledad( uuid, - self._password.encode("utf-8"), - secrets_path=secrets_path, - local_db_path=local_db_path, + self._password, + secrets_path=secrets_path.encode(encoding), + local_db_path=local_db_path.encode(encoding), server_url=server_url, - cert_file=cert_file, + cert_file=cert_file.encode(encoding), auth_token=auth_token) # XXX All these errors should be handled by soledad itself, @@ -257,7 +260,10 @@ class SoledadBootstrapper(AbstractBootstrapper): logger.debug("SOLEDAD initialization TIMED OUT...") self.soledad_timeout.emit() except socket.error as exc: - logger.error("Socket error while initializing soledad") + logger.warning("Socket error while initializing soledad") + self.soledad_timeout.emit() + except BootstrapSequenceError as exc: + logger.warning("Error while initializing soledad") self.soledad_timeout.emit() # unrecoverable @@ -280,7 +286,7 @@ class SoledadBootstrapper(AbstractBootstrapper): Raises SoledadSyncError if not successful. """ try: - logger.error("trying to sync soledad....") + logger.debug("trying to sync soledad....") self._soledad.sync() except SSLError as exc: logger.error("%r" % (exc,)) @@ -412,9 +418,9 @@ class SoledadBootstrapper(AbstractBootstrapper): :param provider_config: Provider configuration :type provider_config: ProviderConfig :param user: User's login - :type user: str + :type user: unicode :param password: User's password - :type password: str + :type password: unicode :param download_if_needed: If True, it will only download files if the have changed since the time it was previously downloaded. |