diff options
| author | Tomás Touceda <chiiph@leap.se> | 2014-04-04 16:58:18 -0300 | 
|---|---|---|
| committer | Tomás Touceda <chiiph@leap.se> | 2014-04-04 16:58:18 -0300 | 
| commit | 81715dc47d77934c4f67d2527a56c28f58f0345d (patch) | |
| tree | 6e66940af735e089803c5ce05ad1ce1df16c9d1e | |
| parent | 496036f15cf257d16b6594770812da64a249280c (diff) | |
| parent | eb4cdab9c6b8ff66bb4667cc6195d2c366122540 (diff) | |
Merge branch 'release-0.5.0'0.5.0
65 files changed, 4384 insertions, 1384 deletions
| @@ -2,6 +2,10 @@  *.swo  *.pyc  *.log +*.lprof +*.pstats +*.data +*.cprofile  *.*~  .*  *_rc.py @@ -31,3 +35,4 @@ config/*  CHANGELOG~  data/bitmask.pro +bitmask-resources.png diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 866d6f28..1728f358 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,101 @@  History  ------- +2014 +==== + +0.5.0 Apr 4 -- the "Long time no see" release: +++++++++++++++++++++++++++++++++++++++++++++++ +- Fix logging out typo, closes #4815. +- Improve logout action, related to #5131. +- In case of soledad bootstrap error (e.g.: network failure), re run +  all the setup process. +- Correct resolvconf usage. Avoids permanent break of +  resolv.conf. Closes #4633. +- Disable and stop EIP when you set EIP as disabled in the preferences +  dialog. Closes #4670. +- Advanced Key Management: add view for stored public keys. Closes +  #4734. +- Reset registration error and input widgets if the user goes back to +  provider selection in wizard. Closes #4742. +- Disconnect signals before closing the wizard. Closes #4817. +- Fix logout error message, display it similarly to other errors in +  the app. Closes #4942. +- Client should say 1 unread email, not emails. Closes #4952. +- Update menu name in Wizard. Closes #4984. +- Config help menu: do not use an empty password. Closes #4985. +- Handle wizard close correctly. Closes #4986. +- Fix "Something went wrong with the logout" misleading error in every +  logout. Closes #4995 and #5071. +- Use version checks in the wizard when the user choose to use an +  existing provider. Closes #5048. +- Move error messages from srpauth to the GUI and refactor +  signals. Closes #5219. +- Fix psutil version to avoid conflicts with gnupg required +  version. Closes #5309. +- Update bitmask url in PKG-INFO. Closes #5395. +- Disable 'next' button if the checks passed but the provider is +  changed. Closes #5396. +- Do not start soledad and mail if the mail service is +  disabled. Closes #5411. +- Don't escape logs for pastebin. Closes #5433. +- Handle closed Soledad database on quit, speedup exit. Closes #5130. +- Catch shutdown errors. Closes: #5313 +- Properly reset imap session on logout. Closes: #4925 +- Sync Soledad before bootstrapping mail only if the key for the user +  is not found locally. Otherwise, defer to thread and +  continue. Closes #5083. +- Set as selected default for the eip preferences window the item +  selented in the bitmask main window. Closes #5153. +- Cancel login does not work or needs to be pressed twice. Closes +  #4869, #4973. +- Fail gracefully against keyring import errors. +- Update requirements and code for the new psutil version. +- Use Bitmask icon instead of LEAP's for the super user dialog in +  OSX. Fixes #4273. +- Workaround a bug in Ubuntu where the menu is not displayed in the +  global menu bar. Fixes #5420. +- Wizard: select by default the use of an existing provider if we have +  configured at least one. Closes #4488. +- Add in-app indication of how to connect to local imap and +  smtp. Closes #4530. +- Warn the user on incompatible api error. +- Warn the user if is using an old app version. Closes #4636. +- Minor UI changes: re-arrange main window so that the login widget is +  at the top and preferences are available under the menu. +- Disable Advanced Key Manager import feature since it's experimental +  and may cause data loss. Closes #4877. +- Offline mode for debugging. Closes: #4943 +- Add pastebin button to upload logs from the logs window to ease bug +  report. Closes #5163. +- Add support for self signed certs. Closes #5391. +- Add hotkey for the Help menu. Closes #5401. +- Add --repair-mailboxes command line option. It will be needed to +  migrate existing account after a data schema changes, like it will +  be happening for 0.5.0. Closes #4792. +- Make first Soledad sync wait for EIP to come up after logging in. +  Fixes #4885. +- Ensure IMAP flushes data to disk before quitting. Closes #5095. +- Update key manager auth to interact with webapp v2. Fixes #5120. +- Handle invalid auth tokens when syncing Soledad, and show an error +  on the GUI. Fixes #5191. +- After connecting EIP check for DNS resolution and warn the user on +  error. Closes #5301. +- Display domain for provider the user has just logged in. Fixes +  #4631. +- Add ability to import a maildir into a local mailbox. +- Add ability to write mail logs to a separate file. +- Show hash info in About bitmask (for debian versions). +- Add the appname in the reported version string. +- Move/refactor SRPRegister to the backend. +- Add ability to nice application via environment variable. +- Refactor ProviderBootstrapper out of the UI modules to a Backend +  module, obscuring all the details. +- Remove qt4reactor as a dependency. + +2013 +==== +  0.3.8 Dec 6 -- the "Three week child" release:  +++++++++++++++++++++++++++++++++++++++++++++++  - Make the preferences window selects the current selected provider in @@ -674,8 +674,16 @@ Public License instead of this License.  But first, please read  <http://www.gnu.org/philosophy/why-not-lgpl.html>.  -------------------------------------------------------------------------------- +We are "embedding" PastebinAPI class since currently the installation through +pip does not work. -Leap-client also uses third party icons: +file: src/leap/bitmask/util/pastebin.py +License: GPL +Author: Ian Havelock +website: https://github.com/Morrolan/PastebinAPI +-------------------------------------------------------------------------------- + +Bitmask also uses third party icons:  ---  data/images/Arrow-Up-32.png @@ -694,3 +702,9 @@ data/images/mail-unlocked.png  License: LGPL - http://www.gnu.org/licenses/lgpl.html  Website: http://www.oxygen-icons.org/ +-- +data/images/pastebin.png + +author: Marco Martin +License: LGPL +website: https://www.iconfinder.com/icons/7897/binary_icon @@ -39,6 +39,14 @@ COMPILED_RESOURCES = $(RESOURCES:%.qrc=$(COMPILED_DIR)/%_rc.py)  DEBVER = $(shell dpkg-parsechangelog | sed -ne 's,Version: ,,p') +ifndef EDITOR +	export EDITOR=vim +endif + +ifndef RESOURCE_TIME +	export RESOURCE_TIME=10 +endif +  #  all : resources ui @@ -64,5 +72,28 @@ manpages:  apidocs:  	@sphinx-apidoc -o docs/api src/leap/bitmask +do_cprofile: +	python -m cProfile -o bitmask.cprofile src/leap/bitmask/app.py --debug -N + +view_cprofile: +	cprofilev bitmask.cprofile + +mailprofile: +	gprof2dot -f pstats /tmp/leap_mail_profile.pstats -n 0.2 -e 0.2 | dot -Tpdf -o /tmp/leap_mail_profile.pdf + +do_lineprof: +	LEAP_PROFILE_IMAPCMD=1 LEAP_MAIL_MANHOLE=1 kernprof.py -l src/leap/bitmask/app.py --debug + +do_lineprof_offline: +	LEAP_PROFILE_IMAPCMD=1 LEAP_MAIL_MANHOLE=1 kernprof.py -l src/leap/bitmask/app.py --offline --debug -N + +view_lineprof: +	@python -m line_profiler app.py.lprof | $(EDITOR) - + +resource_graph: +	#./pkg/scripts/monitor_resource.zsh `ps aux | grep app.py | head -1 | awk '{print $$2}'` $(RESOURCE_TIME) +	./pkg/scripts/monitor_resource.zsh `pgrep bitmask` $(RESOURCE_TIME) +	display bitmask-resources.png +  clean :  	$(RM) $(COMPILED_UI) $(COMPILED_RESOURCES) $(COMPILED_UI:.py=.pyc) $(COMPILED_RESOURCES:.py=.pyc) @@ -17,111 +17,16 @@ Currently we distribute pre-compiled `bundles`_ for Linux, OSX and Windows.  .. _`PySide`: http://qt-project.org/wiki/PySide  .. _`the LEAP Platform`: https://github.com/leapcode/leap_platform -.. _`bundles`: https://downloads.leap.se/client/ +.. _`bundles`: https://dl.bitmask.net  Read the Docs!  ------------------ -The latest documentation is available at `Read The Docs`_. +The latest documentation is available at `LEAP`_. -.. _`Read The Docs`: http://bitmask.rtfd.org +.. _`LEAP`: https://leap.se/en/docs/client -Dependencies ------------------- - -Bitmask depends on these libraries: - -* ``python 2.6`` or ``2.7`` -* ``qt4 libraries`` -* ``libopenssl`` -* ``openvpn`` - -Python packages are listed in ``pkg/requirements.pip`` and ``pkg/test-requirements.pip`` - -Getting dependencies under debian -++++++++++++++++++++++++++++++++++ - -With a Debian based system, to be able to run Bitmask you need to run the following command:: - -    $ sudo apt-get install git python-dev python-setuptools -    python-virtualenv python-pip python-openssl libsqlite3-dev g++ openvpn -    pyside-tools python-pyside  - -Installing ------------ - -Quick install, from the cheese shop:: - -  $ sudo pip install leap.bitmask - -If you prefer to install from sources:: - - $ make - $ sudo python2 setup.py install - - -Running -------- - -After a successful installation, there should be a launcher called ``bitmask`` somewhere in your path:: - -  $ bitmask - -If you are testing a new provider and do not have a CA certificate chain tied to your SSL certificate, you should execute Bitmask in the following way:: - -  $ bitmask --danger - -But **DO NOT use it on a regular basis**. - -**WARNING**: If you use the --danger flag you may be victim to a MITM_ attack without noticing. Use at your own risk. - -.. _MITM: http://en.wikipedia.org/wiki/Man-in-the-middle_attack - -Hacking -======= - -Get the source from the main Bitmask repo:: - -    git clone https://leap.se/git/bitmask_client - -The code is also browsable online at:: - -    https://leap.se/git/?p=bitmask_client.git - -Some steps need to be run when setting a development environment for the first time. - -Enable a **virtualenv** to isolate your libraries. (Current *.gitignore* knows about a virtualenv in the root tree. If you do not like that place, just change ``.`` for *<path.to.environment>*):: - -  $ virtualenv . -  $ source bin/activate - -Make sure you are in the development branch:: - -  (bitmask)$ git checkout develop - -Symlink your global pyside libraries:: - -  (bitmask)$ pkg/postmkvenv.sh - -And make your working tree available to your pythonpath:: - -  (bitmask)$ python2 setup.py develop - -Run Bitmask:: - -  (bitmask)$ bitmask --debug - -Testing -======= - -Have a look at ``pkg/test-requirements.pip`` for the tests dependencies. - -To run the test suite:: - -    $ ./run_tests.sh - -which the first time should automagically install all the needed dependencies in your virtualenv for you.  License  ======= diff --git a/data/images/pastebin.png b/data/images/pastebin.pngBinary files differ new file mode 100644 index 00000000..62179840 --- /dev/null +++ b/data/images/pastebin.png diff --git a/data/resources/loggerwindow.qrc b/data/resources/loggerwindow.qrc index 847ca9a1..a385c50e 100644 --- a/data/resources/loggerwindow.qrc +++ b/data/resources/loggerwindow.qrc @@ -1,5 +1,6 @@  <RCC>    <qresource prefix="/"> +    <file>../images/pastebin.png</file>      <file>../images/oxygen-icons/edit-bomb.png</file>      <file>../images/oxygen-icons/document-save-as.png</file>      <file>../images/oxygen-icons/dialog-information.png</file> diff --git a/data/ts/en_US.ts b/data/ts/en_US.ts index 64a400ba..562df1a3 100644 --- a/data/ts/en_US.ts +++ b/data/ts/en_US.ts @@ -1,85 +1,234 @@  <?xml version="1.0" encoding="utf-8"?>  <!DOCTYPE TS><TS version="1.1">  <context> -    <name>EIPPreferences</name> +    <name>AdvancedKeyManagement</name>      <message> -        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="14"/> -        <source>EIP Preferences</source> +        <location filename="../src/leap/bitmask/gui/ui/advanced_key_management.ui" line="14"/> +        <source>Advanced Key Management</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="27"/> -        <source>Select gateway for provider</source> +        <location filename="../src/leap/bitmask/gui/ui/advanced_key_management.ui" line="24"/> +        <source>My key pair</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="36"/> -        <source>&Select provider:</source> +        <location filename="../src/leap/bitmask/gui/ui/advanced_key_management.ui" line="30"/> +        <source>User:</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="140"/> -        <source><Select provider></source> +        <location filename="../src/leap/bitmask/gui/ui/advanced_key_management.ui" line="40"/> +        <source>user_name@provider</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="55"/> -        <source>Save this provider settings</source> +        <location filename="../src/leap/bitmask/gui/ui/advanced_key_management.ui" line="53"/> +        <source>Key ID:</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="62"/> -        <source>< Providers Gateway Status ></source> +        <location filename="../src/leap/bitmask/gui/ui/advanced_key_management.ui" line="63"/> +        <source>key ID</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="72"/> -        <source>Select gateway:</source> +        <location filename="../src/leap/bitmask/gui/ui/advanced_key_management.ui" line="76"/> +        <source>Key fingerprint:</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/ui/advanced_key_management.ui" line="86"/> +        <source>fingerprint</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/ui/advanced_key_management.ui" line="101"/> +        <source>Export current key pair</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/ui/advanced_key_management.ui" line="121"/> +        <source>Import custom key pair</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/ui/advanced_key_management.ui" line="139"/> +        <source>Stored public keys</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/ui/advanced_key_management.ui" line="164"/> +        <source>Email</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/ui/advanced_key_management.ui" line="169"/> +        <source>Key ID</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/advanced_key_management.py" line="60"/> +        <source><span style='color:#0000FF;'>NOTE</span>: To use this, you need to enable/start {0}.</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/advanced_key_management.py" line="99"/> +        <source>Open keys file</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/advanced_key_management.py" line="191"/> +        <source>Input/Output error</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/advanced_key_management.py" line="112"/> +        <source>There was an error accessing the file. +Import canceled.</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/advanced_key_management.py" line="122"/> +        <source>Data mismatch</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/advanced_key_management.py" line="125"/> +        <source>The public and private key should have the same address and fingerprint. +Import canceled.</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/advanced_key_management.py" line="130"/> +        <source>Missing key</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/advanced_key_management.py" line="133"/> +        <source>You need to provide the public AND private key in the same file. +Import canceled.</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/advanced_key_management.py" line="139"/> +        <source>Address mismatch</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/advanced_key_management.py" line="142"/> +        <source>The identity for the key needs to be the same as your user address. +Import canceled.</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/advanced_key_management.py" line="147"/> +        <source>Are you sure that you want to replace the current key pair whith the imported?</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/advanced_key_management.py" line="164"/> +        <source>Import Successful</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/advanced_key_management.py" line="165"/> +        <source>The key pair was imported successfully.</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/advanced_key_management.py" line="174"/> +        <source>Save keys file</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/advanced_key_management.py" line="185"/> +        <source>Export Successful</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/advanced_key_management.py" line="187"/> +        <source>The key pair was exported successfully. +Please, store your private key in a safe place.</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/advanced_key_management.py" line="193"/> +        <source>There was an error accessing the file. +Export canceled.</source> +        <translation type="unfinished"></translation> +    </message> +</context> +<context> +    <name>EIPPreferences</name> +    <message> +        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="27"/> +        <source>Select gateway for provider</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="47"/> +        <source><Select provider></source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="80"/> +        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="62"/> +        <source>< Providers Gateway Status ></source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="83"/>          <source>Automatic</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="91"/> -        <source>Automatic EIP start</source> +        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="14"/> +        <source>Encrypted Internet Preferences</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="109"/> -        <source><font color='green'><b>Automatic EIP start saved!</b></font></source> +        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="36"/> +        <source>Select &provider:</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="119"/> -        <source>Save auto start setting</source> +        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="55"/> +        <source>&Save this provider settings</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="129"/> -        <source>Enable Automatic start of EIP</source> +        <location filename="../src/leap/bitmask/gui/ui/eippreferences.ui" line="72"/> +        <source>Select &gateway:</source>          <translation type="unfinished"></translation>      </message>  </context>  <context>      <name>EIPPreferencesWindow</name>      <message> -        <location filename="../src/leap/bitmask/gui/eip_preferenceswindow.py" line="47"/> +        <location filename="../src/leap/bitmask/gui/eip_preferenceswindow.py" line="48"/>          <source>Automatic</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/eip_preferenceswindow.py" line="156"/> +        <location filename="../src/leap/bitmask/gui/eip_preferenceswindow.py" line="125"/>          <source>Gateway settings for provider '{0}' saved.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/eip_preferenceswindow.py" line="198"/> +        <location filename="../src/leap/bitmask/gui/eip_preferenceswindow.py" line="179"/>          <source>There was a problem with configuration files.</source>          <translation type="unfinished"></translation>      </message> +    <message> +        <location filename="../src/leap/bitmask/gui/eip_preferenceswindow.py" line="101"/> +        <source> (uninitialized)</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/eip_preferenceswindow.py" line="151"/> +        <source>This is an uninitialized provider, please log in first.</source> +        <translation type="unfinished"></translation> +    </message>  </context>  <context>      <name>EIPStatus</name> @@ -89,22 +238,22 @@          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/eip_status.ui" line="31"/> +        <location filename="../src/leap/bitmask/gui/ui/eip_status.ui" line="34"/>          <source>Turn On</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/eip_status.ui" line="63"/> +        <location filename="../src/leap/bitmask/gui/ui/eip_status.ui" line="66"/>          <source>...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/eip_status.ui" line="86"/> +        <location filename="../src/leap/bitmask/gui/ui/eip_status.ui" line="82"/>          <source>Traffic is being routed in the clear</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/eip_status.ui" line="253"/> +        <location filename="../src/leap/bitmask/gui/ui/eip_status.ui" line="212"/>          <source>0.0 KB/s</source>          <translation type="unfinished"></translation>      </message> @@ -112,83 +261,78 @@  <context>      <name>EIPStatusWidget</name>      <message> -        <location filename="../src/leap/bitmask/gui/eip_status.py" line="191"/> -        <source>All services are OFF</source> -        <translation type="unfinished"></translation> -    </message> -    <message> -        <location filename="../src/leap/bitmask/gui/eip_status.py" line="197"/> -        <source>Encrypted Internet: {0}</source> -        <translation type="unfinished"></translation> -    </message> -    <message> -        <location filename="../src/leap/bitmask/gui/eip_status.py" line="249"/> -        <source>You must login to use Encrypted Internet</source> -        <translation type="unfinished"></translation> -    </message> -    <message> -        <location filename="../src/leap/bitmask/gui/eip_status.py" line="300"/> +        <location filename="../src/leap/bitmask/gui/eip_status.py" line="327"/>          <source>Turn OFF</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/eip_status.py" line="314"/> +        <location filename="../src/leap/bitmask/gui/eip_status.py" line="341"/>          <source>Turn ON</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/eip_status.py" line="321"/> +        <location filename="../src/leap/bitmask/gui/eip_status.py" line="348"/>          <source>Traffic is being routed in the clear</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/eip_status.py" line="378"/> +        <location filename="../src/leap/bitmask/gui/eip_status.py" line="405"/>          <source>Authenticating...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/eip_status.py" line="380"/> +        <location filename="../src/leap/bitmask/gui/eip_status.py" line="407"/>          <source>Retrieving configuration...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/eip_status.py" line="382"/> +        <location filename="../src/leap/bitmask/gui/eip_status.py" line="409"/>          <source>Waiting to start...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/eip_status.py" line="384"/> +        <location filename="../src/leap/bitmask/gui/eip_status.py" line="411"/>          <source>Assigning IP</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/eip_status.py" line="386"/> +        <location filename="../src/leap/bitmask/gui/eip_status.py" line="413"/>          <source>Reconnecting...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/eip_status.py" line="395"/> +        <location filename="../src/leap/bitmask/gui/eip_status.py" line="420"/>          <source>Unable to start VPN, it's already running.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/eip_status.py" line="418"/> -        <source>Encrypted Internet: OFF</source> +        <location filename="../src/leap/bitmask/gui/eip_status.py" line="463"/> +        <source>Route traffic through: {0}</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/eip_status.py" line="423"/> -        <source>Encrypted Internet: Starting...</source> +        <location filename="../src/leap/bitmask/gui/eip_status.py" line="260"/> +        <source>disabled</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/eip_status.py" line="425"/> -        <source>Encrypted Internet: ON</source> +        <location filename="../src/leap/bitmask/gui/eip_status.py" line="442"/> +        <source>{0}: OFF</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/eip_status.py" line="437"/> -        <source>Route traffic through: {0}</source> +        <location filename="../src/leap/bitmask/gui/eip_status.py" line="257"/> +        <source>You must login to use {0}</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/eip_status.py" line="447"/> +        <source>{0}: Starting...</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/eip_status.py" line="450"/> +        <source>{0}: ON</source>          <translation type="unfinished"></translation>      </message>  </context> @@ -230,7 +374,7 @@          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/loggerwindow.py" line="148"/> +        <location filename="../src/leap/bitmask/gui/loggerwindow.py" line="162"/>          <source>Save As</source>          <translation type="unfinished"></translation>      </message> @@ -244,6 +388,36 @@          <source>Case Insensitive</source>          <translation type="unfinished"></translation>      </message> +    <message> +        <location filename="../src/leap/bitmask/gui/loggerwindow.py" line="193"/> +        <source>Send to Pastebin.com</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/loggerwindow.py" line="190"/> +        <source>Sending to pastebin...</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/loggerwindow.py" line="224"/> +        <source>Your pastebin link <a href='{0}'>{0}</a></source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/loggerwindow.py" line="227"/> +        <source>Pastebin OK</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/loggerwindow.py" line="240"/> +        <source>Sending logs to Pastebin failed!</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/loggerwindow.py" line="241"/> +        <source>Pastebin Error</source> +        <translation type="unfinished"></translation> +    </message>  </context>  <context>      <name>LoginWidget</name> @@ -273,53 +447,53 @@          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/login.py" line="234"/> +        <location filename="../src/leap/bitmask/gui/login.py" line="247"/>          <source>Log In</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/login.py" line="121"/> +        <location filename="../src/leap/bitmask/gui/login.py" line="125"/>          <source>Other...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/login.py" line="229"/> +        <location filename="../src/leap/bitmask/gui/login.py" line="242"/>          <source>Cancel</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/login.ui" line="250"/> +        <location filename="../src/leap/bitmask/gui/ui/login.ui" line="230"/>          <source>...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/login.py" line="366"/> +        <location filename="../src/leap/bitmask/gui/login.py" line="368"/>          <source>Logout</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/login.py" line="283"/> +        <location filename="../src/leap/bitmask/gui/login.py" line="296"/>          <source>Please select a valid provider</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/login.py" line="288"/> +        <location filename="../src/leap/bitmask/gui/login.py" line="301"/>          <source>Please provide a valid username</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/login.py" line="293"/> +        <location filename="../src/leap/bitmask/gui/login.py" line="306"/>          <source>Please provide a valid password</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/login.py" line="296"/> +        <location filename="../src/leap/bitmask/gui/login.py" line="309"/>          <source>Logging in...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/login.py" line="359"/> -        <source>Loggin out...</source> +        <location filename="../src/leap/bitmask/gui/login.py" line="361"/> +        <source>Logging out...</source>          <translation type="unfinished"></translation>      </message>  </context> @@ -341,352 +515,382 @@          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="171"/> -        <source>All services are OFF</source> -        <translation type="unfinished"></translation> -    </message> -    <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="204"/> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="192"/>          <source>There was an unexpected problem with Soledad.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="418"/> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="413"/>          <source>OFF</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="221"/> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="209"/>          <source>Mail is OFF</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="224"/> -        <source>You must be logged in to use encrypted email.</source> -        <translation type="unfinished"></translation> -    </message> -    <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="228"/> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="217"/>          <source>Starting..</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="229"/> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="218"/>          <source>Mail is starting</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="383"/> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="436"/>          <source>ON</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="233"/> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="222"/>          <source>Mail is ON</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="236"/> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="225"/>          <source>Mail is disabled</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="298"/> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="422"/>          <source>Starting...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="265"/> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="254"/>          <source>Soledad has started...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="267"/> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="256"/>          <source>Soledad is starting, please wait...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="304"/> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="291"/>          <source>Looking for key for this user</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="308"/> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="295"/>          <source>Found key! Starting mail...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="310"/> -        <source>Generating new key, please wait...</source> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="300"/> +        <source>Finished generating key!</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="312"/> -        <source>Finished generating key!</source> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="302"/> +        <source>Starting mail...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="314"/> -        <source>Starting mail...</source> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="334"/> +        <source>SMTP failed to start, check the logs.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="344"/> -        <source>SMTP has started...</source> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="390"/> +        <source>About to start, please wait...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="350"/> -        <source>SMTP failed to start, check the logs.</source> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="397"/> +        <source>Disabled</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="387"/> -        <source>Failed</source> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="162"/> +        <source>{0}: OFF</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="381"/> -        <source>IMAP has started...</source> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="444"/> +        <source>You must be logged in to use {0}.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="387"/> -        <source>IMAP failed to start, check the logs.</source> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="298"/> +        <source>Generating new key, this may take a few minutes.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="391"/> -        <source>%s Unread Emails</source> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="373"/> +        <source>{0} Unread Emails in your Inbox</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="405"/> -        <source>About to start, please wait...</source> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="377"/> +        <source>1 Unread Email in your Inbox</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mail_status.py" line="412"/> -        <source>Disabled</source> +        <location filename="../src/leap/bitmask/gui/mail_status.py" line="429"/> +        <source>Disconnecting...</source>          <translation type="unfinished"></translation>      </message>  </context>  <context>      <name>MainWindow</name>      <message> -        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="287"/> +        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="238"/>          <source>There are new updates available, please restart.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="329"/> +        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="280"/>          <source>More...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="637"/> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="758"/>          <source>Help</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="390"/> +        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="352"/>          <source>&Quit</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="400"/> +        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="362"/>          <source>&Help</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="405"/> +        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="367"/>          <source>&Wizard</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="683"/> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="806"/>          <source>Hide Main Window</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="537"/> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="638"/>          <source> The following components will be updated:  %s</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="540"/> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="641"/>          <source>Updates available</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="682"/> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="805"/>          <source>Show Main Window</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1326"/> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1524"/>          <source>We could not find any authentication agent in your system.<br/>Make sure you have <b>polkit-gnome-authentication-agent-1</b> running and try again.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1338"/> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1536"/>          <source>We could not find <b>pkexec</b> in your system.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1343"/> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1541"/>          <source>We could not find openvpn binary.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1505"/> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1705"/>          <source>Starting...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1513"/> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1719"/>          <source>Not supported</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1516"/> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1723"/>          <source>Disabled</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1549"/> -        <source>Could not load Encrypted Internet Configuration.</source> +        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="32"/> +        <source>Bitmask</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1471"/> -        <source>Encrypted Internet could not be launched because you did not authenticate properly.</source> +        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="357"/> +        <source>About &Bitmask</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1479"/> -        <source>Encrypted Internet finished in an unexpected manner!</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="289"/> +        <source>Mail is OFF</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="32"/> -        <source>Bitmask</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="629"/> +        <source>The Bitmask app is ready to update, please restart the application.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="395"/> -        <source>About &Bitmask</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="870"/> +        <source>About Bitmask - %s</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="283"/> -        <source>Mail is OFF</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1011"/> +        <source>Unable to login: Problem with provider</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="528"/> -        <source>The Bitmask app is ready to update, please restart the application.</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1089"/> +        <source>Log in cancelled by the user.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="745"/> -        <source>About Bitmask - %s</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1547"/> +        <source>Another openvpn instance is already running, and could not be stopped.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="759"/> -        <source>Version: <b>%s</b><br><br>Bitmask is the Desktop client application for the LEAP platform, supporting encrypted internet proxy, secure email, and secure chat (coming soon).<br><br>LEAP is a non-profit dedicated to giving all internet users access to secure communication. Our focus is on adapting encryption technology to make it easy to use and widely available. <br><br><a href='https://leap.se'>More about LEAP</a></source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1554"/> +        <source>Another openvpn instance is already running, and could not be stopped because it was not launched by Bitmask. Please stop it and try again.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="847"/> -        <source>Unable to login: Problem with provider</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1740"/> +        <source>There was a problem with the provider</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="885"/> -        <source>Log in cancelled by the user.</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1845"/> +        <source>Something went wrong with the logout.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1333"/> -        <source>Encrypted Internet cannot be started because the tuntap extension is not installed properly in your system.</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1864"/> +        <source>Unable to connect: Problem with provider</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1349"/> -        <source>Another openvpn instance is already running, and could not be stopped.</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1836"/> +        <source>Login</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1356"/> -        <source>Another openvpn instance is already running, and could not be stopped because it was not launched by Bitmask. Please stop it and try again.</source> +        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="315"/> +        <source>&Bitmask</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1531"/> -        <source>There was a problem with the provider</source> +        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="372"/> +        <source>Show &Log</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1631"/> -        <source>Something went wrong with the logout.</source> +        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="377"/> +        <source>Create a new account...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1652"/> -        <source>Unable to connect: Problem with provider</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="278"/> +        <source>File</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="119"/> -        <source>Encrypted Internet</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1606"/> +        <source>Network is unreachable</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1622"/> -        <source>Login</source> +        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="123"/> +        <source>Please Log In</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="364"/> -        <source>&Bitmask</source> +        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="342"/> +        <source>Account Preferences...</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="347"/> +        <source>Internet Preferences...</source>          <translation type="unfinished"></translation>      </message>      <message>          <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="385"/> -        <source>Preferences...</source> +        <source>Advanced Key Management</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="410"/> -        <source>Show &Log</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="742"/> +        <source> (offline mode)</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/mainwindow.ui" line="415"/> -        <source>Create a new account...</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="765"/> +        <source>OFF</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="272"/> -        <source>File</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="884"/> +        <source>Version: <b>%s</b> (%s)<br><br>%sBitmask is the Desktop client application for the LEAP platform, supporting encrypted internet proxy, secure email, and secure chat (coming soon).<br><br>LEAP is a non-profit dedicated to giving all internet users access to secure communication. Our focus is on adapting encryption technology to make it easy to use and widely available. <br><br><a href='https://leap.se'>More about LEAP</a></source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="644"/> -        <source>Encrypted Internet: OFF</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="911"/> +        <source><strong>Instructions to use mail:</strong><br>If you use Thunderbird you can use the Bitmask extension helper. Search for 'Bitmask' in the add-on manager or download it from: {0}.<br><br>You can configure Bitmask manually with these options:<br><em>   Incoming -> IMAP, port: {1}<br>   Outgoing -> SMTP, port: {2}<br>   Username -> your bitmask username.<br>   Password -> does not matter, use any text.  Just don't leave it empty and don't use your account's password.</em></source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1407"/> -        <source>Network is unreachable</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="912"/> +        <source>Bitmask Help</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="924"/> +        <source>The current client version is not supported by this provider.<br>Please update to latest version.<br><br>You can get the latest version from <a href='{0}'>{1}</a></source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="925"/> +        <source>Update Needed</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="935"/> +        <source>This provider is not compatible with the client.<br><br>Error: API version incompatible.</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="935"/> +        <source>Incompatible Provider</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1430"/> -        <source>EIP has stopped</source> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1757"/> +        <source>Could not load {0} configuration.</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1531"/> +        <source>{0} cannot be started because the tuntap extension is not installed properly in your system.</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1670"/> +        <source>{0} could not be launched because you did not authenticate properly.</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/mainwindow.py" line="1677"/> +        <source>{0} finished in an unexpected manner!</source>          <translation type="unfinished"></translation>      </message>  </context> @@ -761,45 +965,60 @@  <context>      <name>PreferencesWindow</name>      <message> -        <location filename="../src/leap/bitmask/gui/preferenceswindow.py" line="53"/> +        <location filename="../src/leap/bitmask/gui/preferenceswindow.py" line="63"/>          <source>Automatic</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/preferenceswindow.py" line="115"/> +        <location filename="../src/leap/bitmask/gui/preferenceswindow.py" line="159"/>          <source>Changing password...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/preferenceswindow.py" line="166"/> +        <location filename="../src/leap/bitmask/gui/preferenceswindow.py" line="210"/>          <source>Password changed successfully.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/preferenceswindow.py" line="181"/> +        <location filename="../src/leap/bitmask/gui/preferenceswindow.py" line="225"/>          <source>There was a problem changing the password.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/preferenceswindow.py" line="184"/> +        <location filename="../src/leap/bitmask/gui/preferenceswindow.py" line="228"/>          <source>You did not enter a correct current password.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/preferenceswindow.py" line="317"/> +        <location filename="../src/leap/bitmask/gui/preferenceswindow.py" line="371"/>          <source>Services settings for provider '{0}' saved.</source>          <translation type="unfinished"></translation>      </message> +    <message> +        <location filename="../src/leap/bitmask/gui/preferenceswindow.py" line="97"/> +        <source>You need to enable {0} in order to change the password.</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/preferenceswindow.py" line="103"/> +        <source>You need to wait until {0} is ready in order to change the password.</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/preferenceswindow.py" line="113"/> +        <source>In order to change your password you need to be logged in.</source> +        <translation type="unfinished"></translation> +    </message>  </context>  <context>      <name>ProviderBootstrapper</name>      <message> -        <location filename="../src/leap/bitmask/provider/providerbootstrapper.py" line="137"/> +        <location filename="../src/leap/bitmask/provider/providerbootstrapper.py" line="146"/>          <source>Provider certificate could not be verified</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/provider/providerbootstrapper.py" line="144"/> +        <location filename="../src/leap/bitmask/provider/providerbootstrapper.py" line="153"/>          <source>Provider does not support HTTPS</source>          <translation type="unfinished"></translation>      </message> @@ -807,7 +1026,7 @@  <context>      <name>SRPAuth</name>      <message> -        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="711"/> +        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="714"/>          <source>Succeeded</source>          <translation type="unfinished"></translation>      </message> @@ -825,11 +1044,6 @@          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="62"/> -        <source><html><head/><body><p>Now we will guide you through some configuration that is needed before you can connect for the first time.</p><p>If you ever need to modify these options again, you can find the wizard in the <span style=" font-style:italic;">'Settings'</span> menu from the main window.</p><p>Do you want to <span style=" font-weight:600;">sign up</span> for a new account, or <span style=" font-weight:600;">log in</span> with an already existing username?</p></body></html></source> -        <translation type="unfinished"></translation> -    </message> -    <message>          <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="75"/>          <source>Sign up for a new account</source>          <translation type="unfinished"></translation> @@ -845,12 +1059,12 @@          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="305"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="298"/>          <source>Check</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="312"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="315"/>          <source>https://</source>          <translation type="unfinished"></translation>      </message> @@ -870,202 +1084,192 @@          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="333"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="336"/>          <source>Provider Information</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="336"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="339"/>          <source>Description of services offered by this provider</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="345"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="348"/>          <source>Name</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="377"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="380"/>          <source>Desc</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="387"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="390"/>          <source><b>Services offered:</b></source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="397"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="400"/>          <source>services</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="417"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="420"/>          <source><b>Enrollment policy:</b></source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="427"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="430"/>          <source>policy</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="447"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="450"/>          <source><b>URL:</b></source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="457"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="460"/>          <source>URL</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="464"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="467"/>          <source><b>Description:</b></source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="475"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="478"/>          <source>Provider setup</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="478"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="481"/>          <source>Gathering configuration options for this provider</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="500"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="503"/>          <source>We are downloading some bits that we need to establish a secure connection with the provider for the first time.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="523"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="526"/>          <source>Setting up provider</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="573"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="576"/>          <source>Getting info from the Certificate Authority</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="580"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="583"/>          <source>Do we trust this Certificate Authority?</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="587"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="590"/>          <source>Establishing a trust relationship with this provider</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="646"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="649"/>          <source>Register new user</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="649"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="652"/>          <source>Register a new user with provider</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="664"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="667"/>          <source><b>Password:</b></source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="691"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="694"/>          <source><b>Re-enter password:</b></source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="701"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="704"/>          <source>Register</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="747"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="750"/>          <source>Remember my username and password</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="771"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="774"/>          <source>Service selection</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="774"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="777"/>          <source>Please select the services you would like to have</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/wizard.py" line="138"/> +        <location filename="../src/leap/bitmask/gui/wizard.py" line="132"/>          <source>&Next ></source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/wizard.py" line="140"/> +        <location filename="../src/leap/bitmask/gui/wizard.py" line="134"/>          <source>Connect</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/wizard.py" line="235"/> +        <location filename="../src/leap/bitmask/gui/wizard.py" line="265"/>          <source>Starting registration...</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/wizard.py" line="266"/> +        <location filename="../src/leap/bitmask/gui/wizard.py" line="296"/>          <source>User %s successfully registered.</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/wizard.py" line="286"/> -        <source>Unknown error</source> -        <translation type="unfinished"></translation> -    </message> -    <message> -        <location filename="../src/leap/bitmask/gui/wizard.py" line="411"/> +        <location filename="../src/leap/bitmask/gui/wizard.py" line="448"/>          <source><font color='red'><b>Non-existent provider</b></font></source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/wizard.py" line="429"/> +        <location filename="../src/leap/bitmask/gui/wizard.py" line="466"/>          <source><font color='red'><b>%s</b></font></source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/wizard.py" line="457"/> +        <location filename="../src/leap/bitmask/gui/wizard.py" line="492"/>          <source>Unable to load provider configuration</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/wizard.py" line="463"/> +        <location filename="../src/leap/bitmask/gui/wizard.py" line="498"/>          <source><font color='red'><b>Not a valid provider</b></font></source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/wizard.py" line="529"/> +        <location filename="../src/leap/bitmask/gui/wizard.py" line="565"/>          <source>Services by %s</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/wizard.py" line="549"/> +        <location filename="../src/leap/bitmask/gui/wizard.py" line="585"/>          <source>Something went wrong while trying to load service %s</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/wizard.py" line="566"/> -        <source>Gathering configuration options for %s</source> -        <translation type="unfinished"></translation> -    </message> -    <message> -        <location filename="../src/leap/bitmask/gui/wizard.py" line="575"/> +        <location filename="../src/leap/bitmask/gui/wizard.py" line="616"/>          <source>Description of services offered by %s</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/wizard.py" line="596"/> +        <location filename="../src/leap/bitmask/gui/wizard.py" line="637"/>          <source>Register a new user with %s</source>          <translation type="unfinished"></translation>      </message> @@ -1085,7 +1289,7 @@          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="734"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="737"/>          <source><b>Username:</b></source>          <translation type="unfinished"></translation>      </message> @@ -1095,70 +1299,85 @@          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="275"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="288"/>          <source>Configure new provider:</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="285"/> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="305"/>          <source>Use existing one:</source>          <translation type="unfinished"></translation>      </message> +    <message> +        <location filename="../src/leap/bitmask/gui/ui/wizard.ui" line="62"/> +        <source><html><head/><body><p>Now we will guide you through some configuration that is needed before you can connect for the first time.</p><p>If you ever need to modify these options again, you can find the wizard in the <span style=" font-style:italic;">'Bitmask -&gt; Create new account...'</span> menu from the main window.</p><p>Do you want to <span style=" font-weight:600;">sign up</span> for a new account, or <span style=" font-weight:600;">log in</span> with an already existing username?</p></body></html></source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/wizard.py" line="317"/> +        <source>Something has gone wrong. Please try again.</source> +        <translation type="unfinished"></translation> +    </message> +    <message> +        <location filename="../src/leap/bitmask/gui/wizard.py" line="607"/> +        <source>Gathering configuration options for {0}</source> +        <translation type="unfinished"></translation> +    </message>  </context>  <context>      <name>__impl</name>      <message> -        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="288"/> +        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="298"/>          <source>The server did not send the salt parameter</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="292"/> +        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="302"/>          <source>The server did not send the B parameter</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="324"/> +        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="334"/>          <source>The data sent from the server had errors</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="346"/> +        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="356"/>          <source>Could not connect to the server</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="372"/> +        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="382"/>          <source>Unknown error (%s)</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="403"/> +        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="413"/>          <source>Problem getting data from server</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="429"/> +        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="439"/>          <source>Bad data from server</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="436"/> +        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="446"/>          <source>Auth verification failed</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="444"/> +        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="454"/>          <source>Session cookie verification failed</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="279"/> +        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="289"/>          <source>There was a problem with authentication</source>          <translation type="unfinished"></translation>      </message>      <message> -        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="174"/> +        <location filename="../src/leap/bitmask/crypto/srpauth.py" line="178"/>          <source>Invalid username or password.</source>          <translation type="unfinished"></translation>      </message> diff --git a/docs/dev/quickstart.rst b/docs/dev/quickstart.rst index 96dcaeb9..a8be955e 100644 --- a/docs/dev/quickstart.rst +++ b/docs/dev/quickstart.rst @@ -25,8 +25,8 @@ Prerequisites      $ sudo apt-get install git python-dev python-setuptools      python-virtualenv python-pip python-openssl libsqlite3-dev g++ openvpn -    pyside-tools python-pyside  -     +    pyside-tools python-pyside libffi-dev +  .. python-qt4  ??? (for translations)  .. TODO I'm pretty sure python-qt4 shoudln't be there...     Nor libsqlite-dev, that's a bug in python-sqlcipher/soledad. @@ -75,7 +75,7 @@ Running  Run ``bitmask_client`` in debug mode:: -    (bitmask)$ bitmask --debug   +    (bitmask)$ bitmask --debug  You should see the ``bitmask_client`` window prompting to connect to an  existing node or add a new one. If not, something went wrong, maybe ask diff --git a/docs/dev/signals.rst b/docs/dev/signals.rst new file mode 100644 index 00000000..536a3746 --- /dev/null +++ b/docs/dev/signals.rst @@ -0,0 +1,12 @@ +Startup process +--------------- + +mainwindow._login -> backend.run_provider_setup_checks +[...provider bootstrap...] +self._provider_config_loaded +[...login...] +authentication_finished +_start_eip_bootstrap +_maybe_start_eip +_maybe_run_soledad_setup_checks +soledadbootstrapper diff --git a/pkg/linux/resolv-update b/pkg/linux/resolv-update index 601d3bd2..c308b788 100755 --- a/pkg/linux/resolv-update +++ b/pkg/linux/resolv-update @@ -70,7 +70,7 @@ SETVAR            R="${R}nameserver $NS  "    done -  mv /etc/resolv.conf /etc/resolv.conf.bak +  cp /etc/resolv.conf /etc/resolv.conf.bak    echo "$comment  $custom_head  $R @@ -79,8 +79,8 @@ $custom_tail" > /etc/resolv.conf  function down() {    if [ -f /etc/resolv.conf.bak ] ; then -    unlink /etc/resolv.conf -    mv /etc/resolv.conf.bak /etc/resolv.conf +    cat /etc/resolv.conf.bak > /etc/resolv.conf +    rm /etc/resolv.conf.bak    fi  } diff --git a/pkg/requirements.pip b/pkg/requirements.pip index 04cd33de..be4ea858 100644 --- a/pkg/requirements.pip +++ b/pkg/requirements.pip @@ -10,10 +10,9 @@ requests>=1.1.0  srp>=1.0.2  pyopenssl  python-dateutil -psutil +psutil==1.2.1  ipaddr  twisted -qt4reactor  python-daemon # this should not be needed for Windows.  keyring  zope.proxy @@ -21,7 +20,7 @@ zope.proxy  leap.common>=0.3.4  leap.soledad.client>=0.4.2  leap.keymanager>=0.3.6 -leap.mail>=0.3.7 +leap.mail>=0.3.9  # Remove this when u1db fixes its dependency on oauth  oauth diff --git a/pkg/scripts/bootstrap_develop.sh b/pkg/scripts/bootstrap_develop.sh index 07063a81..7027a908 100755 --- a/pkg/scripts/bootstrap_develop.sh +++ b/pkg/scripts/bootstrap_develop.sh @@ -17,6 +17,7 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  ######################################################################  set -e  # Exit immediately if a command exits with a non-zero status. +  REPOSITORIES="bitmask_client leap_pycommon soledad keymanager leap_mail"  PACKAGES="leap_pycommon keymanager soledad/common soledad/client soledad/server leap_mail bitmask_client"  REPOS_ROOT=`pwd`  # Root path for all the needed repositories @@ -40,20 +41,16 @@ clone_repos() {      if [[ "$1" == "ro" ]]; then          # read-only remotes: -        git clone https://leap.se/git/bitmask_client -        git clone https://leap.se/git/leap_pycommon -        git clone https://leap.se/git/soledad -        git clone https://leap.se/git/keymanager -        git clone https://leap.se/git/leap_mail +        src="https://leap.se/git"      else          # read-write remotes: -        git clone ssh://gitolite@leap.se/bitmask_client -        git clone ssh://gitolite@leap.se/leap_pycommon -        git clone ssh://gitolite@leap.se/soledad -        git clone ssh://gitolite@leap.se/keymanager -        git clone ssh://gitolite@leap.se/leap_mail +        src="ssh://gitolite@leap.se"      fi +    for repo in $REPOSITORIES; do +        [ ! -d $repo ] && git clone $src/$repo +    done +      set +x      echo "${cc_green}Status: $status done!${cc_normal}"  } @@ -115,7 +112,7 @@ setup_develop() {      # do a setup develop in every package      for package in $PACKAGES; do          cd $REPOS_ROOT/$package -        python setup.py develop +        python setup.py develop --always-unzip      done      # hack to solve gnupg version problem @@ -163,10 +160,12 @@ update() {  }  run() { +    shift  # remove 'run' from arg list +    passthrough_args=$@      echo "${cc_green}Status: running client...${cc_normal}"      source bitmask.venv/bin/activate      set -x -    python bitmask_client/src/leap/bitmask/app.py -d $* +    python bitmask_client/src/leap/bitmask/app.py -d $passthrough_args      set +x  } @@ -175,7 +174,7 @@ help() {      echo "Bootstraps the environment to start developing the bitmask client"      echo "with all the needed repositories and dependencies."      echo -    echo "Usage: $0 {init | update | help}" +    echo "Usage: $0 {init | update | run | help}"      echo      echo "   init : Initialize repositories, create virtualenv and \`python setup.py develop\` all."      echo "          You can use \`init ro\` in order to use the https remotes if you don't have rw access." @@ -193,7 +192,7 @@ case "$1" in          update          ;;      run) -        run +        run "$@"          ;;      *)          help diff --git a/pkg/scripts/check_code.sh b/pkg/scripts/check_code.sh new file mode 100755 index 00000000..7bbd91f6 --- /dev/null +++ b/pkg/scripts/check_code.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# Escape code +esc=`echo -en "\033"` + +# Set colors +cc_green="${esc}[0;32m" +cc_red="${esc}[0;31m" +cc_normal=`echo -en "${esc}[m\017"` + + +[[ -z "$1" ]] && WHERE='src/leap/bitmask' || WHERE=$1 + +PEP8="pep8 --ignore=E202,W602 --exclude=*_rc.py,ui_*,_version.py $WHERE" +echo "${cc_green}$PEP8${cc_normal}" +$PEP8 + +echo +FLAKE8="flake8 --ignore=E202,W602 --exclude=*_rc.py,ui_*,_version.py $WHERE" +echo "${cc_green}$FLAKE8${cc_normal}" +$FLAKE8 + +echo +echo "${cc_green}Looking for 'print's, no prints in code, use logging/twisted.log.${cc_normal}" +echo `git grep -n "print " | wc -l` 'coincidences.' + +echo +echo "${cc_green}Grepping for 'pdb' code left behind.${cc_normal}" +git grep -n "pdb" + +echo +echo "${cc_green}Grepping for 'XXX|TODO|FIXME|NOTE|HACK'.${cc_normal}" +echo `git grep -E "XXX|TODO|FIXME|NOTE|HACK" | wc -l` 'coincidences.' diff --git a/pkg/scripts/monitor_resource.zsh b/pkg/scripts/monitor_resource.zsh new file mode 100755 index 00000000..ac468e34 --- /dev/null +++ b/pkg/scripts/monitor_resource.zsh @@ -0,0 +1,26 @@ +#!/bin/zsh +if (( ! $# )); then +    echo "Usage: $0:t <PID> <minutes to monitor>" >&2 +    return 1; +fi + +OUT="$1-usage.data" +GRAPH="bitmask-resources.png" +MAX=150 +let "ticks=$2*60/3" +echo "cpu mem" >> $OUT +for i in {1..$ticks}; do; +    cpu=$(ps -p $1 -o pcpu | grep -v %) +    mem=$(ps wuh -p $1 | awk '{print $4}') +    echo "$cpu $mem" >> $OUT; +    sleep 3; +    echo $i / $ticks; +done; + +gnuplot -e "set term dumb; \ +set key outside; set yrange [0:$MAX]; \ +plot for [col=1:2] '$OUT' using 0:col title columnheader s c" + +gnuplot -e "set term png; set output '$GRAPH'; \ +set key outside; set yrange [0:$MAX]; \ +plot for [col=1:2] '$OUT' using 0:col with lines title columnheader" diff --git a/pkg/scripts/stats.sh b/pkg/scripts/stats.sh new file mode 100755 index 00000000..2b7a8b18 --- /dev/null +++ b/pkg/scripts/stats.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +REPOSITORIES="bitmask_client leap_pycommon soledad keymanager leap_mail" + +CHANGED=0 +INSERTIONS=0 +DELETIONS=0 +MERGES_TOTAL=0 + +echo "Changes summary between closest annotated tag to HEAD" +echo "=====================================================" +echo + +for repo in $REPOSITORIES; do +    cd $repo +    echo "Stats for: $repo" +    # the 'describe' command gives the closest annotated tag +    STATS=$(git diff --shortstat `git describe --abbrev=0`..HEAD) +    MERGES=$(git log --merges `git describe --abbrev=0`..HEAD | wc -l) +    echo "Stats:$STATS" +    echo "Merges: $MERGES" +    VALUES=(`echo $STATS | awk '{ print $1, $4, $6 }'`)  # use array to store values +    CHANGED=$(echo $CHANGED + ${VALUES[0]} | bc) +    INSERTIONS=$(echo $INSERTIONS + ${VALUES[1]} | bc) +    DELETIONS=$(echo $DELETIONS + ${VALUES[2]} | bc) +    MERGES_TOTAL=$(echo $MERGES_TOTAL + $MERGES | bc) +    echo "----------------------------------------------------------------------" +    cd .. +done + +echo +echo "TOTAL" +echo "Stats: $CHANGED files changed, $INSERTIONS insertions(+), $DELETIONS deletions(-)" +echo "Merges: $MERGES_TOTAL" diff --git a/relnotes.txt b/relnotes.txt index 98af7a30..071b671e 100644 --- a/relnotes.txt +++ b/relnotes.txt @@ -1,8 +1,8 @@ -ANNOUNCING Bitmask, the internet encryption toolkit, release 0.3.7 +ANNOUNCING Bitmask, the Internet Encryption Toolkit, release 0.5.0  The LEAP  team is  pleased to announce  the immediate  availability of -version 0.3.8  of Bitmask,  the Internet Encryption  Toolkit, codename -"Three week child". +version 0.5.0  of Bitmask,  the Internet Encryption  Toolkit, codename +"Long time no see".  https://downloads.leap.se/client/ @@ -30,11 +30,11 @@ http://bitmask.rtfd.org/  WARNING: This is still  part of a beta release of  our software, a lot  of testing and auditing is still needed, so indeed use it, and feed us  back, fork it  and contribute to its development, but  by any means DO -NOT trust your life to it (yet!). +NOT trust your life to it.  WHAT CAN THIS VERSION OF BITMASK DO FOR ME? -Bitmask  0.3.8 improves  greatly  its mail  support  and stability  in +Bitmask  0.5.0 improves  greatly  its mail  support  and stability  in  general, among other various bug fixes. You can refer to the CHANGELOG  for the meat. @@ -65,14 +65,16 @@ the two.  INSTALLATION  We distribute the current version of Bitmask as standalone bundles for -GNU/Linux and OSX, but it is likely  that you are able to run it under -other systems,  specially if you are  skillful and patience is  one of -your virtues. +GNU/Linux, OSX and Windows, but it is likely  that you are able to run +it under other systems, specially if you are  skillful and patience is +one of your virtues.  Have a look at "docs/user/install.rst". -Packages will be soon provided for  debian and ubuntu, and the release -of windows bundles will be resumed shortly. +Packages  are  also  provided  for  debian  and  ubuntu,  add the leap +repository to your apt sources: + +deb http://deb.leap.se/debian wheezy main  We will  love to hear  if you are  interested in help  making packages  available for any other system. @@ -93,6 +95,6 @@ beyond any border.  The LEAP team, -Dec 6, 2013 +Apr 4, 2014  Somewhere in the middle of the intertubes.  EOF @@ -229,7 +229,7 @@ setup(      author_email='info@leap.se',      maintainer='Kali Kaneko',      maintainer_email='kali@leap.se', -    url='https://bitmask.rtfd.org', +    url='https://bitmask.net',      download_url=DOWNLOAD_URL,      license='GPL-3+',      packages=find_packages( diff --git a/src/leap/bitmask/__init__.py b/src/leap/bitmask/__init__.py index a4642e27..c844beb1 100644 --- a/src/leap/bitmask/__init__.py +++ b/src/leap/bitmask/__init__.py @@ -56,6 +56,7 @@ __short_version__ = "unknown"  try:      from leap.bitmask._version import get_versions      __version__ = get_versions()['version'] +    __version_hash__ = get_versions()['full']      IS_RELEASE_VERSION = _is_release_version(__version__)      del get_versions  except ImportError: diff --git a/src/leap/bitmask/_appname.py b/src/leap/bitmask/_appname.py new file mode 100644 index 00000000..82e8bd43 --- /dev/null +++ b/src/leap/bitmask/_appname.py @@ -0,0 +1 @@ +__appname__ = "bitmask" diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index 3bb9c8c3..02e27123 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -1,6 +1,6 @@  # -*- coding: utf-8 -*-  # app.py -# Copyright (C) 2013 LEAP +# Copyright (C) 2013, 2014 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 @@ -50,11 +50,16 @@ from PySide import QtCore, QtGui  from leap.bitmask import __version__ as VERSION  from leap.bitmask.util import leap_argparse -from leap.bitmask.util import log_silencer +from leap.bitmask.util import log_silencer, LOG_FORMAT  from leap.bitmask.util.leap_log_handler import LeapLogHandler  from leap.bitmask.util.streamtologger import StreamToLogger  from leap.bitmask.platform_init import IS_WIN +from leap.bitmask.services.mail import plumber  from leap.common.events import server as event_server +from leap.mail import __version__ as MAIL_VERSION + +from twisted.internet import reactor +from twisted.internet.task import LoopingCall  import codecs  codecs.register(lambda name: codecs.lookup('utf-8') @@ -72,13 +77,7 @@ def sigint_handler(*args, **kwargs):      mainwindow.quit() -def install_qtreactor(logger): -    import qt4reactor -    qt4reactor.install() -    logger.debug("Qt4 reactor installed") - - -def add_logger_handlers(debug=False, logfile=None): +def add_logger_handlers(debug=False, logfile=None, replace_stdout=True):      """      Create the logger and attach the handlers. @@ -98,10 +97,7 @@ def add_logger_handlers(debug=False, logfile=None):      # Create logger and formatter      logger = logging.getLogger(name='leap')      logger.setLevel(level) - -    log_format = ('%(asctime)s - %(name)s:%(funcName)s:L#%(lineno)s ' -                  '- %(levelname)s - %(message)s') -    formatter = logging.Formatter(log_format) +    formatter = logging.Formatter(LOG_FORMAT)      # Console handler      try: @@ -115,6 +111,9 @@ def add_logger_handlers(debug=False, logfile=None):      else:          using_coloredlog = True +    if using_coloredlog: +        replace_stdout = False +      silencer = log_silencer.SelectiveSilencerFilter()      console.addFilter(silencer)      logger.addHandler(console) @@ -137,7 +136,7 @@ def add_logger_handlers(debug=False, logfile=None):          logger.addHandler(fileh)          logger.debug('File handler plugged!') -    if not using_coloredlog: +    if replace_stdout:          replace_stdout_stderr_with_logging(logger)      return logger @@ -162,28 +161,47 @@ def replace_stdout_stderr_with_logging(logger):          log.startLogging(sys.stdout) -def main(): +def do_display_version(opts):      """ -    Starts the main event loop and launches the main window. +    Display version and exit.      """ -    _, opts = leap_argparse.init_leapc_args() - +    # TODO move to a different module: commands?      if opts.version:          print "Bitmask version: %s" % (VERSION,) +        print "leap.mail version: %s" % (MAIL_VERSION,) +        sys.exit(0) + + +def do_mail_plumbing(opts): +    """ +    Analize options and do mailbox plumbing if requested. +    """ +    # TODO move to a different module: commands? +    if opts.repair: +        plumber.repair_account(opts.acct) +        sys.exit(0) +    if opts.import_maildir and opts.acct: +        plumber.import_maildir(opts.acct, opts.import_maildir)          sys.exit(0) +    # XXX catch when import is used w/o acct + + +def main(): +    """ +    Starts the main event loop and launches the main window. +    """ +    # TODO move boilerplate outa here! +    _, opts = leap_argparse.init_leapc_args() +    do_display_version(opts)      standalone = opts.standalone +    offline = opts.offline      bypass_checks = getattr(opts, 'danger', False)      debug = opts.debug      logfile = opts.log_file +    mail_logfile = opts.mail_log_file      openvpn_verb = opts.openvpn_verb -    try: -        event_server.ensure_server(event_server.SERVER_PORT) -    except Exception as e: -        # We don't even have logger configured in here -        print "Could not ensure server: %r" % (e,) -      #############################################################      # Given how paths and bundling works, we need to delay the imports      # of certain parts that depend on this path settings. @@ -191,11 +209,39 @@ def main():      from leap.bitmask.config import flags      from leap.common.config.baseconfig import BaseConfig      flags.STANDALONE = standalone +    flags.OFFLINE = offline +    flags.MAIL_LOGFILE = mail_logfile +    flags.APP_VERSION_CHECK = opts.app_version_check +    flags.API_VERSION_CHECK = opts.api_version_check + +    flags.CA_CERT_FILE = opts.ca_cert_file +      BaseConfig.standalone = standalone -    logger = add_logger_handlers(debug, logfile) +    replace_stdout = True +    if opts.repair or opts.import_maildir: +        # We don't want too much clutter on the comand mode +        # this could be more generic with a Command class. +        replace_stdout = False +    logger = add_logger_handlers(debug, logfile, replace_stdout) + +    # ok, we got logging in place, we can satisfy mail plumbing requests +    # and show logs there. it normally will exit there if we got that path. +    do_mail_plumbing(opts) + +    try: +        event_server.ensure_server(event_server.SERVER_PORT) +    except Exception as e: +        # We don't even have logger configured in here +        print "Could not ensure server: %r" % (e,) + +    PLAY_NICE = os.environ.get("LEAP_NICE") +    if PLAY_NICE and PLAY_NICE.isdigit(): +        nice = os.nice(int(PLAY_NICE)) +        logger.info("Setting NICE: %s" % nice)      # And then we import all the other stuff +    # I think it's safe to import at the top by now -- kali      from leap.bitmask.gui import locale_rc      from leap.bitmask.gui import twisted_main      from leap.bitmask.gui.mainwindow import MainWindow @@ -206,6 +252,7 @@ def main():      # pylint: avoid unused import      assert(locale_rc) +    # TODO move to a different module: commands?      if not we_are_the_one_and_only():          # Bitmask is already running          logger.warning("Tried to launch more than one instance " @@ -217,6 +264,7 @@ def main():      logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')      logger.info('Bitmask version %s', VERSION) +    logger.info('leap.mail version %s', MAIL_VERSION)      logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')      logger.info('Starting app') @@ -230,9 +278,6 @@ def main():      app = QtGui.QApplication(sys.argv) -    # install the qt4reactor. -    install_qtreactor(logger) -      # To test:      # $ LANG=es ./app.py      locale = QtCore.QLocale.system().name() @@ -275,8 +320,9 @@ def main():      #tx_app = leap_services()      #assert(tx_app) -    # Run main loop -    twisted_main.start(app) +    l = LoopingCall(QtCore.QCoreApplication.processEvents, 0, 10) +    l.start(0.01) +    reactor.run()  if __name__ == "__main__":      main() diff --git a/src/leap/bitmask/backend.py b/src/leap/bitmask/backend.py new file mode 100644 index 00000000..45ea451c --- /dev/null +++ b/src/leap/bitmask/backend.py @@ -0,0 +1,489 @@ +# -*- coding: utf-8 -*- +# backend.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/>. +""" +Backend for everything +""" +import logging + +from functools import partial +from Queue import Queue, Empty + +from twisted.internet import threads, defer +from twisted.internet.task import LoopingCall +from twisted.python import log + +import zope.interface + +from leap.bitmask.config.providerconfig import ProviderConfig +from leap.bitmask.crypto.srpregister import SRPRegister +from leap.bitmask.provider import get_provider_path +from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper + +# Frontend side +from PySide import QtCore + +logger = logging.getLogger(__name__) + + +class ILEAPComponent(zope.interface.Interface): +    """ +    Interface that every component for the backend should comply to +    """ + +    key = zope.interface.Attribute("Key id for this component") + + +class ILEAPService(ILEAPComponent): +    """ +    Interface that every Service needs to implement +    """ + +    def start(self): +        """ +        Starts the service. +        """ +        pass + +    def stop(self): +        """ +        Stops the service. +        """ +        pass + +    def terminate(self): +        """ +        Terminates the service, not necessarily in a nice way. +        """ +        pass + +    def status(self): +        """ +        Returns a json object with the current status for the service. + +        :rtype: object (list, str, dict) +        """ +        # XXX: Use a namedtuple or a specific object instead of a json +        # object, since parsing it will be problematic otherwise. +        # It has to be something easily serializable though. +        pass + +    def set_configs(self, keyval): +        """ +        Sets the config parameters for this Service. + +        :param keyval: values to configure +        :type keyval: dict, {str: str} +        """ +        pass + +    def get_configs(self, keys): +        """ +        Returns the configuration values for the list of keys. + +        :param keys: keys to retrieve +        :type keys: list of str + +        :rtype: dict, {str: str} +        """ +        pass + + +class Provider(object): +    """ +    Interfaces with setup and bootstrapping operations for a provider +    """ + +    zope.interface.implements(ILEAPComponent) + +    PROBLEM_SIGNAL = "prov_problem_with_provider" + +    def __init__(self, signaler=None, bypass_checks=False): +        """ +        Constructor for the Provider component + +        :param signaler: Object in charge of handling communication +                         back to the frontend +        :type signaler: Signaler +        :param bypass_checks: Set to true if the app should bypass +                              first round of checks for CA +                              certificates at bootstrap +        :type bypass_checks: bool +        """ +        object.__init__(self) +        self.key = "provider" +        self._provider_bootstrapper = ProviderBootstrapper(signaler, +                                                           bypass_checks) +        self._download_provider_defer = None +        self._provider_config = ProviderConfig() + +    def setup_provider(self, provider): +        """ +        Initiates the setup for a provider + +        :param provider: URL for the provider +        :type provider: unicode + +        :returns: the defer for the operation running in a thread. +        :rtype: twisted.internet.defer.Deferred +        """ +        log.msg("Setting up provider %s..." % (provider.encode("idna"),)) +        pb = self._provider_bootstrapper +        d = pb.run_provider_select_checks(provider, download_if_needed=True) +        self._download_provider_defer = d +        return d + +    def cancel_setup_provider(self): +        """ +        Cancel the ongoing setup provider defer (if any). +        """ +        d = self._download_provider_defer +        if d is not None: +            d.cancel() + +    def bootstrap(self, provider): +        """ +        Second stage of bootstrapping for a provider. + +        :param provider: URL for the provider +        :type provider: unicode + +        :returns: the defer for the operation running in a thread. +        :rtype: twisted.internet.defer.Deferred +        """ +        d = None + +        # If there's no loaded provider or +        # we want to connect to other provider... +        if (not self._provider_config.loaded() or +                self._provider_config.get_domain() != provider): +            self._provider_config.load(get_provider_path(provider)) + +        if self._provider_config.loaded(): +            d = self._provider_bootstrapper.run_provider_setup_checks( +                self._provider_config, +                download_if_needed=True) +        else: +            if self._signaler is not None: +                self._signaler.signal(self.PROBLEM_SIGNAL) +            logger.error("Could not load provider configuration.") +            self._login_widget.set_enabled(True) + +        if d is None: +            d = defer.Deferred() +        return d + + +class Register(object): +    """ +    Interfaces with setup and bootstrapping operations for a provider +    """ + +    zope.interface.implements(ILEAPComponent) + +    def __init__(self, signaler=None): +        """ +        Constructor for the Register component + +        :param signaler: Object in charge of handling communication +                         back to the frontend +        :type signaler: Signaler +        """ +        object.__init__(self) +        self.key = "register" +        self._signaler = signaler +        self._provider_config = ProviderConfig() + +    def register_user(self, domain, username, password): +        """ +        Register a user using the domain and password given as parameters. + +        :param domain: the domain we need to register the user. +        :type domain: unicode +        :param username: the user name +        :type username: unicode +        :param password: the password for the username +        :type password: unicode + +        :returns: the defer for the operation running in a thread. +        :rtype: twisted.internet.defer.Deferred +        """ +        # If there's no loaded provider or +        # we want to connect to other provider... +        if (not self._provider_config.loaded() or +                self._provider_config.get_domain() != domain): +            self._provider_config.load(get_provider_path(domain)) + +        if self._provider_config.loaded(): +            srpregister = SRPRegister(signaler=self._signaler, +                                      provider_config=self._provider_config) +            return threads.deferToThread( +                partial(srpregister.register_user, username, password)) +        else: +            if self._signaler is not None: +                self._signaler.signal(self._signaler.srp_registration_failed) +            logger.error("Could not load provider configuration.") + + +class Signaler(QtCore.QObject): +    """ +    Signaler object, handles converting string commands to Qt signals. + +    This is intended for the separation in frontend/backend, this will +    live in the frontend. +    """ + +    #################### +    # These will only exist in the frontend +    # Signals for the ProviderBootstrapper +    prov_name_resolution = QtCore.Signal(object) +    prov_https_connection = QtCore.Signal(object) +    prov_download_provider_info = QtCore.Signal(object) + +    prov_download_ca_cert = QtCore.Signal(object) +    prov_check_ca_fingerprint = QtCore.Signal(object) +    prov_check_api_certificate = QtCore.Signal(object) + +    prov_problem_with_provider = QtCore.Signal(object) + +    prov_unsupported_client = QtCore.Signal(object) +    prov_unsupported_api = QtCore.Signal(object) + +    prov_cancelled_setup = QtCore.Signal(object) + +    # Signals for SRPRegister +    srp_registration_finished = QtCore.Signal(object) +    srp_registration_failed = QtCore.Signal(object) +    srp_registration_taken = QtCore.Signal(object) + +    #################### +    # These will exist both in the backend AND the front end. +    # The frontend might choose to not "interpret" all the signals +    # from the backend, but the backend needs to have all the signals +    # it's going to emit defined here +    PROV_NAME_RESOLUTION_KEY = "prov_name_resolution" +    PROV_HTTPS_CONNECTION_KEY = "prov_https_connection" +    PROV_DOWNLOAD_PROVIDER_INFO_KEY = "prov_download_provider_info" +    PROV_DOWNLOAD_CA_CERT_KEY = "prov_download_ca_cert" +    PROV_CHECK_CA_FINGERPRINT_KEY = "prov_check_ca_fingerprint" +    PROV_CHECK_API_CERTIFICATE_KEY = "prov_check_api_certificate" +    PROV_PROBLEM_WITH_PROVIDER_KEY = "prov_problem_with_provider" +    PROV_UNSUPPORTED_CLIENT = "prov_unsupported_client" +    PROV_UNSUPPORTED_API = "prov_unsupported_api" +    PROV_CANCELLED_SETUP = "prov_cancelled_setup" + +    SRP_REGISTRATION_FINISHED = "srp_registration_finished" +    SRP_REGISTRATION_FAILED = "srp_registration_failed" +    SRP_REGISTRATION_TAKEN = "srp_registration_taken" + +    def __init__(self): +        """ +        Constructor for the Signaler +        """ +        QtCore.QObject.__init__(self) +        self._signals = {} + +        signals = [ +            self.PROV_NAME_RESOLUTION_KEY, +            self.PROV_HTTPS_CONNECTION_KEY, +            self.PROV_DOWNLOAD_PROVIDER_INFO_KEY, +            self.PROV_DOWNLOAD_CA_CERT_KEY, +            self.PROV_CHECK_CA_FINGERPRINT_KEY, +            self.PROV_CHECK_API_CERTIFICATE_KEY, +            self.PROV_PROBLEM_WITH_PROVIDER_KEY, +            self.PROV_UNSUPPORTED_CLIENT, +            self.PROV_UNSUPPORTED_API, +            self.PROV_CANCELLED_SETUP, + +            self.SRP_REGISTRATION_FINISHED, +            self.SRP_REGISTRATION_FAILED, +            self.SRP_REGISTRATION_TAKEN, +        ] + +        for sig in signals: +            self._signals[sig] = getattr(self, sig) + +    def signal(self, key, data=None): +        """ +        Emits a Qt signal based on the key provided, with the data if provided. + +        :param key: string identifying the signal to emit +        :type key: str +        :param data: object to send with the data +        :type data: object + +        NOTE: The data object will be a serialized str in the backend, +        and an unserialized object in the frontend, but for now we +        just care about objects. +        """ +        # Right now it emits Qt signals. The backend version of this +        # will do zmq.send_multipart, and the frontend version will be +        # similar to this +        log.msg("Signaling %s :: %s" % (key, data)) + +        # for some reason emitting 'None' gives a segmentation fault. +        if data is None: +            data = '' + +        try: +            self._signals[key].emit(data) +        except KeyError: +            log.msg("Unknown key for signal %s!" % (key,)) + + +class Backend(object): +    """ +    Backend for everything, the UI should only use this class. +    """ + +    PASSED_KEY = "passed" +    ERROR_KEY = "error" + +    def __init__(self, bypass_checks=False): +        """ +        Constructor for the backend. +        """ +        object.__init__(self) + +        # Components map for the commands received +        self._components = {} + +        # Ongoing defers that will be cancelled at stop time +        self._ongoing_defers = [] + +        # Signaler object to translate commands into Qt signals +        self._signaler = Signaler() + +        # Component registration +        self._register(Provider(self._signaler, bypass_checks)) +        self._register(Register(self._signaler)) + +        # We have a looping call on a thread executing all the +        # commands in queue. Right now this queue is an actual Queue +        # object, but it'll become the zmq recv_multipart queue +        self._lc = LoopingCall(threads.deferToThread, self._worker) + +        # Temporal call_queue for worker, will be replaced with +        # recv_multipart os something equivalent in the loopingcall +        self._call_queue = Queue() + +    @property +    def signaler(self): +        """ +        Public signaler access to let the UI connect to its signals. +        """ +        return self._signaler + +    def start(self): +        """ +        Starts the looping call +        """ +        log.msg("Starting worker...") +        self._lc.start(0.01) + +    def stop(self): +        """ +        Stops the looping call and tries to cancel all the defers. +        """ +        log.msg("Stopping worker...") +        if self._lc.running: +            self._lc.stop() +        while len(self._ongoing_defers) > 0: +            d = self._ongoing_defers.pop() +            d.cancel() + +    def _register(self, component): +        """ +        Registers a component in this backend + +        :param component: Component to register +        :type component: any object that implements ILEAPComponent +        """ +        # TODO: assert that the component implements the interfaces +        # expected +        try: +            self._components[component.key] = component +        except Exception: +            log.msg("There was a problem registering %s" % (component,)) +            log.err() + +    def _signal_back(self, _, signal): +        """ +        Helper method to signal back (callback like behavior) to the +        UI that an operation finished. + +        :param signal: signal name +        :type signal: str +        """ +        self._signaler.signal(signal) + +    def _worker(self): +        """ +        Worker method, called from a different thread and as a part of +        a looping call +        """ +        try: +            # this'll become recv_multipart +            cmd = self._call_queue.get(block=False) + +            # cmd is: component, method, signalback, *args +            func = getattr(self._components[cmd[0]], cmd[1]) +            d = func(*cmd[3:]) +            if d is not None:  # d may be None if a defer chain is cancelled. +                # A call might not have a callback signal, but if it does, +                # we add it to the chain +                if cmd[2] is not None: +                    d.addCallbacks(self._signal_back, log.err, cmd[2]) +                d.addCallbacks(self._done_action, log.err, +                               callbackKeywords={"d": d}) +                d.addErrback(log.err) +                self._ongoing_defers.append(d) +        except Empty: +            # If it's just empty we don't have anything to do. +            pass +        except defer.CancelledError: +            logger.debug("defer cancelled somewhere (CancelledError).") +        except Exception: +            # But we log the rest +            log.err() + +    def _done_action(self, _, d): +        """ +        Remover of the defer once it's done + +        :param d: defer to remove +        :type d: twisted.internet.defer.Deferred +        """ +        if d in self._ongoing_defers: +            self._ongoing_defers.remove(d) + +    # XXX: Temporal interface until we migrate to zmq +    # We simulate the calls to zmq.send_multipart. Once we separate +    # this in two processes, the methods bellow can be changed to +    # send_multipart and this backend class will be really simple. + +    def setup_provider(self, provider): +        self._call_queue.put(("provider", "setup_provider", None, provider)) + +    def cancel_setup_provider(self): +        self._call_queue.put(("provider", "cancel_setup_provider", None)) + +    def provider_bootstrap(self, provider): +        self._call_queue.put(("provider", "bootstrap", None, provider)) + +    def register_user(self, provider, username, password): +        self._call_queue.put(("register", "register_user", None, provider, +                              username, password)) diff --git a/src/leap/bitmask/config/flags.py b/src/leap/bitmask/config/flags.py index 98395def..5d8bc9b3 100644 --- a/src/leap/bitmask/config/flags.py +++ b/src/leap/bitmask/config/flags.py @@ -30,3 +30,23 @@ WARNING: You should NOT use this kind of flags unless you're sure of what  #   - search for binaries inside the bundled app instead of the system ones.  #     e.g.: openvpn, gpg  STANDALONE = False + +MAIL_LOGFILE = None + +# The APP/API version check flags are used to provide a way to skip +# that checks. +# This can be used for: +#   - allow the use of a client that is not compatible with a provider. +#   - use a development version of the client with an older version number +#     since it's not released yet, and it is compatible with a newer provider. +APP_VERSION_CHECK = True +API_VERSION_CHECK = True + +# Offline mode? +# Used for skipping soledad bootstrapping/syncs. +OFFLINE = False + + +# CA cert path +# used to allow self signed certs in requests that needs SSL +CA_CERT_FILE = None diff --git a/src/leap/bitmask/config/leapsettings.py b/src/leap/bitmask/config/leapsettings.py index c524425e..13a1e99e 100644 --- a/src/leap/bitmask/config/leapsettings.py +++ b/src/leap/bitmask/config/leapsettings.py @@ -1,6 +1,6 @@  # -*- coding: utf-8 -*-  # leapsettings.py -# Copyright (C) 2013 LEAP +# Copyright (C) 2013, 2014 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 @@ -14,9 +14,8 @@  #  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. -  """ -QSettings abstraction +QSettings abstraction.  """  import os  import logging @@ -70,6 +69,7 @@ class LeapSettings(object):      GATEWAY_KEY = "Gateway"      PINNED_KEY = "Pinned"      SKIPFIRSTRUN_KEY = "SkipFirstRun" +    UUIDFORUSER_KEY = "%s/%s_uuid"      # values      GATEWAY_AUTOMATIC = "Automatic" @@ -353,3 +353,35 @@ class LeapSettings(object):          """          leap_assert_type(skip, bool)          self._settings.setValue(self.SKIPFIRSTRUN_KEY, skip) + +    def get_uuid(self, username): +        """ +        Gets the uuid for a given username. + +        :param username: the full user identifier in the form user@provider +        :type username: basestring +        """ +        leap_assert("@" in username, +                    "Expected username in the form user@provider") +        user, provider = username.split('@') +        return self._settings.value( +            self.UUIDFORUSER_KEY % (provider, user), "") + +    def set_uuid(self, username, value): +        """ +        Sets the uuid for a given username. + +        :param username: the full user identifier in the form user@provider +        :type username: str or unicode +        :param value: the uuid to save or None to remove it +        :type value: str or unicode or None +        """ +        leap_assert("@" in username, +                    "Expected username in the form user@provider") +        user, provider = username.split('@') +        key = self.UUIDFORUSER_KEY % (provider, user) +        if value is None: +            self._settings.remove(key) +        else: +            leap_assert(len(value) > 0, "We cannot save an empty uuid") +            self._settings.setValue(key, value) diff --git a/src/leap/bitmask/config/providerconfig.py b/src/leap/bitmask/config/providerconfig.py index e80b2337..2ebe05ce 100644 --- a/src/leap/bitmask/config/providerconfig.py +++ b/src/leap/bitmask/config/providerconfig.py @@ -21,11 +21,12 @@ Provider configuration  import logging  import os -from leap.common.check import leap_check -from leap.common.config.baseconfig import BaseConfig, LocalizedKey +from leap.bitmask import provider  from leap.bitmask.config.provider_spec import leap_provider_spec  from leap.bitmask.services import get_service_display_name  from leap.bitmask.util import get_path_prefix +from leap.common.check import leap_check +from leap.common.config.baseconfig import BaseConfig, LocalizedKey  logger = logging.getLogger(__name__) @@ -55,10 +56,7 @@ class ProviderConfig(BaseConfig):          :rtype: ProviderConfig or None if there is a problem loading the config          """          provider_config = ProviderConfig() -        provider_config_path = os.path.join( -            "leap", "providers", domain, "provider.json") - -        if not provider_config.load(provider_config_path): +        if not provider_config.load(provider.get_provider_path(domain)):              provider_config = None          return provider_config diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py index 85b9b003..7cf7e55a 100644 --- a/src/leap/bitmask/crypto/srpauth.py +++ b/src/leap/bitmask/crypto/srpauth.py @@ -31,6 +31,7 @@ from requests.adapters import HTTPAdapter  from PySide import QtCore  from twisted.internet import threads +from leap.bitmask.config.leapsettings import LeapSettings  from leap.bitmask.util import request_helpers as reqhelper  from leap.bitmask.util.compat import requests_has_max_retries  from leap.bitmask.util.constants import REQUEST_TIMEOUT @@ -147,6 +148,7 @@ class SRPAuth(QtCore.QObject):                          "We need a provider config to authenticate")              self._provider_config = provider_config +            self._settings = LeapSettings()              # **************************************************** #              # Dependency injection helpers, override this for more @@ -161,17 +163,14 @@ class SRPAuth(QtCore.QObject):              self._session_id = None              self._session_id_lock = QtCore.QMutex() -            self._uid = None -            self._uid_lock = QtCore.QMutex() +            self._uuid = None +            self._uuid_lock = QtCore.QMutex()              self._token = None              self._token_lock = QtCore.QMutex()              self._srp_user = None              self._srp_a = None -            # Error msg displayed if the username or the password is invalid -            self._WRONG_USER_PASS = self.tr("Invalid username or password.") -              # User credentials stored for password changing checks              self._username = None              self._password = None @@ -265,14 +264,11 @@ class SRPAuth(QtCore.QObject):                  # Clean up A value, we don't need it anymore                  self._srp_a = None              except requests.exceptions.ConnectionError as e: -                logger.error("No connection made (salt): %r" % -                             (e,)) -                raise SRPAuthConnectionError("Could not establish a " -                                             "connection") +                logger.error("No connection made (salt): {0!r}".format(e)) +                raise SRPAuthConnectionError()              except Exception as e:                  logger.error("Unknown error: %r" % (e,)) -                raise SRPAuthenticationError("Unknown error: %r" % -                                             (e,)) +                raise SRPAuthenticationError()              content, mtime = reqhelper.get_content(init_session) @@ -281,23 +277,22 @@ class SRPAuth(QtCore.QObject):                               "Status code = %r. Content: %r" %                               (init_session.status_code, content))                  if init_session.status_code == 422: -                    raise SRPAuthBadUserOrPassword(self._WRONG_USER_PASS) +                    logger.error("Invalid username or password.") +                    raise SRPAuthBadUserOrPassword() -                raise SRPAuthBadStatusCode(self.tr("There was a problem with" -                                                   " authentication")) +                logger.error("There was a problem with authentication.") +                raise SRPAuthBadStatusCode()              json_content = json.loads(content)              salt = json_content.get("salt", None)              B = json_content.get("B", None)              if salt is None: -                logger.error("No salt parameter sent") -                raise SRPAuthNoSalt(self.tr("The server did not send " -                                            "the salt parameter")) +                logger.error("The server didn't send the salt parameter.") +                raise SRPAuthNoSalt()              if B is None: -                logger.error("No B parameter sent") -                raise SRPAuthNoB(self.tr("The server did not send " -                                         "the B parameter")) +                logger.error("The server didn't send the B parameter.") +                raise SRPAuthNoB()              return salt, B @@ -328,8 +323,7 @@ class SRPAuth(QtCore.QObject):                  unhex_B = self._safe_unhexlify(B)              except (TypeError, ValueError) as e:                  logger.error("Bad data from server: %r" % (e,)) -                raise SRPAuthBadDataFromServer( -                    self.tr("The data sent from the server had errors")) +                raise SRPAuthBadDataFromServer()              M = self._srp_user.process_challenge(unhex_salt, unhex_B)              auth_url = "%s/%s/%s/%s" % (self._provider_config.get_api_uri(), @@ -350,13 +344,13 @@ class SRPAuth(QtCore.QObject):                                                  timeout=REQUEST_TIMEOUT)              except requests.exceptions.ConnectionError as e:                  logger.error("No connection made (HAMK): %r" % (e,)) -                raise SRPAuthConnectionError(self.tr("Could not connect to " -                                                     "the server")) +                raise SRPAuthConnectionError()              try:                  content, mtime = reqhelper.get_content(auth_result)              except JSONDecodeError: -                raise SRPAuthJSONDecodeError("Bad JSON content in auth result") +                logger.error("Bad JSON content in auth result.") +                raise SRPAuthJSONDecodeError()              if auth_result.status_code == 422:                  error = "" @@ -370,14 +364,13 @@ class SRPAuth(QtCore.QObject):                                   "received: %s", (content,))                  logger.error("[%s] Wrong password (HAMK): [%s]" %                               (auth_result.status_code, error)) -                raise SRPAuthBadUserOrPassword(self._WRONG_USER_PASS) +                raise SRPAuthBadUserOrPassword()              if auth_result.status_code not in (200,):                  logger.error("No valid response (HAMK): "                               "Status code = %s. Content = %r" %                               (auth_result.status_code, content)) -                raise SRPAuthBadStatusCode(self.tr("Unknown error (%s)") % -                                           (auth_result.status_code,)) +                raise SRPAuthBadStatusCode()              return json.loads(content) @@ -394,24 +387,22 @@ class SRPAuth(QtCore.QObject):              """              try:                  M2 = json_content.get("M2", None) -                uid = json_content.get("id", None) +                uuid = json_content.get("id", None)                  token = json_content.get("token", None)              except Exception as e:                  logger.error(e) -                raise SRPAuthBadDataFromServer("Something went wrong with the " -                                               "login") +                raise SRPAuthBadDataFromServer() -            self.set_uid(uid) +            self.set_uuid(uuid)              self.set_token(token) -            if M2 is None or self.get_uid() is None: +            if M2 is None or self.get_uuid() is None:                  logger.error("Something went wrong. Content = %r" %                               (json_content,)) -                raise SRPAuthBadDataFromServer(self.tr("Problem getting data " -                                                       "from server")) +                raise SRPAuthBadDataFromServer()              events_signal( -                proto.CLIENT_UID, content=uid, +                proto.CLIENT_UID, content=uuid,                  reqcbk=lambda req, res: None)  # make the rpc call async              return M2 @@ -434,22 +425,19 @@ class SRPAuth(QtCore.QObject):                  unhex_M2 = self._safe_unhexlify(M2)              except TypeError:                  logger.error("Bad data from server (HAWK)") -                raise SRPAuthBadDataFromServer(self.tr("Bad data from server")) +                raise SRPAuthBadDataFromServer()              self._srp_user.verify_session(unhex_M2)              if not self._srp_user.authenticated(): -                logger.error("Auth verification failed") -                raise SRPAuthVerificationFailed(self.tr("Auth verification " -                                                        "failed")) +                logger.error("Auth verification failed.") +                raise SRPAuthVerificationFailed()              logger.debug("Session verified.")              session_id = self._session.cookies.get(self.SESSION_ID_KEY, None)              if not session_id:                  logger.error("Bad cookie from server (missing _session_id)") -                raise SRPAuthNoSessionId(self.tr("Session cookie " -                                                 "verification " -                                                 "failed")) +                raise SRPAuthNoSessionId()              events_signal(                  proto.CLIENT_SESSION_ID, content=session_id, @@ -475,7 +463,7 @@ class SRPAuth(QtCore.QObject):              :param new_password: the new password for the user              :type new_password: str              """ -            leap_assert(self.get_uid() is not None) +            leap_assert(self.get_uuid() is not None)              if current_password != self._password:                  raise SRPAuthBadUserOrPassword @@ -483,7 +471,7 @@ class SRPAuth(QtCore.QObject):              url = "%s/%s/users/%s.json" % (                  self._provider_config.get_api_uri(),                  self._provider_config.get_api_version(), -                self.get_uid()) +                self.get_uuid())              salt, verifier = self._srp.create_salted_verification_key(                  self._username.encode('utf-8'), new_password.encode('utf-8'), @@ -580,7 +568,7 @@ class SRPAuth(QtCore.QObject):                  raise              else:                  self.set_session_id(None) -                self.set_uid(None) +                self.set_uuid(None)                  self.set_token(None)                  # Also reset the session                  self._session = self._fetcher.session() @@ -594,13 +582,17 @@ class SRPAuth(QtCore.QObject):              QtCore.QMutexLocker(self._session_id_lock)              return self._session_id -        def set_uid(self, uid): -            QtCore.QMutexLocker(self._uid_lock) -            self._uid = uid +        def set_uuid(self, uuid): +            QtCore.QMutexLocker(self._uuid_lock) +            full_uid = "%s@%s" % ( +                self._username, self._provider_config.get_domain()) +            if uuid is not None:  # avoid removing the uuid from settings +                self._settings.set_uuid(full_uid, uuid) +            self._uuid = uuid -        def get_uid(self): -            QtCore.QMutexLocker(self._uid_lock) -            return self._uid +        def get_uuid(self): +            QtCore.QMutexLocker(self._uuid_lock) +            return self._uuid          def set_token(self, token):              QtCore.QMutexLocker(self._token_lock) @@ -612,8 +604,9 @@ class SRPAuth(QtCore.QObject):      __instance = None -    authentication_finished = QtCore.Signal(bool, str) -    logout_finished = QtCore.Signal(bool, str) +    authentication_finished = QtCore.Signal() +    logout_ok = QtCore.Signal() +    logout_error = QtCore.Signal()      def __init__(self, provider_config):          """ @@ -650,7 +643,6 @@ class SRPAuth(QtCore.QObject):          username = username.lower()          d = self.__instance.authenticate(username, password)          d.addCallback(self._gui_notify) -        d.addErrback(self._errback)          return d      def change_password(self, current_password, new_password): @@ -676,7 +668,7 @@ class SRPAuth(QtCore.QObject):          :rtype: str or None          """ -        if self.get_uid() is None: +        if self.get_uuid() is None:              return None          return self.__instance._username @@ -688,25 +680,13 @@ class SRPAuth(QtCore.QObject):          :type _: IGNORED          """          logger.debug("Successful login!") -        self.authentication_finished.emit(True, self.tr("Succeeded")) - -    def _errback(self, failure): -        """ -        General errback for the whole login process. Will notify the -        UI with the proper signal. - -        :param failure: Failure object captured from a callback. -        :type failure: twisted.python.failure.Failure -        """ -        logger.error("Error logging in %s" % (failure,)) -        self.authentication_finished.emit(False, "%s" % (failure.value,)) -        failure.trap(Exception) +        self.authentication_finished.emit()      def get_session_id(self):          return self.__instance.get_session_id() -    def get_uid(self): -        return self.__instance.get_uid() +    def get_uuid(self): +        return self.__instance.get_uuid()      def get_token(self):          return self.__instance.get_token() @@ -718,8 +698,10 @@ class SRPAuth(QtCore.QObject):          """          try:              self.__instance.logout() -            self.logout_finished.emit(True, self.tr("Succeeded")) +            logger.debug("Logout success") +            self.logout_ok.emit()              return True          except Exception as e: -            self.logout_finished.emit(False, "%s" % (e,)) +            logger.debug("Logout error: {0!r}".format(e)) +            self.logout_error.emit()          return False diff --git a/src/leap/bitmask/crypto/srpregister.py b/src/leap/bitmask/crypto/srpregister.py index 02a1ea63..4c52db42 100644 --- a/src/leap/bitmask/crypto/srpregister.py +++ b/src/leap/bitmask/crypto/srpregister.py @@ -16,6 +16,7 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  import binascii +import json  import logging  import requests @@ -26,6 +27,7 @@ from urlparse import urlparse  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.util.constants import SIGNUP_TIMEOUT +from leap.bitmask.util.request_helpers import get_content  from leap.common.check import leap_assert, leap_assert_type  logger = logging.getLogger(__name__) @@ -40,16 +42,22 @@ class SRPRegister(QtCore.QObject):      USER_VERIFIER_KEY = 'user[password_verifier]'      USER_SALT_KEY = 'user[password_salt]' +    STATUS_OK = (200, 201) +    STATUS_TAKEN = 422 +    STATUS_ERROR = -999  # Custom error status +      registration_finished = QtCore.Signal(bool, object) -    def __init__(self, -                 provider_config=None, -                 register_path="users"): +    def __init__(self, signaler=None, +                 provider_config=None, register_path="users"):          """          Constructor +        :param signaler: Signaler object used to receive notifications +                         from the backend +        :type signaler: Signaler          :param provider_config: provider configuration instance, -        properly loaded +                                properly loaded          :type privider_config: ProviderConfig          :param register_path: webapp path for registering users          :type register_path; str @@ -59,6 +67,7 @@ class SRPRegister(QtCore.QObject):          leap_assert_type(provider_config, ProviderConfig)          self._provider_config = provider_config +        self._signaler = signaler          # **************************************************** #          # Dependency injection helpers, override this for more @@ -104,8 +113,8 @@ class SRPRegister(QtCore.QObject):          :param password: password for this username          :type password: str -        :rtype: tuple -        :rparam: (ok, request) +        :returns: if the registration went ok or not. +        :rtype: bool          """          username = username.lower().encode('utf-8') @@ -129,11 +138,7 @@ class SRPRegister(QtCore.QObject):          logger.debug("Will try to register user = %s" % (username,))          ok = False -        # This should be None, but we don't like when PySide segfaults, -        # so it something else. -        # To reproduce it, just do: -        # self.registration_finished.emit(False, None) -        req = [] +        req = None          try:              req = self._session.post(uri,                                       data=user_data, @@ -143,13 +148,45 @@ class SRPRegister(QtCore.QObject):          except requests.exceptions.RequestException as exc:              logger.error(exc.message) -            ok = False          else:              ok = req.ok -        self.registration_finished.emit(ok, req) +        status_code = self.STATUS_ERROR +        if req is not None: +            status_code = req.status_code +        self._emit_result(status_code) + +        if not ok: +            try: +                content, _ = get_content(req) +                json_content = json.loads(content) +                error_msg = json_content.get("errors").get("login")[0] +                if not error_msg.istitle(): +                    error_msg = "%s %s" % (username, error_msg) +                logger.error(error_msg) +            except Exception as e: +                logger.error("Unknown error: %r" % (e, )) +          return ok +    def _emit_result(self, status_code): +        """ +        Emit the corresponding signal depending on the status code. + +        :param status_code: the status code received. +        :type status_code: int or str +        """ +        logger.debug("Status code is: {0}".format(status_code)) +        if self._signaler is None: +            return + +        if status_code in self.STATUS_OK: +            self._signaler.signal(self._signaler.SRP_REGISTRATION_FINISHED) +        elif status_code == self.STATUS_TAKEN: +            self._signaler.signal(self._signaler.SRP_REGISTRATION_TAKEN) +        else: +            self._signaler.signal(self._signaler.SRP_REGISTRATION_FAILED) +  if __name__ == "__main__":      logger = logging.getLogger(name='leap') diff --git a/src/leap/bitmask/crypto/tests/fake_provider.py b/src/leap/bitmask/crypto/tests/fake_provider.py index 54af485d..b8cdbb12 100755 --- a/src/leap/bitmask/crypto/tests/fake_provider.py +++ b/src/leap/bitmask/crypto/tests/fake_provider.py @@ -280,7 +280,6 @@ class FakeSession(Resource):          if HAMK is None:              print '[server] verification failed!!!'              raise Exception("Authentication failed!") -            #import ipdb;ipdb.set_trace()          assert svr.authenticated()          print "***" diff --git a/src/leap/bitmask/crypto/tests/test_srpauth.py b/src/leap/bitmask/crypto/tests/test_srpauth.py index e63c1385..511a12ed 100644 --- a/src/leap/bitmask/crypto/tests/test_srpauth.py +++ b/src/leap/bitmask/crypto/tests/test_srpauth.py @@ -520,9 +520,9 @@ class SRPAuthTestCase(unittest.TestCase):          m2 = self.auth_backend._extract_data(test_data)          self.assertEqual(m2, test_m2) -        self.assertEqual(self.auth_backend.get_uid(), test_uid) -        self.assertEqual(self.auth_backend.get_uid(), -                         self.auth.get_uid()) +        self.assertEqual(self.auth_backend.get_uuid(), test_uid) +        self.assertEqual(self.auth_backend.get_uuid(), +                         self.auth.get_uuid())          self.assertEqual(self.auth_backend.get_token(), test_token)          self.assertEqual(self.auth_backend.get_token(),                           self.auth.get_token()) @@ -691,7 +691,7 @@ class SRPAuthTestCase(unittest.TestCase):              old_session = self.auth_backend._session              self.auth_backend.logout()              self.assertIsNone(self.auth_backend.get_session_id()) -            self.assertIsNone(self.auth_backend.get_uid()) +            self.assertIsNone(self.auth_backend.get_uuid())              self.assertNotEqual(old_session, self.auth_backend._session)          d = threads.deferToThread(wrapper) diff --git a/src/leap/bitmask/gui/advanced_key_management.py b/src/leap/bitmask/gui/advanced_key_management.py index 2c0fa034..cbc8c3e3 100644 --- a/src/leap/bitmask/gui/advanced_key_management.py +++ b/src/leap/bitmask/gui/advanced_key_management.py @@ -48,20 +48,25 @@ class AdvancedKeyManagement(QtGui.QWidget):          self.ui = Ui_AdvancedKeyManagement()          self.ui.setupUi(self) +        # XXX: Temporarily disable the key import. +        self.ui.pbImportKeys.setVisible(False) +          # if Soledad is not started yet          if sameProxiedObjects(soledad, None): -            self.ui.container.setEnabled(False) +            self.ui.gbMyKeyPair.setEnabled(False) +            self.ui.gbStoredPublicKeys.setEnabled(False)              msg = self.tr("<span style='color:#0000FF;'>NOTE</span>: "                            "To use this, you need to enable/start {0}.")              msg = msg.format(get_service_display_name(MX_SERVICE))              self.ui.lblStatus.setText(msg)              return -        else: -            msg = self.tr( -                "<span style='color:#ff0000;'>WARNING</span>:<br>" -                "This is an experimental feature, you can lose access to " -                "existing e-mails.") -            self.ui.lblStatus.setText(msg) +        # XXX: since import is disabled this is no longer a dangerous feature. +        # else: +        #     msg = self.tr( +        #         "<span style='color:#ff0000;'>WARNING</span>:<br>" +        #         "This is an experimental feature, you can lose access to " +        #         "existing e-mails.") +        #     self.ui.lblStatus.setText(msg)          self._keymanager = keymanager          self._soledad = soledad @@ -79,6 +84,12 @@ class AdvancedKeyManagement(QtGui.QWidget):          self.ui.pbImportKeys.clicked.connect(self._import_keys)          self.ui.pbExportKeys.clicked.connect(self._export_keys) +        # Stretch columns to content +        self.ui.twPublicKeys.horizontalHeader().setResizeMode( +            0, QtGui.QHeaderView.Stretch) + +        self._list_keys() +      def _import_keys(self):          """          Imports the user's key pair. @@ -183,3 +194,16 @@ class AdvancedKeyManagement(QtGui.QWidget):                  return          else:              logger.debug('Export canceled by the user.') + +    def _list_keys(self): +        """ +        Loads all the public keys stored in the local db to the keys table. +        """ +        keys = self._keymanager.get_all_keys_in_local_db() + +        keys_table = self.ui.twPublicKeys +        for key in keys: +            row = keys_table.rowCount() +            keys_table.insertRow(row) +            keys_table.setItem(row, 0, QtGui.QTableWidgetItem(key.address)) +            keys_table.setItem(row, 1, QtGui.QTableWidgetItem(key.key_id)) diff --git a/src/leap/bitmask/gui/eip_preferenceswindow.py b/src/leap/bitmask/gui/eip_preferenceswindow.py index 504d1cf1..dcaa8b1e 100644 --- a/src/leap/bitmask/gui/eip_preferenceswindow.py +++ b/src/leap/bitmask/gui/eip_preferenceswindow.py @@ -22,7 +22,7 @@ import os  import logging  from functools import partial -from PySide import QtGui +from PySide import QtCore, QtGui  from leap.bitmask.config.leapsettings import LeapSettings  from leap.bitmask.config.providerconfig import ProviderConfig @@ -37,10 +37,12 @@ class EIPPreferencesWindow(QtGui.QDialog):      """      Window that displays the EIP preferences.      """ -    def __init__(self, parent): +    def __init__(self, parent, domain):          """          :param parent: parent object of the EIPPreferencesWindow. -        :parent type: QWidget +        :type parent: QWidget +        :param domain: the selected by default domain. +        :type domain: unicode          """          QtGui.QDialog.__init__(self, parent)          self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic") @@ -59,7 +61,7 @@ class EIPPreferencesWindow(QtGui.QDialog):          self.ui.cbGateways.currentIndexChanged[unicode].connect(              lambda x: self.ui.lblProvidersGatewayStatus.setVisible(False)) -        self._add_configured_providers() +        self._add_configured_providers(domain)      def _set_providers_gateway_status(self, status, success=False,                                        error=False): @@ -83,9 +85,12 @@ class EIPPreferencesWindow(QtGui.QDialog):          self.ui.lblProvidersGatewayStatus.setVisible(True)          self.ui.lblProvidersGatewayStatus.setText(status) -    def _add_configured_providers(self): +    def _add_configured_providers(self, domain=None):          """          Add the client's configured providers to the providers combo boxes. + +        :param domain: the domain to be selected by default. +        :type domain: unicode          """          self.ui.cbProvidersGateway.clear()          providers = self._settings.get_configured_providers() @@ -100,6 +105,12 @@ class EIPPreferencesWindow(QtGui.QDialog):                  label = provider + self.tr(" (uninitialized)")              self.ui.cbProvidersGateway.addItem(label, userData=provider) +        # Select provider by name +        if domain is not None: +            provider_index = self.ui.cbProvidersGateway.findText( +                domain, QtCore.Qt.MatchStartsWith) +            self.ui.cbProvidersGateway.setCurrentIndex(provider_index) +      def _save_selected_gateway(self, provider):          """          SLOT diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py index 4b4d360f..19942d9d 100644 --- a/src/leap/bitmask/gui/eip_status.py +++ b/src/leap/bitmask/gui/eip_status.py @@ -41,8 +41,8 @@ class EIPStatusWidget(QtGui.QWidget):      EIP Status widget that displays the current state of the EIP service      """      DISPLAY_TRAFFIC_RATES = True -    RATE_STR = "%14.2f KB/s" -    TOTAL_STR = "%14.2f Kb" +    RATE_STR = "%1.2f KB/s" +    TOTAL_STR = "%1.2f Kb"      eip_connection_connected = QtCore.Signal() @@ -248,10 +248,10 @@ class EIPStatusWidget(QtGui.QWidget):          Triggered when a default provider_config has not been found.          Disables the start button and adds instructions to the user.          """ -        logger.debug('Hiding EIP start button') +        #logger.debug('Hiding EIP start button')          # you might be tempted to change this for a .setEnabled(False).          # it won't work. it's under the claws of the state machine. -        # probably the best thing would be to make a transitional +        # probably the best thing would be to make a conditional          # transition there, but that's more involved.          self.eip_button.hide()          msg = self.tr("You must login to use {0}".format(self._service_name)) @@ -272,7 +272,7 @@ class EIPStatusWidget(QtGui.QWidget):          Triggered after a successful login.          Enables the start button.          """ -        logger.debug('Showing EIP start button') +        #logger.debug('Showing EIP start button')          self.eip_button.show()          # Restore the eip action menu diff --git a/src/leap/bitmask/gui/loggerwindow.py b/src/leap/bitmask/gui/loggerwindow.py index 6ef58558..9f396574 100644 --- a/src/leap/bitmask/gui/loggerwindow.py +++ b/src/leap/bitmask/gui/loggerwindow.py @@ -22,10 +22,13 @@ import logging  import cgi  from PySide import QtGui +from twisted.internet import threads  from ui_loggerwindow import Ui_LoggerWindow +from leap.bitmask.util.constants import PASTEBIN_API_DEV_KEY  from leap.bitmask.util.leap_log_handler import LeapLogHandler +from leap.bitmask.util.pastebin import PastebinAPI, PastebinError  from leap.common.check import leap_assert, leap_assert_type  logger = logging.getLogger(__name__) @@ -42,6 +45,9 @@ class LoggerWindow(QtGui.QDialog):          :param handler: Custom handler that supports history and signal.          :type handler: LeapLogHandler.          """ +        from twisted.internet import reactor +        self.reactor = reactor +          QtGui.QDialog.__init__(self)          leap_assert(handler, "We need a handler for the logger window")          leap_assert_type(handler, LeapLogHandler) @@ -59,8 +65,10 @@ class LoggerWindow(QtGui.QDialog):          self.ui.btnCritical.toggled.connect(self._load_history)          self.ui.leFilterBy.textEdited.connect(self._filter_by)          self.ui.cbCaseInsensitive.stateChanged.connect(self._load_history) +        self.ui.btnPastebin.clicked.connect(self._pastebin_this)          self._current_filter = "" +        self._current_history = ""          # Load logging history and connect logger with the widget          self._logging_handler = handler @@ -116,8 +124,13 @@ class LoggerWindow(QtGui.QDialog):          self._set_logs_to_display()          self.ui.txtLogHistory.clear()          history = self._logging_handler.log_history +        current_history = []          for line in history:              self._add_log_line(line) +            message = line[LeapLogHandler.MESSAGE_KEY] +            current_history.append(message) + +        self._current_history = "\n".join(current_history)      def _set_logs_to_display(self):          """ @@ -164,3 +177,72 @@ class LoggerWindow(QtGui.QDialog):                  logger.error("Error saving log file: %r" % (e, ))          else:              logger.debug('Log not saved!') + +    def _set_pastebin_sending(self, sending): +        """ +        Define the status of the pastebin button. +        Change the text and enable/disable according to the current action. + +        :param sending: if we are sending to pastebin or not. +        :type sending: bool +        """ +        if sending: +            self.ui.btnPastebin.setText(self.tr("Sending to pastebin...")) +            self.ui.btnPastebin.setEnabled(False) +        else: +            self.ui.btnPastebin.setText(self.tr("Send to Pastebin.com")) +            self.ui.btnPastebin.setEnabled(True) + +    def _pastebin_this(self): +        """ +        Send the current log history to pastebin.com and gives the user a link +        to see it. +        """ +        def do_pastebin(): +            """ +            Send content to pastebin and return the link. +            """ +            content = self._current_history +            pb = PastebinAPI() +            link = pb.paste(PASTEBIN_API_DEV_KEY, content, +                            paste_name="Bitmask log", +                            paste_expire_date='1W') + +            # convert to 'raw' link +            link = "http://pastebin.com/raw.php?i=" + link.split('/')[-1] + +            return link + +        def pastebin_ok(link): +            """ +            Callback handler for `do_pastebin`. + +            :param link: the recently created pastebin link. +            :type link: str +            """ +            msg = self.tr("Your pastebin link <a href='{0}'>{0}</a>") +            msg = msg.format(link) +            show_info = lambda: QtGui.QMessageBox.information( +                self, self.tr("Pastebin OK"), msg) +            self._set_pastebin_sending(False) +            self.reactor.callLater(0, show_info) + +        def pastebin_err(failure): +            """ +            Errback handler for `do_pastebin`. + +            :param failure: the failure that triggered the errback. +            :type failure: twisted.python.failure.Failure +            """ +            logger.error(repr(failure)) +            msg = self.tr("Sending logs to Pastebin failed!") +            show_err = lambda: QtGui.QMessageBox.critical( +                self, self.tr("Pastebin Error"), msg) +            self._set_pastebin_sending(False) +            self.reactor.callLater(0, show_err) +            failure.trap(PastebinError) + +        self._set_pastebin_sending(True) +        d = threads.deferToThread(do_pastebin) +        d.addCallback(pastebin_ok) +        d.addErrback(pastebin_err) diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py index b21057f0..4a483c32 100644 --- a/src/leap/bitmask/gui/login.py +++ b/src/leap/bitmask/gui/login.py @@ -19,12 +19,13 @@ Login widget implementation  """  import logging -import keyring -  from PySide import QtCore, QtGui  from ui_login import Ui_LoginWidget +from leap.bitmask.config import flags +from leap.bitmask.util import make_address  from leap.bitmask.util.keyring_helpers import has_keyring +from leap.bitmask.util.keyring_helpers import get_keyring  from leap.common.check import leap_assert_type  logger = logging.getLogger(__name__) @@ -221,6 +222,15 @@ class LoginWidget(QtGui.QWidget):          self._set_cancel(not enabled) +    def set_logout_btn_enabled(self, enabled): +        """ +        Enables or disables the logout button. + +        :param enabled: wether they should be enabled or not +        :type enabled: bool +        """ +        self.ui.btnLogout.setEnabled(enabled) +      def _set_cancel(self, enabled=False):          """          Enables or disables the cancel action in the "log in" process. @@ -304,14 +314,15 @@ class LoginWidget(QtGui.QWidget):          if self.get_remember() and has_keyring():              # in the keyring and in the settings              # we store the value 'usename@provider' -            username_domain = (username + '@' + provider).encode("utf8") +            full_user_id = make_address(username, provider).encode("utf8")              try: +                keyring = get_keyring()                  keyring.set_password(self.KEYRING_KEY, -                                     username_domain, +                                     full_user_id,                                       password.encode("utf8"))                  # Only save the username if it was saved correctly in                  # the keyring -                self._settings.set_user(username_domain) +                self._settings.set_user(full_user_id)              except Exception as e:                  logger.exception("Problem saving data to keyring. %r"                                   % (e,)) @@ -323,15 +334,19 @@ class LoginWidget(QtGui.QWidget):          """          self.ui.login_widget.hide()          self.ui.logged_widget.show() -        self.ui.lblUser.setText("%s@%s" % (self.get_user(), -                                           self.get_selected_provider())) -        self.set_login_status("") -        self.logged_in_signal.emit() +        self.ui.lblUser.setText(make_address( +            self.get_user(), self.get_selected_provider())) + +        if flags.OFFLINE is False: +            self.logged_in_signal.emit()      def logged_out(self):          """          Sets the widgets to the logged out state          """ +        # TODO consider "logging out offline" too... +        # how that would be ??? +          self.ui.login_widget.show()          self.ui.logged_widget.hide() @@ -339,27 +354,11 @@ class LoginWidget(QtGui.QWidget):          self.set_enabled(True)          self.set_status("", error=False) -    def set_login_status(self, msg, error=False): -        """ -        Sets the status label for the logged in state. - -        :param msg: status message -        :type msg: str or unicode -        :param error: if the status is an erroneous one, then set this -                      to True -        :type error: bool -        """ -        leap_assert_type(error, bool) -        if error: -            msg = "<font color='red'><b>%s</b></font>" % (msg,) -        self.ui.lblLoginStatus.setText(msg) -        self.ui.lblLoginStatus.show() -      def start_logout(self):          """          Sets the widgets to the logging out state          """ -        self.ui.btnLogout.setText(self.tr("Loggin out...")) +        self.ui.btnLogout.setText(self.tr("Logging out..."))          self.ui.btnLogout.setEnabled(False)      def done_logout(self): @@ -396,6 +395,7 @@ class LoginWidget(QtGui.QWidget):          saved_password = None          try: +            keyring = get_keyring()              saved_password = keyring.get_password(self.KEYRING_KEY,                                                    saved_user                                                    .encode("utf8")) diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py index 3c933c9a..44a138e2 100644 --- a/src/leap/bitmask/gui/mail_status.py +++ b/src/leap/bitmask/gui/mail_status.py @@ -112,6 +112,10 @@ class MailStatusWidget(QtGui.QWidget):                   callback=self._mail_handle_imap_events,                   reqcbk=lambda req, resp: None) +        register(signal=proto.SOLEDAD_INVALID_AUTH_TOKEN, +                 callback=self.set_soledad_invalid_auth_token, +                 reqcbk=lambda req, resp: None) +          self._soledad_event.connect(              self._mail_handle_soledad_events_slot)          self._imap_event.connect( @@ -191,6 +195,17 @@ class MailStatusWidget(QtGui.QWidget):          msg = self.tr("There was an unexpected problem with Soledad.")          self._set_mail_status(msg, ready=-1) +    def set_soledad_invalid_auth_token(self): +        """ +        SLOT +        TRIGGER: +            SoledadBootstrapper.soledad_invalid_token + +        This method is called when the auth token is invalid +        """ +        msg = self.tr("Invalid auth token, try logging in again.") +        self._set_mail_status(msg, ready=-1) +      def _set_mail_status(self, status, ready=0):          """          Sets the Mail status in the label and in the tray icon. @@ -213,7 +228,7 @@ class MailStatusWidget(QtGui.QWidget):                      self._service_name))          elif ready == 1:              icon = self.CONNECTING_ICON -            self._mx_status = self.tr('Starting..') +            self._mx_status = self.tr('Starting…')              tray_status = self.tr('Mail is starting')          elif ready >= 2:              icon = self.CONNECTED_ICON @@ -362,10 +377,19 @@ class MailStatusWidget(QtGui.QWidget):          ext_status = None          if req.event == proto.IMAP_UNREAD_MAIL: +            # By now, the semantics of the UNREAD_MAIL event are +            # limited to mails with the Unread flag *in the Inbox". +            # We could make this configurable to include all unread mail +            # or all unread mail in subscribed folders.              if self._started: -                if req.content != "0": -                    self._set_mail_status(self.tr("%s Unread Emails") % -                                          (req.content,), ready=2) +                count = req.content +                if count != "0": +                    status = self.tr("{0} Unread Emails " +                                     "in your Inbox").format(count) +                    if count == "1": +                        status = self.tr("1 Unread Email in your Inbox") + +                    self._set_mail_status(status, ready=2)                  else:                      self._set_mail_status("", ready=2)          elif req.event == proto.IMAP_SERVICE_STARTED: @@ -375,7 +399,7 @@ class MailStatusWidget(QtGui.QWidget):      def about_to_start(self):          """ -        Displays the correct UI for the point where mail components +        Display the correct UI for the point where mail components          haven't really started, but they are about to in a second.          """          self._set_mail_status(self.tr("About to start, please wait..."), @@ -383,7 +407,7 @@ class MailStatusWidget(QtGui.QWidget):      def set_disabled(self):          """ -        Displays the correct UI for disabled mail. +        Display the correct UI for disabled mail.          """          self._set_mail_status(self.tr("Disabled"), -1) @@ -394,7 +418,7 @@ class MailStatusWidget(QtGui.QWidget):      @QtCore.Slot()      def mail_state_disconnected(self):          """ -        Displays the correct UI for the disconnected state. +        Display the correct UI for the disconnected state.          """          # XXX this should handle the disabled state better.          self._started = False @@ -406,7 +430,7 @@ class MailStatusWidget(QtGui.QWidget):      @QtCore.Slot()      def mail_state_connecting(self):          """ -        Displays the correct UI for the connecting state. +        Display the correct UI for the connecting state.          """          self._disabled = False          self._started = True @@ -415,23 +439,32 @@ class MailStatusWidget(QtGui.QWidget):      @QtCore.Slot()      def mail_state_disconnecting(self):          """ -        Displays the correct UI for the connecting state. +        Display the correct UI for the connecting state.          """          self._set_mail_status(self.tr("Disconnecting..."), 1)      @QtCore.Slot()      def mail_state_connected(self):          """ -        Displays the correct UI for the connected state. +        Display the correct UI for the connected state.          """          self._set_mail_status(self.tr("ON"), 2)      @QtCore.Slot()      def mail_state_disabled(self):          """ -        Displays the correct UI for the disabled state. +        Display the correct UI for the disabled state.          """          self._disabled = True          status = self.tr("You must be logged in to use {0}.").format(              self._service_name)          self._set_mail_status(status, -1) + +    @QtCore.Slot() +    def soledad_invalid_auth_token(self): +        """ +        Display the correct UI for the invalid token state +        """ +        self._disabled = True +        status = self.tr("Invalid auth token, try logging in again.") +        self._set_mail_status(status, -1) diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 929919ac..5abfaa67 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -1,6 +1,6 @@  # -*- coding: utf-8 -*-  # mainwindow.py -# Copyright (C) 2013 LEAP +# Copyright (C) 2013, 2014 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 @@ -18,16 +18,25 @@  Main window for Bitmask.  """  import logging -import os +import socket + +from threading import Condition +from datetime import datetime  from PySide import QtCore, QtGui -from twisted.internet import threads  from zope.proxy import ProxyBase, setProxiedObject +from twisted.internet import reactor, threads +from twisted.internet.defer import CancelledError  from leap.bitmask import __version__ as VERSION +from leap.bitmask import __version_hash__ as VERSION_HASH +from leap.bitmask.config import flags  from leap.bitmask.config.leapsettings import LeapSettings  from leap.bitmask.config.providerconfig import ProviderConfig + +from leap.bitmask.crypto import srpauth  from leap.bitmask.crypto.srpauth import SRPAuth +  from leap.bitmask.gui.loggerwindow import LoggerWindow  from leap.bitmask.gui.advanced_key_management import AdvancedKeyManagement  from leap.bitmask.gui.login import LoginWidget @@ -40,11 +49,12 @@ from leap.bitmask.gui.wizard import Wizard  from leap.bitmask.gui.systray import SysTray  from leap.bitmask import provider -from leap.bitmask.platform_init import IS_WIN, IS_MAC +from leap.bitmask.platform_init import IS_WIN, IS_MAC, IS_LINUX  from leap.bitmask.platform_init.initializers import init_platform -from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper -from leap.bitmask.services import get_service_display_name, EIP_SERVICE +from leap.bitmask import backend + +from leap.bitmask.services import get_service_display_name  from leap.bitmask.services.mail import conductor as mail_conductor @@ -66,6 +76,7 @@ from leap.bitmask.services.eip.darwinvpnlauncher import EIPNoTunKextLoaded  from leap.bitmask.services.soledad.soledadbootstrapper import \      SoledadBootstrapper +from leap.bitmask.util import make_address  from leap.bitmask.util.keyring_helpers import has_keyring  from leap.bitmask.util.leap_log_handler import LeapLogHandler @@ -77,6 +88,8 @@ from leap.common.check import leap_assert  from leap.common.events import register  from leap.common.events import events_pb2 as proto +from leap.mail.imap.service.imap import IMAP_PORT +  from ui_mainwindow import Ui_MainWindow  logger = logging.getLogger(__name__) @@ -93,6 +106,7 @@ class MainWindow(QtGui.QMainWindow):      # Signals      eip_needs_login = QtCore.Signal([]) +    offline_mode_bypass_login = QtCore.Signal([])      new_updates = QtCore.Signal(object)      raise_window = QtCore.Signal([])      soledad_ready = QtCore.Signal([]) @@ -102,6 +116,12 @@ class MainWindow(QtGui.QMainWindow):      # We use this flag to detect abnormal terminations      user_stopped_eip = False +    # We give EIP some time to come up before starting soledad anyway +    EIP_TIMEOUT = 60000  # in milliseconds + +    # We give each service some time to come to a halt before forcing quit +    SERVICE_STOP_TIMEOUT = 20 +      def __init__(self, quit_callback,                   openvpn_verb=1,                   bypass_checks=False): @@ -132,11 +152,14 @@ class MainWindow(QtGui.QMainWindow):          # end register leap events ####################################          self._quit_callback = quit_callback -          self._updates_content = "" +        # setup UI          self.ui = Ui_MainWindow()          self.ui.setupUi(self) +        self.menuBar().setNativeMenuBar(not IS_LINUX) +        self._backend = backend.Backend(bypass_checks) +        self._backend.start()          self._settings = LeapSettings() @@ -173,6 +196,11 @@ class MainWindow(QtGui.QMainWindow):          self._eip_status.eip_connection_connected.connect(              self._on_eip_connected) +        self._eip_status.eip_connection_connected.connect( +            self._maybe_run_soledad_setup_checks) +        self.offline_mode_bypass_login.connect( +            self._maybe_run_soledad_setup_checks) +          self.eip_needs_login.connect(              self._eip_status.disable_eip_start)          self.eip_needs_login.connect( @@ -180,36 +208,23 @@ class MainWindow(QtGui.QMainWindow):          # This is loaded only once, there's a bug when doing that more          # than once -        self._provider_config = ProviderConfig() +        # XXX HACK!! But we need it as long as we are using +        # provider_config in here +        self._provider_config = ( +            self._backend._components["provider"]._provider_config)          # Used for automatic start of EIP          self._provisional_provider_config = ProviderConfig()          self._eip_config = eipconfig.EIPConfig()          self._already_started_eip = False +        self._already_started_soledad = False          # This is created once we have a valid provider config          self._srp_auth = None          self._logged_user = None +        self._logged_in_offline = False -        # This thread is always running, although it's quite -        # lightweight when it's done setting up provider -        # configuration and certificate. -        self._provider_bootstrapper = ProviderBootstrapper(bypass_checks) - -        # Intermediate stages, only do something if there was an error -        self._provider_bootstrapper.name_resolution.connect( -            self._intermediate_stage) -        self._provider_bootstrapper.https_connection.connect( -            self._intermediate_stage) -        self._provider_bootstrapper.download_ca_cert.connect( -            self._intermediate_stage) - -        # Important stages, loads the provider config and checks -        # certificates -        self._provider_bootstrapper.download_provider_info.connect( -            self._load_provider_config) -        self._provider_bootstrapper.check_api_certificate.connect( -            self._provider_config_loaded) +        self._backend_connect()          # This thread is similar to the provider bootstrapper          self._eip_bootstrapper = EIPBootstrapper() @@ -243,15 +258,23 @@ class MainWindow(QtGui.QMainWindow):              self._soledad_intermediate_stage)          self._soledad_bootstrapper.gen_key.connect(              self._soledad_bootstrapped_stage) +        self._soledad_bootstrapper.local_only_ready.connect( +            self._soledad_bootstrapped_stage)          self._soledad_bootstrapper.soledad_timeout.connect(              self._retry_soledad_connection) +        self._soledad_bootstrapper.soledad_invalid_auth_token.connect( +            self._mail_status.set_soledad_invalid_auth_token)          self._soledad_bootstrapper.soledad_failed.connect(              self._mail_status.set_soledad_failed) +        self.ui.action_preferences.triggered.connect(self._show_preferences) +        self.ui.action_eip_preferences.triggered.connect( +            self._show_eip_preferences)          self.ui.action_about_leap.triggered.connect(self._about)          self.ui.action_quit.triggered.connect(self.quit)          self.ui.action_wizard.triggered.connect(self._launch_wizard)          self.ui.action_show_logs.triggered.connect(self._show_logger_window) +        self.ui.action_help.triggered.connect(self._help)          self.ui.action_create_new_account.triggered.connect(              self._launch_wizard) @@ -279,13 +302,15 @@ class MainWindow(QtGui.QMainWindow):          self._action_visible = QtGui.QAction(self.tr("Hide Main Window"), self)          self._action_visible.triggered.connect(self._toggle_visible) -        self.ui.btnPreferences.clicked.connect(self._show_preferences) -        self.ui.btnEIPPreferences.clicked.connect(self._show_eip_preferences) +        # disable buttons for now, may come back later. +        # self.ui.btnPreferences.clicked.connect(self._show_preferences) +        # self.ui.btnEIPPreferences.clicked.connect(self._show_eip_preferences)          self._enabled_services = [] -        self._center_window() +        # last minute UI manipulations +        self._center_window()          self.ui.lblNewUpdates.setVisible(False)          self.ui.btnMore.setVisible(False)          ######################################### @@ -294,6 +319,8 @@ class MainWindow(QtGui.QMainWindow):          self.ui.btnMore.resize(0, 0)          #########################################          self.ui.btnMore.clicked.connect(self._updates_details) +        if flags.OFFLINE is True: +            self._set_label_offline()          # Services signals/slots connection          self.new_updates.connect(self._react_to_new_updates) @@ -323,7 +350,7 @@ class MainWindow(QtGui.QMainWindow):          self._keymanager = ProxyBase(None)          self._login_defer = None -        self._download_provider_defer = None +        self._soledad_defer = None          self._mail_conductor = mail_conductor.MailConductor(              self._soledad, self._keymanager) @@ -345,7 +372,9 @@ class MainWindow(QtGui.QMainWindow):          if self._first_run():              self._wizard_firstrun = True -            self._wizard = Wizard(bypass_checks=bypass_checks) +            self._backend_disconnect() +            self._wizard = Wizard(backend=self._backend, +                                  bypass_checks=bypass_checks)              # Give this window time to finish init and then show the wizard              QtCore.QTimer.singleShot(1, self._launch_wizard)              self._wizard.accepted.connect(self._finish_init) @@ -355,6 +384,42 @@ class MainWindow(QtGui.QMainWindow):              # so this has to be done after eip_machine is started              self._finish_init() +    def _backend_connect(self): +        """ +        Helper to connect to backend signals +        """ +        sig = self._backend.signaler +        sig.prov_name_resolution.connect(self._intermediate_stage) +        sig.prov_https_connection.connect(self._intermediate_stage) +        sig.prov_download_ca_cert.connect(self._intermediate_stage) + +        sig.prov_download_provider_info.connect(self._load_provider_config) +        sig.prov_check_api_certificate.connect(self._provider_config_loaded) + +        # Only used at login, no need to disconnect this like we do +        # with the other +        sig.prov_problem_with_provider.connect(self._login_problem_provider) + +        sig.prov_unsupported_client.connect(self._needs_update) +        sig.prov_unsupported_api.connect(self._incompatible_api) + +        sig.prov_cancelled_setup.connect(self._set_login_cancelled) + +    def _backend_disconnect(self): +        """ +        Helper to disconnect from backend signals. + +        Some signals are emitted from the wizard, and we want to +        ignore those. +        """ +        sig = self._backend.signaler +        sig.prov_name_resolution.disconnect(self._intermediate_stage) +        sig.prov_https_connection.disconnect(self._intermediate_stage) +        sig.prov_download_ca_cert.disconnect(self._intermediate_stage) + +        sig.prov_download_provider_info.disconnect(self._load_provider_config) +        sig.prov_check_api_certificate.disconnect(self._provider_config_loaded) +      def _rejected_wizard(self):          """          SLOT @@ -375,7 +440,9 @@ class MainWindow(QtGui.QMainWindow):              # This happens if the user finishes the provider              # setup but does not register              self._wizard = None -            self._finish_init() +            self._backend_connect() +            if self._wizard_firstrun: +                self._finish_init()      def _launch_wizard(self):          """ @@ -390,9 +457,11 @@ class MainWindow(QtGui.QMainWindow):          there.          """          if self._wizard is None: -            self._wizard = Wizard(bypass_checks=self._bypass_checks) +            self._backend_disconnect() +            self._wizard = Wizard(backend=self._backend, +                                  bypass_checks=self._bypass_checks)              self._wizard.accepted.connect(self._finish_init) -            self._wizard.rejected.connect(self._wizard.close) +            self._wizard.rejected.connect(self._rejected_wizard)          self.setVisible(False)          # Do NOT use exec_, it will use a child event loop! @@ -467,26 +536,66 @@ class MainWindow(QtGui.QMainWindow):          """          SLOT          TRIGGERS: -          self.ui.btnPreferences.clicked +          self.ui.btnPreferences.clicked (disabled for now) +          self.ui.action_preferences          Displays the preferences window.          """ -        preferences_window = PreferencesWindow( +        preferences = PreferencesWindow(              self, self._srp_auth, self._provider_config, self._soledad,              self._login_widget.get_selected_provider()) -        self.soledad_ready.connect(preferences_window.set_soledad_ready) -        preferences_window.show() +        self.soledad_ready.connect(preferences.set_soledad_ready) +        preferences.show() +        preferences.preferences_saved.connect(self._update_eip_enabled_status) + +    def _update_eip_enabled_status(self): +        """ +        SLOT +        TRIGGER: +            PreferencesWindow.preferences_saved + +        Enable or disable the EIP start/stop actions and stop EIP if the user +        disabled that service. + +        :returns: if the eip actions were enabled or disabled +        :rtype: bool +        """ +        settings = self._settings +        default_provider = settings.get_defaultprovider() +        enabled_services = [] +        if default_provider is not None: +            enabled_services = settings.get_enabled_services(default_provider) + +        eip_enabled = False +        if EIP_SERVICE in enabled_services: +            should_autostart = settings.get_autostart_eip() +            if should_autostart and default_provider is not None: +                self._eip_status.enable_eip_start() +                self._eip_status.set_eip_status("") +                eip_enabled = True +            else: +                # we don't have an usable provider +                # so the user needs to log in first +                self._eip_status.disable_eip_start() +        else: +            self._stop_eip() +            self._eip_status.disable_eip_start() +            self._eip_status.set_eip_status(self.tr("Disabled")) + +        return eip_enabled      def _show_eip_preferences(self):          """          SLOT          TRIGGERS:            self.ui.btnEIPPreferences.clicked +          self.ui.action_eip_preferences (disabled for now)          Displays the EIP preferences window.          """ -        EIPPreferencesWindow(self).show() +        domain = self._login_widget.get_selected_provider() +        EIPPreferencesWindow(self, domain).show()      #      # updates @@ -585,6 +694,7 @@ class MainWindow(QtGui.QMainWindow):                  self.eip_needs_login.emit()              self._wizard = None +            self._backend_connect()          else:              self._try_autostart_eip() @@ -626,6 +736,19 @@ class MainWindow(QtGui.QMainWindow):          self.ui.eipWidget.setVisible(EIP_SERVICE in services)          self.ui.mailWidget.setVisible(MX_SERVICE in services) +    def _set_label_offline(self): +        """ +        Set the login label to reflect offline status. +        """ +        if self._logged_in_offline: +            provider = "" +        else: +            provider = self.ui.lblLoginProvider.text() + +        self.ui.lblLoginProvider.setText( +            provider + +            self.tr(" (offline mode)")) +      #      # systray      # @@ -748,10 +871,13 @@ class MainWindow(QtGui.QMainWindow):          Display the About Bitmask dialog          """ +        today = datetime.now().date() +        greet = ("Happy New 1984!... or not ;)<br><br>" +                 if today.month == 1 and today.day < 15 else "")          QtGui.QMessageBox.about(              self, self.tr("About Bitmask - %s") % (VERSION,), -            self.tr("Version: <b>%s</b><br>" -                    "<br>" +            self.tr("Version: <b>%s</b> (%s)<br>" +                    "<br>%s"                      "Bitmask is the Desktop client application for "                      "the LEAP platform, supporting encrypted internet "                      "proxy, secure email, and secure chat (coming soon).<br>" @@ -763,7 +889,58 @@ class MainWindow(QtGui.QMainWindow):                      "and widely available. <br>"                      "<br>"                      "<a href='https://leap.se'>More about LEAP" -                    "</a>") % (VERSION,)) +                    "</a>") % (VERSION, VERSION_HASH[:10], greet)) + +    def _help(self): +        """ +        SLOT +        TRIGGERS: self.ui.action_help.triggered + +        Display the Bitmask help dialog. +        """ +        # TODO: don't hardcode! +        smtp_port = 2013 + +        url = ("<a href='https://addons.mozilla.org/es/thunderbird/" +               "addon/bitmask/'>bitmask addon</a>") + +        msg = self.tr( +            "<strong>Instructions to use mail:</strong><br>" +            "If you use Thunderbird you can use the Bitmask extension helper. " +            "Search for 'Bitmask' in the add-on manager or download it " +            "from: {0}.<br><br>" +            "You can configure Bitmask manually with these options:<br>" +            "<em>" +            "   Incoming -> IMAP, port: {1}<br>" +            "   Outgoing -> SMTP, port: {2}<br>" +            "   Username -> your bitmask username.<br>" +            "   Password -> does not matter, use any text. " +            " Just don't leave it empty and don't use your account's password." +            "</em>").format(url, IMAP_PORT, smtp_port) +        QtGui.QMessageBox.about(self, self.tr("Bitmask Help"), msg) + +    def _needs_update(self): +        """ +        Display a warning dialog to inform the user that the app needs update. +        """ +        url = "https://dl.bitmask.net/" +        msg = self.tr( +            "The current client version is not supported " +            "by this provider.<br>" +            "Please update to latest version.<br><br>" +            "You can get the latest version from " +            "<a href='{0}'>{1}</a>").format(url, url) +        QtGui.QMessageBox.warning(self, self.tr("Update Needed"), msg) + +    def _incompatible_api(self): +        """ +        Display a warning dialog to inform the user that the provider has an +        incompatible API. +        """ +        msg = self.tr( +            "This provider is not compatible with the client.<br><br>" +            "Error: API version incompatible.") +        QtGui.QMessageBox.warning(self, self.tr("Incompatible Provider"), msg)      def changeEvent(self, e):          """ @@ -812,51 +989,36 @@ class MainWindow(QtGui.QMainWindow):          """          # XXX should rename this provider, name clash.          provider = self._login_widget.get_selected_provider() - -        pb = self._provider_bootstrapper -        d = pb.run_provider_select_checks(provider, download_if_needed=True) -        self._download_provider_defer = d +        self._backend.setup_provider(provider)      def _load_provider_config(self, data):          """          SLOT -        TRIGGER: self._provider_bootstrapper.download_provider_info +        TRIGGER: self._backend.signaler.prov_download_provider_info          Once the provider config has been downloaded, this loads the          self._provider_config instance with it and starts the second          part of the bootstrapping sequence          :param data: result from the last stage of the -        run_provider_select_checks +                     run_provider_select_checks          :type data: dict          """ -        if data[self._provider_bootstrapper.PASSED_KEY]: -            # XXX should rename this provider, name clash. -            provider = self._login_widget.get_selected_provider() - -            # If there's no loaded provider or -            # we want to connect to other provider... -            if (not self._provider_config.loaded() or -                    self._provider_config.get_domain() != provider): -                self._provider_config.load( -                    os.path.join("leap", "providers", -                                 provider, "provider.json")) - -            if self._provider_config.loaded(): -                self._provider_bootstrapper.run_provider_setup_checks( -                    self._provider_config, -                    download_if_needed=True) -            else: -                self._login_widget.set_status( -                    self.tr("Unable to login: Problem with provider")) -                logger.error("Could not load provider configuration.") -                self._login_widget.set_enabled(True) +        if data[self._backend.PASSED_KEY]: +            selected_provider = self._login_widget.get_selected_provider() +            self._backend.provider_bootstrap(selected_provider)          else: -            self._login_widget.set_status( -                self.tr("Unable to login: Problem with provider")) -            logger.error(data[self._provider_bootstrapper.ERROR_KEY]) +            logger.error(data[self._backend.ERROR_KEY])              self._login_widget.set_enabled(True) +    def _login_problem_provider(self): +        """ +        Warns the user about a problem with the provider during login. +        """ +        self._login_widget.set_status( +            self.tr("Unable to login: Problem with provider")) +        self._login_widget.set_enabled(True) +      def _login(self):          """          SLOT @@ -868,10 +1030,57 @@ class MainWindow(QtGui.QMainWindow):          start the SRP authentication, and as the last step          bootstrapping the EIP service          """ -        leap_assert(self._provider_config, "We need a provider config") +        # TODO most of this could ve handled by the login widget, +        # but we'd have to move lblLoginProvider into the widget itself, +        # instead of having it as a top-level attribute. +        if flags.OFFLINE is True: +            logger.debug("OFFLINE mode! bypassing remote login") +            # TODO reminder, we're not handling logout for offline +            # mode. +            self._login_widget.logged_in() +            self._logged_in_offline = True +            self._set_label_offline() +            self.offline_mode_bypass_login.emit() +        else: +            leap_assert(self._provider_config, "We need a provider config") +            if self._login_widget.start_login(): +                self._download_provider_config() -        if self._login_widget.start_login(): -            self._download_provider_config() +    def _login_errback(self, failure): +        """ +        Error handler for the srpauth.authenticate method. + +        :param failure: failure object that Twisted generates +        :type failure: twisted.python.failure.Failure +        """ +        # NOTE: this behavior needs to be managed through the signaler, +        # as we are doing with the prov_cancelled_setup signal. +        # After we move srpauth to the backend, we need to update this. +        logger.error("Error logging in, {0!r}".format(failure)) + +        if failure.check(CancelledError): +            logger.debug("Defer cancelled.") +            failure.trap(Exception) +            self._set_login_cancelled() +            return +        elif failure.check(srpauth.SRPAuthBadUserOrPassword): +            msg = self.tr("Invalid username or password.") +        elif failure.check(srpauth.SRPAuthBadStatusCode, +                           srpauth.SRPAuthenticationError, +                           srpauth.SRPAuthVerificationFailed, +                           srpauth.SRPAuthNoSessionId, +                           srpauth.SRPAuthNoSalt, srpauth.SRPAuthNoB, +                           srpauth.SRPAuthBadDataFromServer, +                           srpauth.SRPAuthJSONDecodeError): +            msg = self.tr("There was a server problem with authentication.") +        elif failure.check(srpauth.SRPAuthConnectionError): +            msg = self.tr("Could not establish a connection.") +        else: +            # this shouldn't happen, but just in case. +            msg = self.tr("Unknown error: {0!r}".format(failure.value)) + +        self._login_widget.set_status(msg) +        self._login_widget.set_enabled(True)      def _cancel_login(self):          """ @@ -882,28 +1091,48 @@ class MainWindow(QtGui.QMainWindow):          Stops the login sequence.          """          logger.debug("Cancelling log in.") +        self._cancel_ongoing_defers() -        if self._download_provider_defer: -            logger.debug("Cancelling download provider defer.") -            self._download_provider_defer.cancel() +    def _cancel_ongoing_defers(self): +        """ +        Cancel the running defers to avoid app blocking. +        """ +        self._backend.cancel_setup_provider() -        if self._login_defer: +        if self._login_defer is not None:              logger.debug("Cancelling login defer.")              self._login_defer.cancel() +            self._login_defer = None +        if self._soledad_defer is not None: +            logger.debug("Cancelling soledad defer.") +            self._soledad_defer.cancel() +            self._soledad_defer = None + +    def _set_login_cancelled(self): +        """ +        SLOT +        TRIGGERS: +            Signaler.prov_cancelled_setup fired by +            self._backend.cancel_setup_provider() + +        This method re-enables the login widget and display a message for +        the cancelled operation. +        """          self._login_widget.set_status(self.tr("Log in cancelled by the user.")) +        self._login_widget.set_enabled(True)      def _provider_config_loaded(self, data):          """          SLOT -        TRIGGER: self._provider_bootstrapper.check_api_certificate +        TRIGGER: self._backend.signaler.prov_check_api_certificate          Once the provider configuration is loaded, this starts the SRP          authentication          """          leap_assert(self._provider_config, "We need a provider config!") -        if data[self._provider_bootstrapper.PASSED_KEY]: +        if data[self._backend.PASSED_KEY]:              username = self._login_widget.get_user()              password = self._login_widget.get_password() @@ -913,18 +1142,18 @@ class MainWindow(QtGui.QMainWindow):                  self._srp_auth = SRPAuth(self._provider_config)                  self._srp_auth.authentication_finished.connect(                      self._authentication_finished) -                self._srp_auth.logout_finished.connect( -                    self._done_logging_out) +                self._srp_auth.logout_ok.connect(self._logout_ok) +                self._srp_auth.logout_error.connect(self._logout_error) -            # TODO Add errback!              self._login_defer = self._srp_auth.authenticate(username, password) +            self._login_defer.addErrback(self._login_errback)          else:              self._login_widget.set_status(                  "Unable to login: Problem with provider") -            logger.error(data[self._provider_bootstrapper.ERROR_KEY]) +            logger.error(data[self._backend.ERROR_KEY])              self._login_widget.set_enabled(True) -    def _authentication_finished(self, ok, message): +    def _authentication_finished(self):          """          SLOT          TRIGGER: self._srp_auth.authentication_finished @@ -932,22 +1161,23 @@ class MainWindow(QtGui.QMainWindow):          Once the user is properly authenticated, try starting the EIP          service          """ -        # In general we want to "filter" likely complicated error -        # messages, but in this case, the messages make more sense as -        # they come. Since they are "Unknown user" or "Unknown -        # password" -        self._login_widget.set_status(message, error=not ok) - -        if ok: -            self._logged_user = self._login_widget.get_user() -            user = self._logged_user -            domain = self._provider_config.get_domain() -            userid = "%s@%s" % (user, domain) -            self._mail_conductor.userid = userid -            self._login_defer = None -            self._start_eip_bootstrap() -        else: -            self._login_widget.set_enabled(True) +        self._login_widget.set_status(self.tr("Succeeded"), error=False) + +        self._logged_user = self._login_widget.get_user() +        user = self._logged_user +        domain = self._provider_config.get_domain() +        full_user_id = make_address(user, domain) +        self._mail_conductor.userid = full_user_id +        self._login_defer = None +        self._start_eip_bootstrap() + +        # if soledad/mail is enabled: +        if MX_SERVICE in self._enabled_services: +            btn_enabled = self._login_widget.set_logout_btn_enabled +            btn_enabled(False) +            self.soledad_ready.connect(lambda: btn_enabled(True)) +            self._soledad_bootstrapper.soledad_failed.connect( +                lambda: btn_enabled(True))      def _start_eip_bootstrap(self):          """ @@ -956,28 +1186,83 @@ class MainWindow(QtGui.QMainWindow):          """          self._login_widget.logged_in() -        self.ui.lblLoginProvider.setText(self._provider_config.get_name()) +        provider = self._provider_config.get_domain() +        self.ui.lblLoginProvider.setText(provider)          self._enabled_services = self._settings.get_enabled_services(              self._provider_config.get_domain())          # TODO separate UI from logic. -        # TODO soledad should check if we want to run only over EIP. -        if self._provider_config.provides_mx() and \ -           self._enabled_services.count(MX_SERVICE) > 0: +        if self._provides_mx_and_enabled():              self._mail_status.about_to_start() - -            self._soledad_bootstrapper.run_soledad_setup_checks( -                self._provider_config, -                self._login_widget.get_user(), -                self._login_widget.get_password(), -                download_if_needed=True)          else:              self._mail_status.set_disabled() -        # XXX the config should be downloaded from the start_eip -        # method. -        self._download_eip_config() +        self._maybe_start_eip() + +    def _provides_mx_and_enabled(self): +        """ +        Defines if the current provider provides mx and if we have it enabled. + +        :returns: True if provides and is enabled, False otherwise +        :rtype: bool +        """ +        provider_config = self._get_best_provider_config() +        return (provider_config.provides_mx() and +                MX_SERVICE in self._enabled_services) + +    def _provides_eip_and_enabled(self): +        """ +        Defines if the current provider provides eip and if we have it enabled. + +        :returns: True if provides and is enabled, False otherwise +        :rtype: bool +        """ +        provider_config = self._get_best_provider_config() +        return (provider_config.provides_eip() and +                EIP_SERVICE in self._enabled_services) + +    def _maybe_run_soledad_setup_checks(self): +        """ +        Conditionally start Soledad. +        """ +        # TODO split. +        if self._already_started_soledad is True: +            return + +        if not self._provides_mx_and_enabled(): +            return + +        username = self._login_widget.get_user() +        password = unicode(self._login_widget.get_password()) +        provider_domain = self._login_widget.get_selected_provider() + +        sb = self._soledad_bootstrapper +        if flags.OFFLINE is True: +            provider_domain = self._login_widget.get_selected_provider() +            sb._password = password + +            self._provisional_provider_config.load( +                provider.get_provider_path(provider_domain)) + +            full_user_id = make_address(username, provider_domain) +            uuid = self._settings.get_uuid(full_user_id) +            self._mail_conductor.userid = full_user_id + +            if uuid is None: +                # We don't need more visibility at the moment, +                # this is mostly for internal use/debug for now. +                logger.warning("Sorry! Log-in at least one time.") +                return +            fun = sb.load_offline_soledad +            fun(full_user_id, password, uuid) +        else: +            provider_config = self._provider_config + +            if self._logged_user is not None: +                self._soledad_defer = sb.run_soledad_setup_checks( +                    provider_config, username, password, +                    download_if_needed=True)      ###################################################################      # Service control methods: soledad @@ -1010,8 +1295,9 @@ class MainWindow(QtGui.QMainWindow):          logger.debug("Retrying soledad connection.")          if self._soledad_bootstrapper.should_retry_initialization():              self._soledad_bootstrapper.increment_retries_count() -            threads.deferToThread( -                self._soledad_bootstrapper.load_and_sync_soledad) +            # XXX should cancel the existing socket --- this +            # is avoiding a clean termination. +            self._maybe_run_soledad_setup_checks()          else:              logger.warning("Max number of soledad initialization "                             "retries reached.") @@ -1021,6 +1307,7 @@ class MainWindow(QtGui.QMainWindow):          SLOT          TRIGGERS:            self._soledad_bootstrapper.gen_key +          self._soledad_bootstrapper.local_only_ready          If there was a problem, displays it, otherwise it does nothing.          This is used for intermediate bootstrapping stages, in case @@ -1047,6 +1334,7 @@ class MainWindow(QtGui.QMainWindow):          # Ok, now soledad is ready, so we can allow other things that          # depend on soledad to start. +        self._soledad_defer = None          # this will trigger start_imap_service          # and start_smtp_boostrapping @@ -1062,10 +1350,13 @@ class MainWindow(QtGui.QMainWindow):          TRIGGERS:              self.soledad_ready          """ +        if flags.OFFLINE is True: +            logger.debug("not starting smtp in offline mode") +            return +          # TODO for simmetry, this should be called start_smtp_service          # (and delegate all the checks to the conductor) -        if self._provider_config.provides_mx() and \ -                self._enabled_services.count(MX_SERVICE) > 0: +        if self._provides_mx_and_enabled():              self._mail_conductor.smtp_bootstrapper.run_smtp_setup_checks(                  self._provider_config,                  self._mail_conductor.smtp_config, @@ -1093,9 +1384,22 @@ class MainWindow(QtGui.QMainWindow):          TRIGGERS:              self.soledad_ready          """ -        if self._provider_config.provides_mx() and \ -                self._enabled_services.count(MX_SERVICE) > 0: -            self._mail_conductor.start_imap_service() +        # TODO in the OFFLINE mode we should also modify the  rules +        # in the mail state machine so it shows that imap is active +        # (but not smtp since it's not yet ready for offline use) +        start_fun = self._mail_conductor.start_imap_service +        if flags.OFFLINE is True: +            provider_domain = self._login_widget.get_selected_provider() +            self._provider_config.load( +                provider.get_provider_path(provider_domain)) +        provides_mx = self._provider_config.provides_mx() + +        if flags.OFFLINE is True and provides_mx: +            start_fun() +            return + +        if self._provides_mx_and_enabled(): +            start_fun()      def _on_mail_client_logged_in(self, req):          """ @@ -1120,8 +1424,13 @@ class MainWindow(QtGui.QMainWindow):          TRIGGERS:              self.logout          """ +        cv = Condition() +        cv.acquire()          # TODO call stop_mail_service -        self._mail_conductor.stop_imap_service() +        threads.deferToThread(self._mail_conductor.stop_imap_service, cv) +        # and wait for it to be stopped +        logger.debug('Waiting for imap service to stop.') +        cv.wait(self.SERVICE_STOP_TIMEOUT)      # end service control methods (imap) @@ -1171,27 +1480,65 @@ class MainWindow(QtGui.QMainWindow):          """          self._eip_connection.qtsigs.connected_signal.emit() +        # check for connectivity +        provider_config = self._get_best_provider_config() +        domain = provider_config.get_domain() +        self._check_name_resolution(domain) + +    def _check_name_resolution(self, domain): +        """ +        Check if we can resolve the given domain name. + +        :param domain: the domain to check. +        :type domain: str +        """ +        def do_check(): +            """ +            Try to resolve the domain name. +            """ +            socket.gethostbyname(domain.encode('idna')) + +        def check_err(failure): +            """ +            Errback handler for `do_check`. + +            :param failure: the failure that triggered the errback. +            :type failure: twisted.python.failure.Failure +            """ +            logger.error(repr(failure)) +            logger.error("Can't resolve hostname.") + +            msg = self.tr( +                "The server at {0} can't be found, because the DNS lookup " +                "failed. DNS is the network service that translates a " +                "website's name to its Internet address. Either your computer " +                "is having trouble connecting to the network, or you are " +                "missing some helper files that are needed to securely use " +                "DNS while {1} is active. To install these helper files, quit " +                "this application and start it again." +            ).format(domain, self._eip_name) + +            show_err = lambda: QtGui.QMessageBox.critical( +                self, self.tr("Connection Error"), msg) +            reactor.callLater(0, show_err) + +            # python 2.7.4 raises socket.error +            # python 2.7.5 raises socket.gaierror +            failure.trap(socket.gaierror, socket.error) + +        d = threads.deferToThread(do_check) +        d.addErrback(check_err) +      def _try_autostart_eip(self):          """          Tries to autostart EIP          """          settings = self._settings -        should_autostart = settings.get_autostart_eip() -        if not should_autostart: -            logger.debug('Will not autostart EIP since it is setup ' -                         'to not to do it') -            self.eip_needs_login.emit() +        if not self._update_eip_enabled_status():              return          default_provider = settings.get_defaultprovider() - -        if default_provider is None: -            logger.info("Cannot autostart Encrypted Internet because there is " -                        "no default provider configured") -            self.eip_needs_login.emit() -            return -          self._enabled_services = settings.get_enabled_services(              default_provider) @@ -1202,7 +1549,7 @@ class MainWindow(QtGui.QMainWindow):              # it adds some delay.              # Maybe if it's the first run in a session,              # or we can try only if it fails. -            self._download_eip_config() +            self._maybe_start_eip()          else:              # XXX: Display a proper message to the user              self.eip_needs_login.emit() @@ -1336,8 +1683,9 @@ class MainWindow(QtGui.QMainWindow):          if self._logged_user:              self._eip_status.set_provider( -                "%s@%s" % (self._logged_user, -                           self._get_best_provider_config().get_domain())) +                make_address( +                    self._logged_user, +                    self._get_best_provider_config().get_domain()))          self._eip_status.eip_stopped()      @QtCore.Slot() @@ -1434,18 +1782,16 @@ class MainWindow(QtGui.QMainWindow):      # eip boostrapping, config etc... -    def _download_eip_config(self): +    def _maybe_start_eip(self):          """ -        Starts the EIP bootstrapping sequence +        Start the EIP bootstrapping sequence if the client is configured to +        do so.          """          leap_assert(self._eip_bootstrapper, "We need an eip bootstrapper!")          provider_config = self._get_best_provider_config() -        if provider_config.provides_eip() and \ -                self._enabled_services.count(EIP_SERVICE) > 0 and \ -                not self._already_started_eip: - +        if self._provides_eip_and_enabled() and not self._already_started_eip:              # XXX this should be handled by the state machine.              self._eip_status.set_eip_status(                  self.tr("Starting...")) @@ -1453,14 +1799,22 @@ class MainWindow(QtGui.QMainWindow):                  provider_config,                  download_if_needed=True)              self._already_started_eip = True -        elif not self._already_started_eip: -            if self._enabled_services.count(EIP_SERVICE) > 0: -                self._eip_status.set_eip_status( -                    self.tr("Not supported"), -                    error=True) -            else: -                self._eip_status.disable_eip_start() -                self._eip_status.set_eip_status(self.tr("Disabled")) +            # we want to start soledad anyway after a certain timeout if eip +            # fails to come up +            QtCore.QTimer.singleShot( +                self.EIP_TIMEOUT, +                self._maybe_run_soledad_setup_checks) +        else: +            if not self._already_started_eip: +                if EIP_SERVICE in self._enabled_services: +                    self._eip_status.set_eip_status( +                        self.tr("Not supported"), +                        error=True) +                else: +                    self._eip_status.disable_eip_start() +                    self._eip_status.set_eip_status(self.tr("Disabled")) +            # eip will not start, so we start soledad anyway +            self._maybe_run_soledad_setup_checks()      def _finish_eip_bootstrap(self, data):          """ @@ -1506,11 +1860,11 @@ class MainWindow(QtGui.QMainWindow):          This is used for intermediate bootstrapping stages, in case          they fail.          """ -        passed = data[self._provider_bootstrapper.PASSED_KEY] +        passed = data[self._backend.PASSED_KEY]          if not passed:              self._login_widget.set_status(                  self.tr("Unable to connect: Problem with provider")) -            logger.error(data[self._provider_bootstrapper.ERROR_KEY]) +            logger.error(data[self._backend.ERROR_KEY])              self._already_started_eip = False      # end of EIP methods --------------------------------------------- @@ -1547,20 +1901,35 @@ class MainWindow(QtGui.QMainWindow):          Starts the logout sequence          """ -          self._soledad_bootstrapper.cancel_bootstrap()          setProxiedObject(self._soledad, None) +        self._cancel_ongoing_defers() + +        # reset soledad status flag +        self._already_started_soledad = False +          # XXX: If other defers are doing authenticated stuff, this          # might conflict with those. CHECK!          threads.deferToThread(self._srp_auth.logout)          self.logout.emit() -    def _done_logging_out(self, ok, message): -        # TODO missing params in docstring +    def _logout_error(self): +        """ +        SLOT +        TRIGGER: self._srp_auth.logout_error + +        Inform the user about a logout error. +        """ +        self._login_widget.done_logout() +        self.ui.lblLoginProvider.setText(self.tr("Login")) +        self._login_widget.set_status( +            self.tr("Something went wrong with the logout.")) + +    def _logout_ok(self):          """          SLOT -        TRIGGER: self._srp_auth.logout_finished +        TRIGGER: self._srp_auth.logout_ok          Switches the stackedWidget back to the login stage after          logging out @@ -1568,36 +1937,30 @@ class MainWindow(QtGui.QMainWindow):          self._login_widget.done_logout()          self.ui.lblLoginProvider.setText(self.tr("Login")) -        if ok: -            self._logged_user = None -            self._login_widget.logged_out() -            self._mail_status.mail_state_disabled() - -        else: -            self._login_widget.set_login_status( -                self.tr("Something went wrong with the logout."), -                error=True) +        self._logged_user = None +        self._login_widget.logged_out() +        self._mail_status.mail_state_disabled()      def _intermediate_stage(self, data):          # TODO this method name is confusing as hell.          """          SLOT          TRIGGERS: -          self._provider_bootstrapper.name_resolution -          self._provider_bootstrapper.https_connection -          self._provider_bootstrapper.download_ca_cert +          self._backend.signaler.prov_name_resolution +          self._backend.signaler.prov_https_connection +          self._backend.signaler.prov_download_ca_cert            self._eip_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.          """ -        passed = data[self._provider_bootstrapper.PASSED_KEY] +        passed = data[self._backend.PASSED_KEY]          if not passed: +            msg = self.tr("Unable to connect: Problem with provider") +            self._login_widget.set_status(msg)              self._login_widget.set_enabled(True) -            self._login_widget.set_status( -                self.tr("Unable to connect: Problem with provider")) -            logger.error(data[self._provider_bootstrapper.ERROR_KEY]) +            logger.error(data[self._backend.ERROR_KEY])      #      # window handling methods @@ -1647,7 +2010,7 @@ class MainWindow(QtGui.QMainWindow):          """          logger.debug('About to quit, doing cleanup...') -        self._mail_conductor.stop_imap_service() +        self._stop_imap_service()          if self._srp_auth is not None:              if self._srp_auth.get_session_id() is not None or \ @@ -1664,13 +2027,7 @@ class MainWindow(QtGui.QMainWindow):          logger.debug('Terminating vpn')          self._vpn.terminate(shutdown=True) -        if self._login_defer: -            logger.debug("Cancelling login defer.") -            self._login_defer.cancel() - -        if self._download_provider_defer: -            logger.debug("Cancelling download provider defer.") -            self._download_provider_defer.cancel() +        self._cancel_ongoing_defers()          # TODO missing any more cancels? @@ -1687,8 +2044,8 @@ class MainWindow(QtGui.QMainWindow):          # Set this in case that the app is hidden          QtGui.QApplication.setQuitOnLastWindowClosed(True) +        self._backend.stop()          self._cleanup_and_quit() -          self._really_quit = True          if self._wizard: diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index b4bddef2..b2cc2236 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -18,7 +18,6 @@  """  Preferences window  """ -import os  import logging  from functools import partial @@ -26,6 +25,7 @@ from functools import partial  from PySide import QtCore, QtGui  from zope.proxy import sameProxiedObjects +from leap.bitmask.provider import get_provider_path  from leap.bitmask.config.leapsettings import LeapSettings  from leap.bitmask.gui.ui_preferences import Ui_Preferences  from leap.soledad.client import NoStorageSecret @@ -42,6 +42,8 @@ class PreferencesWindow(QtGui.QDialog):      """      Window that displays the preferences.      """ +    preferences_saved = QtCore.Signal() +      def __init__(self, parent, srp_auth, provider_config, soledad, domain):          """          :param parent: parent object of the PreferencesWindow. @@ -369,6 +371,7 @@ class PreferencesWindow(QtGui.QDialog):              "Services settings for provider '{0}' saved.".format(provider))          logger.debug(msg)          self._set_providers_services_status(msg, success=True) +        self.preferences_saved.emit()      def _get_provider_config(self, domain):          """ @@ -380,10 +383,7 @@ class PreferencesWindow(QtGui.QDialog):          :rtype: ProviderConfig or None if there is a problem loading the config          """          provider_config = ProviderConfig() -        provider_config_path = os.path.join( -            "leap", "providers", domain, "provider.json") - -        if not provider_config.load(provider_config_path): +        if not provider_config.load(get_provider_path(domain)):              provider_config = None          return provider_config diff --git a/src/leap/bitmask/gui/twisted_main.py b/src/leap/bitmask/gui/twisted_main.py index e11af7bd..1e876c57 100644 --- a/src/leap/bitmask/gui/twisted_main.py +++ b/src/leap/bitmask/gui/twisted_main.py @@ -27,24 +27,6 @@ from twisted.internet import error  logger = logging.getLogger(__name__) -def start(app): -    """ -    Start the mainloop. - -    :param app: the main qt QApplication instance. -    :type app: QtCore.QApplication -    """ -    from twisted.internet import reactor -    logger.debug('starting twisted reactor') - -    # this seems to be troublesome under some -    # unidentified settings. -    #reactor.run() - -    reactor.runReturn() -    app.exec_() - -  def quit(app):      """      Stop the mainloop. diff --git a/src/leap/bitmask/gui/ui/advanced_key_management.ui b/src/leap/bitmask/gui/ui/advanced_key_management.ui index d61aa87e..1112670f 100644 --- a/src/leap/bitmask/gui/ui/advanced_key_management.ui +++ b/src/leap/bitmask/gui/ui/advanced_key_management.ui @@ -6,8 +6,8 @@     <rect>      <x>0</x>      <y>0</y> -    <width>431</width> -    <height>188</height> +    <width>504</width> +    <height>546</height>     </rect>    </property>    <property name="windowTitle"> @@ -17,10 +17,13 @@     <iconset resource="../../../../../data/resources/mainwindow.qrc">      <normaloff>:/images/mask-icon.png</normaloff>:/images/mask-icon.png</iconset>    </property> -  <layout class="QGridLayout" name="gridLayout_3"> -   <item row="0" column="0"> -    <widget class="QWidget" name="container" native="true"> -     <layout class="QGridLayout" name="gridLayout_2"> +  <layout class="QGridLayout" name="gridLayout_4"> +   <item row="1" column="0" colspan="2"> +    <widget class="QGroupBox" name="gbMyKeyPair"> +     <property name="title"> +      <string>My key pair</string> +     </property> +     <layout class="QGridLayout" name="gridLayout_3">        <item row="0" column="0">         <widget class="QLabel" name="label">          <property name="text"> @@ -90,20 +93,7 @@          </property>         </widget>        </item> -      <item row="3" column="1"> -       <spacer name="verticalSpacer"> -        <property name="orientation"> -         <enum>Qt::Vertical</enum> -        </property> -        <property name="sizeHint" stdset="0"> -         <size> -          <width>20</width> -          <height>40</height> -         </size> -        </property> -       </spacer> -      </item> -      <item row="4" column="0" colspan="2"> +      <item row="3" column="0" colspan="2">         <layout class="QGridLayout" name="gridLayout">          <item row="1" column="1">           <widget class="QPushButton" name="pbExportKeys"> @@ -135,9 +125,56 @@         </layout>        </item>       </layout> +     <zorder>leKeyID</zorder> +     <zorder>leUser</zorder> +     <zorder>leFingerprint</zorder> +     <zorder>label_3</zorder> +     <zorder>label_5</zorder> +     <zorder>label</zorder> +    </widget> +   </item> +   <item row="2" column="0" colspan="2"> +    <widget class="QGroupBox" name="gbStoredPublicKeys"> +     <property name="title"> +      <string>Stored public keys</string> +     </property> +     <layout class="QGridLayout" name="gridLayout_2"> +      <item row="0" column="0"> +       <widget class="QTableWidget" name="twPublicKeys"> +        <property name="editTriggers"> +         <set>QAbstractItemView::NoEditTriggers</set> +        </property> +        <property name="alternatingRowColors"> +         <bool>true</bool> +        </property> +        <property name="selectionMode"> +         <enum>QAbstractItemView::SingleSelection</enum> +        </property> +        <property name="selectionBehavior"> +         <enum>QAbstractItemView::SelectRows</enum> +        </property> +        <property name="textElideMode"> +         <enum>Qt::ElideRight</enum> +        </property> +        <attribute name="horizontalHeaderStretchLastSection"> +         <bool>true</bool> +        </attribute> +        <column> +         <property name="text"> +          <string>Email</string> +         </property> +        </column> +        <column> +         <property name="text"> +          <string>Key ID</string> +         </property> +        </column> +       </widget> +      </item> +     </layout>      </widget>     </item> -   <item row="1" column="0"> +   <item row="3" column="0" colspan="2">      <widget class="QLabel" name="lblStatus">       <property name="text">        <string/> diff --git a/src/leap/bitmask/gui/ui/eip_status.ui b/src/leap/bitmask/gui/ui/eip_status.ui index d078ca0c..64821ad6 100644 --- a/src/leap/bitmask/gui/ui/eip_status.ui +++ b/src/leap/bitmask/gui/ui/eip_status.ui @@ -25,6 +25,9 @@     </property>     <item>      <layout class="QGridLayout" name="gridLayout"> +     <property name="verticalSpacing"> +      <number>0</number> +     </property>       <item row="0" column="2">        <widget class="QPushButton" name="btnEipStartStop">         <property name="text"> @@ -75,13 +78,6 @@           <verstretch>0</verstretch>          </sizepolicy>         </property> -       <property name="font"> -        <font> -         <pointsize>14</pointsize> -         <weight>75</weight> -         <bold>true</bold> -        </font> -       </property>         <property name="text">          <string>Traffic is being routed in the clear</string>         </property> @@ -124,12 +120,6 @@       </item>       <item row="2" column="1" colspan="3">        <widget class="QWidget" name="eip_bandwidth" native="true"> -       <property name="maximumSize"> -        <size> -         <width>16777215</width> -         <height>32</height> -        </size> -       </property>         <layout class="QHBoxLayout" name="horizontalLayout">          <property name="spacing">           <number>0</number> @@ -146,16 +136,6 @@             <enum>QLayout::SetDefaultConstraint</enum>            </property>            <item> -           <widget class="QLabel" name="label_5"> -            <property name="text"> -             <string/> -            </property> -            <property name="pixmap"> -             <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/down-arrow.png</pixmap> -            </property> -           </widget> -          </item> -          <item>             <widget class="QPushButton" name="btnDownload">              <property name="sizePolicy">               <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> @@ -175,25 +155,18 @@                <height>16777215</height>               </size>              </property> -            <property name="font"> -             <font> -              <pointsize>11</pointsize> -              <weight>75</weight> -              <bold>true</bold> -             </font> -            </property>              <property name="cursor">               <cursorShape>PointingHandCursor</cursorShape>              </property> -            <property name="styleSheet"> -             <string notr="true">text-align: left;</string> -            </property>              <property name="text">               <string>0.0 KB/s</string>              </property>              <property name="flat">               <bool>true</bool>              </property> +            <property name="icon"> +             <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/down-arrow.png</pixmap> +            </property>             </widget>            </item>            <item> @@ -206,22 +179,12 @@              </property>              <property name="sizeHint" stdset="0">               <size> -              <width>10</width> +              <width>20</width>                <height>20</height>               </size>              </property>             </spacer>            </item> -          <item> -           <widget class="QLabel" name="label_7"> -            <property name="text"> -             <string/> -            </property> -            <property name="pixmap"> -             <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/up-arrow.png</pixmap> -            </property> -           </widget> -          </item>            <item alignment="Qt::AlignLeft">             <widget class="QPushButton" name="btnUpload">              <property name="sizePolicy"> @@ -242,25 +205,18 @@                <height>16777215</height>               </size>              </property> -            <property name="font"> -             <font> -              <pointsize>11</pointsize> -              <weight>75</weight> -              <bold>true</bold> -             </font> -            </property>              <property name="cursor">               <cursorShape>PointingHandCursor</cursorShape>              </property> -            <property name="styleSheet"> -             <string notr="true">text-align: left;</string> -            </property>              <property name="text">               <string>0.0 KB/s</string>              </property>              <property name="flat">               <bool>true</bool>              </property> +            <property name="icon"> +             <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/up-arrow.png</pixmap> +            </property>             </widget>            </item>            <item> @@ -271,7 +227,7 @@              <property name="sizeHint" stdset="0">               <size>                <width>0</width> -              <height>20</height> +              <height>0</height>               </size>              </property>             </spacer> diff --git a/src/leap/bitmask/gui/ui/loggerwindow.ui b/src/leap/bitmask/gui/ui/loggerwindow.ui index 3de786f7..b19ed91a 100644 --- a/src/leap/bitmask/gui/ui/loggerwindow.ui +++ b/src/leap/bitmask/gui/ui/loggerwindow.ui @@ -6,8 +6,8 @@     <rect>      <x>0</x>      <y>0</y> -    <width>648</width> -    <height>469</height> +    <width>769</width> +    <height>464</height>     </rect>    </property>    <property name="windowTitle"> @@ -154,6 +154,17 @@         </property>        </widget>       </item> +     <item> +      <widget class="QPushButton" name="btnPastebin"> +       <property name="text"> +        <string>Send to Pastebin.com</string> +       </property> +       <property name="icon"> +        <iconset resource="../../../../../data/resources/loggerwindow.qrc"> +         <normaloff>:/images/pastebin.png</normaloff>:/images/pastebin.png</iconset> +       </property> +      </widget> +     </item>      </layout>     </item>    </layout> diff --git a/src/leap/bitmask/gui/ui/login.ui b/src/leap/bitmask/gui/ui/login.ui index e7ca1652..f5725d5a 100644 --- a/src/leap/bitmask/gui/ui/login.ui +++ b/src/leap/bitmask/gui/ui/login.ui @@ -215,28 +215,8 @@         <number>0</number>        </property>        <property name="bottomMargin"> -       <number>24</number> +       <number>0</number>        </property> -      <item row="1" column="1"> -       <spacer name="horizontalSpacer"> -        <property name="orientation"> -         <enum>Qt::Horizontal</enum> -        </property> -        <property name="sizeHint" stdset="0"> -         <size> -          <width>40</width> -          <height>20</height> -         </size> -        </property> -       </spacer> -      </item> -      <item row="1" column="0"> -       <widget class="QPushButton" name="btnLogout"> -        <property name="text"> -         <string>Logout</string> -        </property> -       </widget> -      </item>        <item row="0" column="0" colspan="2">         <widget class="QLabel" name="lblUser">          <property name="font"> @@ -251,17 +231,26 @@          </property>         </widget>        </item> -      <item row="2" column="0" colspan="2"> -       <widget class="QLabel" name="lblLoginStatus"> -        <property name="styleSheet"> -         <string notr="true">color: rgb(132, 132, 132); -font: 75 12pt "Lucida Grande";</string> -        </property> +      <item row="1" column="0"> +       <widget class="QPushButton" name="btnLogout">          <property name="text"> -         <string/> +         <string>Logout</string>          </property>         </widget>        </item> +      <item row="1" column="1"> +       <spacer name="horizontalSpacer"> +        <property name="orientation"> +         <enum>Qt::Horizontal</enum> +        </property> +        <property name="sizeHint" stdset="0"> +         <size> +          <width>40</width> +          <height>20</height> +         </size> +        </property> +       </spacer> +      </item>       </layout>      </widget>     </item> diff --git a/src/leap/bitmask/gui/ui/mainwindow.ui b/src/leap/bitmask/gui/ui/mainwindow.ui index 3b83788e..d755115a 100644 --- a/src/leap/bitmask/gui/ui/mainwindow.ui +++ b/src/leap/bitmask/gui/ui/mainwindow.ui @@ -75,7 +75,7 @@           <x>0</x>           <y>0</y>           <width>524</width> -         <height>651</height> +         <height>667</height>          </rect>         </property>         <layout class="QVBoxLayout" name="verticalLayout"> @@ -86,108 +86,6 @@           <number>0</number>          </property>          <item> -         <widget class="QWidget" name="eipWidget" native="true"> -          <layout class="QVBoxLayout" name="verticalLayout_2"> -           <property name="spacing"> -            <number>0</number> -           </property> -           <property name="margin"> -            <number>0</number> -           </property> -           <item> -            <widget class="QFrame" name="frame_2"> -             <property name="sizePolicy"> -              <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> -               <horstretch>0</horstretch> -               <verstretch>0</verstretch> -              </sizepolicy> -             </property> -             <property name="styleSheet"> -              <string notr="true">QFrame{background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgba(160, 160, 160, 128), stop:1 rgba(255, 255, 255, 0));}</string> -             </property> -             <layout class="QHBoxLayout" name="horizontalLayout_3"> -              <property name="leftMargin"> -               <number>24</number> -              </property> -              <property name="rightMargin"> -               <number>24</number> -              </property> -              <item> -               <widget class="QLabel" name="label_2"> -                <property name="font"> -                 <font> -                  <pointsize>16</pointsize> -                  <weight>75</weight> -                  <bold>true</bold> -                 </font> -                </property> -                <property name="styleSheet"> -                 <string notr="true">background-color: rgba(255, 255, 255, 0);</string> -                </property> -                <property name="text"> -                 <string>Encrypted Internet</string> -                </property> -               </widget> -              </item> -              <item> -               <widget class="QPushButton" name="btnEIPPreferences"> -                <property name="maximumSize"> -                 <size> -                  <width>48</width> -                  <height>20</height> -                 </size> -                </property> -                <property name="styleSheet"> -                 <string notr="true"/> -                </property> -                <property name="text"> -                 <string/> -                </property> -                <property name="icon"> -                 <iconset resource="../../../../../data/resources/mainwindow.qrc"> -                  <normaloff>:/images/black/32/gear.png</normaloff>:/images/black/32/gear.png</iconset> -                </property> -                <property name="autoDefault"> -                 <bool>false</bool> -                </property> -                <property name="default"> -                 <bool>false</bool> -                </property> -                <property name="flat"> -                 <bool>false</bool> -                </property> -               </widget> -              </item> -             </layout> -            </widget> -           </item> -           <item> -            <layout class="QVBoxLayout" name="eipLayout"> -             <property name="leftMargin"> -              <number>12</number> -             </property> -             <property name="topMargin"> -              <number>0</number> -             </property> -             <property name="rightMargin"> -              <number>12</number> -             </property> -             <property name="bottomMargin"> -              <number>0</number> -             </property> -            </layout> -           </item> -          </layout> -         </widget> -        </item> -        <item> -         <widget class="Line" name="line"> -          <property name="orientation"> -           <enum>Qt::Horizontal</enum> -          </property> -         </widget> -        </item> -        <item>           <widget class="QFrame" name="frame">            <property name="sizePolicy">             <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> @@ -199,9 +97,7 @@             <bool>false</bool>            </property>            <property name="styleSheet"> -           <string notr="true">QFrame{ -background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgba(160, 160, 160, 128), stop:1 rgba(255, 255, 255, 0)); -}</string> +           <string notr="true">background-color: rgba(0,0,0,20); border-bottom: 1px solid rgba(0,0,0,30);</string>            </property>            <layout class="QHBoxLayout" name="horizontalLayout">             <property name="leftMargin"> @@ -214,36 +110,15 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb              <widget class="QLabel" name="lblLoginProvider">               <property name="font">                <font> -               <pointsize>16</pointsize>                 <weight>75</weight>                 <bold>true</bold>                </font>               </property>               <property name="styleSheet"> -              <string notr="true">background-color: rgba(255, 255, 255, 0);</string> -             </property> -             <property name="text"> -              <string>Login</string> -             </property> -            </widget> -           </item> -           <item> -            <widget class="QPushButton" name="btnPreferences"> -             <property name="maximumSize"> -              <size> -               <width>48</width> -               <height>20</height> -              </size> -             </property> -             <property name="styleSheet"> -              <string notr="true"/> +              <string notr="true">background-color: rgba(255, 255, 255, 0); border: none;</string>               </property>               <property name="text"> -              <string/> -             </property> -             <property name="icon"> -              <iconset resource="../../../../../data/resources/mainwindow.qrc"> -               <normaloff>:/images/black/32/gear.png</normaloff>:/images/black/32/gear.png</iconset> +              <string>Please Log In</string>               </property>              </widget>             </item> @@ -258,7 +133,42 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb           </layout>          </item>          <item> -         <widget class="Line" name="line_2"> +         <widget class="Line" name="lineUnderLogin"> +          <property name="orientation"> +           <enum>Qt::Horizontal</enum> +          </property> +         </widget> +        </item> +        <item> +         <widget class="QWidget" name="eipWidget" native="true"> +          <layout class="QVBoxLayout" name="eipVerticalLayout"> +           <property name="spacing"> +            <number>0</number> +           </property> +           <property name="margin"> +            <number>0</number> +           </property> +           <item> +            <layout class="QVBoxLayout" name="eipLayout"> +             <property name="leftMargin"> +              <number>12</number> +             </property> +             <property name="topMargin"> +              <number>0</number> +             </property> +             <property name="rightMargin"> +              <number>12</number> +             </property> +             <property name="bottomMargin"> +              <number>0</number> +             </property> +            </layout> +           </item> +          </layout> +         </widget> +        </item> +        <item> +         <widget class="Line" name="lineUnderEIP">            <property name="orientation">             <enum>Qt::Horizontal</enum>            </property> @@ -287,6 +197,13 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb           </widget>          </item>          <item> +         <widget class="Line" name="lineUnderEmail"> +          <property name="orientation"> +           <enum>Qt::Horizontal</enum> +          </property> +         </widget> +        </item> +        <item>           <spacer name="verticalSpacer">            <property name="orientation">             <enum>Qt::Vertical</enum> @@ -380,7 +297,7 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb       <x>0</x>       <y>0</y>       <width>524</width> -     <height>21</height> +     <height>23</height>      </rect>     </property>     <widget class="QMenu" name="menuFile"> @@ -390,11 +307,14 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb      <addaction name="action_create_new_account"/>      <addaction name="action_advanced_key_management"/>      <addaction name="separator"/> +    <addaction name="action_preferences"/> +    <addaction name="action_eip_preferences"/> +    <addaction name="separator"/>      <addaction name="action_quit"/>     </widget>     <widget class="QMenu" name="menuHelp">      <property name="title"> -     <string>Help</string> +     <string>&Help</string>      </property>      <addaction name="action_help"/>      <addaction name="action_show_logs"/> @@ -404,10 +324,17 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb     <addaction name="menuFile"/>     <addaction name="menuHelp"/>    </widget> -  <widget class="QStatusBar" name="statusbar"/>    <action name="action_preferences"> +   <property name="enabled"> +    <bool>true</bool> +   </property> +   <property name="text"> +    <string>Account Preferences...</string> +   </property> +  </action> +  <action name="action_eip_preferences">     <property name="text"> -    <string>Preferences...</string> +    <string>Internet Preferences...</string>     </property>    </action>    <action name="action_quit"> diff --git a/src/leap/bitmask/gui/ui/wizard.ui b/src/leap/bitmask/gui/ui/wizard.ui index cf591470..6c592522 100644 --- a/src/leap/bitmask/gui/ui/wizard.ui +++ b/src/leap/bitmask/gui/ui/wizard.ui @@ -59,7 +59,7 @@      <item row="0" column="0">       <widget class="QLabel" name="label_3">        <property name="text"> -       <string><html><head/><body><p>Now we will guide you through some configuration that is needed before you can connect for the first time.</p><p>If you ever need to modify these options again, you can find the wizard in the <span style=" font-style:italic;">'Settings'</span> menu from the main window.</p><p>Do you want to <span style=" font-weight:600;">sign up</span> for a new account, or <span style=" font-weight:600;">log in</span> with an already existing username?</p></body></html></string> +       <string><html><head/><body><p>Now we will guide you through some configuration that is needed before you can connect for the first time.</p><p>If you ever need to modify these options again, you can find the wizard in the <span style=" font-style:italic;">'Bitmask -&gt; Create new account...'</span> menu from the main window.</p><p>Do you want to <span style=" font-weight:600;">sign up</span> for a new account, or <span style=" font-weight:600;">log in</span> with an already existing username?</p></body></html></string>        </property>        <property name="textFormat">         <enum>Qt::RichText</enum> @@ -269,7 +269,7 @@         <string>Configure or select a provider</string>        </property>        <layout class="QGridLayout" name="gridLayout_5"> -       <item row="0" column="0"> +       <item row="1" column="0">          <widget class="QRadioButton" name="rbNewProvider">           <property name="text">            <string>Configure new provider:</string> @@ -279,14 +279,14 @@           </property>          </widget>         </item> -       <item row="2" column="0"> -        <widget class="QRadioButton" name="rbExistingProvider"> -         <property name="text"> -          <string>Use existing one:</string> +       <item row="0" column="2"> +        <widget class="QComboBox" name="cbProviders"> +         <property name="enabled"> +          <bool>false</bool>           </property>          </widget>         </item> -       <item row="1" column="0"> +       <item row="1" column="1">          <widget class="QLabel" name="label">           <property name="text">            <string>https://</string> @@ -296,17 +296,20 @@           </property>          </widget>         </item> -       <item row="1" column="1"> +       <item row="1" column="2">          <widget class="QLineEdit" name="lnProvider"/>         </item> -       <item row="1" column="2"> -        <widget class="QPushButton" name="btnCheck"> +       <item row="0" column="0"> +        <widget class="QRadioButton" name="rbExistingProvider">           <property name="text"> -          <string>Check</string> +          <string>Use existing one:</string> +         </property> +         <property name="checked"> +          <bool>false</bool>           </property>          </widget>         </item> -       <item row="3" column="0"> +       <item row="0" column="1">          <widget class="QLabel" name="label_8">           <property name="text">            <string>https://</string> @@ -316,12 +319,29 @@           </property>          </widget>         </item> -       <item row="3" column="1"> -        <widget class="QComboBox" name="cbProviders"> -         <property name="enabled"> -          <bool>false</bool> -         </property> -        </widget> +       <item row="2" column="2"> +        <layout class="QHBoxLayout" name="horizontalLayout"> +         <item> +          <spacer name="horizontalSpacer_3"> +           <property name="orientation"> +            <enum>Qt::Horizontal</enum> +           </property> +           <property name="sizeHint" stdset="0"> +            <size> +             <width>40</width> +             <height>20</height> +            </size> +           </property> +          </spacer> +         </item> +         <item> +          <widget class="QPushButton" name="btnCheck"> +           <property name="text"> +            <string>Check</string> +           </property> +          </widget> +         </item> +        </layout>         </item>        </layout>       </widget> @@ -820,8 +840,8 @@     <slot>setFocus()</slot>     <hints>      <hint type="sourcelabel"> -     <x>167</x> -     <y>192</y> +     <x>174</x> +     <y>174</y>      </hint>      <hint type="destinationlabel">       <x>265</x> @@ -836,12 +856,12 @@     <slot>setFocus()</slot>     <hints>      <hint type="sourcelabel"> -     <x>171</x> -     <y>164</y> +     <x>174</x> +     <y>227</y>      </hint>      <hint type="destinationlabel"> -     <x>246</x> -     <y>164</y> +     <x>425</x> +     <y>254</y>      </hint>     </hints>    </connection> @@ -852,12 +872,12 @@     <slot>setDisabled(bool)</slot>     <hints>      <hint type="sourcelabel"> -     <x>169</x> -     <y>196</y> +     <x>174</x> +     <y>174</y>      </hint>      <hint type="destinationlabel"> -     <x>327</x> -     <y>163</y> +     <x>450</x> +     <y>266</y>      </hint>     </hints>    </connection> @@ -868,8 +888,8 @@     <slot>setDisabled(bool)</slot>     <hints>      <hint type="sourcelabel"> -     <x>169</x> -     <y>162</y> +     <x>174</x> +     <y>227</y>      </hint>      <hint type="destinationlabel">       <x>269</x> @@ -881,15 +901,15 @@     <sender>rbExistingProvider</sender>     <signal>toggled(bool)</signal>     <receiver>btnCheck</receiver> -   <slot>setDisabled(bool)</slot> +   <slot>setEnabled(bool)</slot>     <hints>      <hint type="sourcelabel"> -     <x>154</x> -     <y>193</y> +     <x>169</x> +     <y>174</y>      </hint>      <hint type="destinationlabel"> -     <x>498</x> -     <y>170</y> +     <x>520</x> +     <y>255</y>      </hint>     </hints>    </connection> diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index 5f5224ae..e2c1a16e 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -17,22 +17,17 @@  """  First run wizard  """ -import os  import logging -import json  import random  from functools import partial  from PySide import QtCore, QtGui -from twisted.internet import threads  from leap.bitmask.config.leapsettings import LeapSettings  from leap.bitmask.config.providerconfig import ProviderConfig -from leap.bitmask.crypto.srpregister import SRPRegister -from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper +from leap.bitmask.provider import get_provider_path  from leap.bitmask.services import get_service_display_name, get_supported -from leap.bitmask.util.request_helpers import get_content  from leap.bitmask.util.keyring_helpers import has_keyring  from leap.bitmask.util.password import basic_password_checks @@ -55,12 +50,15 @@ class Wizard(QtGui.QWizard):      BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" -    def __init__(self, bypass_checks=False): +    def __init__(self, backend, bypass_checks=False):          """          Constructor for the main Wizard. +        :param backend: Backend being used +        :type backend: Backend          :param bypass_checks: Set to true if the app should bypass -        first round of checks for CA certificates at bootstrap +                              first round of checks for CA +                              certificates at bootstrap          :type bypass_checks: bool          """          QtGui.QWizard.__init__(self) @@ -86,23 +84,14 @@ class Wizard(QtGui.QWizard):          self.ui.btnCheck.clicked.connect(self._check_provider)          self.ui.lnProvider.returnPressed.connect(self._check_provider) -        self._provider_bootstrapper = ProviderBootstrapper(bypass_checks) -        self._provider_bootstrapper.name_resolution.connect( -            self._name_resolution) -        self._provider_bootstrapper.https_connection.connect( -            self._https_connection) -        self._provider_bootstrapper.download_provider_info.connect( -            self._download_provider_info) - -        self._provider_bootstrapper.download_ca_cert.connect( -            self._download_ca_cert) -        self._provider_bootstrapper.check_ca_fingerprint.connect( -            self._check_ca_fingerprint) -        self._provider_bootstrapper.check_api_certificate.connect( -            self._check_api_certificate) +        self._backend = backend +        self._backend_connect()          self._domain = None -        self._provider_config = ProviderConfig() +        # HACK!! We need provider_config for the time being, it'll be +        # removed +        self._provider_config = ( +            self._backend._components["provider"]._provider_config)          # We will store a reference to the defers for eventual use          # (eg, to cancel them) but not doing anything with them right now. @@ -114,6 +103,8 @@ class Wizard(QtGui.QWizard):          self.ui.lnProvider.textChanged.connect(self._enable_check)          self.ui.rbNewProvider.toggled.connect(              lambda x: self._enable_check()) +        self.ui.cbProviders.currentIndexChanged[int].connect( +            self._reset_provider_check)          self.ui.lblUser.returnPressed.connect(              self._focus_password) @@ -166,6 +157,7 @@ class Wizard(QtGui.QWizard):          self._provider_setup_ok = False          self.ui.lnProvider.setText('')          self.ui.grpCheckProvider.setVisible(False) +        self._backend_disconnect()      def _load_configured_providers(self):          """ @@ -199,6 +191,10 @@ class Wizard(QtGui.QWizard):              random.shuffle(pinned)  # don't prioritize alphabetically              self.ui.cbProviders.addItems(pinned) +        # We have configured providers, so by default we select the +        # 'Use existing provider' option. +        self.ui.rbExistingProvider.setChecked(True) +      def get_domain(self):          return self._domain @@ -225,7 +221,7 @@ class Wizard(QtGui.QWizard):          depending on the lnProvider content.          """          enabled = len(self.ui.lnProvider.text()) != 0 -        enabled = enabled and self.ui.rbNewProvider.isChecked() +        enabled = enabled or self.ui.rbExistingProvider.isChecked()          self.ui.btnCheck.setEnabled(enabled)          if reset: @@ -255,16 +251,11 @@ class Wizard(QtGui.QWizard):          ok, msg = basic_password_checks(username, password, password2)          if ok: -            register = SRPRegister(provider_config=self._provider_config) -            register.registration_finished.connect( -                self._registration_finished) - -            threads.deferToThread( -                partial(register.register_user, username, password)) +            self._set_register_status(self.tr("Starting registration...")) +            self._backend.register_user(self._domain, username, password)              self._username = username              self._password = password -            self._set_register_status(self.tr("Starting registration..."))          else:              self._set_register_status(msg, error=True)              self._focus_password() @@ -291,42 +282,59 @@ class Wizard(QtGui.QWizard):          # register button          self.ui.btnRegister.setVisible(visible) -    def _registration_finished(self, ok, req): -        if ok: -            user_domain = self._username + "@" + self._domain -            message = "<font color='green'><h3>" -            message += self.tr("User %s successfully registered.") % ( -                user_domain, ) -            message += "</h3></font>" -            self._set_register_status(message) - -            self.ui.lblPassword2.clearFocus() -            self._set_registration_fields_visibility(False) - -            # Allow the user to remember his password -            if has_keyring(): -                self.ui.chkRemember.setVisible(True) -                self.ui.chkRemember.setEnabled(True) - -            self.page(self.REGISTER_USER_PAGE).set_completed() -            self.button(QtGui.QWizard.BackButton).setEnabled(False) -        else: -            old_username = self._username -            self._username = None -            self._password = None -            error_msg = self.tr("Something has gone wrong. " -                                "Please try again.") -            try: -                content, _ = get_content(req) -                json_content = json.loads(content) -                error_msg = json_content.get("errors").get("login")[0] -                if not error_msg.istitle(): -                    error_msg = "%s %s" % (old_username, error_msg) -            except Exception as e: -                logger.error("Unknown error: %r" % (e,)) - -            self._set_register_status(error_msg, error=True) -            self.ui.btnRegister.setEnabled(True) +    def _registration_finished(self): +        """ +        SLOT +        TRIGGERS: +          self._backend.signaler.srp_registration_finished + +        The registration has finished successfully, so we do some final steps. +        """ +        user_domain = self._username + "@" + self._domain +        message = "<font color='green'><h3>" +        message += self.tr("User %s successfully registered.") % ( +            user_domain, ) +        message += "</h3></font>" +        self._set_register_status(message) + +        self.ui.lblPassword2.clearFocus() +        self._set_registration_fields_visibility(False) + +        # Allow the user to remember his password +        if has_keyring(): +            self.ui.chkRemember.setVisible(True) +            self.ui.chkRemember.setEnabled(True) + +        self.page(self.REGISTER_USER_PAGE).set_completed() +        self.button(QtGui.QWizard.BackButton).setEnabled(False) + +    def _registration_failed(self): +        """ +        SLOT +        TRIGGERS: +          self._backend.signaler.srp_registration_failed + +        The registration has failed, so we report the problem. +        """ +        self._username = self._password = None + +        error_msg = self.tr("Something has gone wrong. Please try again.") +        self._set_register_status(error_msg, error=True) +        self.ui.btnRegister.setEnabled(True) + +    def _registration_taken(self): +        """ +        SLOT +        TRIGGERS: +          self._backend.signaler.srp_registration_taken + +        The requested username is taken, warn the user about that. +        """ +        self._username = self._password = None + +        error_msg = self.tr("The requested username is taken, choose another.") +        self._set_register_status(error_msg, error=True) +        self.ui.btnRegister.setEnabled(True)      def _set_register_status(self, status, error=False):          """ @@ -369,8 +377,10 @@ class Wizard(QtGui.QWizard):          Starts the checks for a given provider          """ -        if len(self.ui.lnProvider.text()) == 0: -            return +        if self.ui.rbNewProvider.isChecked(): +            self._domain = self.ui.lnProvider.text() +        else: +            self._domain = self.ui.cbProviders.currentText()          self._provider_checks_ok = False @@ -382,11 +392,10 @@ class Wizard(QtGui.QWizard):          self.ui.btnCheck.setEnabled(False)          self.ui.lnProvider.setEnabled(False)          self.button(QtGui.QWizard.BackButton).clearFocus() -        self._domain = self.ui.lnProvider.text()          self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON) -        self._provider_select_defer = self._provider_bootstrapper.\ -            run_provider_select_checks(self._domain) +        self._provider_select_defer = self._backend.\ +            setup_provider(self._domain)      def _skip_provider_checks(self, skip):          """ @@ -403,8 +412,6 @@ class Wizard(QtGui.QWizard):          if skip:              self._reset_provider_check() -        self.page(self.SELECT_PROVIDER_PAGE).set_completed(skip) -        self.button(QtGui.QWizard.NextButton).setEnabled(skip)          self._use_existing_provider = skip      def _complete_task(self, data, label, complete=False, complete_page=-1): @@ -423,8 +430,8 @@ class Wizard(QtGui.QWizard):          :param complete_page: page id to complete          :type complete_page: int          """ -        passed = data[self._provider_bootstrapper.PASSED_KEY] -        error = data[self._provider_bootstrapper.ERROR_KEY] +        passed = data[self._backend.PASSED_KEY] +        error = data[self._backend.ERROR_KEY]          if passed:              label.setPixmap(self.OK_ICON)              if complete: @@ -437,13 +444,13 @@ class Wizard(QtGui.QWizard):      def _name_resolution(self, data):          """          SLOT -        TRIGGER: self._provider_bootstrapper.name_resolution +        TRIGGER: self._backend.signaler.prov_name_resolution          Sets the status for the name resolution check          """          self._complete_task(data, self.ui.lblNameResolution)          status = "" -        passed = data[self._provider_bootstrapper.PASSED_KEY] +        passed = data[self._backend.PASSED_KEY]          if not passed:              status = self.tr("<font color='red'><b>Non-existent "                               "provider</b></font>") @@ -456,16 +463,16 @@ class Wizard(QtGui.QWizard):      def _https_connection(self, data):          """          SLOT -        TRIGGER: self._provider_bootstrapper.https_connection +        TRIGGER: self._backend.signaler.prov_https_connection          Sets the status for the https connection check          """          self._complete_task(data, self.ui.lblHTTPS)          status = "" -        passed = data[self._provider_bootstrapper.PASSED_KEY] +        passed = data[self._backend.PASSED_KEY]          if not passed:              status = self.tr("<font color='red'><b>%s</b></font>") \ -                % (data[self._provider_bootstrapper.ERROR_KEY]) +                % (data[self._backend.ERROR_KEY])              self.ui.lblProviderSelectStatus.setText(status)          else:              self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON) @@ -475,29 +482,26 @@ class Wizard(QtGui.QWizard):      def _download_provider_info(self, data):          """          SLOT -        TRIGGER: self._provider_bootstrapper.download_provider_info +        TRIGGER: self._backend.signaler.prov_download_provider_info          Sets the status for the provider information download          check. Since this check is the last of this set, it also          completes the page if passed          """ -        if self._provider_config.load(os.path.join("leap", -                                                   "providers", -                                                   self._domain, -                                                   "provider.json")): +        if self._provider_config.load(get_provider_path(self._domain)):              self._complete_task(data, self.ui.lblProviderInfo,                                  True, self.SELECT_PROVIDER_PAGE)              self._provider_checks_ok = True          else:              new_data = { -                self._provider_bootstrapper.PASSED_KEY: False, -                self._provider_bootstrapper.ERROR_KEY: +                self._backend.PASSED_KEY: False, +                self._backend.ERROR_KEY:                  self.tr("Unable to load provider configuration")              }              self._complete_task(new_data, self.ui.lblProviderInfo)          status = "" -        if not data[self._provider_bootstrapper.PASSED_KEY]: +        if not data[self._backend.PASSED_KEY]:              status = self.tr("<font color='red'><b>Not a valid provider"                               "</b></font>")              self.ui.lblProviderSelectStatus.setText(status) @@ -507,31 +511,31 @@ class Wizard(QtGui.QWizard):      def _download_ca_cert(self, data):          """          SLOT -        TRIGGER: self._provider_bootstrapper.download_ca_cert +        TRIGGER: self._backend.signaler.prov_download_ca_cert          Sets the status for the download of the CA certificate check          """          self._complete_task(data, self.ui.lblDownloadCaCert) -        passed = data[self._provider_bootstrapper.PASSED_KEY] +        passed = data[self._backend.PASSED_KEY]          if passed:              self.ui.lblCheckCaFpr.setPixmap(self.QUESTION_ICON)      def _check_ca_fingerprint(self, data):          """          SLOT -        TRIGGER: self._provider_bootstrapper.check_ca_fingerprint +        TRIGGER: self._backend.signaler.prov_check_ca_fingerprint          Sets the status for the CA fingerprint check          """          self._complete_task(data, self.ui.lblCheckCaFpr) -        passed = data[self._provider_bootstrapper.PASSED_KEY] +        passed = data[self._backend.PASSED_KEY]          if passed:              self.ui.lblCheckApiCert.setPixmap(self.QUESTION_ICON)      def _check_api_certificate(self, data):          """          SLOT -        TRIGGER: self._provider_bootstrapper.check_api_certificate +        TRIGGER: self._backend.signaler.prov_check_api_certificate          Sets the status for the API certificate check. Also finishes          the provider bootstrapper thread since it's not needed anymore @@ -597,6 +601,7 @@ class Wizard(QtGui.QWizard):          Prepares the pages when they appear          """          if pageId == self.SELECT_PROVIDER_PAGE: +            self._clear_register_widgets()              skip = self.ui.rbExistingProvider.isChecked()              if not self._provider_checks_ok:                  self._enable_check() @@ -611,8 +616,8 @@ class Wizard(QtGui.QWizard):                  sub_title = sub_title.format(self._provider_config.get_name())                  self.page(pageId).setSubTitle(sub_title)                  self.ui.lblDownloadCaCert.setPixmap(self.QUESTION_ICON) -                self._provider_setup_defer = self._provider_bootstrapper.\ -                    run_provider_setup_checks(self._provider_config) +                self._provider_setup_defer = self._backend.\ +                    provider_bootstrap(self._domain)          if pageId == self.PRESENT_PROVIDER_PAGE:              self.page(pageId).setSubTitle(self.tr("Description of services " @@ -670,3 +675,50 @@ class Wizard(QtGui.QWizard):                      return self.SERVICES_PAGE          return QtGui.QWizard.nextId(self) + +    def _clear_register_widgets(self): +        """ +        Clears the widgets that my be filled and a possible error message. +        """ +        self._set_register_status("") +        self.ui.lblUser.setText("") +        self.ui.lblPassword.setText("") +        self.ui.lblPassword2.setText("") + +    def _backend_connect(self): +        """ +        Connects all the backend signals with the wizard. +        """ +        sig = self._backend.signaler +        sig.prov_name_resolution.connect(self._name_resolution) +        sig.prov_https_connection.connect(self._https_connection) +        sig.prov_download_provider_info.connect(self._download_provider_info) + +        sig.prov_download_ca_cert.connect(self._download_ca_cert) +        sig.prov_check_ca_fingerprint.connect(self._check_ca_fingerprint) +        sig.prov_check_api_certificate.connect(self._check_api_certificate) + +        sig.srp_registration_finished.connect(self._registration_finished) +        sig.srp_registration_failed.connect(self._registration_failed) +        sig.srp_registration_taken.connect(self._registration_taken) + +    def _backend_disconnect(self): +        """ +        This method is called when the wizard dialog is closed. +        We disconnect all the backend signals in here. +        """ +        sig = self._backend.signaler +        try: +            # disconnect backend signals +            sig.prov_name_resolution.disconnect(self._name_resolution) +            sig.prov_https_connection.disconnect(self._https_connection) +            sig.prov_download_provider_info.disconnect( +                self._download_provider_info) + +            sig.prov_download_ca_cert.disconnect(self._download_ca_cert) +            sig.prov_check_ca_fingerprint.disconnect( +                self._check_ca_fingerprint) +            sig.prov_check_api_certificate.disconnect( +                self._check_api_certificate) +        except RuntimeError: +            pass  # Signal was not connected diff --git a/src/leap/bitmask/platform_init/locks.py b/src/leap/bitmask/platform_init/locks.py index 34f884dc..78ebf4cd 100644 --- a/src/leap/bitmask/platform_init/locks.py +++ b/src/leap/bitmask/platform_init/locks.py @@ -83,8 +83,6 @@ if platform_init.IS_UNIX:                  flock(self._fd, LOCK_EX | LOCK_NB)              except IOError as exc:                  # could not get the lock -                #import ipdb; ipdb.set_trace() -                  if exc.args[0] in (errno.EDEADLK, errno.EAGAIN):                      # errno 11 or 35                      # Resource temporarily unavailable diff --git a/src/leap/bitmask/provider/__init__.py b/src/leap/bitmask/provider/__init__.py index 53587d65..89ff5d95 100644 --- a/src/leap/bitmask/provider/__init__.py +++ b/src/leap/bitmask/provider/__init__.py @@ -15,12 +15,20 @@  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  """ -Module initialization for leap.bitmask.provider +Provider utilities.  """  import os + +from pkg_resources import parse_version + +from leap.bitmask import __short_version__ as BITMASK_VERSION  from leap.common.check import leap_assert +# The currently supported API versions by the client. +SUPPORTED_APIS = ["1"] + +  def get_provider_path(domain):      """      Returns relative path for provider config. @@ -32,3 +40,26 @@ def get_provider_path(domain):      """      leap_assert(domain is not None, "get_provider_path: We need a domain")      return os.path.join("leap", "providers", domain, "provider.json") + + +def supports_api(api_version): +    """ +    :param api_version: the version number of the api that we need to check +    :type api_version: str + +    :returns: if that version is supported or not. +    :return type: bool +    """ +    return api_version in SUPPORTED_APIS + + +def supports_client(minimum_version): +    """ +    :param minimum_version: the version number of the client that +                            we need to check. +    :type minimum_version: str + +    :returns: True if that version is supported or False otherwise. +    :return type: bool +    """ +    return parse_version(minimum_version) <= parse_version(BITMASK_VERSION) diff --git a/src/leap/bitmask/provider/providerbootstrapper.py b/src/leap/bitmask/provider/providerbootstrapper.py index f5a2003f..2a519206 100644 --- a/src/leap/bitmask/provider/providerbootstrapper.py +++ b/src/leap/bitmask/provider/providerbootstrapper.py @@ -24,18 +24,18 @@ import sys  import requests -from PySide import QtCore - -from leap.bitmask.config.providerconfig import ProviderConfig, MissingCACert -from leap.bitmask.util.request_helpers import get_content +from leap.bitmask import provider  from leap.bitmask import util -from leap.bitmask.util.constants import REQUEST_TIMEOUT +from leap.bitmask.config import flags +from leap.bitmask.config.providerconfig import ProviderConfig, MissingCACert +from leap.bitmask.provider import get_provider_path  from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper -from leap.bitmask.provider.supportedapis import SupportedAPIs +from leap.bitmask.util.constants import REQUEST_TIMEOUT +from leap.bitmask.util.request_helpers import get_content  from leap.common import ca_bundle  from leap.common.certs import get_digest -from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p  from leap.common.check import leap_assert, leap_assert_type, leap_check +from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p  logger = logging.getLogger(__name__) @@ -47,6 +47,14 @@ class UnsupportedProviderAPI(Exception):      pass +class UnsupportedClientVersionError(Exception): +    """ +    Raised when attempting to use a provider with an older +    client than supported. +    """ +    pass + +  class WrongFingerprint(Exception):      """      Raised when a fingerprint comparison does not match. @@ -61,25 +69,21 @@ class ProviderBootstrapper(AbstractBootstrapper):      If a check fails, the subsequent checks are not executed      """ -    # All dicts returned are of the form -    # {"passed": bool, "error": str} -    name_resolution = QtCore.Signal(dict) -    https_connection = QtCore.Signal(dict) -    download_provider_info = QtCore.Signal(dict) +    MIN_CLIENT_VERSION = 'x-minimum-client-version' -    download_ca_cert = QtCore.Signal(dict) -    check_ca_fingerprint = QtCore.Signal(dict) -    check_api_certificate = QtCore.Signal(dict) - -    def __init__(self, bypass_checks=False): +    def __init__(self, signaler=None, bypass_checks=False):          """          Constructor for provider bootstrapper object +        :param signaler: Signaler object used to receive notifications +                         from the backend +        :type signaler: Signaler          :param bypass_checks: Set to true if the app should bypass -        first round of checks for CA certificates at bootstrap +                              first round of checks for CA +                              certificates at bootstrap          :type bypass_checks: bool          """ -        AbstractBootstrapper.__init__(self, bypass_checks) +        AbstractBootstrapper.__init__(self, signaler, bypass_checks)          self._domain = None          self._provider_config = None @@ -95,9 +99,14 @@ class ProviderBootstrapper(AbstractBootstrapper):          :rtype: bool or str          """          if self._bypass_checks: -            verify = False +            return False + +        cert = flags.CA_CERT_FILE +        if cert is not None: +            verify = cert          else:              verify = ca_bundle.where() +          return verify      def _check_name_resolution(self): @@ -163,8 +172,8 @@ class ProviderBootstrapper(AbstractBootstrapper):          headers = {}          domain = self._domain.encode(sys.getfilesystemencoding())          provider_json = os.path.join(util.get_path_prefix(), -                                     "leap", "providers", domain, -                                     "provider.json") +                                     get_provider_path(domain)) +          mtime = get_mtime(provider_json)          if self._download_if_needed and mtime: @@ -195,6 +204,8 @@ class ProviderBootstrapper(AbstractBootstrapper):          res.raise_for_status()          logger.debug("Request status code: {0}".format(res.status_code)) +        min_client_version = res.headers.get(self.MIN_CLIENT_VERSION, '0') +          # Not modified          if res.status_code == 304:              logger.debug("Provider definition has not been modified") @@ -202,6 +213,13 @@ class ProviderBootstrapper(AbstractBootstrapper):          # end refactor, more or less...          # XXX Watch out, have to check the supported api yet.          else: +            if flags.APP_VERSION_CHECK: +                # TODO split +                if not provider.supports_client(min_client_version): +                    self._signaler.signal( +                        self._signaler.PROV_UNSUPPORTED_CLIENT) +                    raise UnsupportedClientVersionError() +              provider_definition, mtime = get_content(res)              provider_config = ProviderConfig() @@ -209,17 +227,20 @@ class ProviderBootstrapper(AbstractBootstrapper):              provider_config.save(["leap", "providers",                                    domain, "provider.json"]) -            api_version = provider_config.get_api_version() -            if SupportedAPIs.supports(api_version): -                logger.debug("Provider definition has been modified") -            else: -                api_supported = ', '.join(SupportedAPIs.SUPPORTED_APIS) -                error = ('Unsupported provider API version. ' -                         'Supported versions are: {0}. ' -                         'Found: {1}.').format(api_supported, api_version) - -                logger.error(error) -                raise UnsupportedProviderAPI(error) +            if flags.API_VERSION_CHECK: +                # TODO split +                api_version = provider_config.get_api_version() +                if provider.supports_api(api_version): +                    logger.debug("Provider definition has been modified") +                else: +                    api_supported = ', '.join(provider.SUPPORTED_APIS) +                    error = ('Unsupported provider API version. ' +                             'Supported versions are: {0}. ' +                             'Found: {1}.').format(api_supported, api_version) + +                    logger.error(error) +                    self._signaler.signal(self._signaler.PROV_UNSUPPORTED_API) +                    raise UnsupportedProviderAPI(error)      def run_provider_select_checks(self, domain, download_if_needed=False):          """ @@ -238,9 +259,11 @@ class ProviderBootstrapper(AbstractBootstrapper):          self._download_if_needed = download_if_needed          cb_chain = [ -            (self._check_name_resolution, self.name_resolution), -            (self._check_https, self.https_connection), -            (self._download_provider_info, self.download_provider_info) +            (self._check_name_resolution, +             self._signaler.PROV_NAME_RESOLUTION_KEY), +            (self._check_https, self._signaler.PROV_HTTPS_CONNECTION_KEY), +            (self._download_provider_info, +             self._signaler.PROV_DOWNLOAD_PROVIDER_INFO_KEY)          ]          return self.addCallbackChain(cb_chain) @@ -367,9 +390,11 @@ class ProviderBootstrapper(AbstractBootstrapper):          self._download_if_needed = download_if_needed          cb_chain = [ -            (self._download_ca_cert, self.download_ca_cert), -            (self._check_ca_fingerprint, self.check_ca_fingerprint), -            (self._check_api_certificate, self.check_api_certificate) +            (self._download_ca_cert, self._signaler.PROV_DOWNLOAD_CA_CERT_KEY), +            (self._check_ca_fingerprint, +             self._signaler.PROV_CHECK_CA_FINGERPRINT_KEY), +            (self._check_api_certificate, +             self._signaler.PROV_CHECK_API_CERTIFICATE_KEY)          ]          return self.addCallbackChain(cb_chain) diff --git a/src/leap/bitmask/provider/supportedapis.py b/src/leap/bitmask/provider/supportedapis.py deleted file mode 100644 index 3e650ba2..00000000 --- a/src/leap/bitmask/provider/supportedapis.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -# supportedapis.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/>. - -""" -API Support check. -""" - - -class SupportedAPIs(object): -    """ -    Class responsible of checking for API compatibility. -    """ -    SUPPORTED_APIS = ["1"] - -    @classmethod -    def supports(self, api_version): -        """ -        :param api_version: the version number of the api that we need to check -        :type api_version: str - -        :returns: if that version is supported or not. -        :return type: bool -        """ -        return api_version in self.SUPPORTED_APIS diff --git a/src/leap/bitmask/provider/tests/test_providerbootstrapper.py b/src/leap/bitmask/provider/tests/test_providerbootstrapper.py index 88a4ff0b..6cf3e469 100644 --- a/src/leap/bitmask/provider/tests/test_providerbootstrapper.py +++ b/src/leap/bitmask/provider/tests/test_providerbootstrapper.py @@ -36,21 +36,22 @@ from nose.twistedtools import deferred, reactor  from twisted.internet import threads  from requests.models import Response +from leap.bitmask import provider +from leap.bitmask import util +from leap.bitmask.backend import Signaler  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.crypto.tests import fake_provider  from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper  from leap.bitmask.provider.providerbootstrapper import UnsupportedProviderAPI  from leap.bitmask.provider.providerbootstrapper import WrongFingerprint -from leap.bitmask.provider.supportedapis import SupportedAPIs -from leap.bitmask import util  from leap.common.files import mkdir_p -from leap.common.testing.https_server import where  from leap.common.testing.basetest import BaseLeapTest +from leap.common.testing.https_server import where  class ProviderBootstrapperTest(BaseLeapTest):      def setUp(self): -        self.pb = ProviderBootstrapper() +        self.pb = ProviderBootstrapper(Signaler())      def tearDown(self):          pass @@ -488,7 +489,7 @@ class ProviderBootstrapperActiveTest(unittest.TestCase):          'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path',          lambda x: where('cacert.pem'))      def test_download_provider_info_unsupported_api(self): -        self._setup_provider_config_with(SupportedAPIs.SUPPORTED_APIS[0], +        self._setup_provider_config_with(provider.SUPPORTED_APIS[0],                                           tempfile.mkdtemp())          self._setup_providerbootstrapper(False)          self._produce_dummy_provider_json() diff --git a/src/leap/bitmask/services/abstractbootstrapper.py b/src/leap/bitmask/services/abstractbootstrapper.py index 6d4d319b..fc6bd3e9 100644 --- a/src/leap/bitmask/services/abstractbootstrapper.py +++ b/src/leap/bitmask/services/abstractbootstrapper.py @@ -25,7 +25,10 @@ import requests  from functools import partial  from PySide import QtCore + +from twisted.python import log  from twisted.internet import threads +from twisted.internet.defer import CancelledError  from leap.common.check import leap_assert, leap_assert_type @@ -40,10 +43,13 @@ class AbstractBootstrapper(QtCore.QObject):      PASSED_KEY = "passed"      ERROR_KEY = "error" -    def __init__(self, bypass_checks=False): +    def __init__(self, signaler=None, bypass_checks=False):          """          Constructor for the abstract bootstrapper +        :param signaler: Signaler object used to receive notifications +                         from the backend +        :type signaler: Signaler          :param bypass_checks: Set to true if the app should bypass                                first round of checks for CA                                certificates at bootstrap @@ -71,6 +77,7 @@ class AbstractBootstrapper(QtCore.QObject):          self._bypass_checks = bypass_checks          self._signal_to_emit = None          self._err_msg = None +        self._signaler = signaler      def _gui_errback(self, failure):          """ @@ -85,14 +92,30 @@ class AbstractBootstrapper(QtCore.QObject):          :param failure: failure object that Twisted generates          :type failure: twisted.python.failure.Failure          """ +        if failure.check(CancelledError): +            logger.debug("Defer cancelled.") +            failure.trap(Exception) +            self._signaler.signal(self._signaler.PROV_CANCELLED_SETUP) +            return +          if self._signal_to_emit:              err_msg = self._err_msg \                  if self._err_msg is not None \                  else str(failure.value) -            self._signal_to_emit.emit({ +            data = {                  self.PASSED_KEY: False,                  self.ERROR_KEY: err_msg -            }) +            } +            # TODO: Remove this check when all the bootstrappers are +            # in the backend form +            if isinstance(self._signal_to_emit, basestring): +                if self._signaler is not None: +                    self._signaler.signal(self._signal_to_emit, data) +                else: +                    logger.warning("Tried to notify but no signaler found") +            else: +                self._signal_to_emit.emit(data) +            log.err(failure)              failure.trap(Exception)      def _errback(self, failure, signal=None): @@ -127,8 +150,15 @@ class AbstractBootstrapper(QtCore.QObject):          :param signal: Signal to emit if it fails here first          :type signal: QtCore.SignalInstance          """ -        if signal: -            signal.emit({self.PASSED_KEY: True, self.ERROR_KEY: ""}) +        if signal is not None: +            data = {self.PASSED_KEY: True, self.ERROR_KEY: ""} +            if isinstance(signal, basestring): +                if self._signaler is not None: +                    self._signaler.signal(signal, data) +                else: +                    logger.warning("Tried to notify but no signaler found") +            else: +                signal.emit(data)      def _callback_threader(self, cb, res, *args, **kwargs):          return threads.deferToThread(cb, res, *args, **kwargs) diff --git a/src/leap/bitmask/services/eip/darwinvpnlauncher.py b/src/leap/bitmask/services/eip/darwinvpnlauncher.py index fe3fe4c1..a03bfc44 100644 --- a/src/leap/bitmask/services/eip/darwinvpnlauncher.py +++ b/src/leap/bitmask/services/eip/darwinvpnlauncher.py @@ -95,7 +95,7 @@ class DarwinVPNLauncher(VPNLauncher):          resources_path = os.path.abspath(              os.path.join(os.getcwd(), "../../Contents/Resources")) -        return os.path.join(resources_path, "leap-client.tiff") +        return os.path.join(resources_path, "bitmask.tiff")      @classmethod      def get_cocoasudo_ovpn_cmd(kls): diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index 51f0f738..5c100036 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -19,14 +19,20 @@ VPN Manager, spawned in a custom processProtocol.  """  import logging  import os -import psutil -import psutil.error  import shutil  import socket  import sys  from itertools import chain, repeat +import psutil +try: +    # psutil < 2.0.0 +    from psutil.error import AccessDenied as psutil_AccessDenied +except ImportError: +    # psutil >= 2.0.0 +    from psutil import AccessDenied as psutil_AccessDenied +  from PySide import QtCore  from leap.bitmask.config.providerconfig import ProviderConfig @@ -672,7 +678,7 @@ class VPNManager(object):                  if any(map(lambda s: s.find("LEAPOPENVPN") != -1, p.cmdline)):                      openvpn_process = p                      break -            except psutil.error.AccessDenied: +            except psutil_AccessDenied:                  pass          return openvpn_process diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py index addf9bef..79f324dc 100644 --- a/src/leap/bitmask/services/mail/conductor.py +++ b/src/leap/bitmask/services/mail/conductor.py @@ -35,6 +35,7 @@ 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__) @@ -72,6 +73,8 @@ class IMAPControl(object):          """          Starts imap service.          """ +        from leap.bitmask.config import flags +          logger.debug('Starting imap service')          leap_assert(sameProxiedObjects(self._soledad, None)                      is not True, @@ -81,16 +84,25 @@ class IMAPControl(object):                      "We need a non-null keymanager for initializing imap "                      "service") +        offline = flags.OFFLINE          self.imap_service, self.imap_port, \              self.imap_factory = imap.start_imap_service(                  self._soledad,                  self._keymanager, -                userid=self.userid) -        self.imap_service.start_loop() +                userid=self.userid, +                offline=offline) + +        if offline is False: +            logger.debug("Starting loop") +            self.imap_service.start_loop() -    def stop_imap_service(self): +    def stop_imap_service(self, cv):          """          Stops imap service (fetcher, factory and port). + +        :param cv: A condition variable to which we can signal when imap +                   indeed stops. +        :type cv: threading.Condition          """          self.imap_connection.qtsigs.disconnecting_signal.emit()          # TODO We should homogenize both services. @@ -102,7 +114,14 @@ class IMAPControl(object):              # Stop listening on the IMAP port              self.imap_port.stopListening()              # Stop the protocol -            self.imap_factory.doStop() +            self.imap_factory.theAccount.closed = True +            self.imap_factory.doStop(cv) +        else: +            # main window does not have to wait because there's no service to +            # be stopped, so we release the condition variable +            cv.acquire() +            cv.notify() +            cv.release()      def fetch_incoming_mail(self):          """ @@ -339,7 +358,7 @@ class MailConductor(IMAPControl, SMTPControl):          self._mail_machine = None          self._mail_connection = mail_connection.MailConnection() -        self.userid = None +        self._userid = None      @property      def userid(self): @@ -388,3 +407,4 @@ class MailConductor(IMAPControl, SMTPControl):          qtsigs.connecting_signal.connect(widget.mail_state_connecting)          qtsigs.disconnecting_signal.connect(widget.mail_state_disconnecting)          qtsigs.disconnected_signal.connect(widget.mail_state_disconnected) +        qtsigs.soledad_invalid_auth_token.connect(widget.soledad_invalid_auth_token) diff --git a/src/leap/bitmask/services/mail/connection.py b/src/leap/bitmask/services/mail/connection.py index 29378f62..fdc28fe4 100644 --- a/src/leap/bitmask/services/mail/connection.py +++ b/src/leap/bitmask/services/mail/connection.py @@ -93,6 +93,8 @@ class MailConnectionSignals(QtCore.QObject):      connection_died_signal = QtCore.Signal()      connection_aborted_signal = QtCore.Signal() +    soledad_invalid_auth_token = QtCore.Signal() +  class MailConnection(AbstractLEAPConnection): diff --git a/src/leap/bitmask/services/mail/imap.py b/src/leap/bitmask/services/mail/imap.py index 2667f156..5db18cb9 100644 --- a/src/leap/bitmask/services/mail/imap.py +++ b/src/leap/bitmask/services/mail/imap.py @@ -19,10 +19,10 @@ Initialization of imap service  """  import logging  import os -#import sys +import sys  from leap.mail.imap.service import imap -#from twisted.python import log +from twisted.python import log  logger = logging.getLogger(__name__) @@ -58,15 +58,15 @@ def start_imap_service(*args, **kwargs):      :returns: twisted.internet.task.LoopingCall instance      """ +    from leap.bitmask.config import flags      logger.debug('Launching imap service')      override_period = get_mail_check_period()      if override_period:          kwargs['check_period'] = override_period -    # Uncomment the next two lines to get a separate debugging log -    # TODO handle this by a separate flag. -    #log.startLogging(open('/tmp/leap-imap.log', 'w')) -    #log.startLogging(sys.stdout) +    if flags.MAIL_LOGFILE: +        log.startLogging(open(flags.MAIL_LOGFILE, 'w')) +        log.startLogging(sys.stdout)      return imap.run_service(*args, **kwargs) diff --git a/src/leap/bitmask/services/mail/plumber.py b/src/leap/bitmask/services/mail/plumber.py new file mode 100644 index 00000000..c16a1fed --- /dev/null +++ b/src/leap/bitmask/services/mail/plumber.py @@ -0,0 +1,341 @@ +# -*- coding: utf-8 -*- +# plumber.py +# Copyright (C) 2013, 2014  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/>. +""" +Utils for manipulating local mailboxes. +""" +import getpass +import logging +import os + +from collections import defaultdict +from functools import partial + +from twisted.internet import defer + +from leap.bitmask.config.leapsettings import LeapSettings +from leap.bitmask.config.providerconfig import ProviderConfig +from leap.bitmask.provider import get_provider_path +from leap.bitmask.services.soledad.soledadbootstrapper import get_db_paths +from leap.bitmask.util import flatten, get_path_prefix + +from leap.mail.imap.account import SoledadBackedAccount +from leap.mail.imap.memorystore import MemoryStore +from leap.mail.imap.soledadstore import SoledadStore +from leap.soledad.client import Soledad + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + + +def initialize_soledad(uuid, email, passwd, +                       secrets, localdb, +                       gnupg_home, tempdir): +    """ +    Initializes soledad by hand + +    :param email: ID for the user +    :param gnupg_home: path to home used by gnupg +    :param tempdir: path to temporal dir +    :rtype: Soledad instance +    """ +    # XXX TODO unify with an authoritative source of mocks +    # for soledad (or partial initializations). +    # This is copied from the imap tests. + +    server_url = "http://provider" +    cert_file = "" + +    class Mock(object): +        def __init__(self, return_value=None): +            self._return = return_value + +        def __call__(self, *args, **kwargs): +            return self._return + +    class MockSharedDB(object): + +        get_doc = Mock() +        put_doc = Mock() +        lock = Mock(return_value=('atoken', 300)) +        unlock = Mock(return_value=True) + +        def __call__(self): +            return self + +    Soledad._shared_db = MockSharedDB() +    soledad = Soledad( +        uuid, +        passwd, +        secrets, +        localdb, +        server_url, +        cert_file) + +    return soledad + + +class MBOXPlumber(object): +    """ +    An class that can fix things inside a soledadbacked account. +    The idea is to gather in this helper different fixes for mailboxes +    that can be invoked when data migration in the client is needed. +    """ + +    def __init__(self, userid, passwd, mdir=None): +        """ +        Initialize the plumber with all that's needed to authenticate +        against the provider. + +        :param userid: user identifier, foo@bar +        :type userid: basestring +        :param passwd: the soledad passphrase +        :type passwd: basestring +        :param mdir: a path to a maildir to import +        :type mdir: str or None +        """ +        self.userid = userid +        self.passwd = passwd +        user, provider = userid.split('@') +        self.user = user +        self.mdir = mdir +        self.sol = None +        self._settings = LeapSettings() + +        provider_config_path = os.path.join(get_path_prefix(), +                                            get_provider_path(provider)) +        provider_config = ProviderConfig() +        loaded = provider_config.load(provider_config_path) +        if not loaded: +            print "could not load provider config!" +            return self.exit() + +    def _init_local_soledad(self): +        """ +        Initialize local Soledad instance. +        """ +        self.uuid = self._settings.get_uuid(self.userid) +        if not self.uuid: +            print "Cannot get UUID from settings. Log in at least once." +            return False + +        print "UUID: %s" % (self.uuid) + +        secrets, localdb = get_db_paths(self.uuid) + +        self.sol = initialize_soledad( +            self.uuid, self.userid, self.passwd, +            secrets, localdb, "/tmp", "/tmp") +        memstore = MemoryStore( +            permanent_store=SoledadStore(self.sol), +            write_period=5) +        self.acct = SoledadBackedAccount(self.userid, self.sol, +                                         memstore=memstore) +        return True + +    # +    # Account repairing +    # + +    def repair_account(self, *args): +        """ +        Repair mbox uids for all mboxes in this account. +        """ +        init = self._init_local_soledad() +        if not init: +            return self.exit() + +        for mbox_name in self.acct.mailboxes: +            self.repair_mbox_uids(mbox_name) +        print "done." +        self.exit() + +    def repair_mbox_uids(self, mbox_name): +        """ +        Repair indexes for a given mbox. + +        :param mbox_name: mailbox to repair +        :type mbox_name: basestring +        """ +        print +        print "REPAIRING INDEXES FOR MAILBOX %s" % (mbox_name,) +        print "----------------------------------------------" +        mbox = self.acct.getMailbox(mbox_name) +        len_mbox = mbox.getMessageCount() +        print "There are %s messages" % (len_mbox,) + +        last_ok = True if mbox.last_uid == len_mbox else False +        uids_iter = mbox.messages.all_msg_iter() +        dupes = self._has_dupes(uids_iter) +        if last_ok and not dupes: +            print "Mbox does not need repair." +            return + +        # XXX CHANGE? ---- +        msgs = mbox.messages.get_all() +        for zindex, doc in enumerate(msgs): +            mindex = zindex + 1 +            old_uid = doc.content['uid'] +            doc.content['uid'] = mindex +            self.sol.put_doc(doc) +            if mindex != old_uid: +                print "%s -> %s (%s)" % (mindex, doc.content['uid'], old_uid) + +        old_last_uid = mbox.last_uid +        mbox.last_uid = len_mbox +        print "LAST UID: %s (%s)" % (mbox.last_uid, old_last_uid) + +    def _has_dupes(self, sequence): +        """ +        Return True if the given sequence of ints has duplicates. + +        :param sequence: a sequence of ints +        :type sequence: sequence +        :rtype: bool +        """ +        d = defaultdict(lambda: 0) +        for uid in sequence: +            d[uid] += 1 +            if d[uid] != 1: +                return True +        return False + +    # +    # Maildir import +    # +    def import_mail(self, mail_filename): +        """ +        Import a single mail into a mailbox. + +        :param mbox: the Mailbox instance to save in. +        :type mbox: SoledadMailbox +        :param mail_filename: the filename to the mail file to save +        :type mail_filename: basestring +        :return: a deferred +        """ +        def saved(_): +            print "message added" + +        with open(mail_filename) as f: +            mail_string = f.read() +            #uid = self._mbox.getUIDNext() +            #print "saving with UID: %s" % uid +            d = self._mbox.messages.add_msg( +                mail_string, notify_on_disk=True) +        return d + +    def import_maildir(self, mbox_name="INBOX"): +        """ +        Import all mails in a maildir. + +        We will process all subfolders as beloging +        to the same mailbox (cur, new, tmp). +        """ +        # TODO parse hierarchical subfolders into +        # inferior mailboxes. + +        if not os.path.isdir(self.mdir): +            print "ERROR: maildir path does not exist." +            return + +        init = self._init_local_soledad() +        if not init: +            return self.exit() + +        mbox = self.acct.getMailbox(mbox_name) +        self._mbox = mbox +        len_mbox = mbox.getMessageCount() + +        mail_files_g = flatten( +            map(partial(os.path.join, f), files) +            for f, _, files in os.walk(self.mdir)) + +        # we only coerce the generator to give the +        # len, but we could skip than and inform at the end. +        mail_files = list(mail_files_g) +        print "Got %s mails to import into %s (%s)" % ( +            len(mail_files), mbox_name, len_mbox) + +        def all_saved(_): +            print "all messages imported" + +        deferreds = [] +        for f_name in mail_files: +            deferreds.append(self.import_mail(f_name)) +        print "deferreds: ", deferreds + +        d1 = defer.gatherResults(deferreds, consumeErrors=False) +        d1.addCallback(all_saved) +        d1.addCallback(self._cbExit) + +    def _cbExit(self, ignored): +        return self.exit() + +    def exit(self): +        from twisted.internet import reactor +        try: +            if self.sol: +                self.sol.close() +            reactor.stop() +        except Exception: +            pass +        return + + +def repair_account(userid): +    """ +    Start repair process for a given account. + +    :param userid: the user id (email-like) +    """ +    from twisted.internet import reactor +    passwd = unicode(getpass.getpass("Passphrase: ")) + +    # go mario! +    plumber = MBOXPlumber(userid, passwd) +    reactor.callLater(1, plumber.repair_account) +    reactor.run() + + +def import_maildir(userid, maildir_path): +    """ +    Start import-maildir process for a given account. + +    :param userid: the user id (email-like) +    """ +    from twisted.internet import reactor +    passwd = unicode(getpass.getpass("Passphrase: ")) + +    # go mario! +    plumber = MBOXPlumber(userid, passwd, mdir=maildir_path) +    reactor.callLater(1, plumber.import_maildir) +    reactor.run() + + +if __name__ == "__main__": +    import sys + +    logging.basicConfig() + +    if len(sys.argv) != 3: +        print "Usage: plumber [repair|import] <username>" +        sys.exit(1) + +    # this would be better with a dict if it grows +    if sys.argv[1] == "repair": +        repair_account(sys.argv[2]) +    if sys.argv[1] == "import": +        print "Not implemented yet." diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index d078ae96..ad5ee4d0 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -23,12 +23,13 @@ import socket  import sys  from ssl import SSLError +from sqlite3 import ProgrammingError as sqlite_ProgrammingError  from PySide import QtCore  from u1db import errors as u1db_errors +from twisted.internet import threads  from zope.proxy import sameProxiedObjects - -from twisted.internet.threads import deferToThread +from pysqlcipher.dbapi2 import ProgrammingError as sqlcipher_ProgrammingError  from leap.bitmask.config import flags  from leap.bitmask.config.providerconfig import ProviderConfig @@ -36,21 +37,56 @@ from leap.bitmask.crypto.srpauth import SRPAuth  from leap.bitmask.services import download_service_config  from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper  from leap.bitmask.services.soledad.soledadconfig import SoledadConfig -from leap.bitmask.util import is_file, is_empty_file +from leap.bitmask.util import first, is_file, is_empty_file, make_address  from leap.bitmask.util import get_path_prefix  from leap.bitmask.platform_init import IS_WIN  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.common.errors import InvalidAuthTokenError  from leap.soledad.client import Soledad, BootstrapSequenceError  logger = logging.getLogger(__name__) +""" +These mocks are replicated from imap tests and the repair utility. +They are needed for the moment to knock out the remote capabilities of soledad +during the use of the offline mode. + +They should not be needed after we allow a null remote initialization in the +soledad client, and a switch to remote sync-able mode during runtime. +""" + + +class Mock(object): +    """ +    A generic simple mock class +    """ +    def __init__(self, return_value=None): +        self._return = return_value + +    def __call__(self, *args, **kwargs): +        return self._return + + +class MockSharedDB(object): +    """ +    Mocked  SharedDB object to replace in soledad before +    instantiating it in offline mode. +    """ +    get_doc = Mock() +    put_doc = Mock() +    lock = Mock(return_value=('atoken', 300)) +    unlock = Mock(return_value=True) + +    def __call__(self): +        return self  # TODO these exceptions could be moved to soledad itself  # after settling this down. +  class SoledadSyncError(Exception):      message = "Error while syncing Soledad" @@ -59,9 +95,36 @@ class SoledadInitError(Exception):      message = "Error while initializing Soledad" +def get_db_paths(uuid): +    """ +    Return the secrets and local db paths needed for soledad +    initialization + +    :param uuid: uuid for user +    :type uuid: str + +    :return: a tuple with secrets, local_db paths +    :rtype: tuple +    """ +    prefix = os.path.join(get_path_prefix(), "leap", "soledad") +    secrets = "%s/%s.secret" % (prefix, uuid) +    local_db = "%s/%s.db" % (prefix, uuid) + +    # We remove an empty file if found to avoid complains +    # about the db not being properly initialized +    if is_file(local_db) and is_empty_file(local_db): +        try: +            os.remove(local_db) +        except OSError: +            logger.warning( +                "Could not remove empty file %s" +                % local_db) +    return secrets, local_db + +  class SoledadBootstrapper(AbstractBootstrapper):      """ -    Soledad init procedure +    Soledad init procedure.      """      SOLEDAD_KEY = "soledad"      KEYMANAGER_KEY = "keymanager" @@ -75,7 +138,9 @@ class SoledadBootstrapper(AbstractBootstrapper):      # {"passed": bool, "error": str}      download_config = QtCore.Signal(dict)      gen_key = QtCore.Signal(dict) +    local_only_ready = QtCore.Signal(dict)      soledad_timeout = QtCore.Signal() +    soledad_invalid_auth_token = QtCore.Signal()      soledad_failed = QtCore.Signal()      def __init__(self): @@ -88,6 +153,9 @@ class SoledadBootstrapper(AbstractBootstrapper):          self._user = ""          self._password = "" +        self._address = "" +        self._uuid = "" +          self._srpauth = None          self._soledad = None @@ -103,6 +171,8 @@ class SoledadBootstrapper(AbstractBootstrapper):      @property      def srpauth(self): +        if flags.OFFLINE is True: +            return None          leap_assert(self._provider_config is not None,                      "We need a provider config")          return SRPAuth(self._provider_config) @@ -114,7 +184,7 @@ class SoledadBootstrapper(AbstractBootstrapper):      def should_retry_initialization(self):          """ -        Returns True if we should retry the initialization. +        Return True if we should retry the initialization.          """          logger.debug("current retries: %s, max retries: %s" % (              self._soledad_retries, @@ -123,72 +193,100 @@ class SoledadBootstrapper(AbstractBootstrapper):      def increment_retries_count(self):          """ -        Increments the count of initialization retries. +        Increment the count of initialization retries.          """          self._soledad_retries += 1 -    def _get_db_paths(self, uuid): +    # initialization + +    def load_offline_soledad(self, username, password, uuid): +        """ +        Instantiate Soledad for offline use. + +        :param username: full user id (user@provider) +        :type username: basestring +        :param password: the soledad passphrase +        :type password: unicode +        :param uuid: the user uuid +        :type uuid: basestring          """ -        Returns the secrets and local db paths needed for soledad -        initialization +        print "UUID ", uuid +        self._address = username +        self._uuid = uuid +        return self.load_and_sync_soledad(uuid, offline=True) -        :param uuid: uuid for user -        :type uuid: str +    def _get_soledad_local_params(self, uuid, offline=False): +        """ +        Return the locals parameters needed for the soledad initialization. -        :return: a tuple with secrets, local_db paths +        :param uuid: the uuid of the user, used in offline mode. +        :type uuid: unicode, or None. +        :return: secrets_path, local_db_path, token          :rtype: tuple          """ -        prefix = os.path.join(get_path_prefix(), "leap", "soledad") -        secrets = "%s/%s.secret" % (prefix, uuid) -        local_db = "%s/%s.db" % (prefix, uuid) +        # in the future, when we want to be able to switch to +        # online mode, this should be a proxy object too. +        # Same for server_url below. -        # We remove an empty file if found to avoid complains -        # about the db not being properly initialized -        if is_file(local_db) and is_empty_file(local_db): -            try: -                os.remove(local_db) -            except OSError: -                logger.warning("Could not remove empty file %s" -                               % local_db) -        return secrets, local_db +        if offline is False: +            token = self.srpauth.get_token() +        else: +            token = "" -    # initialization +        secrets_path, local_db_path = get_db_paths(uuid) + +        logger.debug('secrets_path:%s' % (secrets_path,)) +        logger.debug('local_db:%s' % (local_db_path,)) +        return (secrets_path, local_db_path, token) -    def load_and_sync_soledad(self): +    def _get_soledad_server_params(self, uuid, offline):          """ -        Once everthing is in the right place, we instantiate and sync -        Soledad +        Return the remote parameters needed for the soledad initialization. + +        :param uuid: the uuid of the user, used in offline mode. +        :type uuid: unicode, or None. +        :return: server_url, cert_file +        :rtype: tuple          """ -        # TODO this method is still too large -        uuid = self.srpauth.get_uid() -        token = self.srpauth.get_token() +        if uuid is None: +            uuid = self.srpauth.get_uuid() -        secrets_path, local_db_path = self._get_db_paths(uuid) +        if offline is True: +            server_url = "http://localhost:9999/" +            cert_file = "" +        else: +            server_url = self._pick_server(uuid) +            cert_file = self._provider_config.get_ca_cert_path() -        # TODO: Select server based on timezone (issue #3308) -        server_dict = self._soledad_config.get_hosts() +        return server_url, cert_file -        if not server_dict.keys(): -            # XXX raise more specific exception, and catch it properly! -            raise Exception("No soledad server found") +    def _soledad_sync_errback(self, failure): +        failure.trap(InvalidAuthTokenError) +        # in the case of an invalid token we have already turned off mail and +        # warned the user in _do_soledad_sync() -        selected_server = server_dict[server_dict.keys()[0]] -        server_url = "https://%s:%s/user-%s" % ( -            selected_server["hostname"], -            selected_server["port"], -            uuid) -        logger.debug("Using soledad server url: %s" % (server_url,)) -        cert_file = self._provider_config.get_ca_cert_path() +    def load_and_sync_soledad(self, uuid=None, offline=False): +        """ +        Once everthing is in the right place, we instantiate and sync +        Soledad + +        :param uuid: the uuid of the user, used in offline mode. +        :type uuid: unicode, or None. +        :param offline: whether to instantiate soledad for offline use. +        :type offline: bool +        """ +        local_param = self._get_soledad_local_params(uuid, offline) +        remote_param = self._get_soledad_server_params(uuid, offline) -        logger.debug('local_db:%s' % (local_db_path,)) -        logger.debug('secrets_path:%s' % (secrets_path,)) +        secrets_path, local_db_path, token = local_param +        server_url, cert_file = remote_param          try:              self._try_soledad_init(                  uuid, secrets_path, local_db_path,                  server_url, cert_file, token) -        except: +        except Exception:              # re-raise the exceptions from try_init,              # we're currently handling the retries from the              # soledad-launcher in the gui. @@ -196,11 +294,52 @@ class SoledadBootstrapper(AbstractBootstrapper):          leap_assert(not sameProxiedObjects(self._soledad, None),                      "Null soledad, error while initializing") -        self._do_soledad_sync() + +        if flags.OFFLINE is True: +            self._init_keymanager(self._address, token) +            self.local_only_ready.emit({self.PASSED_KEY: True}) +        else: +            try: +                address = make_address( +                    self._user, self._provider_config.get_domain()) +                self._init_keymanager(address, token) +                self._keymanager.get_key( +                    address, openpgp.OpenPGPKey, +                    private=True, fetch_remote=False) +                d = threads.deferToThread(self._do_soledad_sync) +                d.addErrback(self._soledad_sync_errback) +            except KeyNotFound: +                logger.debug("Key not found. Generating key for %s" % +                             (address,)) +                self._do_soledad_sync() + +    def _pick_server(self, uuid): +        """ +        Choose a soledad server to sync against. + +        :param uuid: the uuid for the user. +        :type uuid: unicode +        :returns: the server url +        :rtype: unicode +        """ +        # TODO: Select server based on timezone (issue #3308) +        server_dict = self._soledad_config.get_hosts() + +        if not server_dict.keys(): +            # XXX raise more specific exception, and catch it properly! +            raise Exception("No soledad server found") + +        selected_server = server_dict[first(server_dict.keys())] +        server_url = "https://%s:%s/user-%s" % ( +            selected_server["hostname"], +            selected_server["port"], +            uuid) +        logger.debug("Using soledad server url: %s" % (server_url,)) +        return server_url      def _do_soledad_sync(self):          """ -        Does several retries to get an initial soledad sync. +        Do several retries to get an initial soledad sync.          """          # and now, let's sync          sync_tries = self.MAX_SYNC_RETRIES @@ -220,6 +359,13 @@ class SoledadBootstrapper(AbstractBootstrapper):                  # ubuntu folks.                  sync_tries -= 1                  continue +            except InvalidAuthTokenError: +                self.soledad_invalid_auth_token.emit() +                raise +            except Exception as e: +                logger.exception("Unhandled error while syncing " +                                 "soledad: %r" % (e,)) +                break          # reached bottom, failed to sync          # and there's nothing we can do... @@ -229,7 +375,7 @@ class SoledadBootstrapper(AbstractBootstrapper):      def _try_soledad_init(self, uuid, secrets_path, local_db_path,                            server_url, cert_file, auth_token):          """ -        Tries to initialize soledad. +        Try to initialize soledad.          :param uuid: user identifier          :param secrets_path: path to secrets file @@ -245,6 +391,10 @@ class SoledadBootstrapper(AbstractBootstrapper):          # TODO: If selected server fails, retry with another host          # (issue #3309)          encoding = sys.getfilesystemencoding() + +        # XXX We should get a flag in soledad itself +        if flags.OFFLINE is True: +            Soledad._shared_db = MockSharedDB()          try:              self._soledad = Soledad(                  uuid, @@ -279,7 +429,7 @@ class SoledadBootstrapper(AbstractBootstrapper):              self.soledad_failed.emit()              raise          except u1db_errors.HTTPError as exc: -            logger.exception("Error whie initializing soledad " +            logger.exception("Error while initializing soledad "                               "(HTTPError)")              self.soledad_failed.emit()              raise @@ -291,7 +441,7 @@ class SoledadBootstrapper(AbstractBootstrapper):      def _try_soledad_sync(self):          """ -        Tries to sync soledad. +        Try to sync soledad.          Raises SoledadSyncError if not successful.          """          try: @@ -300,6 +450,16 @@ class SoledadBootstrapper(AbstractBootstrapper):          except SSLError as exc:              logger.error("%r" % (exc,))              raise SoledadSyncError("Failed to sync soledad") +        except u1db_errors.InvalidGeneration as exc: +            logger.error("%r" % (exc,)) +            raise SoledadSyncError("u1db: InvalidGeneration") +        except (sqlite_ProgrammingError, sqlcipher_ProgrammingError) as e: +            logger.exception("%r" % (e,)) +            raise +        except InvalidAuthTokenError: +            # token is invalid, probably expired +            logger.error('Invalid auth token while trying to sync Soledad') +            raise          except Exception as exc:              logger.exception("Unhandled error while syncing "                               "soledad: %r" % (exc,)) @@ -307,7 +467,7 @@ class SoledadBootstrapper(AbstractBootstrapper):      def _download_config(self):          """ -        Downloads the Soledad config for the given provider +        Download the Soledad config for the given provider          """          leap_assert(self._provider_config, @@ -326,11 +486,14 @@ class SoledadBootstrapper(AbstractBootstrapper):          # XXX but honestly, this is a pretty strange entry point for that.          # it feels like it should be the other way around:          # load_and_sync, and from there, if needed, call download_config -        self.load_and_sync_soledad() + +        uuid = self.srpauth.get_uuid() +        self.load_and_sync_soledad(uuid)      def _get_gpg_bin_path(self):          """ -        Returns the path to gpg binary. +        Return the path to gpg binary. +          :returns: the gpg binary path          :rtype: str          """ @@ -356,40 +519,62 @@ class SoledadBootstrapper(AbstractBootstrapper):          leap_check(gpgbin is not None, "Could not find gpg binary")          return gpgbin -    def _init_keymanager(self, address): +    def _init_keymanager(self, address, token):          """ -        Initializes the keymanager. +        Initialize the keymanager. +          :param address: the address to initialize the keymanager with.          :type address: str +        :param token: the auth token for accessing webapp. +        :type token: str          """          srp_auth = self.srpauth          logger.debug('initializing keymanager...') -        try: -            self._keymanager = KeyManager( + +        if flags.OFFLINE is True: +            args = (address, "https://localhost", self._soledad) +            kwargs = { +                "ca_cert_path": "", +                "api_uri": "", +                "api_version": "", +                "uid": self._uuid, +                "gpgbinary": self._get_gpg_bin_path() +            } +        else: +            args = (                  address,                  "https://nicknym.%s:6425" % (                      self._provider_config.get_domain(),), -                self._soledad, -                #token=srp_auth.get_token(),  # TODO: enable token usage -                session_id=srp_auth.get_session_id(), -                ca_cert_path=self._provider_config.get_ca_cert_path(), -                api_uri=self._provider_config.get_api_uri(), -                api_version=self._provider_config.get_api_version(), -                uid=srp_auth.get_uid(), -                gpgbinary=self._get_gpg_bin_path()) +                self._soledad +            ) +            kwargs = { +                "token": token, +                "ca_cert_path": self._provider_config.get_ca_cert_path(), +                "api_uri": self._provider_config.get_api_uri(), +                "api_version": self._provider_config.get_api_version(), +                "uid": srp_auth.get_uuid(), +                "gpgbinary": self._get_gpg_bin_path() +            } +        try: +            self._keymanager = KeyManager(*args, **kwargs) +        except KeyNotFound: +            logger.debug('key for %s not found.' % address)          except Exception as exc:              logger.exception(exc)              raise -        logger.debug('sending key to server...') - -        # make sure key is in server -        try: -            self._keymanager.send_key(openpgp.OpenPGPKey) -        except Exception as exc: -            logger.error("Error sending key to server.") -            logger.exception(exc) -            # but we do not raise +        if flags.OFFLINE is False: +            # make sure key is in server +            logger.debug('Trying to send key to server...') +            try: +                self._keymanager.send_key(openpgp.OpenPGPKey) +            except KeyNotFound: +                logger.debug('No key found for %s, will generate soon.' +                             % address) +            except Exception as exc: +                logger.error("Error sending key to server.") +                logger.exception(exc) +                # but we do not raise      def _gen_key(self, _):          """ @@ -401,8 +586,8 @@ class SoledadBootstrapper(AbstractBootstrapper):          leap_assert(self._soledad is not None,                      "We need a non-null soledad to generate keys") -        address = "%s@%s" % (self._user, self._provider_config.get_domain()) -        self._init_keymanager(address) +        address = make_address( +            self._user, self._provider_config.get_domain())          logger.debug("Retrieving key for %s" % (address,))          try: @@ -462,4 +647,4 @@ class SoledadBootstrapper(AbstractBootstrapper):              (self._gen_key, self.gen_key)          ] -        self.addCallbackChain(cb_chain) +        return self.addCallbackChain(cb_chain) diff --git a/src/leap/bitmask/util/__init__.py b/src/leap/bitmask/util/__init__.py index b58e6e3b..2b2cd874 100644 --- a/src/leap/bitmask/util/__init__.py +++ b/src/leap/bitmask/util/__init__.py @@ -18,19 +18,28 @@  Some small and handy functions.  """  import datetime +import itertools  import os  from leap.bitmask.config import flags  from leap.common.config import get_path_prefix as common_get_path_prefix +# functional goodies for a healthier life: +# We'll give your money back if it does not alleviate the eye strain, at least. -def get_path_prefix(): -    return common_get_path_prefix(flags.STANDALONE) + +# levelname length == 8, since 'CRITICAL' is the longest +LOG_FORMAT = ('%(asctime)s - %(levelname)-8s - ' +              'L#%(lineno)-4s : %(name)s:%(funcName)s() - %(message)s')  def first(things):      """      Return the head of a collection. + +    :param things: a sequence to extract the head from. +    :type things: sequence +    :return: object, or None      """      try:          return things[0] @@ -38,6 +47,23 @@ def first(things):          return None +def flatten(things): +    """ +    Return a generator iterating through a flattened sequence. + +    :param things: a nested sequence, eg, a list of lists. +    :type things: sequence +    :rtype: generator +    """ +    return itertools.chain(*things) + + +# leap repetitive chores + +def get_path_prefix(): +    return common_get_path_prefix(flags.STANDALONE) + +  def get_modification_ts(path):      """      Gets modification time of a file. @@ -76,3 +102,16 @@ def is_empty_file(path):      Returns True if the file at path is empty.      """      return os.stat(path).st_size is 0 + + +def make_address(user, provider): +    """ +    Return a full identifier for an user, as a email-like +    identifier. + +    :param user: the username +    :type user: basestring +    :param provider: the provider domain +    :type provider: basestring +    """ +    return "%s@%s" % (user, provider) diff --git a/src/leap/bitmask/util/constants.py b/src/leap/bitmask/util/constants.py index e6a6bdce..e7e72cc4 100644 --- a/src/leap/bitmask/util/constants.py +++ b/src/leap/bitmask/util/constants.py @@ -17,3 +17,4 @@  SIGNUP_TIMEOUT = 5  REQUEST_TIMEOUT = 15 +PASTEBIN_API_DEV_KEY = "09563100642af6085d641f749a1922b4" diff --git a/src/leap/bitmask/util/keyring_helpers.py b/src/leap/bitmask/util/keyring_helpers.py index 4b3eb57f..ee2d7a1c 100644 --- a/src/leap/bitmask/util/keyring_helpers.py +++ b/src/leap/bitmask/util/keyring_helpers.py @@ -19,30 +19,67 @@ Keyring helpers.  """  import logging -import keyring +try: +    import keyring +    from keyring.backends.file import EncryptedKeyring, PlaintextKeyring +    OBSOLETE_KEYRINGS = [ +        EncryptedKeyring, +        PlaintextKeyring +    ] +    canuse = lambda kr: (kr is not None +                         and kr.__class__ not in OBSOLETE_KEYRINGS) + +except Exception: +    # Problems when importing keyring! It might be a problem binding to the +    # dbus socket, or stuff like that. +    keyring = None -from keyring.backends.file import EncryptedKeyring, PlaintextKeyring  logger = logging.getLogger(__name__) -OBSOLETE_KEYRINGS = [ -    EncryptedKeyring, -    PlaintextKeyring -] +def _get_keyring_with_fallback(): +    """ +    Get the default keyring, and if obsolete try to pick SecretService keyring +    if available. + +    This is a workaround for the cases in which the keyring module chooses +    an insecure keyring by default (ie, inside a virtualenv). +    """ +    if not keyring: +        return None +    kr = keyring.get_keyring() +    if not canuse(kr): +        try: +            kr_klass = keyring.backends.SecretService +            kr = kr_klass.Keyring() +        except AttributeError: +            logger.warning("Keyring cannot find SecretService Backend") +    logger.debug("Selected keyring: %s" % (kr.__class__,)) +    if not canuse(kr): +        logger.debug("Not using default keyring since it is obsolete") +    return kr  def has_keyring():      """ -    Returns whether we have an useful keyring to use. +    Return whether we have an useful keyring to use.      :rtype: bool      """ -    kr = keyring.get_keyring() -    klass = kr.__class__ -    logger.debug("Selected keyring: %s" % (klass,)) +    if not keyring: +        return False +    kr = _get_keyring_with_fallback() +    return canuse(kr) + -    canuse = kr is not None and klass not in OBSOLETE_KEYRINGS -    if not canuse: -        logger.debug("Not using this keyring since it is obsolete") -    return canuse +def get_keyring(): +    """ +    Return an usable keyring. + +    :rtype: keyringBackend or None +    """ +    if not keyring: +        return False +    kr = _get_keyring_with_fallback() +    return kr if canuse(kr) else None diff --git a/src/leap/bitmask/util/leap_argparse.py b/src/leap/bitmask/util/leap_argparse.py index e8a9fda9..88267ff8 100644 --- a/src/leap/bitmask/util/leap_argparse.py +++ b/src/leap/bitmask/util/leap_argparse.py @@ -27,30 +27,77 @@ def build_parser():      All the options for the leap arg parser      Some of these could be switched on only if debug flag is present!      """ -    epilog = "Copyright 2012-2013 The LEAP Encryption Access Project" -    parser = argparse.ArgumentParser(description=""" -Launches Bitmask""", epilog=epilog) +    parser = argparse.ArgumentParser( +        description="Launches the Bitmask client.", +        epilog="Copyright 2012-2014 The LEAP Encryption Access Project") +      parser.add_argument('-d', '--debug', action="store_true", -                        help=("Launches Bitmask in debug mode, writing debug" -                              "info to stdout")) -    if not IS_RELEASE_VERSION: -        help_text = "Bypasses the certificate check for bootstrap" -        parser.add_argument('--danger', action="store_true", help=help_text) +                        help=("Launches Bitmask in debug mode, writing debug " +                              "info to stdout.")) +    parser.add_argument('-V', '--version', action="store_true", +                        help='Displays Bitmask version and exits.') +    # files      parser.add_argument('-l', '--logfile', metavar="LOG FILE", nargs='?',                          action="store", dest="log_file", -                        #type=argparse.FileType('w'), -                        help='optional log file') +                        help='Optional log file.') +    parser.add_argument('-m', '--mail-logfile', +                        metavar="MAIL LOG FILE", nargs='?', +                        action="store", dest="mail_log_file", +                        help='Optional log file for email.') + +    # flags +    parser.add_argument('-s', '--standalone', action="store_true", +                        help='Makes Bitmask use standalone ' +                        'directories for configuration and binary ' +                        'searching.') +    parser.add_argument('-N', '--no-app-version-check', default=True, +                        action="store_false", dest="app_version_check", +                        help='Skip the app version compatibility check with ' +                        'the provider.') +    parser.add_argument('-M', '--no-api-version-check', default=True, +                        action="store_false", dest="api_version_check", +                        help='Skip the api version compatibility check with ' +                        'the provider.') + +    # openvpn options      parser.add_argument('--openvpn-verbosity', nargs='?',                          type=int,                          action="store", dest="openvpn_verb", -                        help='verbosity level for openvpn logs [1-6]') -    parser.add_argument('-s', '--standalone', action="store_true", -                        help='Makes Bitmask use standalone' -                        'directories for configuration and binary' -                        'searching') -    parser.add_argument('-V', '--version', action="store_true", -                        help='Displays Bitmask version and exits') +                        help='Verbosity level for openvpn logs [1-6]') + +    # mail stuff +    parser.add_argument('-o', '--offline', action="store_true", +                        help='Starts Bitmask in offline mode: will not ' +                             'try to sync with remote replicas for email.') + +    parser.add_argument('--acct', metavar="user@provider", +                        nargs='?', +                        action="store", dest="acct", +                        help='Manipulate mailboxes for this account') +    parser.add_argument('-r', '--repair-mailboxes', default=False, +                        action="store_true", dest="repair", +                        help='Repair mailboxes for a given account. ' +                             'Use when upgrading versions after a schema ' +                             'change. Use with --acct') +    parser.add_argument('--import-maildir', metavar="/path/to/Maildir", +                        nargs='?', +                        action="store", dest="import_maildir", +                        help='Import the given maildir. Use with the ' +                             '--to-mbox flag to import to folders other ' +                             'than INBOX. Use with --acct') + +    if not IS_RELEASE_VERSION: +        help_text = ("Bypasses the certificate check during provider " +                     "bootstraping, for debugging development servers. " +                     "Use at your own risk!") +        parser.add_argument('--danger', action="store_true", help=help_text) + +    # optional cert file used to check domains with self signed certs. +    parser.add_argument('--ca-cert-file', metavar="/path/to/cacert.pem", +                        nargs='?', action="store", dest="ca_cert_file", +                        help='Uses the given cert file to verify ' +                             'against domains.')      # Not in use, we might want to reintroduce them.      #parser.add_argument('-i', '--no-provider-checks', diff --git a/src/leap/bitmask/util/leap_log_handler.py b/src/leap/bitmask/util/leap_log_handler.py index 1ab62331..807e53d4 100644 --- a/src/leap/bitmask/util/leap_log_handler.py +++ b/src/leap/bitmask/util/leap_log_handler.py @@ -21,6 +21,8 @@ import logging  from PySide import QtCore +from leap.bitmask.util import LOG_FORMAT +  class LogHandler(logging.Handler):      """ @@ -52,10 +54,7 @@ class LogHandler(logging.Handler):          :param logging_level: the debug level to define the color.          :type logging_level: str.          """ -        log_format = ('%(asctime)s - %(name)s:%(funcName)s:L#%(lineno)s ' -                      '- %(levelname)s - %(message)s') -        formatter = logging.Formatter(log_format) - +        formatter = logging.Formatter(LOG_FORMAT)          return formatter      def emit(self, logRecord): diff --git a/src/leap/bitmask/util/log_silencer.py b/src/leap/bitmask/util/log_silencer.py index b9f69ad2..56b290e4 100644 --- a/src/leap/bitmask/util/log_silencer.py +++ b/src/leap/bitmask/util/log_silencer.py @@ -46,6 +46,7 @@ class SelectiveSilencerFilter(logging.Filter):      # to us.      SILENCER_RULES = (          'leap.common.events', +        'leap.common.decorators',      )      def __init__(self): diff --git a/src/leap/bitmask/util/pastebin.py b/src/leap/bitmask/util/pastebin.py new file mode 100755 index 00000000..21b8a0b7 --- /dev/null +++ b/src/leap/bitmask/util/pastebin.py @@ -0,0 +1,814 @@ +#!/usr/bin/env python
 +
 +#############################################################################
 +#    Pastebin.py - Python 3.2 Pastebin API.
 +#    Copyright (C) 2012  Ian Havelock
 +#
 +#    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/>.
 +#
 +
 +#############################################################################
 +
 +# This software is a derivative work of:
 +# http://winappdbg.sourceforge.net/blog/pastebin.py
 +
 +#############################################################################
 +
 +
 +__ALL__ = ['delete_paste', 'user_details', 'trending', 'pastes_by_user',
 +           'generate_user_key', 'legacy_paste', 'paste', 'Pastebin',
 +           'PastebinError']
 +
 +import urllib
 +
 +
 +class PastebinError(RuntimeError):
 +    """Pastebin API error.
 +
 +    The error message returned by the web application is stored as the Python
 +    exception message."""
 +
 +
 +class PastebinAPI(object):
 +    """Pastebin API interaction object.
 +
 +    Public functions:
 +
 +    paste -- Pastes a user-specified file or string using the new API-key POST
 +    method.
 +
 +    legacy_paste -- Pastes a user-specified file or string using the old
 +    anonymous POST method.
 +
 +    generate_user_key -- Generates a session-key that is required for other
 +    functions.
 +
 +    pastes_by_user -- Returns all public pastes submitted by the specified
 +    login credentials.
 +
 +    trending -- Returns the top trending paste.
 +
 +    user_details -- Returns details about the user for the specified API user
 +    key.
 +
 +    delete_paste -- Adds two numbers together and returns the result."""
 +
 +    # String to determine bad API requests
 +    _bad_request = 'Bad API request'
 +
 +    # Base domain name
 +    _base_domain = 'pastebin.com'
 +
 +    # Valid Pastebin URLs begin with this string (kinda obvious)
 +    _prefix_url = 'http://%s/' % _base_domain
 +
 +    # Valid Pastebin URLs with a custom subdomain begin with this string
 +    _subdomain_url = 'http://%%s.%s/' % _base_domain
 +
 +    # URL to the LEGACY POST API
 +    _legacy_api_url = 'http://%s/api_public.php' % _base_domain
 +
 +    # URL to the POST API
 +    _api_url = 'http://%s/api/api_post.php' % _base_domain
 +
 +    # URL to the login POST API
 +    _api_login_url = 'http://%s/api/api_login.php' % _base_domain
 +
 +    # Valid paste_expire_date values: Never, 10 minutes, 1 Hour, 1 Day, 1 Month
 +    paste_expire_date = ('N', '10M', '1H', '1D', '1M')
 +
 +    # Valid paste_expire_date values (0 = public, 1 = unlisted, 2 = private)
 +    paste_private = ('public', 'unlisted', 'private')
 +
 +    # Valid parse_format values
 +    paste_format = (
 +        '4cs',              # 4CS
 +        '6502acme',         # 6502 ACME Cross Assembler
 +        '6502kickass',      # 6502 Kick Assembler
 +        '6502tasm',         # 6502 TASM/64TASS
 +        'abap',             # ABAP
 +        'actionscript',     # ActionScript
 +        'actionscript3',    # ActionScript 3
 +        'ada',              # Ada
 +        'algol68',          # ALGOL 68
 +        'apache',           # Apache Log
 +        'applescript',      # AppleScript
 +        'apt_sources',      # APT Sources
 +        'asm',              # ASM (NASM)
 +        'asp',              # ASP
 +        'autoconf',         # autoconf
 +        'autohotkey',       # Autohotkey
 +        'autoit',           # AutoIt
 +        'avisynth',         # Avisynth
 +        'awk',              # Awk
 +        'bascomavr',        # BASCOM AVR
 +        'bash',             # Bash
 +        'basic4gl',         # Basic4GL
 +        'bibtex',           # BibTeX
 +        'blitzbasic',       # Blitz Basic
 +        'bnf',              # BNF
 +        'boo',              # BOO
 +        'bf',               # BrainFuck
 +        'c',                # C
 +        'c_mac',            # C for Macs
 +        'cil',              # C Intermediate Language
 +        'csharp',           # C#
 +        'cpp',              # C++
 +        'cpp-qt',           # C++ (with QT extensions)
 +        'c_loadrunner',     # C: Loadrunner
 +        'caddcl',           # CAD DCL
 +        'cadlisp',          # CAD Lisp
 +        'cfdg',             # CFDG
 +        'chaiscript',       # ChaiScript
 +        'clojure',          # Clojure
 +        'klonec',           # Clone C
 +        'klonecpp',         # Clone C++
 +        'cmake',            # CMake
 +        'cobol',            # COBOL
 +        'coffeescript',     # CoffeeScript
 +        'cfm',              # ColdFusion
 +        'css',              # CSS
 +        'cuesheet',         # Cuesheet
 +        'd',                # D
 +        'dcs',              # DCS
 +        'delphi',           # Delphi
 +        'oxygene',          # Delphi Prism (Oxygene)
 +        'diff',             # Diff
 +        'div',              # DIV
 +        'dos',              # DOS
 +        'dot',              # DOT
 +        'e',                # E
 +        'ecmascript',       # ECMAScript
 +        'eiffel',           # Eiffel
 +        'email',            # Email
 +        'epc',              # EPC
 +        'erlang',           # Erlang
 +        'fsharp',           # F#
 +        'falcon',           # Falcon
 +        'fo',               # FO Language
 +        'f1',               # Formula One
 +        'fortran',          # Fortran
 +        'freebasic',        # FreeBasic
 +        'freeswitch',       # FreeSWITCH
 +        'gambas',           # GAMBAS
 +        'gml',              # Game Maker
 +        'gdb',              # GDB
 +        'genero',           # Genero
 +        'genie',            # Genie
 +        'gettext',          # GetText
 +        'go',               # Go
 +        'groovy',           # Groovy
 +        'gwbasic',          # GwBasic
 +        'haskell',          # Haskell
 +        'hicest',           # HicEst
 +        'hq9plus',          # HQ9 Plus
 +        'html4strict',      # HTML
 +        'html5',            # HTML 5
 +        'icon',             # Icon
 +        'idl',              # IDL
 +        'ini',              # INI file
 +        'inno',             # Inno Script
 +        'intercal',         # INTERCAL
 +        'io',               # IO
 +        'j',                # J
 +        'java',             # Java
 +        'java5',            # Java 5
 +        'javascript',       # JavaScript
 +        'jquery',           # jQuery
 +        'kixtart',          # KiXtart
 +        'latex',            # Latex
 +        'lb',               # Liberty BASIC
 +        'lsl2',             # Linden Scripting
 +        'lisp',             # Lisp
 +        'llvm',             # LLVM
 +        'locobasic',        # Loco Basic
 +        'logtalk',          # Logtalk
 +        'lolcode',          # LOL Code
 +        'lotusformulas',    # Lotus Formulas
 +        'lotusscript',      # Lotus Script
 +        'lscript',          # LScript
 +        'lua',              # Lua
 +        'm68k',             # M68000 Assembler
 +        'magiksf',          # MagikSF
 +        'make',             # Make
 +        'mapbasic',         # MapBasic
 +        'matlab',           # MatLab
 +        'mirc',             # mIRC
 +        'mmix',             # MIX Assembler
 +        'modula2',          # Modula 2
 +        'modula3',          # Modula 3
 +        '68000devpac',      # Motorola 68000 HiSoft Dev
 +        'mpasm',            # MPASM
 +        'mxml',             # MXML
 +        'mysql',            # MySQL
 +        'newlisp',          # newLISP
 +        'text',             # None
 +        'nsis',             # NullSoft Installer
 +        'oberon2',          # Oberon 2
 +        'objeck',           # Objeck Programming Langua
 +        'objc',             # Objective C
 +        'ocaml-brief',      # OCalm Brief
 +        'ocaml',            # OCaml
 +        'pf',               # OpenBSD PACKET FILTER
 +        'glsl',             # OpenGL Shading
 +        'oobas',            # Openoffice BASIC
 +        'oracle11',         # Oracle 11
 +        'oracle8',          # Oracle 8
 +        'oz',               # Oz
 +        'pascal',           # Pascal
 +        'pawn',             # PAWN
 +        'pcre',             # PCRE
 +        'per',              # Per
 +        'perl',             # Perl
 +        'perl6',            # Perl 6
 +        'php',              # PHP
 +        'php-brief',        # PHP Brief
 +        'pic16',            # Pic 16
 +        'pike',             # Pike
 +        'pixelbender',      # Pixel Bender
 +        'plsql',            # PL/SQL
 +        'postgresql',       # PostgreSQL
 +        'povray',           # POV-Ray
 +        'powershell',       # Power Shell
 +        'powerbuilder',     # PowerBuilder
 +        'proftpd',          # ProFTPd
 +        'progress',         # Progress
 +        'prolog',           # Prolog
 +        'properties',       # Properties
 +        'providex',         # ProvideX
 +        'purebasic',        # PureBasic
 +        'pycon',            # PyCon
 +        'python',           # Python
 +        'q',                # q/kdb+
 +        'qbasic',           # QBasic
 +        'rsplus',           # R
 +        'rails',            # Rails
 +        'rebol',            # REBOL
 +        'reg',              # REG
 +        'robots',           # Robots
 +        'rpmspec',          # RPM Spec
 +        'ruby',             # Ruby
 +        'gnuplot',          # Ruby Gnuplot
 +        'sas',              # SAS
 +        'scala',            # Scala
 +        'scheme',           # Scheme
 +        'scilab',           # Scilab
 +        'sdlbasic',         # SdlBasic
 +        'smalltalk',        # Smalltalk
 +        'smarty',           # Smarty
 +        'sql',              # SQL
 +        'systemverilog',    # SystemVerilog
 +        'tsql',             # T-SQL
 +        'tcl',              # TCL
 +        'teraterm',         # Tera Term
 +        'thinbasic',        # thinBasic
 +        'typoscript',       # TypoScript
 +        'unicon',           # Unicon
 +        'uscript',          # UnrealScript
 +        'vala',             # Vala
 +        'vbnet',            # VB.NET
 +        'verilog',          # VeriLog
 +        'vhdl',             # VHDL
 +        'vim',              # VIM
 +        'visualprolog',     # Visual Pro Log
 +        'vb',               # VisualBasic
 +        'visualfoxpro',     # VisualFoxPro
 +        'whitespace',       # WhiteSpace
 +        'whois',            # WHOIS
 +        'winbatch',         # Winbatch
 +        'xbasic',           # XBasic
 +        'xml',              # XML
 +        'xorg_conf',        # Xorg Config
 +        'xpp',              # XPP
 +        'yaml',             # YAML
 +        'z80',              # Z80 Assembler
 +        'zxbasic',          # ZXBasic
 +    )
 +
 +    def __init__(self):
 +        pass
 +
 +    def delete_paste(self, api_dev_key, api_user_key, api_paste_key):
 +        """Delete the paste specified by the api_paste_key.
 +
 +        Usage Example::
 +        from pastebin import PastebinAPI
 +        x = PastebinAPI()
 +        paste_to_delete = x.delete_paste(
 +            '453a994e0e2f1efae07f8759e59e075b',
 +            'c57a18e6c0ae228cd4bd16fe36da381a',
 +            'WkgcTFtv')
 +        print paste_to_delete
 +        Paste Removed
 +
 +
 +        @type   api_dev_key: string
 +        @param  api_dev_key: The API Developer Key of a registered
 +                             U{http://pastebin.com} account.
 +
 +        @type   api_user_key: string
 +        @param  api_user_key: The API User Key of a U{http://pastebin.com}
 +                              registered user.
 +
 +        @type   api_paste_key: string
 +        @param  api_paste_key: The Paste Key of the paste to be deleted
 +                               (string after final / in
 +                               U{http://pastebin.com} URL).
 +
 +        @rtype: string
 +        @returns: A successful deletion returns 'Paste Removed'.
 +        """
 +
 +        # Valid api developer key
 +        argv = {'api_dev_key': str(api_dev_key)}
 +
 +        # Requires pre-registered account
 +        if api_user_key is not None:
 +            argv['api_user_key'] = str(api_user_key)
 +
 +        # Key of the paste to be deleted.
 +        if api_paste_key is not None:
 +            argv['api_paste_key'] = str(api_paste_key)
 +
 +        # Valid API option - 'user_details' in this instance
 +        argv['api_option'] = str('delete')
 +
 +        # lets try to read the URL that we've just built.
 +        request_string = urllib.urlopen(self._api_url, urllib.urlencode(argv))
 +        response = request_string.read()
 +
 +        return response
 +
 +    def user_details(self, api_dev_key, api_user_key):
 +        """Return user details of the user specified by the api_user_key.
 +
 +
 +        Usage Example::
 +            from pastebin import PastebinAPI
 +            x = PastebinAPI()
 +            details = x.user_details('453a994e0e2f1efae07f8759e59e075b', 'c57a18e6c0ae228cd4bd16fe36da381a')
 +            print details
 +            <user>
 +            <user_name>MonkeyPuzzle</user_name>
 +            <user_format_short>python</user_format_short>
 +            <user_expiration>N</user_expiration>
 +            <user_avatar_url>http://pastebin.com/i/guest.gif</user_avatar_url>
 +            <user_private>0</user_private>
 +            <user_website></user_website>
 +            <user_email>user@email.com</user_email>
 +            <user_location></user_location>
 +            <user_account_type>0</user_account_type>
 +            </user>
 +
 +
 +        @type   api_dev_key: string
 +        @param  api_dev_key: The API Developer Key of a registered
 +                             U{http://pastebin.com} account.
 +
 +        @type   api_user_key: string
 +        @param  api_user_key: The API User Key of a U{http://pastebin.com}
 +                              registered user.
 +
 +        @rtype: string
 +        @returns: Returns an XML string containing user information.
 +        """
 +
 +        # Valid api developer key
 +        argv = {'api_dev_key': str(api_dev_key)}
 +
 +        # Requires pre-registered account to generate an api_user_key
 +        # (see generate_user_key)
 +        if api_user_key is not None:
 +            argv['api_user_key'] = str(api_user_key)
 +
 +        # Valid API option - 'user_details' in this instance
 +        argv['api_option'] = str('userdetails')
 +
 +        # lets try to read the URL that we've just built.
 +        request_string = urllib.urlopen(self._api_url, urllib.urlencode(argv))
 +        response = request_string.read()
 +
 +        # do some basic error checking here so we can gracefully handle any
 +        # errors we are likely to encounter
 +        if response.startswith(self._bad_request):
 +            raise PastebinError(response)
 +
 +        elif not response.startswith('<user>'):
 +            raise PastebinError(response)
 +
 +        return response
 +
 +    def trending(self, api_dev_key):
 +        """Returns the top trending paste details.
 +
 +
 +        Usage Example::
 +            from pastebin import PastebinAPI
 +            x = PastebinAPI()
 +            details = x.trending('453a994e0e2f1efae07f8759e59e075b')
 +            print details
 +            <paste>
 +            <paste_key>jjMRFDH6</paste_key>
 +            <paste_date>1333230838</paste_date>
 +            <paste_title></paste_title>
 +            <paste_size>6416</paste_size>
 +            <paste_expire_date>0</paste_expire_date>
 +            <paste_private>0</paste_private>
 +            <paste_format_long>None</paste_format_long>
 +            <paste_format_short>text</paste_format_short>
 +            <paste_url>http://pastebin.com/jjMRFDH6</paste_url>
 +            <paste_hits>6384</paste_hits>
 +            </paste>
 +
 +        Note: Returns multiple trending pastes, not just 1.
 +
 +
 +        @type   api_dev_key: string
 +        @param  api_dev_key: The API Developer Key of a registered
 +                             U{http://pastebin.com} account.
 +
 +
 +        @rtype:  string
 +        @return: Returns the string (XML formatted) containing the top
 +                 trending pastes.
 +        """
 +
 +        # Valid api developer key
 +        argv = {'api_dev_key': str(api_dev_key), 'api_option': str('trends')}
 +
 +        # Valid API option - 'trends' is returns trending pastes
 +
 +        # lets try to read the URL that we've just built.
 +        request_string = urllib.urlopen(self._api_url, urllib.urlencode(argv))
 +        response = request_string.read()
 +
 +        # do some basic error checking here so we can gracefully handle any
 +        # errors we are likely to encounter
 +        if response.startswith(self._bad_request):
 +            raise PastebinError(response)
 +
 +        elif not response.startswith('<paste>'):
 +            raise PastebinError(response)
 +
 +        return response
 +
 +    def pastes_by_user(self, api_dev_key, api_user_key, results_limit=None):
 +        """Returns all pastes for the provided api_user_key.
 +
 +
 +        Usage Example::
 +            from pastebin import PastebinAPI
 +            x = PastebinAPI()
 +            details = x.user_details('453a994e0e2f1efae07f8759e59e075b',
 +                                     'c57a18e6c0ae228cd4bd16fe36da381a',
 +                                     100)
 +            print details
 +            <paste>
 +            <paste_key>DLiSspYT</paste_key>
 +            <paste_date>1332714730</paste_date>
 +            <paste_title>Pastebin.py - Python 3.2 Pastebin.com API</paste_title>
 +            <paste_size>25300</paste_size>
 +            <paste_expire_date>0</paste_expire_date>
 +            <paste_private>0</paste_private>
 +            <paste_format_long>Python</paste_format_long>
 +            <paste_format_short>python</paste_format_short>
 +            <paste_url>http://pastebin.com/DLiSspYT</paste_url>
 +            <paste_hits>70</paste_hits>
 +            </paste>
 +
 +        Note: Returns multiple pastes, not just 1.
 +
 +
 +        @type   api_dev_key: string
 +        @param  api_dev_key: The API Developer Key of a registered
 +                             U{http://pastebin.com} account.
 +
 +        @type   api_user_key: string
 +        @param  api_user_key: The API User Key of a U{http://pastebin.com}
 +                              registered user.
 +
 +        @type   results_limit: number
 +        @param  results_limit: The number of pastes to return between 1 - 1000.
 +
 +        @rtype: string
 +        @returns: Returns an XML string containing number of specified pastes
 +                  by user.
 +        """
 +
 +        # Valid api developer key
 +        argv = {'api_dev_key': str(api_dev_key)}
 +
 +        # Requires pre-registered account
 +        if api_user_key is not None:
 +            argv['api_user_key'] = str(api_user_key)
 +
 +        # Number of results to return - between 1 & 1000, default = 50
 +        if results_limit is None:
 +            argv['api_results_limit'] = 50
 +
 +        if results_limit is not None:
 +            if results_limit < 1:
 +                argv['api_results_limit'] = 50
 +            elif results_limit > 1000:
 +                argv['api_results_limit'] = 1000
 +            else:
 +                argv['api_results_limit'] = int(results_limit)
 +
 +        # Valid API option - 'paste' is default for new paste
 +        argv['api_option'] = str('list')
 +
 +        # lets try to read the URL that we've just built.
 +        request_string = urllib.urlopen(self._api_url, urllib.urlencode(argv))
 +        response = request_string.read()
 +
 +        # do some basic error checking here so we can gracefully handle any
 +        # errors we are likely to encounter
 +        if response.startswith(self._bad_request):
 +            raise PastebinError(response)
 +
 +        elif not response.startswith('<paste>'):
 +            raise PastebinError(response)
 +
 +        return response
 +
 +    def generate_user_key(self, api_dev_key, username, password):
 +        """Generate a user session key - needed for other functions.
 +
 +
 +        Usage Example::
 +            from pastebin import PastebinAPI
 +            x = PastebinAPI()
 +            my_key = x.generate_user_key(
 +                '453a994e0e2f1efae07f8759e59e075b',
 +                'MonkeyPuzzle',
 +                '12345678')
 +            print my_key
 +            c57a18e6c0ae228cd4bd16fe36da381a
 +
 +
 +        @type   api_dev_key: string
 +        @param  api_dev_key: The API Developer Key of a registered
 +                             U{http://pastebin.com} account.
 +
 +        @type   username: string
 +        @param  username: The username of a registered U{http://pastebin.com}
 +                          account.
 +
 +        @type   password: string
 +        @param  password: The password of a registered U{http://pastebin.com}
 +                          account.
 +
 +        @rtype: string
 +        @returns: Session key (api_user_key) to allow authenticated
 +                  interaction to the API.
 +
 +        """
 +        # Valid api developer key
 +        argv = {'api_dev_key': str(api_dev_key)}
 +
 +        # Requires pre-registered pastebin account
 +        if username is not None:
 +            argv['api_user_name'] = str(username)
 +
 +        # Requires pre-registered pastebin account
 +        if password is not None:
 +            argv['api_user_password'] = str(password)
 +
 +        # lets try to read the URL that we've just built.
 +        data = urllib.urlencode(argv)
 +        request_string = urllib.urlopen(self._api_login_url, data)
 +        response = request_string.read()
 +
 +        # do some basic error checking here so we can gracefully handle
 +        # any errors we are likely to encounter
 +        if response.startswith(self._bad_request):
 +            raise PastebinError(response)
 +
 +        return response
 +
 +    def paste(self, api_dev_key, api_paste_code,
 +              api_user_key=None, paste_name=None, paste_format=None,
 +              paste_private=None, paste_expire_date=None):
 +
 +        """Submit a code snippet to Pastebin using the new API.
 +
 +
 +        Usage Example::
 +            from pastebin import PastebinAPI
 +            x = PastebinAPI()
 +            url = x.paste(
 +                '453a994e0e2f1efae07f8759e59e075b' ,
 +                'Snippet of code to paste goes here',
 +                paste_name = 'title of paste',
 +                api_user_key = 'c57a18e6c0ae228cd4bd16fe36da381a',
 +                paste_format = 'python',
 +                paste_private = 'unlisted',
 +                paste_expire_date = '10M')
 +            print url
 +            http://pastebin.com/tawPUgqY
 +
 +
 +        @type   api_dev_key: string
 +        @param  api_dev_key: The API Developer Key of a registered
 +                             U{http://pastebin.com} account.
 +
 +        @type   api_paste_code: string
 +        @param  api_paste_code: The file or string to paste to body of the
 +                                U{http://pastebin.com} paste.
 +
 +        @type   api_user_key: string
 +        @param  api_user_key: The API User Key of a U{http://pastebin.com}
 +                              registered user.
 +                              If none specified, paste is made as a guest.
 +
 +        @type   paste_name: string
 +        @param  paste_name: (Optional) Title of the paste.
 +            Default is to paste anonymously.
 +
 +        @type  paste_format: string
 +        @param paste_format: (Optional) Programming language of the code being
 +            pasted. This enables syntax highlighting when reading the code in
 +            U{http://pastebin.com}. Default is no syntax highlighting (text is
 +            just text and not source code).
 +
 +        @type  paste_private: string
 +        @param paste_private: (Optional) C{'public'} if the paste is public
 +            (visible by everyone), C{'unlisted'} if it's public but not
 +            searchable. C{'private'} if the paste is private and not
 +            searchable or indexed.
 +            The Pastebin FAQ (U{http://pastebin.com/faq}) claims
 +            private pastes are not indexed by search engines (aka Google).
 +
 +        @type  paste_expire_date: str
 +        @param paste_expire_date: (Optional) Expiration date for the paste.
 +            Once past this date the paste is deleted automatically. Valid
 +            values are found in the L{PastebinAPI.paste_expire_date} class
 +            member.
 +            If not provided, the paste never expires.
 +
 +        @rtype:  string
 +        @return: Returns the URL to the newly created paste.
 +        """
 +
 +        # Valid api developer key
 +        argv = {'api_dev_key': str(api_dev_key)}
 +
 +        # Code snippet to submit
 +        if api_paste_code is not None:
 +            argv['api_paste_code'] = str(api_paste_code)
 +
 +        # Valid API option - 'paste' is default for new paste
 +        argv['api_option'] = str('paste')
 +
 +        # API User Key
 +        if api_user_key is not None:
 +            argv['api_user_key'] = str(api_user_key)
 +        elif api_user_key is None:
 +            argv['api_user_key'] = str('')
 +
 +        # Name of the poster
 +        if paste_name is not None:
 +            argv['api_paste_name'] = str(paste_name)
 +
 +        # Syntax highlighting
 +        if paste_format is not None:
 +            paste_format = str(paste_format).strip().lower()
 +            argv['api_paste_format'] = paste_format
 +
 +        # Is the snippet private?
 +        if paste_private is not None:
 +            if paste_private == 'public':
 +                argv['api_paste_private'] = int(0)
 +            elif paste_private == 'unlisted':
 +                argv['api_paste_private'] = int(1)
 +            elif paste_private == 'private':
 +                argv['api_paste_private'] = int(2)
 +
 +        # Expiration for the snippet
 +        if paste_expire_date is not None:
 +            paste_expire_date = str(paste_expire_date).strip().upper()
 +            argv['api_paste_expire_date'] = paste_expire_date
 +
 +        # lets try to read the URL that we've just built.
 +        request_string = urllib.urlopen(self._api_url, urllib.urlencode(argv))
 +        response = request_string.read()
 +
 +        # do some basic error checking here so we can gracefully handle any
 +        # errors we are likely to encounter
 +        if response.startswith(self._bad_request):
 +            raise PastebinError(response)
 +        elif not response.startswith(self._prefix_url):
 +            raise PastebinError(response)
 +
 +        return response
 +
 +    def legacy_paste(self, paste_code,
 +                     paste_name=None, paste_private=None,
 +                     paste_expire_date=None, paste_format=None):
 +        """Unofficial python interface to the Pastebin legacy API.
 +
 +        Unlike the official API, this one doesn't require an API key, so it's
 +        virtually anonymous.
 +
 +
 +        Usage Example::
 +            from pastebin import PastebinAPI
 +            x = PastebinAPI()
 +            url = x.legacy_paste('Snippet of code to paste goes here',
 +                                paste_name = 'title of paste',
 +                                paste_private = 'unlisted',
 +                                paste_expire_date = '10M',
 +                                paste_format = 'python')
 +            print url
 +            http://pastebin.com/tawPUgqY
 +
 +
 +        @type   paste_code: string
 +        @param  paste_code: The file or string to paste to body of the
 +                            U{http://pastebin.com} paste.
 +
 +        @type   paste_name: string
 +        @param  paste_name: (Optional) Title of the paste.
 +            Default is to paste with no title.
 +
 +        @type   paste_private: string
 +        @param  paste_private: (Optional) C{'public'} if the paste is public
 +            (visible by everyone), C{'unlisted'} if it's public but not
 +            searchable. C{'private'} if the paste is private and not
 +            searchable or indexed.
 +            The Pastebin FAQ (U{http://pastebin.com/faq}) claims
 +            private pastes are not indexed by search engines (aka Google).
 +
 +        @type   paste_expire_date: string
 +        @param  paste_expire_date: (Optional) Expiration date for the paste.
 +            Once past this date the paste is deleted automatically. Valid
 +            values are found in the L{PastebinAPI.paste_expire_date} class
 +            member.
 +            If not provided, the paste never expires.
 +
 +        @type   paste_format: string
 +        @param  paste_format: (Optional) Programming language of the code being
 +            pasted. This enables syntax highlighting when reading the code in
 +            U{http://pastebin.com}. Default is no syntax highlighting (text is
 +            just text and not source code).
 +
 +        @rtype:  string
 +        @return: Returns the URL to the newly created paste.
 +        """
 +
 +        # Code snippet to submit
 +        argv = {'paste_code': str(paste_code)}
 +
 +        # Name of the poster
 +        if paste_name is not None:
 +            argv['paste_name'] = str(paste_name)
 +
 +        # Is the snippet private?
 +        if paste_private is not None:
 +            argv['paste_private'] = int(bool(int(paste_private)))
 +
 +        # Expiration for the snippet
 +        if paste_expire_date is not None:
 +            paste_expire_date = str(paste_expire_date).strip().upper()
 +            argv['paste_expire_date'] = paste_expire_date
 +
 +        # Syntax highlighting
 +        if paste_format is not None:
 +            paste_format = str(paste_format).strip().lower()
 +            argv['paste_format'] = paste_format
 +
 +        # lets try to read the URL that we've just built.
 +        data = urllib.urlencode(argv)
 +        request_string = urllib.urlopen(self._legacy_api_url, data)
 +        response = request_string.read()
 +
 +        # do some basic error checking here so we can gracefully handle any
 +        # errors we are likely to encounter
 +        if response.startswith(self._bad_request):
 +            raise PastebinError(response)
 +        elif not response.startswith(self._prefix_url):
 +            raise PastebinError(response)
 +
 +        return response
 +
 +
 +######################################################
 +
 +delete_paste = PastebinAPI.delete_paste
 +user_details = PastebinAPI.user_details
 +trending = PastebinAPI.trending
 +pastes_by_user = PastebinAPI.pastes_by_user
 +generate_user_key = PastebinAPI.generate_user_key
 +legacy_paste = PastebinAPI.legacy_paste
 +paste = PastebinAPI.paste
 | 
