Merge branch 'develop' of https://github.com/micahflee/onionshare into develop

This commit is contained in:
irykoon 2018-07-12 06:31:49 +00:00
commit 09ce97147a
No known key found for this signature in database
GPG Key ID: A14B4DBC6DCDD53C
63 changed files with 2308 additions and 1449 deletions

View File

@ -85,7 +85,7 @@ pip3 install -r install\requirements-windows.txt
Download and install pywin32 (build 221, x86, for python 3.6) from https://sourceforge.net/projects/pywin32/files/pywin32/Build%20221/. I downloaded `pywin32-221.win32-py3.6.exe`.
Download and install Qt5 from https://www.qt.io/download-open-source/. I downloaded `qt-unified-windows-x86-3.0.2-online.exe`. There's no need to login to a Qt account during installation. Make sure you install the latest Qt 5.x. I installed Qt 5.10.0.
Download and install Qt5 from https://www.qt.io/download-open-source/. I downloaded `qt-unified-windows-x86-3.0.4-online.exe`. There's no need to login to a Qt account during installation. Make sure you install the latest Qt 5.x. I installed Qt 5.11.0. You only need to install the `MSVC 2015 32-bit` component, as well as all of the the `Qt` components, for that that version.
After that you can try both the CLI and the GUI version of OnionShare:
@ -100,7 +100,7 @@ These instructions include adding folders to the path in Windows. To do this, go
Download and install the 32-bit [Visual C++ Redistributable for Visual Studio 2015](https://www.microsoft.com/en-US/download/details.aspx?id=48145). I downloaded `vc_redist.x86.exe`.
Download and install 7-Zip from http://www.7-zip.org/download.html. I downloaded `7z1800.exe`.
Download and install 7-Zip from http://www.7-zip.org/download.html. I downloaded `7z1805.exe`.
Download and install the standalone [Windows 10 SDK](https://dev.windows.com/en-us/downloads/windows-10-sdk). Note that you may not need this if you already have Visual Studio.
@ -113,7 +113,7 @@ Add the following directories to the path:
If you want to build the installer:
* Go to http://nsis.sourceforge.net/Download and download the latest NSIS. I downloaded `nsis-3.02.1-setup.exe`.
* Go to http://nsis.sourceforge.net/Download and download the latest NSIS. I downloaded `nsis-3.03-setup.exe`.
* Add `C:\Program Files (x86)\NSIS` to the path.
If you want to sign binaries with Authenticode:

View File

@ -1,5 +1,10 @@
# OnionShare Changelog
## 1.3.1
* Updated Tor to 0.2.3.10
* Windows and Mac binaries are now distributed with licenses for tor and obfs4
## 1.3
* Major UI redesign, introducing many UX improvements

View File

@ -1,7 +1,7 @@
OnionShare
(Note: Third-party licenses can be found under install/licenses/.)
Copyright © 2016
Micah Lee <micah@micahflee.com>
OnionShare
Copyright © 2018 Micah Lee <micah@micahflee.com>
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007

View File

@ -4,7 +4,8 @@ include BUILD.md
include share/*
include share/images/*
include share/locale/*
include share/html/*
include share/templates/*
include share/static/*
include install/onionshare.desktop
include install/onionshare.appdata.xml
include install/onionshare80.xpm

View File

@ -3,7 +3,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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

View File

@ -3,7 +3,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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

View File

@ -8,10 +8,10 @@ REM download tor
python install\get-tor-windows.py
REM sign onionshare-gui.exe
signtool.exe sign /v /d "OnionShare" /a /tr http://time.certum.pl/ /fd sha256 dist\onionshare\onionshare-gui.exe
signtool.exe sign /v /d "OnionShare" /a /tr http://time.certum.pl/ dist\onionshare\onionshare-gui.exe
REM build an installer, dist\onionshare-setup.exe
makensis.exe install\onionshare.nsi
REM sign onionshare-setup.exe
signtool.exe sign /v /d "OnionShare" /a /tr http://time.certum.pl/ /fd sha256 dist\onionshare-setup.exe
signtool.exe sign /v /d "OnionShare" /a /tr http://time.certum.pl/ dist\onionshare-setup.exe

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -24,13 +24,20 @@ In order to avoid a Mac gnupg dependency, I manually verify the signature
and hard-code the sha256 hash.
"""
import inspect, os, sys, hashlib, zipfile, io, shutil, subprocess
import urllib.request
import inspect
import os
import sys
import hashlib
import zipfile
import io
import shutil
import subprocess
import requests
def main():
dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.5/TorBrowser-7.5-osx64_en-US.dmg'
dmg_filename = 'TorBrowser-7.5-osx64_en-US.dmg'
expected_dmg_sha256 = '43a8dc0afd0a77e42766311eb54ad9fc8714f67fcd2d3582a3bcb98b22c2e629'
dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.5.5/TorBrowser-7.5.5-osx64_en-US.dmg'
dmg_filename = 'TorBrowser-7.5.5-osx64_en-US.dmg'
expected_dmg_sha256 = '2b445e4237cdd9be0e71e65f76db5d36f0d6c37532982d642803b57e388e4636'
# Build paths
root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))
@ -46,10 +53,9 @@ def main():
# Make sure the zip is downloaded
if not os.path.exists(dmg_path):
print("Downloading {}".format(dmg_url))
response = urllib.request.urlopen(dmg_url)
dmg_data = response.read()
open(dmg_path, 'wb').write(dmg_data)
dmg_sha256 = hashlib.sha256(dmg_data).hexdigest()
r = requests.get(dmg_url)
open(dmg_path, 'wb').write(r.content)
dmg_sha256 = hashlib.sha256(r.content).hexdigest()
else:
dmg_data = open(dmg_path, 'rb').read()
dmg_sha256 = hashlib.sha256(dmg_data).hexdigest()

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -24,13 +24,18 @@ In order to avoid a Windows gnupg dependency, I manually verify the signature
and hard-code the sha256 hash.
"""
import inspect, os, sys, hashlib, shutil, subprocess
import urllib.request
import inspect
import os
import sys
import hashlib
import shutil
import subprocess
import requests
def main():
exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.5/torbrowser-install-7.5_en-US.exe'
exe_filename = 'torbrowser-install-7.5_en-US.exe'
expected_exe_sha256 = '81ccb9456118cf8fa755a3eafb5c514665fc69599cdd41e9eb36baa335ebe233'
exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.5.5/torbrowser-install-7.5.5_en-US.exe'
exe_filename = 'torbrowser-install-7.5.5_en-US.exe'
expected_exe_sha256 = '992f9a6658001c3419ed3695a908eef4fb7feb1cd549389bdacbadb7f8cb08a7'
# Build paths
root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))
working_path = os.path.join(os.path.join(root_path, 'build'), 'tor')
@ -44,10 +49,9 @@ def main():
# Make sure the zip is downloaded
if not os.path.exists(exe_path):
print("Downloading {}".format(exe_url))
response = urllib.request.urlopen(exe_url)
exe_data = response.read()
open(exe_path, 'wb').write(exe_data)
exe_sha256 = hashlib.sha256(exe_data).hexdigest()
r = requests.get(exe_url)
open(exe_path, 'wb').write(r.content)
exe_sha256 = hashlib.sha256(r.content).hexdigest()
else:
exe_data = open(exe_path, 'rb').read()
exe_sha256 = hashlib.sha256(exe_data).hexdigest()

View File

@ -0,0 +1,55 @@
Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
==============================================================================
Copyright (c) 2012 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,4 +1,5 @@
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
OnionShare
Copyright © 2018 Micah Lee <micah@micahflee.com>
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007

View File

@ -0,0 +1,381 @@
This file contains the license for Tor,
a free software project to provide anonymity on the Internet.
It also lists the licenses for other components used by Tor.
For more information about Tor, see https://www.torproject.org/.
If you got this file as a part of a larger bundle,
there may be other license terms that you should be aware of.
===============================================================================
Tor is distributed under this license:
Copyright (c) 2001-2004, Roger Dingledine
Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson
Copyright (c) 2007-2017, The Tor Project, Inc.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the names of the copyright owners nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
===============================================================================
src/ext/strlcat.c and src/ext/strlcpy.c by Todd C. Miller are licensed
under the following license:
* Copyright (c) 1998 Todd C. Miller <Todd.Miller@courtesan.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
===============================================================================
src/ext/tor_queue.h is licensed under the following license:
* Copyright (c) 1991, 1993
* The Regents of the University of California. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
===============================================================================
src/ext/csiphash.c is licensed under the following license:
Copyright (c) 2013 Marek Majkowski <marek@popcount.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
===============================================================================
Trunnel is distributed under this license:
Copyright 2014 The Tor Project, Inc.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the names of the copyright owners nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
===============================================================================
src/config/geoip is licensed under the following license:
OPEN DATA LICENSE (GeoLite Country and GeoLite City databases)
Copyright (c) 2008 MaxMind, Inc. All Rights Reserved.
All advertising materials and documentation mentioning features or use of
this database must display the following acknowledgment:
"This product includes GeoLite data created by MaxMind, available from
http://maxmind.com/"
Redistribution and use with or without modification, are permitted provided
that the following conditions are met:
1. Redistributions must retain the above copyright notice, this list of
conditions and the following disclaimer in the documentation and/or other
materials provided with the distribution.
2. All advertising materials and documentation mentioning features or use of
this database must display the following acknowledgement:
"This product includes GeoLite data created by MaxMind, available from
http://maxmind.com/"
3. "MaxMind" may not be used to endorse or promote products derived from this
database without specific prior written permission.
THIS DATABASE IS PROVIDED BY MAXMIND, INC ``AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL MAXMIND BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
DATABASE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
===============================================================================
m4/pc_from_ucontext.m4 is available under the following license. Note that
it is *not* built into the Tor software.
Copyright (c) 2005, Google Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
===============================================================================
m4/pkg.m4 is available under the following license. Note that
it is *not* built into the Tor software.
pkg.m4 - Macros to locate and utilise pkg-config. -*- Autoconf -*-
serial 1 (pkg-config-0.24)
Copyright © 2004 Scott James Remnant <scott@netsplit.com>.
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 2 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, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
As a special exception to the GNU General Public License, if you
distribute this file as part of a program that contains a
configuration script generated by Autoconf, you may include it under
the same distribution terms that you use for the rest of that program.
===============================================================================
src/ext/readpassphrase.[ch] are distributed under this license:
Copyright (c) 2000-2002, 2007 Todd C. Miller <Todd.Miller@courtesan.com>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
Sponsored in part by the Defense Advanced Research Projects
Agency (DARPA) and Air Force Research Laboratory, Air Force
Materiel Command, USAF, under agreement number F39502-99-1-0512.
===============================================================================
src/ext/mulodi4.c is distributed under this license:
=========================================================================
compiler_rt License
=========================================================================
The compiler_rt library is dual licensed under both the
University of Illinois "BSD-Like" license and the MIT license.
As a user of this code you may choose to use it under either
license. As a contributor, you agree to allow your code to be
used under both.
Full text of the relevant licenses is included below.
=========================================================================
University of Illinois/NCSA
Open Source License
Copyright (c) 2009-2016 by the contributors listed in CREDITS.TXT
All rights reserved.
Developed by:
LLVM Team
University of Illinois at Urbana-Champaign
http://llvm.org
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal with the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
* Redistributions of source code must retain the above
copyright notice, this list of conditions and the following
disclaimers.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimers in the documentation and/or other materials
provided with the distribution.
* Neither the names of the LLVM Team, University of Illinois
at Urbana-Champaign, nor the names of its contributors may
be used to endorse or promote products derived from this
Software without specific prior written permission.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS WITH THE SOFTWARE.
=========================================================================
Copyright (c) 2009-2015 by the contributors listed in CREDITS.TXT
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
=========================================================================
Copyrights and Licenses for Third Party Software Distributed with LLVM:
=========================================================================
The LLVM software contains code written by third parties. Such
software will have its own individual LICENSE.TXT file in the
directory in which it appears. This file will describe the
copyrights, license, and restrictions which apply to that code.
The disclaimer of warranty in the University of Illinois Open
Source License applies to all code in the LLVM Distribution, and
nothing in any of the other licenses gives permission to use the
names of the LLVM Team or the University of Illinois to endorse
or promote products derived from this Software.
===============================================================================
If you got Tor as a static binary with OpenSSL included, then you should know:
"This product includes software developed by the OpenSSL Project
for use in the OpenSSL Toolkit (http://www.openssl.org/)"
===============================================================================

View File

@ -0,0 +1 @@
This folder contains the software licenses for 3rd-party binaries included with OnionShare.

View File

@ -6,7 +6,7 @@
!define INSTALLSIZE 66537
!define VERSIONMAJOR 1
!define VERSIONMINOR 3
!define VERSIONSTRING "1.3"
!define VERSIONSTRING "1.3.1"
RequestExecutionLevel admin
@ -39,7 +39,7 @@ ${EndIf}
!echo "Creating normal installer"
!system "makensis.exe /DINNER onionshare.nsi" = 0
!system "$%TEMP%\tempinstaller.exe" = 2
!system "signtool.exe sign /v /d $\"Uninstall OnionShare$\" /a /tr http://time.certum.pl/ /fd sha256 $%TEMP%\uninstall.exe" = 0
!system "signtool.exe sign /v /d $\"Uninstall OnionShare$\" /a /tr http://time.certum.pl/ $%TEMP%\uninstall.exe" = 0
# all done, now we can build the real installer
OutFile "..\dist\onionshare-setup.exe"
@ -162,6 +162,12 @@ Section "install"
SetOutPath "$INSTDIR\lib2to3\tests\data"
File "${BINPATH}\lib2to3\tests\data\README"
SetOutPath "$INSTDIR\licenses"
File "${BINPATH}\licenses\license-obfs4.txt"
File "${BINPATH}\licenses\license-onionshare.txt"
File "${BINPATH}\licenses\license-tor.txt"
File "${BINPATH}\licenses\readme.txt"
SetOutPath "$INSTDIR\PyQt5\Qt\bin"
File "${BINPATH}\PyQt5\Qt\bin\qt.conf"
@ -188,7 +194,6 @@ Section "install"
File "${BINPATH}\PyQt5\Qt\plugins\printsupport\windowsprintersupport.dll"
SetOutPath "$INSTDIR\share"
File "${BINPATH}\share\license.txt"
File "${BINPATH}\share\torrc_template"
File "${BINPATH}\share\torrc_template-windows"
File "${BINPATH}\share\torrc_template-obfs4"
@ -207,6 +212,8 @@ Section "install"
File "${BINPATH}\share\images\download_completed_none.png"
File "${BINPATH}\share\images\download_in_progress.png"
File "${BINPATH}\share\images\download_in_progress_none.png"
File "${BINPATH}\share\images\download_window_gray.png"
File "${BINPATH}\share\images\download_window_green.png"
File "${BINPATH}\share\images\favicon.ico"
File "${BINPATH}\share\images\file_delete.png"
File "${BINPATH}\share\images\info.png"
@ -351,6 +358,10 @@ FunctionEnd
Delete "$INSTDIR\lib2to3\tests"
Delete "$INSTDIR\lib2to3\tests\data"
Delete "$INSTDIR\lib2to3\tests\data\README"
Delete "$INSTDIR\licenses\license-obfs4.txt"
Delete "$INSTDIR\licenses\license-onionshare.txt"
Delete "$INSTDIR\licenses\license-tor.txt"
Delete "$INSTDIR\licenses\readme.txt"
Delete "$INSTDIR\mfc140u.dll"
Delete "$INSTDIR\MSVCP140.dll"
Delete "$INSTDIR\onionshare-gui.exe"
@ -393,6 +404,8 @@ FunctionEnd
Delete "$INSTDIR\share\images\download_completed_none.png"
Delete "$INSTDIR\share\images\download_in_progress.png"
Delete "$INSTDIR\share\images\download_in_progress_none.png"
Delete "$INSTDIR\share\images\download_window_gray.png"
Delete "$INSTDIR\share\images\download_window_green.png"
Delete "$INSTDIR\share\images\favicon.ico"
Delete "$INSTDIR\share\images\file_delete.png"
Delete "$INSTDIR\share\images\info.png"
@ -405,7 +418,6 @@ FunctionEnd
Delete "$INSTDIR\share\images\settings.png"
Delete "$INSTDIR\share\images\web_file.png"
Delete "$INSTDIR\share\images\web_folder.png"
Delete "$INSTDIR\share\license.txt"
Delete "$INSTDIR\share\locale\cs.json"
Delete "$INSTDIR\share\locale\de.json"
Delete "$INSTDIR\share\locale\en.json"
@ -466,6 +478,7 @@ FunctionEnd
rmDir "$INSTDIR\lib2to3\tests\data"
rmDir "$INSTDIR\lib2to3\tests"
rmDir "$INSTDIR\lib2to3"
rmDir "$INSTDIR\licenses"
rmDir "$INSTDIR\PyQt5\Qt\bin"
rmDir "$INSTDIR\PyQt5\Qt\plugins\iconengines"
rmDir "$INSTDIR\PyQt5\Qt\plugins\imageformats"

View File

@ -10,7 +10,6 @@ a = Analysis(
pathex=['.'],
binaries=None,
datas=[
('../share/license.txt', 'share'),
('../share/version.txt', 'share'),
('../share/wordlist.txt', 'share'),
('../share/torrc_template', 'share'),
@ -20,7 +19,9 @@ a = Analysis(
('../share/torrc_template-windows', 'share'),
('../share/images/*', 'share/images'),
('../share/locale/*', 'share/locale'),
('../share/html/*', 'share/html')
('../share/static/*', 'share/static'),
('../share/templates/*', 'share/templates'),
('../install/licenses/*', 'licenses')
],
hiddenimports=[],
hookspath=[],

View File

@ -8,6 +8,7 @@ pefile==2017.11.5
PyInstaller==3.3.1
PyQt5==5.9.2
PySocks==1.6.7
requests==2.19.1
sip==4.19.6
stem==1.6.0
Werkzeug==0.14.1

View File

@ -6,6 +6,7 @@ MarkupSafe==1.0
PyInstaller==3.3.1
PyQt5==5.9.2
PySocks==1.6.7
requests==2.19.1
sip==4.19.6
stem==1.6.0
Werkzeug==0.14.1

View File

@ -3,7 +3,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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

View File

@ -3,7 +3,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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

View File

@ -3,7 +3,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -20,21 +20,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import os, sys, time, argparse, threading
from . import strings, common, web
from . import strings
from .common import Common
from .web import Web
from .onion import *
from .onionshare import OnionShare
from .settings import Settings
def main(cwd=None):
"""
The main() function implements all of the logic that the command-line version of
onionshare uses.
"""
common = Common()
strings.load_strings(common)
print(strings._('version_string').format(common.get_version()))
print(strings._('version_string').format(common.version))
# OnionShare CLI in OSX needs to change current working directory (#132)
if common.get_platform() == 'Darwin':
if common.platform == 'Darwin':
if cwd:
os.chdir(cwd)
@ -44,9 +47,10 @@ def main(cwd=None):
parser.add_argument('--stay-open', action='store_true', dest='stay_open', help=strings._("help_stay_open"))
parser.add_argument('--shutdown-timeout', metavar='<int>', dest='shutdown_timeout', default=0, help=strings._("help_shutdown_timeout"))
parser.add_argument('--stealth', action='store_true', dest='stealth', help=strings._("help_stealth"))
parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug"))
parser.add_argument('--receive', action='store_true', dest='receive', help=strings._("help_receive"))
parser.add_argument('--config', metavar='config', default=False, help=strings._('help_config'))
parser.add_argument('filename', metavar='filename', nargs='+', help=strings._('help_filename'))
parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug"))
parser.add_argument('filename', metavar='filename', nargs='*', help=strings._('help_filename'))
args = parser.parse_args()
filenames = args.filename
@ -58,32 +62,55 @@ def main(cwd=None):
stay_open = bool(args.stay_open)
shutdown_timeout = int(args.shutdown_timeout)
stealth = bool(args.stealth)
receive = bool(args.receive)
config = args.config
# Debug mode?
if debug:
common.set_debug(debug)
web.debug_mode()
# Make sure filenames given if not using receiver mode
if not receive and len(filenames) == 0:
print(strings._('no_filenames'))
sys.exit()
# Validation
valid = True
for filename in filenames:
if not os.path.isfile(filename) and not os.path.isdir(filename):
print(strings._("not_a_file").format(filename))
valid = False
if not os.access(filename, os.R_OK):
print(strings._("not_a_readable_file").format(filename))
# Validate filenames
if not receive:
valid = True
for filename in filenames:
if not os.path.isfile(filename) and not os.path.isdir(filename):
print(strings._("not_a_file").format(filename))
valid = False
if not os.access(filename, os.R_OK):
print(strings._("not_a_readable_file").format(filename))
valid = False
if not valid:
sys.exit()
# Load settings
common.load_settings(config)
# Debug mode?
common.debug = debug
# In receive mode, validate downloads dir
if receive:
valid = True
if not os.path.isdir(common.settings.get('downloads_dir')):
try:
os.mkdir(common.settings.get('downloads_dir'), 0o700)
except:
print(strings._('error_cannot_create_downloads_dir').format(common.settings.get('downloads_dir')))
valid = False
if valid and not os.access(common.settings.get('downloads_dir'), os.W_OK):
print(strings._('error_downloads_dir_not_writable').format(common.settings.get('downloads_dir')))
valid = False
if not valid:
sys.exit()
settings = Settings(config)
# Create the Web object
web = Web(common, stay_open, False, receive)
# Start the Onion object
onion = Onion()
onion = Onion(common)
try:
onion.connect(settings=False, config=config)
onion.connect(custom_settings=False, config=config)
except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorNotSupported, BundledTorTimeout) as e:
sys.exit(e.args[0])
except KeyboardInterrupt:
@ -92,7 +119,7 @@ def main(cwd=None):
# Start the onionshare app
try:
app = OnionShare(onion, local_only, stay_open, shutdown_timeout)
app = OnionShare(common, onion, local_only, stay_open, shutdown_timeout)
app.set_stealth(stealth)
app.start_onion_service()
except KeyboardInterrupt:
@ -115,8 +142,7 @@ def main(cwd=None):
print('')
# Start OnionShare http service in new thread
settings.load()
t = threading.Thread(target=web.start, args=(app.port, app.stay_open, settings.get('slug')))
t = threading.Thread(target=web.start, args=(app.port, app.stay_open, common.settings.get('slug')))
t.daemon = True
t.start()
@ -129,18 +155,33 @@ def main(cwd=None):
app.shutdown_timer.start()
# Save the web slug if we are using a persistent private key
if settings.get('save_private_key'):
if not settings.get('slug'):
settings.set('slug', web.slug)
settings.save()
if common.settings.get('save_private_key'):
if not common.settings.get('slug'):
common.settings.set('slug', web.slug)
common.settings.save()
if(stealth):
print(strings._("give_this_url_stealth"))
print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug))
print(app.auth_string)
print('')
if receive:
print(strings._('receive_mode_downloads_dir').format(common.settings.get('downloads_dir')))
print('')
print(strings._('receive_mode_warning'))
print('')
if stealth:
print(strings._("give_this_url_receive_stealth"))
print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug))
print(app.auth_string)
else:
print(strings._("give_this_url_receive"))
print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug))
else:
print(strings._("give_this_url"))
print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug))
if stealth:
print(strings._("give_this_url_stealth"))
print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug))
print(app.auth_string)
else:
print(strings._("give_this_url"))
print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug))
print('')
print(strings._("ctrlc_to_stop"))

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -28,262 +28,207 @@ import sys
import tempfile
import threading
import time
import zipfile
debug = False
from .settings import Settings
def log(module, func, msg=None):
class Common(object):
"""
If debug mode is on, log error messages to stdout
The Common object is shared amongst all parts of OnionShare.
"""
global debug
if debug:
timestamp = time.strftime("%b %d %Y %X")
def __init__(self, debug=False):
self.debug = debug
final_msg = "[{}] {}.{}".format(timestamp, module, func)
if msg:
final_msg = '{}: {}'.format(final_msg, msg)
print(final_msg)
# The platform OnionShare is running on
self.platform = platform.system()
if self.platform.endswith('BSD'):
self.platform = 'BSD'
# The current version of OnionShare
with open(self.get_resource_path('version.txt')) as f:
self.version = f.read().strip()
def set_debug(new_debug):
global debug
debug = new_debug
def load_settings(self, config=None):
"""
Loading settings, optionally from a custom config json file.
"""
self.settings = Settings(self, config)
self.settings.load()
def log(self, module, func, msg=None):
"""
If debug mode is on, log error messages to stdout
"""
if self.debug:
timestamp = time.strftime("%b %d %Y %X")
def get_platform():
"""
Returns the platform OnionShare is running on.
"""
plat = platform.system()
if plat.endswith('BSD'):
plat = 'BSD'
return plat
final_msg = "[{}] {}.{}".format(timestamp, module, func)
if msg:
final_msg = '{}: {}'.format(final_msg, msg)
print(final_msg)
def get_resource_path(self, filename):
"""
Returns the absolute path of a resource, regardless of whether OnionShare is installed
systemwide, and whether regardless of platform
"""
# On Windows, and in Windows dev mode, switch slashes in incoming filename to backslackes
if self.platform == 'Windows':
filename = filename.replace('/', '\\')
def get_resource_path(filename):
"""
Returns the absolute path of a resource, regardless of whether OnionShare is installed
systemwide, and whether regardless of platform
"""
p = get_platform()
if getattr(sys, 'onionshare_dev_mode', False):
# Look for resources directory relative to python file
prefix = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))), 'share')
if not os.path.exists(prefix):
# While running tests during stdeb bdist_deb, look 3 directories up for the share folder
prefix = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(prefix)))), 'share')
# On Windows, and in Windows dev mode, switch slashes in incoming filename to backslackes
if p == 'Windows':
filename = filename.replace('/', '\\')
elif self.platform == 'BSD' or self.platform == 'Linux':
# Assume OnionShare is installed systemwide in Linux, since we're not running in dev mode
prefix = os.path.join(sys.prefix, 'share/onionshare')
if getattr(sys, 'onionshare_dev_mode', False):
# Look for resources directory relative to python file
prefix = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))), 'share')
if not os.path.exists(prefix):
# While running tests during stdeb bdist_deb, look 3 directories up for the share folder
prefix = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(prefix)))), 'share')
elif getattr(sys, 'frozen', False):
# Check if app is "frozen"
# https://pythonhosted.org/PyInstaller/#run-time-information
if self.platform == 'Darwin':
prefix = os.path.join(sys._MEIPASS, 'share')
elif self.platform == 'Windows':
prefix = os.path.join(os.path.dirname(sys.executable), 'share')
elif p == 'BSD' or p == 'Linux':
# Assume OnionShare is installed systemwide in Linux, since we're not running in dev mode
prefix = os.path.join(sys.prefix, 'share/onionshare')
return os.path.join(prefix, filename)
elif getattr(sys, 'frozen', False):
# Check if app is "frozen"
# https://pythonhosted.org/PyInstaller/#run-time-information
if p == 'Darwin':
prefix = os.path.join(sys._MEIPASS, 'share')
elif p == 'Windows':
prefix = os.path.join(os.path.dirname(sys.executable), 'share')
def get_tor_paths(self):
if self.platform == 'Linux':
tor_path = '/usr/bin/tor'
tor_geo_ip_file_path = '/usr/share/tor/geoip'
tor_geo_ipv6_file_path = '/usr/share/tor/geoip6'
obfs4proxy_file_path = '/usr/bin/obfs4proxy'
elif self.platform == 'Windows':
base_path = os.path.join(os.path.dirname(os.path.dirname(self.get_resource_path(''))), 'tor')
tor_path = os.path.join(os.path.join(base_path, 'Tor'), 'tor.exe')
obfs4proxy_file_path = os.path.join(os.path.join(base_path, 'Tor'), 'obfs4proxy.exe')
tor_geo_ip_file_path = os.path.join(os.path.join(os.path.join(base_path, 'Data'), 'Tor'), 'geoip')
tor_geo_ipv6_file_path = os.path.join(os.path.join(os.path.join(base_path, 'Data'), 'Tor'), 'geoip6')
elif self.platform == 'Darwin':
base_path = os.path.dirname(os.path.dirname(os.path.dirname(self.get_resource_path(''))))
tor_path = os.path.join(base_path, 'Resources', 'Tor', 'tor')
tor_geo_ip_file_path = os.path.join(base_path, 'Resources', 'Tor', 'geoip')
tor_geo_ipv6_file_path = os.path.join(base_path, 'Resources', 'Tor', 'geoip6')
obfs4proxy_file_path = os.path.join(base_path, 'Resources', 'Tor', 'obfs4proxy')
elif self.platform == 'BSD':
tor_path = '/usr/local/bin/tor'
tor_geo_ip_file_path = '/usr/local/share/tor/geoip'
tor_geo_ipv6_file_path = '/usr/local/share/tor/geoip6'
obfs4proxy_file_path = '/usr/local/bin/obfs4proxy'
return os.path.join(prefix, filename)
return (tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path)
def build_slug(self):
"""
Returns a random string made from two words from the wordlist, such as "deter-trig".
"""
with open(self.get_resource_path('wordlist.txt')) as f:
wordlist = f.read().split()
def get_tor_paths():
p = get_platform()
if p == 'Linux':
tor_path = '/usr/bin/tor'
tor_geo_ip_file_path = '/usr/share/tor/geoip'
tor_geo_ipv6_file_path = '/usr/share/tor/geoip6'
obfs4proxy_file_path = '/usr/bin/obfs4proxy'
elif p == 'Windows':
base_path = os.path.join(os.path.dirname(os.path.dirname(get_resource_path(''))), 'tor')
tor_path = os.path.join(os.path.join(base_path, 'Tor'), 'tor.exe')
obfs4proxy_file_path = os.path.join(os.path.join(base_path, 'Tor'), 'obfs4proxy.exe')
tor_geo_ip_file_path = os.path.join(os.path.join(os.path.join(base_path, 'Data'), 'Tor'), 'geoip')
tor_geo_ipv6_file_path = os.path.join(os.path.join(os.path.join(base_path, 'Data'), 'Tor'), 'geoip6')
elif p == 'Darwin':
base_path = os.path.dirname(os.path.dirname(os.path.dirname(get_resource_path(''))))
tor_path = os.path.join(base_path, 'Resources', 'Tor', 'tor')
tor_geo_ip_file_path = os.path.join(base_path, 'Resources', 'Tor', 'geoip')
tor_geo_ipv6_file_path = os.path.join(base_path, 'Resources', 'Tor', 'geoip6')
obfs4proxy_file_path = os.path.join(base_path, 'Resources', 'Tor', 'obfs4proxy')
elif p == 'BSD':
tor_path = '/usr/local/bin/tor'
tor_geo_ip_file_path = '/usr/local/share/tor/geoip'
tor_geo_ipv6_file_path = '/usr/local/share/tor/geoip6'
obfs4proxy_file_path = '/usr/local/bin/obfs4proxy'
r = random.SystemRandom()
return '-'.join(r.choice(wordlist) for _ in range(2))
return (tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path)
@staticmethod
def random_string(num_bytes, output_len=None):
"""
Returns a random string with a specified number of bytes.
"""
b = os.urandom(num_bytes)
h = hashlib.sha256(b).digest()[:16]
s = base64.b32encode(h).lower().replace(b'=', b'').decode('utf-8')
if not output_len:
return s
return s[:output_len]
def get_version():
"""
Returns the version of OnionShare that is running.
"""
with open(get_resource_path('version.txt')) as f:
version = f.read().strip()
return version
def random_string(num_bytes, output_len=None):
"""
Returns a random string with a specified number of bytes.
"""
b = os.urandom(num_bytes)
h = hashlib.sha256(b).digest()[:16]
s = base64.b32encode(h).lower().replace(b'=', b'').decode('utf-8')
if not output_len:
return s
return s[:output_len]
def build_slug():
"""
Returns a random string made from two words from the wordlist, such as "deter-trig".
"""
with open(get_resource_path('wordlist.txt')) as f:
wordlist = f.read().split()
r = random.SystemRandom()
return '-'.join(r.choice(wordlist) for _ in range(2))
def human_readable_filesize(b):
"""
Returns filesize in a human readable format.
"""
thresh = 1024.0
if b < thresh:
return '{:.1f} B'.format(b)
units = ('KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB')
u = 0
b /= thresh
while b >= thresh:
@staticmethod
def human_readable_filesize(b):
"""
Returns filesize in a human readable format.
"""
thresh = 1024.0
if b < thresh:
return '{:.1f} B'.format(b)
units = ('KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB')
u = 0
b /= thresh
u += 1
return '{:.1f} {}'.format(b, units[u])
while b >= thresh:
b /= thresh
u += 1
return '{:.1f} {}'.format(b, units[u])
@staticmethod
def format_seconds(seconds):
"""Return a human-readable string of the format 1d2h3m4s"""
days, seconds = divmod(seconds, 86400)
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
def format_seconds(seconds):
"""Return a human-readable string of the format 1d2h3m4s"""
days, seconds = divmod(seconds, 86400)
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
human_readable = []
if days:
human_readable.append("{:.0f}d".format(days))
if hours:
human_readable.append("{:.0f}h".format(hours))
if minutes:
human_readable.append("{:.0f}m".format(minutes))
if seconds or not human_readable:
human_readable.append("{:.0f}s".format(seconds))
return ''.join(human_readable)
human_readable = []
if days:
human_readable.append("{:.0f}d".format(days))
if hours:
human_readable.append("{:.0f}h".format(hours))
if minutes:
human_readable.append("{:.0f}m".format(minutes))
if seconds or not human_readable:
human_readable.append("{:.0f}s".format(seconds))
return ''.join(human_readable)
@staticmethod
def estimated_time_remaining(bytes_downloaded, total_bytes, started):
now = time.time()
time_elapsed = now - started # in seconds
download_rate = bytes_downloaded / time_elapsed
remaining_bytes = total_bytes - bytes_downloaded
eta = remaining_bytes / download_rate
return Common.format_seconds(eta)
def estimated_time_remaining(bytes_downloaded, total_bytes, started):
now = time.time()
time_elapsed = now - started # in seconds
download_rate = bytes_downloaded / time_elapsed
remaining_bytes = total_bytes - bytes_downloaded
eta = remaining_bytes / download_rate
return format_seconds(eta)
def get_available_port(min_port, max_port):
"""
Find a random available port within the given range.
"""
with socket.socket() as tmpsock:
while True:
try:
tmpsock.bind(("127.0.0.1", random.randint(min_port, max_port)))
break
except OSError as e:
raise OSError(e)
_, port = tmpsock.getsockname()
return port
def dir_size(start_path):
"""
Calculates the total size, in bytes, of all of the files in a directory.
"""
total_size = 0
for dirpath, dirnames, filenames in os.walk(start_path):
for f in filenames:
fp = os.path.join(dirpath, f)
if not os.path.islink(fp):
total_size += os.path.getsize(fp)
return total_size
class ZipWriter(object):
"""
ZipWriter accepts files and directories and compresses them into a zip file
with. If a zip_filename is not passed in, it will use the default onionshare
filename.
"""
def __init__(self, zip_filename=None, processed_size_callback=None):
if zip_filename:
self.zip_filename = zip_filename
else:
self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), random_string(4, 6))
self.z = zipfile.ZipFile(self.zip_filename, 'w', allowZip64=True)
self.processed_size_callback = processed_size_callback
if self.processed_size_callback is None:
self.processed_size_callback = lambda _: None
self._size = 0
self.processed_size_callback(self._size)
def add_file(self, filename):
@staticmethod
def get_available_port(min_port, max_port):
"""
Add a file to the zip archive.
Find a random available port within the given range.
"""
self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED)
self._size += os.path.getsize(filename)
self.processed_size_callback(self._size)
with socket.socket() as tmpsock:
while True:
try:
tmpsock.bind(("127.0.0.1", random.randint(min_port, max_port)))
break
except OSError as e:
raise OSError(e)
_, port = tmpsock.getsockname()
return port
def add_dir(self, filename):
@staticmethod
def dir_size(start_path):
"""
Add a directory, and all of its children, to the zip archive.
Calculates the total size, in bytes, of all of the files in a directory.
"""
dir_to_strip = os.path.dirname(filename.rstrip('/'))+'/'
for dirpath, dirnames, filenames in os.walk(filename):
total_size = 0
for dirpath, dirnames, filenames in os.walk(start_path):
for f in filenames:
full_filename = os.path.join(dirpath, f)
if not os.path.islink(full_filename):
arc_filename = full_filename[len(dir_to_strip):]
self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED)
self._size += os.path.getsize(full_filename)
self.processed_size_callback(self._size)
def close(self):
"""
Close the zip archive.
"""
self.z.close()
fp = os.path.join(dirpath, f)
if not os.path.islink(fp):
total_size += os.path.getsize(fp)
return total_size
class close_after_seconds(threading.Thread):
class ShutdownTimer(threading.Thread):
"""
Background thread sleeps t hours and returns.
"""
def __init__(self, time):
def __init__(self, common, time):
threading.Thread.__init__(self)
self.common = common
self.setDaemon(True)
self.time = time
def run(self):
log('Shutdown Timer', 'Server will shut down after {} seconds'.format(self.time))
self.common.log('Shutdown Timer', 'Server will shut down after {} seconds'.format(self.time))
time.sleep(self.time)
return 1

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -125,22 +125,22 @@ class Onion(object):
call this function and pass in a status string while connecting to tor. This
is necessary for status updates to reach the GUI.
"""
def __init__(self):
common.log('Onion', '__init__')
def __init__(self, common):
self.common = common
self.common.log('Onion', '__init__')
self.stealth = False
self.service_id = None
self.system = common.get_platform()
# Is bundled tor supported?
if (self.system == 'Windows' or self.system == 'Darwin') and getattr(sys, 'onionshare_dev_mode', False):
if (self.common.platform == 'Windows' or self.common.platform == 'Darwin') and getattr(sys, 'onionshare_dev_mode', False):
self.bundle_tor_supported = False
else:
self.bundle_tor_supported = True
# Set the path of the tor binary, for bundled tor
(self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = common.get_tor_paths()
(self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = self.common.get_tor_paths()
# The tor process
self.tor_proc = None
@ -148,15 +148,14 @@ class Onion(object):
# Start out not connected to Tor
self.connected_to_tor = False
def connect(self, settings=False, config=False, tor_status_update_func=None):
common.log('Onion', 'connect')
def connect(self, custom_settings=False, config=False, tor_status_update_func=None):
self.common.log('Onion', 'connect')
# Either use settings that are passed in, or load them from disk
if settings:
self.settings = settings
# Either use settings that are passed in, or use them from common
if custom_settings:
self.settings = custom_settings
else:
self.settings = Settings(config)
self.settings.load()
self.settings = self.common.settings
# The Tor controller
self.c = None
@ -168,29 +167,29 @@ class Onion(object):
# Create a torrc for this session
self.tor_data_directory = tempfile.TemporaryDirectory()
if self.system == 'Windows':
if self.common.platform == 'Windows':
# Windows needs to use network ports, doesn't support unix sockets
torrc_template = open(common.get_resource_path('torrc_template-windows')).read()
torrc_template = open(self.common.get_resource_path('torrc_template-windows')).read()
try:
self.tor_control_port = common.get_available_port(1000, 65535)
self.tor_control_port = self.common.get_available_port(1000, 65535)
except:
raise OSError(strings._('no_available_port'))
self.tor_control_socket = None
self.tor_cookie_auth_file = os.path.join(self.tor_data_directory.name, 'cookie')
try:
self.tor_socks_port = common.get_available_port(1000, 65535)
self.tor_socks_port = self.common.get_available_port(1000, 65535)
except:
raise OSError(strings._('no_available_port'))
self.tor_torrc = os.path.join(self.tor_data_directory.name, 'torrc')
else:
# Linux, Mac and BSD can use unix sockets
with open(common.get_resource_path('torrc_template')) as f:
with open(self.common.get_resource_path('torrc_template')) as f:
torrc_template = f.read()
self.tor_control_port = None
self.tor_control_socket = os.path.join(self.tor_data_directory.name, 'control_socket')
self.tor_cookie_auth_file = os.path.join(self.tor_data_directory.name, 'cookie')
try:
self.tor_socks_port = common.get_available_port(1000, 65535)
self.tor_socks_port = self.common.get_available_port(1000, 65535)
except:
raise OSError(strings._('no_available_port'))
self.tor_torrc = os.path.join(self.tor_data_directory.name, 'torrc')
@ -208,17 +207,17 @@ class Onion(object):
# Bridge support
if self.settings.get('tor_bridges_use_obfs4'):
f.write('ClientTransportPlugin obfs4 exec {}\n'.format(self.obfs4proxy_file_path))
with open(common.get_resource_path('torrc_template-obfs4')) as o:
with open(self.common.get_resource_path('torrc_template-obfs4')) as o:
for line in o:
f.write(line)
elif self.settings.get('tor_bridges_use_meek_lite_amazon'):
f.write('ClientTransportPlugin meek_lite exec {}\n'.format(self.obfs4proxy_file_path))
with open(common.get_resource_path('torrc_template-meek_lite_amazon')) as o:
with open(self.common.get_resource_path('torrc_template-meek_lite_amazon')) as o:
for line in o:
f.write(line)
elif self.settings.get('tor_bridges_use_meek_lite_azure'):
f.write('ClientTransportPlugin meek_lite exec {}\n'.format(self.obfs4proxy_file_path))
with open(common.get_resource_path('torrc_template-meek_lite_azure')) as o:
with open(self.common.get_resource_path('torrc_template-meek_lite_azure')) as o:
for line in o:
f.write(line)
@ -232,7 +231,7 @@ class Onion(object):
# Execute a tor subprocess
start_ts = time.time()
if self.system == 'Windows':
if self.common.platform == 'Windows':
# In Windows, hide console window when opening tor.exe subprocess
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
@ -245,7 +244,7 @@ class Onion(object):
# Connect to the controller
try:
if self.system == 'Windows':
if self.common.platform == 'Windows':
self.c = Controller.from_port(port=self.tor_control_port)
self.c.authenticate()
else:
@ -270,7 +269,7 @@ class Onion(object):
if callable(tor_status_update_func):
if not tor_status_update_func(progress, summary):
# If the dialog was canceled, stop connecting to Tor
common.log('Onion', 'connect', 'tor_status_update_func returned false, canceling connecting to Tor')
self.common.log('Onion', 'connect', 'tor_status_update_func returned false, canceling connecting to Tor')
print()
return False
@ -322,7 +321,7 @@ class Onion(object):
socket_file_path = ''
if not found_tor:
try:
if self.system == 'Darwin':
if self.common.platform == 'Darwin':
socket_file_path = os.path.expanduser('~/Library/Application Support/TorBrowser-Data/Tor/control.socket')
self.c = Controller.from_socket_file(path=socket_file_path)
@ -334,11 +333,11 @@ class Onion(object):
# guessing the socket file name next
if not found_tor:
try:
if self.system == 'Linux' or self.system == 'BSD':
if self.common.platform == 'Linux' or self.common.platform == 'BSD':
socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid())
elif self.system == 'Darwin':
elif self.common.platform == 'Darwin':
socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid())
elif self.system == 'Windows':
elif self.common.platform == 'Windows':
# Windows doesn't support unix sockets
raise TorErrorAutomatic(strings._('settings_error_automatic'))
@ -424,7 +423,7 @@ class Onion(object):
Start a onion service on port 80, pointing to the given port, and
return the onion hostname.
"""
common.log('Onion', 'start_onion_service')
self.common.log('Onion', 'start_onion_service')
self.auth_string = None
if not self.supports_ephemeral:
@ -447,11 +446,11 @@ class Onion(object):
if self.settings.get('private_key'):
key_type = "RSA1024"
key_content = self.settings.get('private_key')
common.log('Onion', 'Starting a hidden service with a saved private key')
self.common.log('Onion', 'start_onion_service', 'Starting a hidden service with a saved private key')
else:
key_type = "NEW"
key_content = "RSA1024"
common.log('Onion', 'Starting a hidden service with a new private key')
self.common.log('Onion', 'start_onion_service', 'Starting a hidden service with a new private key')
try:
if basic_auth != None:
@ -498,17 +497,17 @@ class Onion(object):
"""
Stop onion services that were created earlier. If there's a tor subprocess running, kill it.
"""
common.log('Onion', 'cleanup')
self.common.log('Onion', 'cleanup')
# Cleanup the ephemeral onion services, if we have any
try:
onions = self.c.list_ephemeral_hidden_services()
for onion in onions:
try:
common.log('Onion', 'cleanup', 'trying to remove onion {}'.format(onion))
self.common.log('Onion', 'cleanup', 'trying to remove onion {}'.format(onion))
self.c.remove_ephemeral_hidden_service(onion)
except:
common.log('Onion', 'cleanup', 'could not remove onion {}.. moving on anyway'.format(onion))
self.common.log('Onion', 'cleanup', 'could not remove onion {}.. moving on anyway'.format(onion))
pass
except:
pass
@ -545,7 +544,7 @@ class Onion(object):
"""
Returns a (address, port) tuple for the Tor SOCKS port
"""
common.log('Onion', 'get_tor_socks_port')
self.common.log('Onion', 'get_tor_socks_port')
if self.settings.get('connection_type') == 'bundled':
return ('127.0.0.1', self.tor_socks_port)

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -21,14 +21,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import os, shutil
from . import common, strings
from .common import ShutdownTimer
class OnionShare(object):
"""
OnionShare is the main application class. Pass in options and run
start_onion_service and it will do the magic.
"""
def __init__(self, onion, local_only=False, stay_open=False, shutdown_timeout=0):
common.log('OnionShare', '__init__')
def __init__(self, common, onion, local_only=False, stay_open=False, shutdown_timeout=0):
self.common = common
self.common.log('OnionShare', '__init__')
# The Onion object
self.onion = onion
@ -52,7 +55,7 @@ class OnionShare(object):
self.shutdown_timer = None
def set_stealth(self, stealth):
common.log('OnionShare', 'set_stealth', 'stealth={}'.format(stealth))
self.common.log('OnionShare', 'set_stealth', 'stealth={}'.format(stealth))
self.stealth = stealth
self.onion.stealth = stealth
@ -61,11 +64,11 @@ class OnionShare(object):
"""
Start the onionshare onion service.
"""
common.log('OnionShare', 'start_onion_service')
self.common.log('OnionShare', 'start_onion_service')
# Choose a random port
try:
self.port = common.get_available_port(17600, 17650)
self.port = self.common.get_available_port(17600, 17650)
except:
raise OSError(strings._('no_available_port'))
@ -74,7 +77,7 @@ class OnionShare(object):
return
if self.shutdown_timeout > 0:
self.shutdown_timer = common.close_after_seconds(self.shutdown_timeout)
self.shutdown_timer = ShutdownTimer(self.common, self.shutdown_timeout)
self.onion_host = self.onion.start_onion_service(self.port)
@ -85,7 +88,7 @@ class OnionShare(object):
"""
Shut everything down and clean up temporary files, etc.
"""
common.log('OnionShare', 'cleanup')
self.common.log('OnionShare', 'cleanup')
# cleanup files
for filename in self.cleanup_filenames:

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -22,7 +22,7 @@ import json
import os
import platform
from . import strings, common
from . import strings
class Settings(object):
@ -32,8 +32,10 @@ class Settings(object):
which is to attempt to connect automatically using default Tor Browser
settings.
"""
def __init__(self, config=False):
common.log('Settings', '__init__')
def __init__(self, common, config=False):
self.common = common
self.common.log('Settings', '__init__')
# Default config
self.filename = self.build_filename()
@ -43,11 +45,11 @@ class Settings(object):
if os.path.isfile(config):
self.filename = config
else:
common.log('Settings', '__init__', 'Supplied config does not exist or is unreadable. Falling back to default location')
self.common.log('Settings', '__init__', 'Supplied config does not exist or is unreadable. Falling back to default location')
# These are the default settings. They will get overwritten when loading from disk
self.default_settings = {
'version': common.get_version(),
'version': self.common.version,
'connection_type': 'bundled',
'control_port_address': '127.0.0.1',
'control_port_port': 9051,
@ -70,7 +72,8 @@ class Settings(object):
'save_private_key': False,
'private_key': '',
'slug': '',
'hidservauth_string': ''
'hidservauth_string': '',
'downloads_dir': self.build_default_downloads_dir()
}
self._settings = {}
self.fill_in_defaults()
@ -97,16 +100,24 @@ class Settings(object):
else:
return os.path.expanduser('~/.config/onionshare/onionshare.json')
def build_default_downloads_dir(self):
"""
Returns the path of the default Downloads directory for receive mode.
"""
# TODO: Test in Windows, though it looks like it should work
# https://docs.python.org/3/library/os.path.html#os.path.expanduser
return os.path.expanduser('~/OnionShare')
def load(self):
"""
Load the settings from file.
"""
common.log('Settings', 'load')
self.common.log('Settings', 'load')
# If the settings file exists, load it
if os.path.exists(self.filename):
try:
common.log('Settings', 'load', 'Trying to load {}'.format(self.filename))
self.common.log('Settings', 'load', 'Trying to load {}'.format(self.filename))
with open(self.filename, 'r') as f:
self._settings = json.load(f)
self.fill_in_defaults()
@ -117,7 +128,7 @@ class Settings(object):
"""
Save settings to file.
"""
common.log('Settings', 'save')
self.common.log('Settings', 'save')
try:
os.makedirs(os.path.dirname(self.filename))

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -26,422 +26,593 @@ import queue
import socket
import sys
import tempfile
import base64
import zipfile
import re
import io
from distutils.version import LooseVersion as Version
from urllib.request import urlopen
from flask import (
Flask, Response, request, render_template_string, abort, make_response,
__version__ as flask_version
Flask, Response, Request, request, render_template, abort, make_response,
flash, redirect, __version__ as flask_version
)
from werkzeug.utils import secure_filename
from . import strings, common
def _safe_select_jinja_autoescape(self, filename):
if filename is None:
return True
return filename.endswith(('.html', '.htm', '.xml', '.xhtml'))
# Starting in Flask 0.11, render_template_string autoescapes template variables
# by default. To prevent content injection through template variables in
# earlier versions of Flask, we force autoescaping in the Jinja2 template
# engine if we detect a Flask version with insecure default behavior.
if Version(flask_version) < Version('0.11'):
# Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc
Flask.select_jinja_autoescape = _safe_select_jinja_autoescape
app = Flask(__name__)
# information about the file
file_info = []
zip_filename = None
zip_filesize = None
security_headers = [
('Content-Security-Policy', 'default-src \'self\'; style-src \'unsafe-inline\'; script-src \'unsafe-inline\'; img-src \'self\' data:;'),
('X-Frame-Options', 'DENY'),
('X-Xss-Protection', '1; mode=block'),
('X-Content-Type-Options', 'nosniff'),
('Referrer-Policy', 'no-referrer'),
('Server', 'OnionShare')
]
def set_file_info(filenames, processed_size_callback=None):
class Web(object):
"""
Using the list of filenames being shared, fill in details that the web
page will need to display. This includes zipping up the file in order to
get the zip file's name and size.
The Web object is the OnionShare web server, powered by flask
"""
global file_info, zip_filename, zip_filesize
def __init__(self, common, stay_open, gui_mode, receive_mode=False):
self.common = common
# build file info list
file_info = {'files': [], 'dirs': []}
for filename in filenames:
info = {
'filename': filename,
'basename': os.path.basename(filename.rstrip('/'))
}
if os.path.isfile(filename):
info['size'] = os.path.getsize(filename)
info['size_human'] = common.human_readable_filesize(info['size'])
file_info['files'].append(info)
if os.path.isdir(filename):
info['size'] = common.dir_size(filename)
info['size_human'] = common.human_readable_filesize(info['size'])
file_info['dirs'].append(info)
file_info['files'] = sorted(file_info['files'], key=lambda k: k['basename'])
file_info['dirs'] = sorted(file_info['dirs'], key=lambda k: k['basename'])
# The flask app
self.app = Flask(__name__,
static_folder=common.get_resource_path('static'),
template_folder=common.get_resource_path('templates'))
self.app.secret_key = self.common.random_string(8)
# zip up the files and folders
z = common.ZipWriter(processed_size_callback=processed_size_callback)
for info in file_info['files']:
z.add_file(info['filename'])
for info in file_info['dirs']:
z.add_dir(info['filename'])
z.close()
zip_filename = z.zip_filename
zip_filesize = os.path.getsize(zip_filename)
# Debug mode?
if self.common.debug:
self.debug_mode()
# Stay open after the first download?
self.stay_open = stay_open
REQUEST_LOAD = 0
REQUEST_DOWNLOAD = 1
REQUEST_PROGRESS = 2
REQUEST_OTHER = 3
REQUEST_CANCELED = 4
REQUEST_RATE_LIMIT = 5
q = queue.Queue()
# Are we running in GUI mode?
self.gui_mode = gui_mode
# Are we using receive mode?
self.receive_mode = receive_mode
if self.receive_mode:
# Use custom WSGI middleware, to modify environ
self.app.wsgi_app = ReceiveModeWSGIMiddleware(self.app.wsgi_app, self)
# Use a custom Request class to track upload progess
self.app.request_class = ReceiveModeRequest
def add_request(request_type, path, data=None):
"""
Add a request to the queue, to communicate with the GUI.
"""
global q
q.put({
'type': request_type,
'path': path,
'data': data
})
# Starting in Flask 0.11, render_template_string autoescapes template variables
# by default. To prevent content injection through template variables in
# earlier versions of Flask, we force autoescaping in the Jinja2 template
# engine if we detect a Flask version with insecure default behavior.
if Version(flask_version) < Version('0.11'):
# Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc
Flask.select_jinja_autoescape = self._safe_select_jinja_autoescape
# Information about the file
self.file_info = []
self.zip_filename = None
self.zip_filesize = None
# Load and base64 encode images to pass into templates
favicon_b64 = base64.b64encode(open(common.get_resource_path('images/favicon.ico'), 'rb').read()).decode()
logo_b64 = base64.b64encode(open(common.get_resource_path('images/logo.png'), 'rb').read()).decode()
folder_b64 = base64.b64encode(open(common.get_resource_path('images/web_folder.png'), 'rb').read()).decode()
file_b64 = base64.b64encode(open(common.get_resource_path('images/web_file.png'), 'rb').read()).decode()
self.security_headers = [
('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'),
('X-Frame-Options', 'DENY'),
('X-Xss-Protection', '1; mode=block'),
('X-Content-Type-Options', 'nosniff'),
('Referrer-Policy', 'no-referrer'),
('Server', 'OnionShare')
]
slug = None
self.REQUEST_LOAD = 0
self.REQUEST_DOWNLOAD = 1
self.REQUEST_PROGRESS = 2
self.REQUEST_OTHER = 3
self.REQUEST_CANCELED = 4
self.REQUEST_RATE_LIMIT = 5
self.q = queue.Queue()
self.slug = None
def generate_slug(persistent_slug=''):
global slug
if persistent_slug:
slug = persistent_slug
else:
slug = common.build_slug()
self.download_count = 0
self.error404_count = 0
download_count = 0
error404_count = 0
# If "Stop After First Download" is checked (stay_open == False), only allow
# one download at a time.
self.download_in_progress = False
stay_open = False
self.done = False
# If the client closes the OnionShare window while a download is in progress,
# it should immediately stop serving the file. The client_cancel global is
# used to tell the download function that the client is canceling the download.
self.client_cancel = False
def set_stay_open(new_stay_open):
"""
Set stay_open variable.
"""
global stay_open
stay_open = new_stay_open
# shutting down the server only works within the context of flask, so the easiest way to do it is over http
self.shutdown_slug = self.common.random_string(16)
# Define the ewb app routes
self.common_routes()
if self.receive_mode:
self.receive_routes()
else:
self.send_routes()
def get_stay_open():
"""
Get stay_open variable.
"""
return stay_open
def send_routes(self):
"""
The web app routes for sharing files
"""
@self.app.route("/<slug_candidate>")
def index(slug_candidate):
"""
Render the template for the onionshare landing page.
"""
self.check_slug_candidate(slug_candidate)
self.add_request(self.REQUEST_LOAD, request.path)
# Are we running in GUI mode?
gui_mode = False
# Deny new downloads if "Stop After First Download" is checked and there is
# currently a download
deny_download = not self.stay_open and self.download_in_progress
if deny_download:
r = make_response(render_template('denied.html'))
return self.add_security_headers(r)
# If download is allowed to continue, serve download page
r = make_response(render_template(
'send.html',
slug=self.slug,
file_info=self.file_info,
filename=os.path.basename(self.zip_filename),
filesize=self.zip_filesize,
filesize_human=self.common.human_readable_filesize(self.zip_filesize)))
return self.add_security_headers(r)
def set_gui_mode():
"""
Tell the web service that we're running in GUI mode
"""
global gui_mode
gui_mode = True
@self.app.route("/<slug_candidate>/download")
def download(slug_candidate):
"""
Download the zip file.
"""
self.check_slug_candidate(slug_candidate)
# Deny new downloads if "Stop After First Download" is checked and there is
# currently a download
deny_download = not self.stay_open and self.download_in_progress
if deny_download:
r = make_response(render_template('denied.html'))
return self.add_security_headers(r)
def debug_mode():
"""
Turn on debugging mode, which will log flask errors to a debug file.
"""
temp_dir = tempfile.gettempdir()
log_handler = logging.FileHandler(
os.path.join(temp_dir, 'onionshare_server.log'))
log_handler.setLevel(logging.WARNING)
app.logger.addHandler(log_handler)
# each download has a unique id
download_id = self.download_count
self.download_count += 1
# prepare some variables to use inside generate() function below
# which is outside of the request context
shutdown_func = request.environ.get('werkzeug.server.shutdown')
path = request.path
def check_slug_candidate(slug_candidate, slug_compare=None):
if not slug_compare:
slug_compare = slug
if not hmac.compare_digest(slug_compare, slug_candidate):
abort(404)
# tell GUI the download started
self.add_request(self.REQUEST_DOWNLOAD, path, {'id': download_id})
dirname = os.path.dirname(self.zip_filename)
basename = os.path.basename(self.zip_filename)
# If "Stop After First Download" is checked (stay_open == False), only allow
# one download at a time.
download_in_progress = False
def generate():
# The user hasn't canceled the download
self.client_cancel = False
done = False
# Starting a new download
if not self.stay_open:
self.download_in_progress = True
@app.route("/<slug_candidate>")
def index(slug_candidate):
"""
Render the template for the onionshare landing page.
"""
check_slug_candidate(slug_candidate)
chunk_size = 102400 # 100kb
add_request(REQUEST_LOAD, request.path)
fp = open(self.zip_filename, 'rb')
self.done = False
canceled = False
while not self.done:
# The user has canceled the download, so stop serving the file
if self.client_cancel:
self.add_request(self.REQUEST_CANCELED, path, {'id': download_id})
break
# Deny new downloads if "Stop After First Download" is checked and there is
# currently a download
global stay_open, download_in_progress
deny_download = not stay_open and download_in_progress
if deny_download:
r = make_response(render_template_string(
open(common.get_resource_path('html/denied.html')).read(),
favicon_b64=favicon_b64
))
for header, value in security_headers:
r.headers.set(header, value)
return r
chunk = fp.read(chunk_size)
if chunk == b'':
self.done = True
else:
try:
yield chunk
# If download is allowed to continue, serve download page
# tell GUI the progress
downloaded_bytes = fp.tell()
percent = (1.0 * downloaded_bytes / self.zip_filesize) * 100
r = make_response(render_template_string(
open(common.get_resource_path('html/index.html')).read(),
favicon_b64=favicon_b64,
logo_b64=logo_b64,
folder_b64=folder_b64,
file_b64=file_b64,
slug=slug,
file_info=file_info,
filename=os.path.basename(zip_filename),
filesize=zip_filesize,
filesize_human=common.human_readable_filesize(zip_filesize)))
for header, value in security_headers:
r.headers.set(header, value)
return r
# only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304)
if not self.gui_mode or self.common.platform == 'Linux' or self.common.platform == 'BSD':
sys.stdout.write(
"\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent))
sys.stdout.flush()
self.add_request(self.REQUEST_PROGRESS, path, {'id': download_id, 'bytes': downloaded_bytes})
self.done = False
except:
# looks like the download was canceled
self.done = True
canceled = True
# If the client closes the OnionShare window while a download is in progress,
# it should immediately stop serving the file. The client_cancel global is
# used to tell the download function that the client is canceling the download.
client_cancel = False
# tell the GUI the download has canceled
self.add_request(self.REQUEST_CANCELED, path, {'id': download_id})
fp.close()
@app.route("/<slug_candidate>/download")
def download(slug_candidate):
"""
Download the zip file.
"""
check_slug_candidate(slug_candidate)
if self.common.platform != 'Darwin':
sys.stdout.write("\n")
# Deny new downloads if "Stop After First Download" is checked and there is
# currently a download
global stay_open, download_in_progress, done
deny_download = not stay_open and download_in_progress
if deny_download:
r = make_response(render_template_string(
open(common.get_resource_path('html/denied.html')).read(),
favicon_b64=favicon_b64
))
for header,value in security_headers:
r.headers.set(header, value)
return r
# Download is finished
if not self.stay_open:
self.download_in_progress = False
global download_count
# Close the server, if necessary
if not self.stay_open and not canceled:
print(strings._("closing_automatically"))
if shutdown_func is None:
raise RuntimeError('Not running with the Werkzeug Server')
shutdown_func()
# each download has a unique id
download_id = download_count
download_count += 1
r = Response(generate())
r.headers.set('Content-Length', self.zip_filesize)
r.headers.set('Content-Disposition', 'attachment', filename=basename)
r = self.add_security_headers(r)
# guess content type
(content_type, _) = mimetypes.guess_type(basename, strict=False)
if content_type is not None:
r.headers.set('Content-Type', content_type)
return r
# prepare some variables to use inside generate() function below
# which is outside of the request context
shutdown_func = request.environ.get('werkzeug.server.shutdown')
path = request.path
def receive_routes(self):
"""
The web app routes for sharing files
"""
@self.app.route("/<slug_candidate>")
def index(slug_candidate):
self.check_slug_candidate(slug_candidate)
# tell GUI the download started
add_request(REQUEST_DOWNLOAD, path, {'id': download_id})
r = make_response(render_template(
'receive.html',
slug=self.slug))
return self.add_security_headers(r)
dirname = os.path.dirname(zip_filename)
basename = os.path.basename(zip_filename)
@self.app.route("/<slug_candidate>/upload", methods=['POST'])
def upload(slug_candidate):
self.check_slug_candidate(slug_candidate)
def generate():
# The user hasn't canceled the download
global client_cancel, gui_mode
client_cancel = False
files = request.files.getlist('file[]')
filenames = []
for f in files:
if f.filename != '':
# Automatically rename the file, if a file of the same name already exists
filename = secure_filename(f.filename)
filenames.append(filename)
local_path = os.path.join(self.common.settings.get('downloads_dir'), filename)
if os.path.exists(local_path):
if '.' in filename:
# Add "-i", e.g. change "foo.txt" to "foo-2.txt"
parts = filename.split('.')
name = parts[:-1]
ext = parts[-1]
# Starting a new download
global stay_open, download_in_progress, done
if not stay_open:
download_in_progress = True
i = 2
valid = False
while not valid:
new_filename = '{}-{}.{}'.format('.'.join(name), i, ext)
local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename)
if os.path.exists(local_path):
i += 1
else:
valid = True
else:
# If no extension, just add "-i", e.g. change "foo" to "foo-2"
i = 2
valid = False
while not valid:
new_filename = '{}-{}'.format(filename, i)
local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename)
if os.path.exists(local_path):
i += 1
else:
valid = True
chunk_size = 102400 # 100kb
self.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path))
print(strings._('receive_mode_received_file').format(local_path))
f.save(local_path)
fp = open(zip_filename, 'rb')
done = False
canceled = False
while not done:
# The user has canceled the download, so stop serving the file
if client_cancel:
add_request(REQUEST_CANCELED, path, {'id': download_id})
break
chunk = fp.read(chunk_size)
if chunk == b'':
done = True
# Note that flash strings are on English, and not translated, on purpose,
# to avoid leaking the locale of the OnionShare user
if len(filenames) == 0:
flash('No files uploaded')
else:
try:
yield chunk
for filename in filenames:
flash('Uploaded {}'.format(filename))
# tell GUI the progress
downloaded_bytes = fp.tell()
percent = (1.0 * downloaded_bytes / zip_filesize) * 100
return redirect('/{}'.format(slug_candidate))
# only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304)
plat = common.get_platform()
if not gui_mode or plat == 'Linux' or plat == 'BSD':
sys.stdout.write(
"\r{0:s}, {1:.2f}% ".format(common.human_readable_filesize(downloaded_bytes), percent))
sys.stdout.flush()
@self.app.route("/<slug_candidate>/close", methods=['POST'])
def close(slug_candidate):
self.check_slug_candidate(slug_candidate)
self.force_shutdown()
r = make_response(render_template('closed.html'))
return self.add_security_headers(r)
add_request(REQUEST_PROGRESS, path, {'id': download_id, 'bytes': downloaded_bytes})
done = False
except:
# looks like the download was canceled
done = True
canceled = True
def common_routes(self):
"""
Common web app routes between sending and receiving
"""
@self.app.errorhandler(404)
def page_not_found(e):
"""
404 error page.
"""
self.add_request(self.REQUEST_OTHER, request.path)
# tell the GUI the download has canceled
add_request(REQUEST_CANCELED, path, {'id': download_id})
if request.path != '/favicon.ico':
self.error404_count += 1
if self.error404_count == 20:
self.add_request(self.REQUEST_RATE_LIMIT, request.path)
self.force_shutdown()
print(strings._('error_rate_limit'))
fp.close()
r = make_response(render_template('404.html'), 404)
return self.add_security_headers(r)
if common.get_platform() != 'Darwin':
sys.stdout.write("\n")
@self.app.route("/<slug_candidate>/shutdown")
def shutdown(slug_candidate):
"""
Stop the flask web server, from the context of an http request.
"""
self.check_slug_candidate(slug_candidate, self.shutdown_slug)
self.force_shutdown()
return ""
# Download is finished
if not stay_open:
download_in_progress = False
def add_security_headers(self, r):
"""
Add security headers to a request
"""
for header, value in self.security_headers:
r.headers.set(header, value)
return r
# Close the server, if necessary
if not stay_open and not canceled:
print(strings._("closing_automatically"))
if shutdown_func is None:
raise RuntimeError('Not running with the Werkzeug Server')
shutdown_func()
def set_file_info(self, filenames, processed_size_callback=None):
"""
Using the list of filenames being shared, fill in details that the web
page will need to display. This includes zipping up the file in order to
get the zip file's name and size.
"""
# build file info list
self.file_info = {'files': [], 'dirs': []}
for filename in filenames:
info = {
'filename': filename,
'basename': os.path.basename(filename.rstrip('/'))
}
if os.path.isfile(filename):
info['size'] = os.path.getsize(filename)
info['size_human'] = self.common.human_readable_filesize(info['size'])
self.file_info['files'].append(info)
if os.path.isdir(filename):
info['size'] = self.common.dir_size(filename)
info['size_human'] = self.common.human_readable_filesize(info['size'])
self.file_info['dirs'].append(info)
self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename'])
self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename'])
r = Response(generate())
r.headers.set('Content-Length', zip_filesize)
r.headers.set('Content-Disposition', 'attachment', filename=basename)
for header,value in security_headers:
r.headers.set(header, value)
# guess content type
(content_type, _) = mimetypes.guess_type(basename, strict=False)
if content_type is not None:
r.headers.set('Content-Type', content_type)
return r
# zip up the files and folders
z = ZipWriter(self.common, processed_size_callback=processed_size_callback)
for info in self.file_info['files']:
z.add_file(info['filename'])
for info in self.file_info['dirs']:
z.add_dir(info['filename'])
z.close()
self.zip_filename = z.zip_filename
self.zip_filesize = os.path.getsize(self.zip_filename)
def _safe_select_jinja_autoescape(self, filename):
if filename is None:
return True
return filename.endswith(('.html', '.htm', '.xml', '.xhtml'))
@app.errorhandler(404)
def page_not_found(e):
"""
404 error page.
"""
add_request(REQUEST_OTHER, request.path)
def add_request(self, request_type, path, data=None):
"""
Add a request to the queue, to communicate with the GUI.
"""
self.q.put({
'type': request_type,
'path': path,
'data': data
})
global error404_count
if request.path != '/favicon.ico':
error404_count += 1
if error404_count == 20:
add_request(REQUEST_RATE_LIMIT, request.path)
force_shutdown()
print(strings._('error_rate_limit'))
def generate_slug(self, persistent_slug=''):
if persistent_slug:
self.slug = persistent_slug
else:
self.slug = self.common.build_slug()
r = make_response(render_template_string(
open(common.get_resource_path('html/404.html')).read(),
favicon_b64=favicon_b64
), 404)
for header, value in security_headers:
r.headers.set(header, value)
return r
def debug_mode(self):
"""
Turn on debugging mode, which will log flask errors to a debug file.
"""
temp_dir = tempfile.gettempdir()
log_handler = logging.FileHandler(
os.path.join(temp_dir, 'onionshare_server.log'))
log_handler.setLevel(logging.WARNING)
self.app.logger.addHandler(log_handler)
def check_slug_candidate(self, slug_candidate, slug_compare=None):
if not slug_compare:
slug_compare = self.slug
if not hmac.compare_digest(slug_compare, slug_candidate):
abort(404)
# shutting down the server only works within the context of flask, so the easiest way to do it is over http
shutdown_slug = common.random_string(16)
def force_shutdown(self):
"""
Stop the flask web server, from the context of the flask app.
"""
# shutdown the flask service
func = request.environ.get('werkzeug.server.shutdown')
if func is None:
raise RuntimeError('Not running with the Werkzeug Server')
func()
def start(self, port, stay_open=False, persistent_slug=''):
"""
Start the flask web server.
"""
self.generate_slug(persistent_slug)
@app.route("/<slug_candidate>/shutdown")
def shutdown(slug_candidate):
"""
Stop the flask web server, from the context of an http request.
"""
check_slug_candidate(slug_candidate, shutdown_slug)
force_shutdown()
return ""
self.stay_open = stay_open
# In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220)
if os.path.exists('/usr/share/anon-ws-base-files/workstation'):
host = '0.0.0.0'
else:
host = '127.0.0.1'
def force_shutdown():
"""
Stop the flask web server, from the context of the flask app.
"""
# shutdown the flask service
func = request.environ.get('werkzeug.server.shutdown')
if func is None:
raise RuntimeError('Not running with the Werkzeug Server')
func()
self.app.run(host=host, port=port, threaded=True)
def stop(self, port):
"""
Stop the flask web server by loading /shutdown.
"""
def start(port, stay_open=False, persistent_slug=''):
"""
Start the flask web server.
"""
generate_slug(persistent_slug)
# If the user cancels the download, let the download function know to stop
# serving the file
self.client_cancel = True
set_stay_open(stay_open)
# In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220)
if os.path.exists('/usr/share/anon-ws-base-files/workstation'):
host = '0.0.0.0'
else:
host = '127.0.0.1'
app.run(host=host, port=port, threaded=True)
def stop(port):
"""
Stop the flask web server by loading /shutdown.
"""
# If the user cancels the download, let the download function know to stop
# serving the file
global client_cancel
client_cancel = True
# to stop flask, load http://127.0.0.1:<port>/<shutdown_slug>/shutdown
try:
s = socket.socket()
s.connect(('127.0.0.1', port))
s.sendall('GET /{0:s}/shutdown HTTP/1.1\r\n\r\n'.format(shutdown_slug))
except:
# to stop flask, load http://127.0.0.1:<port>/<shutdown_slug>/shutdown
try:
urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, shutdown_slug)).read()
s = socket.socket()
s.connect(('127.0.0.1', port))
s.sendall('GET /{0:s}/shutdown HTTP/1.1\r\n\r\n'.format(self.shutdown_slug))
except:
pass
try:
urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, self.shutdown_slug)).read()
except:
pass
class ZipWriter(object):
"""
ZipWriter accepts files and directories and compresses them into a zip file
with. If a zip_filename is not passed in, it will use the default onionshare
filename.
"""
def __init__(self, common, zip_filename=None, processed_size_callback=None):
self.common = common
if zip_filename:
self.zip_filename = zip_filename
else:
self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), self.common.random_string(4, 6))
self.z = zipfile.ZipFile(self.zip_filename, 'w', allowZip64=True)
self.processed_size_callback = processed_size_callback
if self.processed_size_callback is None:
self.processed_size_callback = lambda _: None
self._size = 0
self.processed_size_callback(self._size)
def add_file(self, filename):
"""
Add a file to the zip archive.
"""
self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED)
self._size += os.path.getsize(filename)
self.processed_size_callback(self._size)
def add_dir(self, filename):
"""
Add a directory, and all of its children, to the zip archive.
"""
dir_to_strip = os.path.dirname(filename.rstrip('/'))+'/'
for dirpath, dirnames, filenames in os.walk(filename):
for f in filenames:
full_filename = os.path.join(dirpath, f)
if not os.path.islink(full_filename):
arc_filename = full_filename[len(dir_to_strip):]
self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED)
self._size += os.path.getsize(full_filename)
self.processed_size_callback(self._size)
def close(self):
"""
Close the zip archive.
"""
self.z.close()
class ReceiveModeWSGIMiddleware(object):
"""
Custom WSGI middleware in order to attach the Web object to environ, so
ReceiveModeRequest can access it.
"""
def __init__(self, app, web):
self.app = app
self.web = web
def __call__(self, environ, start_response):
environ['web'] = self.web
return self.app(environ, start_response)
class ReceiveModeTemporaryFile(object):
"""
A custom TemporaryFile that tells ReceiveModeRequest every time data gets
written to it, in order to track the progress of uploads.
"""
def __init__(self, filename, update_func):
self.onionshare_filename = filename
self.onionshare_update_func = update_func
# Create a temporary file
self.f = tempfile.TemporaryFile('wb+')
# Make all the file-like methods and attributes actually access the
# TemporaryFile, except for write
attrs = ['close', 'closed', 'detach', 'fileno', 'flush', 'isatty', 'mode',
'name', 'peek', 'raw', 'read', 'read1', 'readable', 'readinto',
'readinto1', 'readline', 'readlines', 'seek', 'seekable', 'tell',
'truncate', 'writable', 'writelines']
for attr in attrs:
setattr(self, attr, getattr(self.f, attr))
def write(self, b):
"""
Custom write method that calls out to onionshare_update_func
"""
bytes_written = self.f.write(b)
self.onionshare_update_func(self.onionshare_filename, bytes_written)
class ReceiveModeRequest(Request):
"""
A custom flask Request object that keeps track of how much data has been
uploaded for each file, for receive mode.
"""
def __init__(self, environ, populate_request=True, shallow=False):
super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow)
self.web = environ['web']
# A dictionary that maps filenames to the bytes uploaded so far
self.onionshare_progress = {}
def _get_file_stream(self, total_content_length, content_type, filename=None, content_length=None):
"""
This gets called for each file that gets uploaded, and returns an file-like
writable stream.
"""
if len(self.onionshare_progress) > 0:
print('')
self.onionshare_progress[filename] = 0
return ReceiveModeTemporaryFile(filename, self.onionshare_update_func)
def close(self):
"""
When closing the request, print a newline if this was a file upload.
"""
super(ReceiveModeRequest, self).close()
if len(self.onionshare_progress) > 0:
print('')
def onionshare_update_func(self, filename, length):
"""
Keep track of the bytes uploaded so far for all files.
"""
self.onionshare_progress[filename] += length
print('{} - {} '.format(self.web.common.human_readable_filesize(self.onionshare_progress[filename]), filename), end='\r')

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -22,10 +22,11 @@ import os, sys, platform, argparse
from .alert import Alert
from PyQt5 import QtCore, QtWidgets
from onionshare import strings, common, web
from onionshare import strings
from onionshare.common import Common
from onionshare.web import Web
from onionshare.onion import Onion
from onionshare.onionshare import OnionShare
from onionshare.settings import Settings
from .onionshare_gui import OnionShareGui
@ -34,9 +35,8 @@ class Application(QtWidgets.QApplication):
This is Qt's QApplication class. It has been overridden to support threads
and the quick keyboard shortcut.
"""
def __init__(self):
system = common.get_platform()
if system == 'Linux' or system == 'BSD':
def __init__(self, common):
if common.platform == 'Linux' or common.platform == 'BSD':
self.setAttribute(QtCore.Qt.AA_X11InitThreads, True)
QtWidgets.QApplication.__init__(self, sys.argv)
self.installEventFilter(self)
@ -53,12 +53,14 @@ def main():
"""
The main() function implements all of the logic that the GUI version of onionshare uses.
"""
common = Common()
strings.load_strings(common)
print(strings._('version_string').format(common.get_version()))
print(strings._('version_string').format(common.version))
# Start the Qt app
global qtapp
qtapp = Application()
qtapp = Application(common)
# Parse arguments
parser = argparse.ArgumentParser(formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=48))
@ -83,32 +85,32 @@ def main():
debug = bool(args.debug)
# Debug mode?
if debug:
common.set_debug(debug)
web.debug_mode()
common.debug = debug
# Validation
if filenames:
valid = True
for filename in filenames:
if not os.path.isfile(filename) and not os.path.isdir(filename):
Alert(strings._("not_a_file", True).format(filename))
Alert(common, strings._("not_a_file", True).format(filename))
valid = False
if not os.access(filename, os.R_OK):
Alert(strings._("not_a_readable_file", True).format(filename))
Alert(common, strings._("not_a_readable_file", True).format(filename))
valid = False
if not valid:
sys.exit()
# Create the Web object
web = Web(common, stay_open, True)
# Start the Onion
onion = Onion()
onion = Onion(common)
# Start the OnionShare app
web.set_stay_open(stay_open)
app = OnionShare(onion, local_only, stay_open, shutdown_timeout)
app = OnionShare(common, onion, local_only, stay_open, shutdown_timeout)
# Launch the gui
gui = OnionShareGui(onion, qtapp, app, filenames, config)
gui = OnionShareGui(common, web, onion, qtapp, app, filenames, config, local_only)
# Clean up when app quits
def shutdown():

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -19,18 +19,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import common
class Alert(QtWidgets.QMessageBox):
"""
An alert box dialog.
"""
def __init__(self, message, icon=QtWidgets.QMessageBox.NoIcon, buttons=QtWidgets.QMessageBox.Ok, autostart=True):
def __init__(self, common, message, icon=QtWidgets.QMessageBox.NoIcon, buttons=QtWidgets.QMessageBox.Ok, autostart=True):
super(Alert, self).__init__(None)
common.log('Alert', '__init__')
self.common = common
self.common.log('Alert', '__init__')
self.setWindowTitle("OnionShare")
self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png')))
self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png')))
self.setText(message)
self.setIcon(icon)
self.setStandardButtons(buttons)

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -19,13 +19,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import time
from PyQt5 import QtCore, QtWidgets
from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings, common
from onionshare import strings
class Download(object):
def __init__(self, download_id, total_bytes):
def __init__(self, common, download_id, total_bytes):
self.common = common
self.download_id = download_id
self.started = time.time()
self.total_bytes = total_bytes
@ -64,7 +66,7 @@ class Download(object):
self.progress_bar.setValue(downloaded_bytes)
if downloaded_bytes == self.progress_bar.total_bytes:
pb_fmt = strings._('gui_download_progress_complete').format(
common.format_seconds(time.time() - self.started))
self.common.format_seconds(time.time() - self.started))
else:
elapsed = time.time() - self.started
if elapsed < 10:
@ -72,10 +74,10 @@ class Download(object):
# This prevents a "Windows copy dialog"-esque experience at
# the beginning of the download.
pb_fmt = strings._('gui_download_progress_starting').format(
common.human_readable_filesize(downloaded_bytes))
self.common.human_readable_filesize(downloaded_bytes))
else:
pb_fmt = strings._('gui_download_progress_eta').format(
common.human_readable_filesize(downloaded_bytes),
self.common.human_readable_filesize(downloaded_bytes),
self.estimated_time_remaining)
self.progress_bar.setFormat(pb_fmt)
@ -85,7 +87,7 @@ class Download(object):
@property
def estimated_time_remaining(self):
return common.estimated_time_remaining(self.downloaded_bytes,
return self.common.estimated_time_remaining(self.downloaded_bytes,
self.total_bytes,
self.started)
@ -95,22 +97,45 @@ class Downloads(QtWidgets.QWidget):
The downloads chunk of the GUI. This lists all of the active download
progress bars.
"""
def __init__(self):
def __init__(self, common):
super(Downloads, self).__init__()
self.common = common
self.downloads = {}
self.downloads_container = QtWidgets.QScrollArea()
self.downloads_container.setWidget(self)
self.downloads_container.setWindowTitle(strings._('gui_downloads', True))
self.downloads_container.setWidgetResizable(True)
self.downloads_container.setMaximumHeight(600)
self.downloads_container.setMinimumHeight(150)
self.downloads_container.setMinimumWidth(350)
self.downloads_container.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png')))
self.downloads_container.setWindowFlags(QtCore.Qt.Sheet | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.CustomizeWindowHint)
self.downloads_container.vbar = self.downloads_container.verticalScrollBar()
self.downloads_label = QtWidgets.QLabel(strings._('gui_downloads', True))
self.downloads_label.setStyleSheet('QLabel { font-weight: bold; font-size 14px; text-align: center; }')
self.no_downloads_label = QtWidgets.QLabel(strings._('gui_no_downloads', True))
self.downloads_layout = QtWidgets.QVBoxLayout()
self.layout = QtWidgets.QVBoxLayout()
self.layout.addWidget(self.downloads_label)
self.layout.addWidget(self.no_downloads_label)
self.layout.addLayout(self.downloads_layout)
self.layout.addStretch()
self.setLayout(self.layout)
def add_download(self, download_id, total_bytes):
"""
Add a new download progress bar.
"""
self.parent().show()
# add it to the list
download = Download(download_id, total_bytes)
download = Download(self.common, download_id, total_bytes)
self.downloads[download_id] = download
self.layout.insertWidget(-1, download.progress_bar)
self.downloads_layout.addWidget(download.progress_bar)
def update_download(self, download_id, downloaded_bytes):
"""
@ -129,6 +154,6 @@ class Downloads(QtWidgets.QWidget):
Reset the downloads back to zero
"""
for download in self.downloads.values():
self.layout.removeWidget(download.progress_bar)
self.downloads_layout.removeWidget(download.progress_bar)
download.progress_bar.close()
self.downloads = {}

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -21,21 +21,24 @@ import os
from PyQt5 import QtCore, QtWidgets, QtGui
from .alert import Alert
from onionshare import strings, common
from onionshare import strings
class DropHereLabel(QtWidgets.QLabel):
"""
When there are no files or folders in the FileList yet, display the
'drop files here' message and graphic.
"""
def __init__(self, parent, image=False):
def __init__(self, common, parent, image=False):
self.parent = parent
super(DropHereLabel, self).__init__(parent=parent)
self.common = common
self.setAcceptDrops(True)
self.setAlignment(QtCore.Qt.AlignCenter)
if image:
self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(common.get_resource_path('images/logo_transparent.png'))))
self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/logo_transparent.png'))))
else:
self.setText(strings._('gui_drag_and_drop', True))
self.setStyleSheet('color: #999999;')
@ -53,9 +56,12 @@ class DropCountLabel(QtWidgets.QLabel):
While dragging files over the FileList, this counter displays the
number of files you're dragging.
"""
def __init__(self, parent):
def __init__(self, common, parent):
self.parent = parent
super(DropCountLabel, self).__init__(parent=parent)
self.common = common
self.setAcceptDrops(True)
self.setAlignment(QtCore.Qt.AlignCenter)
self.setText(strings._('gui_drag_and_drop', True))
@ -74,16 +80,19 @@ class FileList(QtWidgets.QListWidget):
files_dropped = QtCore.pyqtSignal()
files_updated = QtCore.pyqtSignal()
def __init__(self, parent=None):
def __init__(self, common, parent=None):
super(FileList, self).__init__(parent)
self.common = common
self.setAcceptDrops(True)
self.setIconSize(QtCore.QSize(32, 32))
self.setSortingEnabled(True)
self.setMinimumHeight(205)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.drop_here_image = DropHereLabel(self, True)
self.drop_here_text = DropHereLabel(self, False)
self.drop_count = DropCountLabel(self)
self.drop_here_image = DropHereLabel(self.common, self, True)
self.drop_here_text = DropHereLabel(self.common, self, False)
self.drop_count = DropCountLabel(self.common, self)
self.resizeEvent(None)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
@ -206,7 +215,7 @@ class FileList(QtWidgets.QListWidget):
if filename not in filenames:
if not os.access(filename, os.R_OK):
Alert(strings._("not_a_readable_file", True).format(filename))
Alert(self.common, strings._("not_a_readable_file", True).format(filename))
return
fileinfo = QtCore.QFileInfo(filename)
@ -215,10 +224,10 @@ class FileList(QtWidgets.QListWidget):
if os.path.isfile(filename):
size_bytes = fileinfo.size()
size_readable = common.human_readable_filesize(size_bytes)
size_readable = self.common.human_readable_filesize(size_bytes)
else:
size_bytes = common.dir_size(filename)
size_readable = common.human_readable_filesize(size_bytes)
size_bytes = self.common.dir_size(filename)
size_readable = self.common.human_readable_filesize(size_bytes)
# Create a new item
item = QtWidgets.QListWidgetItem()
@ -245,7 +254,7 @@ class FileList(QtWidgets.QListWidget):
item.item_button = QtWidgets.QPushButton()
item.item_button.setDefault(False)
item.item_button.setFlat(True)
item.item_button.setIcon( QtGui.QIcon(common.get_resource_path('images/file_delete.png')) )
item.item_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/file_delete.png')) )
item.item_button.clicked.connect(delete_item)
item.item_button.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
@ -277,12 +286,15 @@ class FileSelection(QtWidgets.QVBoxLayout):
The list of files and folders in the GUI, as well as buttons to add and
delete the files and folders.
"""
def __init__(self):
def __init__(self, common):
super(FileSelection, self).__init__()
self.common = common
self.server_on = False
# File list
self.file_list = FileList()
self.file_list = FileList(self.common)
self.file_list.itemSelectionChanged.connect(self.update)
self.file_list.files_dropped.connect(self.update)
self.file_list.files_updated.connect(self.update)

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -17,11 +17,14 @@ 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/>.
"""
import os, threading, time
import os
import threading
import time
import queue
from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings, common, web
from onionshare.settings import Settings
from onionshare import strings, common
from onionshare.common import Common, ShutdownTimer
from onionshare.onion import *
from .tor_connection_dialog import TorConnectionDialog
@ -43,34 +46,36 @@ class OnionShareGui(QtWidgets.QMainWindow):
starting_server_step3 = QtCore.pyqtSignal()
starting_server_error = QtCore.pyqtSignal(str)
def __init__(self, onion, qtapp, app, filenames, config=False):
def __init__(self, common, web, onion, qtapp, app, filenames, config=False, local_only=False):
super(OnionShareGui, self).__init__()
self.common = common
self.common.log('OnionShareGui', '__init__')
self._initSystemTray()
common.log('OnionShareGui', '__init__')
self.web = web
self.onion = onion
self.qtapp = qtapp
self.app = app
self.local_only = local_only
self.setWindowTitle('OnionShare')
self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png')))
self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png')))
self.setMinimumWidth(430)
# Load settings
self.config = config
self.settings = Settings(self.config)
self.settings.load()
self.common.load_settings(self.config)
# File selection
self.file_selection = FileSelection()
self.file_selection = FileSelection(self.common)
if filenames:
for filename in filenames:
self.file_selection.file_list.add_file(filename)
# Server status
self.server_status = ServerStatus(self.qtapp, self.app, web, self.file_selection, self.settings)
self.server_status = ServerStatus(self.common, self.qtapp, self.app, self.web, self.file_selection)
self.server_status.server_started.connect(self.file_selection.server_started)
self.server_status.server_started.connect(self.start_server)
self.server_status.server_started.connect(self.update_server_status_indicator)
@ -102,14 +107,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.filesize_warning.hide()
# Downloads
self.downloads = Downloads()
self.downloads_container = QtWidgets.QScrollArea()
self.downloads_container.setWidget(self.downloads)
self.downloads_container.setWidgetResizable(True)
self.downloads_container.setMaximumHeight(200)
self.downloads_container.setMinimumHeight(75)
self.vbar = self.downloads_container.verticalScrollBar()
self.downloads_container.hide() # downloads start out hidden
self.downloads = Downloads(self.common)
self.new_download = False
self.downloads_in_progress = 0
self.downloads_completed = 0
@ -119,6 +117,12 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.info_label = QtWidgets.QLabel()
self.info_label.setStyleSheet('QLabel { font-size: 12px; color: #666666; }')
self.info_show_downloads = QtWidgets.QToolButton()
self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_gray.png')))
self.info_show_downloads.setCheckable(True)
self.info_show_downloads.toggled.connect(self.downloads_toggled)
self.info_show_downloads.setToolTip(strings._('gui_downloads_window_tooltip', True))
self.info_in_progress_downloads_count = QtWidgets.QLabel()
self.info_in_progress_downloads_count.setStyleSheet('QLabel { font-size: 12px; color: #666666; }')
@ -132,6 +136,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.info_layout.addStretch()
self.info_layout.addWidget(self.info_in_progress_downloads_count)
self.info_layout.addWidget(self.info_completed_downloads_count)
self.info_layout.addWidget(self.info_show_downloads)
self.info_widget = QtWidgets.QWidget()
self.info_widget.setLayout(self.info_layout)
@ -142,13 +147,13 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.settings_button.setDefault(False)
self.settings_button.setFlat(True)
self.settings_button.setFixedWidth(40)
self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings.png')) )
self.settings_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/settings.png')) )
self.settings_button.clicked.connect(self.open_settings)
# Server status indicator on the status bar
self.server_status_image_stopped = QtGui.QImage(common.get_resource_path('images/server_stopped.png'))
self.server_status_image_working = QtGui.QImage(common.get_resource_path('images/server_working.png'))
self.server_status_image_started = QtGui.QImage(common.get_resource_path('images/server_started.png'))
self.server_status_image_stopped = QtGui.QImage(self.common.get_resource_path('images/server_stopped.png'))
self.server_status_image_working = QtGui.QImage(self.common.get_resource_path('images/server_working.png'))
self.server_status_image_started = QtGui.QImage(self.common.get_resource_path('images/server_started.png'))
self.server_status_image_label = QtWidgets.QLabel()
self.server_status_image_label.setFixedWidth(20)
self.server_status_label = QtWidgets.QLabel()
@ -189,7 +194,6 @@ class OnionShareGui(QtWidgets.QMainWindow):
primary_action_layout = QtWidgets.QVBoxLayout()
primary_action_layout.addWidget(self.server_status)
primary_action_layout.addWidget(self.filesize_warning)
primary_action_layout.addWidget(self.downloads_container)
self.primary_action = QtWidgets.QWidget()
self.primary_action.setLayout(primary_action_layout)
self.primary_action.hide()
@ -216,10 +220,11 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.timer.timeout.connect(self.check_for_requests)
# Start the "Connecting to Tor" dialog, which calls onion.connect()
tor_con = TorConnectionDialog(self.qtapp, self.settings, self.onion)
tor_con = TorConnectionDialog(self.common, self.qtapp, self.onion)
tor_con.canceled.connect(self._tor_connection_canceled)
tor_con.open_settings.connect(self._tor_connection_open_settings)
tor_con.start()
if not self.local_only:
tor_con.start()
# Start the timer
self.timer.start(500)
@ -239,7 +244,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
for index in range(self.file_selection.file_list.count()):
item = self.file_selection.file_list.item(index)
total_size_bytes += item.size_bytes
total_size_readable = common.human_readable_filesize(total_size_bytes)
total_size_readable = self.common.human_readable_filesize(total_size_bytes)
if file_count > 1:
self.info_label.setText(strings._('gui_file_info', True).format(file_count, total_size_readable))
@ -254,7 +259,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.adjustSize()
def update_server_status_indicator(self):
common.log('OnionShareGui', 'update_server_status_indicator')
self.common.log('OnionShareGui', 'update_server_status_indicator')
# Set the status image
if self.server_status.status == self.server_status.STATUS_STOPPED:
@ -268,8 +273,6 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.server_status_label.setText(strings._('gui_status_indicator_started', True))
def _initSystemTray(self):
system = common.get_platform()
menu = QtWidgets.QMenu()
self.settingsAction = menu.addAction(strings._('gui_settings_window_title', True))
self.settingsAction.triggered.connect(self.open_settings)
@ -280,10 +283,10 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.systemTray = QtWidgets.QSystemTrayIcon(self)
# The convention is Mac systray icons are always grayscale
if system == 'Darwin':
self.systemTray.setIcon(QtGui.QIcon(common.get_resource_path('images/logo_grayscale.png')))
if self.common.platform == 'Darwin':
self.systemTray.setIcon(QtGui.QIcon(self.common.get_resource_path('images/logo_grayscale.png')))
else:
self.systemTray.setIcon(QtGui.QIcon(common.get_resource_path('images/logo.png')))
self.systemTray.setIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png')))
self.systemTray.setContextMenu(menu)
self.systemTray.show()
@ -292,10 +295,10 @@ class OnionShareGui(QtWidgets.QMainWindow):
If the user cancels before Tor finishes connecting, ask if they want to
quit, or open settings.
"""
common.log('OnionShareGui', '_tor_connection_canceled')
self.common.log('OnionShareGui', '_tor_connection_canceled')
def ask():
a = Alert(strings._('gui_tor_connection_ask', True), QtWidgets.QMessageBox.Question, buttons=QtWidgets.QMessageBox.NoButton, autostart=False)
a = Alert(self.common, strings._('gui_tor_connection_ask', True), QtWidgets.QMessageBox.Question, buttons=QtWidgets.QMessageBox.NoButton, autostart=False)
settings_button = QtWidgets.QPushButton(strings._('gui_tor_connection_ask_open_settings', True))
quit_button = QtWidgets.QPushButton(strings._('gui_tor_connection_ask_quit', True))
a.addButton(settings_button, QtWidgets.QMessageBox.AcceptRole)
@ -305,12 +308,12 @@ class OnionShareGui(QtWidgets.QMainWindow):
if a.clickedButton() == settings_button:
# Open settings
common.log('OnionShareGui', '_tor_connection_canceled', 'Settings button clicked')
self.common.log('OnionShareGui', '_tor_connection_canceled', 'Settings button clicked')
self.open_settings()
if a.clickedButton() == quit_button:
# Quit
common.log('OnionShareGui', '_tor_connection_canceled', 'Quit button clicked')
self.common.log('OnionShareGui', '_tor_connection_canceled', 'Quit button clicked')
# Wait 1ms for the event loop to finish, then quit
QtCore.QTimer.singleShot(1, self.qtapp.quit)
@ -322,7 +325,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
"""
The TorConnectionDialog wants to open the Settings dialog
"""
common.log('OnionShareGui', '_tor_connection_open_settings')
self.common.log('OnionShareGui', '_tor_connection_open_settings')
# Wait 1ms for the event loop to finish closing the TorConnectionDialog
QtCore.QTimer.singleShot(1, self.open_settings)
@ -331,27 +334,29 @@ class OnionShareGui(QtWidgets.QMainWindow):
"""
Open the SettingsDialog.
"""
common.log('OnionShareGui', 'open_settings')
self.common.log('OnionShareGui', 'open_settings')
def reload_settings():
common.log('OnionShareGui', 'open_settings', 'settings have changed, reloading')
self.settings.load()
self.common.log('OnionShareGui', 'open_settings', 'settings have changed, reloading')
self.common.settings.load()
# We might've stopped the main requests timer if a Tor connection failed.
# If we've reloaded settings, we probably succeeded in obtaining a new
# connection. If so, restart the timer.
if self.onion.is_authenticated():
if not self.timer.isActive():
self.timer.start(500)
# If there were some files listed for sharing, we should be ok to
# re-enable the 'Start Sharing' button now.
if self.server_status.file_selection.get_num_files() > 0:
self.server_status.server_button.setEnabled(True)
self.status_bar.clearMessage()
if not self.local_only:
if self.onion.is_authenticated():
if not self.timer.isActive():
self.timer.start(500)
# If there were some files listed for sharing, we should be ok to
# re-enable the 'Start Sharing' button now.
if self.server_status.file_selection.get_num_files() > 0:
self.primary_action.show()
self.info_widget.show()
self.status_bar.clearMessage()
# If we switched off the shutdown timeout setting, ensure the widget is hidden.
if not self.settings.get('shutdown_timeout'):
if not self.common.settings.get('shutdown_timeout'):
self.server_status.shutdown_timeout_container.hide()
d = SettingsDialog(self.onion, self.qtapp, self.config)
d = SettingsDialog(self.common, self.onion, self.qtapp, self.config, self.local_only)
d.settings_saved.connect(reload_settings)
d.exec_()
@ -363,23 +368,21 @@ class OnionShareGui(QtWidgets.QMainWindow):
Start the onionshare server. This uses multiple threads to start the Tor onion
server and the web app.
"""
common.log('OnionShareGui', 'start_server')
self.common.log('OnionShareGui', 'start_server')
self.set_server_active(True)
self.app.set_stealth(self.settings.get('use_stealth'))
self.app.set_stealth(self.common.settings.get('use_stealth'))
# Hide and reset the downloads if we have previously shared
self.downloads_container.hide()
self.downloads.reset_downloads()
self.reset_info_counters()
self.status_bar.clearMessage()
self.server_share_status_label.setText('')
# Reset web counters
web.download_count = 0
web.error404_count = 0
web.set_gui_mode()
self.web.download_count = 0
self.web.error404_count = 0
# start the onion service in a new thread
def start_onion_service(self):
@ -392,17 +395,17 @@ class OnionShareGui(QtWidgets.QMainWindow):
return
self.app.stay_open = not self.settings.get('close_after_first_download')
self.app.stay_open = not self.common.settings.get('close_after_first_download')
# start onionshare http service in new thread
t = threading.Thread(target=web.start, args=(self.app.port, self.app.stay_open, self.settings.get('slug')))
t = threading.Thread(target=self.web.start, args=(self.app.port, self.app.stay_open, self.common.settings.get('slug')))
t.daemon = True
t.start()
# wait for modules in thread to load, preventing a thread-related cx_Freeze crash
time.sleep(0.2)
common.log('OnionshareGui', 'start_server', 'Starting an onion thread')
self.t = OnionThread(function=start_onion_service, kwargs={'self': self})
self.common.log('OnionshareGui', 'start_server', 'Starting an onion thread')
self.t = OnionThread(self.common, function=start_onion_service, kwargs={'self': self})
self.t.daemon = True
self.t.start()
@ -410,7 +413,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
"""
Step 2 in starting the onionshare server. Zipping up files.
"""
common.log('OnionShareGui', 'start_server_step2')
self.common.log('OnionShareGui', 'start_server_step2')
# add progress bar to the status bar, indicating the compressing of files.
self._zip_progress_bar = ZipProgressBar(0)
@ -428,8 +431,8 @@ class OnionShareGui(QtWidgets.QMainWindow):
if self._zip_progress_bar != None:
self._zip_progress_bar.update_processed_size_signal.emit(x)
try:
web.set_file_info(self.filenames, processed_size_callback=_set_processed_size)
self.app.cleanup_filenames.append(web.zip_filename)
self.web.set_file_info(self.filenames, processed_size_callback=_set_processed_size)
self.app.cleanup_filenames.append(self.web.zip_filename)
self.starting_server_step3.emit()
# done
@ -447,7 +450,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
Step 3 in starting the onionshare server. This displays the large filesize
warning, if applicable.
"""
common.log('OnionShareGui', 'start_server_step3')
self.common.log('OnionShareGui', 'start_server_step3')
# Remove zip progress bar
if self._zip_progress_bar is not None:
@ -455,17 +458,17 @@ class OnionShareGui(QtWidgets.QMainWindow):
self._zip_progress_bar = None
# warn about sending large files over Tor
if web.zip_filesize >= 157286400: # 150mb
if self.web.zip_filesize >= 157286400: # 150mb
self.filesize_warning.setText(strings._("large_filesize", True))
self.filesize_warning.show()
if self.settings.get('shutdown_timeout'):
if self.common.settings.get('shutdown_timeout'):
# Convert the date value to seconds between now and then
now = QtCore.QDateTime.currentDateTime()
self.timeout = now.secsTo(self.server_status.timeout)
# Set the shutdown timeout value
if self.timeout > 0:
self.app.shutdown_timer = common.close_after_seconds(self.timeout)
self.app.shutdown_timer = ShutdownTimer(self.common, self.timeout)
self.app.shutdown_timer.start()
# The timeout has actually already passed since the user clicked Start. Probably the Onion service took too long to start.
else:
@ -476,11 +479,11 @@ class OnionShareGui(QtWidgets.QMainWindow):
"""
If there's an error when trying to start the onion service
"""
common.log('OnionShareGui', 'start_server_error')
self.common.log('OnionShareGui', 'start_server_error')
self.set_server_active(False)
Alert(error, QtWidgets.QMessageBox.Warning)
Alert(self.common, error, QtWidgets.QMessageBox.Warning)
self.server_status.stop_server()
if self._zip_progress_bar is not None:
self.status_bar.removeWidget(self._zip_progress_bar)
@ -499,11 +502,11 @@ class OnionShareGui(QtWidgets.QMainWindow):
"""
Stop the onionshare server.
"""
common.log('OnionShareGui', 'stop_server')
self.common.log('OnionShareGui', 'stop_server')
if self.server_status.status != self.server_status.STATUS_STOPPED:
try:
web.stop(self.app.port)
self.web.stop(self.app.port)
except:
# Probably we had no port to begin with (Onion service didn't start)
pass
@ -523,13 +526,12 @@ class OnionShareGui(QtWidgets.QMainWindow):
"""
Check for updates in a new thread, if enabled.
"""
system = common.get_platform()
if system == 'Windows' or system == 'Darwin':
if self.settings.get('use_autoupdate'):
if self.common.platform == 'Windows' or self.common.platform == 'Darwin':
if self.common.settings.get('use_autoupdate'):
def update_available(update_url, installed_version, latest_version):
Alert(strings._("update_available", True).format(update_url, installed_version, latest_version))
Alert(self.common, strings._("update_available", True).format(update_url, installed_version, latest_version))
self.update_thread = UpdateThread(self.onion, self.config)
self.update_thread = UpdateThread(self.common, self.onion, self.config)
self.update_thread.update_available.connect(update_available)
self.update_thread.start()
@ -540,7 +542,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
if os.path.isfile(filename):
total_size += os.path.getsize(filename)
if os.path.isdir(filename):
total_size += common.dir_size(filename)
total_size += Common.dir_size(filename)
return total_size
def check_for_requests(self):
@ -549,20 +551,22 @@ class OnionShareGui(QtWidgets.QMainWindow):
"""
self.update()
# Have we lost connection to Tor somehow?
if not self.onion.is_authenticated():
self.timer.stop()
if self.server_status.status != self.server_status.STATUS_STOPPED:
self.server_status.stop_server()
self.server_status.server_button.setEnabled(False)
self.status_bar.showMessage(strings._('gui_tor_connection_lost', True))
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('gui_tor_connection_lost', True), strings._('gui_tor_connection_error_settings', True))
if not self.local_only:
# Have we lost connection to Tor somehow?
if not self.onion.is_authenticated():
self.timer.stop()
if self.server_status.status != self.server_status.STATUS_STOPPED:
self.server_status.stop_server()
self.primary_action.hide()
self.info_widget.hide()
self.status_bar.showMessage(strings._('gui_tor_connection_lost', True))
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('gui_tor_connection_lost', True), strings._('gui_tor_connection_error_settings', True))
# scroll to the bottom of the dl progress bar log pane
# if a new download has been added
if self.new_download:
self.vbar.setValue(self.vbar.maximum())
self.downloads.downloads_container.vbar.setValue(self.downloads.downloads_container.vbar.maximum())
self.new_download = False
events = []
@ -570,34 +574,34 @@ class OnionShareGui(QtWidgets.QMainWindow):
done = False
while not done:
try:
r = web.q.get(False)
r = self.web.q.get(False)
events.append(r)
except web.queue.Empty:
except queue.Empty:
done = True
for event in events:
if event["type"] == web.REQUEST_LOAD:
if event["type"] == self.web.REQUEST_LOAD:
self.status_bar.showMessage(strings._('download_page_loaded', True))
elif event["type"] == web.REQUEST_DOWNLOAD:
self.downloads_container.show() # show the downloads layout
elif event["type"] == self.web.REQUEST_DOWNLOAD:
self.downloads.no_downloads_label.hide()
self.downloads.add_download(event["data"]["id"], web.zip_filesize)
self.new_download = True
self.downloads_in_progress += 1
self.update_downloads_in_progress(self.downloads_in_progress)
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
if self.systemTray.supportsMessages() and self.common.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True))
elif event["type"] == web.REQUEST_RATE_LIMIT:
elif event["type"] == self.web.REQUEST_RATE_LIMIT:
self.stop_server()
Alert(strings._('error_rate_limit'), QtWidgets.QMessageBox.Critical)
Alert(self.common, strings._('error_rate_limit'), QtWidgets.QMessageBox.Critical)
elif event["type"] == web.REQUEST_PROGRESS:
elif event["type"] == self.web.REQUEST_PROGRESS:
self.downloads.update_download(event["data"]["id"], event["data"]["bytes"])
# is the download complete?
if event["data"]["bytes"] == web.zip_filesize:
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
if event["data"]["bytes"] == self.web.zip_filesize:
if self.systemTray.supportsMessages() and self.common.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True))
# Update the total 'completed downloads' info
self.downloads_completed += 1
@ -607,7 +611,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.update_downloads_in_progress(self.downloads_in_progress)
# close on finish?
if not web.get_stay_open():
if not self.web.stay_open:
self.server_status.stop_server()
self.status_bar.clearMessage()
self.server_share_status_label.setText(strings._('closing_automatically', True))
@ -618,27 +622,27 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.update_downloads_in_progress(self.downloads_in_progress)
elif event["type"] == web.REQUEST_CANCELED:
elif event["type"] == self.web.REQUEST_CANCELED:
self.downloads.cancel_download(event["data"]["id"])
# Update the 'in progress downloads' info
self.downloads_in_progress -= 1
self.update_downloads_in_progress(self.downloads_in_progress)
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
if self.systemTray.supportsMessages() and self.common.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True))
elif event["path"] != '/favicon.ico':
self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(web.error404_count, strings._('other_page_loaded', True), event["path"]))
self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(self.web.error404_count, strings._('other_page_loaded', True), event["path"]))
# If the auto-shutdown timer has stopped, stop the server
if self.server_status.status == self.server_status.STATUS_STARTED:
if self.app.shutdown_timer and self.settings.get('shutdown_timeout'):
if self.app.shutdown_timer and self.common.settings.get('shutdown_timeout'):
if self.timeout > 0:
now = QtCore.QDateTime.currentDateTime()
seconds_remaining = now.secsTo(self.server_status.timeout)
self.server_status.server_button.setText(strings._('gui_stop_server_shutdown_timeout', True).format(seconds_remaining))
if not self.app.shutdown_timer.is_alive():
# If there were no attempts to download the share, or all downloads are done, we can stop
if web.download_count == 0 or web.done:
if self.web.download_count == 0 or self.web.done:
self.server_status.stop_server()
self.status_bar.clearMessage()
self.server_share_status_label.setText(strings._('close_on_timeout', True))
@ -647,20 +651,30 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.status_bar.clearMessage()
self.server_share_status_label.setText(strings._('timeout_download_still_running', True))
def downloads_toggled(self, checked):
"""
When the 'Show/hide downloads' button is toggled, show or hide the downloads window.
"""
self.common.log('OnionShareGui', 'toggle_downloads')
if checked:
self.downloads.downloads_container.show()
else:
self.downloads.downloads_container.hide()
def copy_url(self):
"""
When the URL gets copied to the clipboard, display this in the status bar.
"""
common.log('OnionShareGui', 'copy_url')
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.common.log('OnionShareGui', 'copy_url')
if self.systemTray.supportsMessages() and self.common.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('gui_copied_url_title', True), strings._('gui_copied_url', True))
def copy_hidservauth(self):
"""
When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar.
"""
common.log('OnionShareGui', 'copy_hidservauth')
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.common.log('OnionShareGui', 'copy_hidservauth')
if self.systemTray.supportsMessages() and self.common.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('gui_copied_hidservauth_title', True), strings._('gui_copied_hidservauth', True))
def clear_message(self):
@ -687,15 +701,18 @@ class OnionShareGui(QtWidgets.QMainWindow):
"""
self.update_downloads_completed(0)
self.update_downloads_in_progress(0)
self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_gray.png')))
self.downloads.no_downloads_label.show()
self.downloads.downloads_container.resize(self.downloads.downloads_container.sizeHint())
def update_downloads_completed(self, count):
"""
Update the 'Downloads completed' info widget.
"""
if count == 0:
self.info_completed_downloads_image = common.get_resource_path('images/download_completed_none.png')
self.info_completed_downloads_image = self.common.get_resource_path('images/download_completed_none.png')
else:
self.info_completed_downloads_image = common.get_resource_path('images/download_completed.png')
self.info_completed_downloads_image = self.common.get_resource_path('images/download_completed.png')
self.info_completed_downloads_count.setText('<img src="{0:s}" /> {1:d}'.format(self.info_completed_downloads_image, count))
self.info_completed_downloads_count.setToolTip(strings._('info_completed_downloads_tooltip', True).format(count))
@ -704,17 +721,18 @@ class OnionShareGui(QtWidgets.QMainWindow):
Update the 'Downloads in progress' info widget.
"""
if count == 0:
self.info_in_progress_downloads_image = common.get_resource_path('images/download_in_progress_none.png')
self.info_in_progress_downloads_image = self.common.get_resource_path('images/download_in_progress_none.png')
else:
self.info_in_progress_downloads_image = common.get_resource_path('images/download_in_progress.png')
self.info_in_progress_downloads_image = self.common.get_resource_path('images/download_in_progress.png')
self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_green.png')))
self.info_in_progress_downloads_count.setText('<img src="{0:s}" /> {1:d}'.format(self.info_in_progress_downloads_image, count))
self.info_in_progress_downloads_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(count))
def closeEvent(self, e):
common.log('OnionShareGui', 'closeEvent')
self.common.log('OnionShareGui', 'closeEvent')
try:
if self.server_status.status != self.server_status.STATUS_STOPPED:
common.log('OnionShareGui', 'closeEvent, opening warning dialog')
self.common.log('OnionShareGui', 'closeEvent, opening warning dialog')
dialog = QtWidgets.QMessageBox()
dialog.setWindowTitle(strings._('gui_quit_title', True))
dialog.setText(strings._('gui_quit_warning', True))
@ -799,9 +817,12 @@ class OnionThread(QtCore.QThread):
decided to cancel (in which case do not proceed with obtaining
the Onion address and starting the web server).
"""
def __init__(self, function, kwargs=None):
def __init__(self, common, function, kwargs=None):
super(OnionThread, self).__init__()
common.log('OnionThread', '__init__')
self.common = common
self.common.log('OnionThread', '__init__')
self.function = function
if not kwargs:
self.kwargs = {}
@ -809,6 +830,6 @@ class OnionThread(QtCore.QThread):
self.kwargs = kwargs
def run(self):
common.log('OnionThread', 'run')
self.common.log('OnionThread', 'run')
self.function(**self.kwargs)

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -21,7 +21,7 @@ import platform
from .alert import Alert
from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings, common, settings
from onionshare import strings
class ServerStatus(QtWidgets.QWidget):
"""
@ -38,8 +38,11 @@ class ServerStatus(QtWidgets.QWidget):
STATUS_WORKING = 1
STATUS_STARTED = 2
def __init__(self, qtapp, app, web, file_selection, settings):
def __init__(self, common, qtapp, app, web, file_selection):
super(ServerStatus, self).__init__()
self.common = common
self.status = self.STATUS_STOPPED
self.qtapp = qtapp
@ -47,8 +50,6 @@ class ServerStatus(QtWidgets.QWidget):
self.web = web
self.file_selection = file_selection
self.settings = settings
# Shutdown timeout layout
self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True))
self.shutdown_timeout = QtWidgets.QDateTimeEdit()
@ -129,16 +130,16 @@ class ServerStatus(QtWidgets.QWidget):
if self.status == self.STATUS_STARTED:
self.url_description.show()
info_image = common.get_resource_path('images/info.png')
info_image = self.common.get_resource_path('images/info.png')
self.url_description.setText(strings._('gui_url_description', True).format(info_image))
# Show a Tool Tip explaining the lifecycle of this URL
if self.settings.get('save_private_key'):
if self.settings.get('close_after_first_download'):
if self.common.settings.get('save_private_key'):
if self.common.settings.get('close_after_first_download'):
self.url_description.setToolTip(strings._('gui_url_label_onetime_and_persistent', True))
else:
self.url_description.setToolTip(strings._('gui_url_label_persistent', True))
else:
if self.settings.get('close_after_first_download'):
if self.common.settings.get('close_after_first_download'):
self.url_description.setToolTip(strings._('gui_url_label_onetime', True))
else:
self.url_description.setToolTip(strings._('gui_url_label_stay_open', True))
@ -148,12 +149,12 @@ class ServerStatus(QtWidgets.QWidget):
self.copy_url_button.show()
if self.settings.get('save_private_key'):
if not self.settings.get('slug'):
self.settings.set('slug', self.web.slug)
self.settings.save()
if self.common.settings.get('save_private_key'):
if not self.common.settings.get('slug'):
self.common.settings.set('slug', self.web.slug)
self.common.settings.save()
if self.settings.get('shutdown_timeout'):
if self.common.settings.get('shutdown_timeout'):
self.shutdown_timeout_container.hide()
if self.app.stealth:
@ -180,26 +181,26 @@ class ServerStatus(QtWidgets.QWidget):
self.server_button.setEnabled(True)
self.server_button.setText(strings._('gui_start_server', True))
self.server_button.setToolTip('')
if self.settings.get('shutdown_timeout'):
if self.common.settings.get('shutdown_timeout'):
self.shutdown_timeout_container.show()
elif self.status == self.STATUS_STARTED:
self.server_button.setStyleSheet(button_started_style)
self.server_button.setEnabled(True)
self.server_button.setText(strings._('gui_stop_server', True))
if self.settings.get('shutdown_timeout'):
if self.common.settings.get('shutdown_timeout'):
self.shutdown_timeout_container.hide()
self.server_button.setToolTip(strings._('gui_stop_server_shutdown_timeout_tooltip', True).format(self.timeout))
elif self.status == self.STATUS_WORKING:
self.server_button.setStyleSheet(button_working_style)
self.server_button.setEnabled(True)
self.server_button.setText(strings._('gui_please_wait'))
if self.settings.get('shutdown_timeout'):
if self.common.settings.get('shutdown_timeout'):
self.shutdown_timeout_container.hide()
else:
self.server_button.setStyleSheet(button_working_style)
self.server_button.setEnabled(False)
self.server_button.setText(strings._('gui_please_wait'))
if self.settings.get('shutdown_timeout'):
if self.common.settings.get('shutdown_timeout'):
self.shutdown_timeout_container.hide()
def server_button_clicked(self):
@ -207,12 +208,12 @@ class ServerStatus(QtWidgets.QWidget):
Toggle starting or stopping the server.
"""
if self.status == self.STATUS_STOPPED:
if self.settings.get('shutdown_timeout'):
if self.common.settings.get('shutdown_timeout'):
# Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen
self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0)
# If the timeout has actually passed already before the user hit Start, refuse to start the server.
if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.timeout:
Alert(strings._('gui_server_timeout_expired', QtWidgets.QMessageBox.Warning))
Alert(self.common, strings._('gui_server_timeout_expired', QtWidgets.QMessageBox.Warning))
else:
self.start_server()
else:
@ -252,7 +253,7 @@ class ServerStatus(QtWidgets.QWidget):
"""
Cancel the server.
"""
common.log('ServerStatus', 'cancel_server', 'Canceling the server mid-startup')
self.common.log('ServerStatus', 'cancel_server', 'Canceling the server mid-startup')
self.status = self.STATUS_WORKING
self.shutdown_timeout_reset()
self.update()

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -34,17 +34,21 @@ class SettingsDialog(QtWidgets.QDialog):
"""
settings_saved = QtCore.pyqtSignal()
def __init__(self, onion, qtapp, config=False):
def __init__(self, common, onion, qtapp, config=False, local_only=False):
super(SettingsDialog, self).__init__()
common.log('SettingsDialog', '__init__')
self.common = common
self.common.log('SettingsDialog', '__init__')
self.onion = onion
self.qtapp = qtapp
self.config = config
self.local_only = local_only
self.setModal(True)
self.setWindowTitle(strings._('gui_settings_window_title', True))
self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png')))
self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png')))
self.system = platform.system()
@ -156,7 +160,7 @@ class SettingsDialog(QtWidgets.QDialog):
# obfs4 option radio
# if the obfs4proxy binary is missing, we can't use obfs4 transports
(self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = common.get_tor_paths()
(self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = self.common.get_tor_paths()
if not os.path.isfile(self.obfs4proxy_file_path):
self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy', True))
self.tor_bridges_use_obfs4_radio.setEnabled(False)
@ -166,7 +170,7 @@ class SettingsDialog(QtWidgets.QDialog):
# meek_lite-amazon option radio
# if the obfs4proxy binary is missing, we can't use meek_lite-amazon transports
(self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = common.get_tor_paths()
(self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = self.common.get_tor_paths()
if not os.path.isfile(self.obfs4proxy_file_path):
self.tor_bridges_use_meek_lite_amazon_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_amazon_radio_option_no_obfs4proxy', True))
self.tor_bridges_use_meek_lite_amazon_radio.setEnabled(False)
@ -176,7 +180,7 @@ class SettingsDialog(QtWidgets.QDialog):
# meek_lite-azure option radio
# if the obfs4proxy binary is missing, we can't use meek_lite-azure transports
(self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = common.get_tor_paths()
(self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = self.common.get_tor_paths()
if not os.path.isfile(self.obfs4proxy_file_path):
self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy', True))
self.tor_bridges_use_meek_lite_azure_radio.setEnabled(False)
@ -290,10 +294,6 @@ class SettingsDialog(QtWidgets.QDialog):
self.authenticate_group = QtWidgets.QGroupBox(strings._("gui_settings_authenticate_label", True))
self.authenticate_group.setLayout(authenticate_group_layout)
# Test tor settings button
self.connection_type_test_button = QtWidgets.QPushButton(strings._('gui_settings_connection_type_test_button', True))
self.connection_type_test_button.clicked.connect(self.test_tor_clicked)
# Put the radios into their own group so they are exclusive
connection_type_radio_group_layout = QtWidgets.QVBoxLayout()
connection_type_radio_group_layout.addWidget(self.connection_type_bundled_radio)
@ -303,16 +303,6 @@ class SettingsDialog(QtWidgets.QDialog):
connection_type_radio_group = QtWidgets.QGroupBox(strings._("gui_settings_connection_type_label", True))
connection_type_radio_group.setLayout(connection_type_radio_group_layout)
# Connection type layout
connection_type_group_layout = QtWidgets.QVBoxLayout()
connection_type_group_layout.addWidget(self.connection_type_control_port_extras)
connection_type_group_layout.addWidget(self.connection_type_socket_file_extras)
connection_type_group_layout.addWidget(self.connection_type_socks)
connection_type_group_layout.addWidget(self.authenticate_group)
connection_type_group_layout.addWidget(self.connection_type_test_button)
connection_type_group = QtWidgets.QGroupBox()
connection_type_group.setLayout(connection_type_group_layout)
# The Bridges options are not exclusive (enabling Bridges offers obfs4 or custom bridges)
connection_type_bridges_radio_group_layout = QtWidgets.QVBoxLayout()
connection_type_bridges_radio_group_layout.addWidget(self.bridges)
@ -320,12 +310,28 @@ class SettingsDialog(QtWidgets.QDialog):
self.connection_type_bridges_radio_group.setLayout(connection_type_bridges_radio_group_layout)
self.connection_type_bridges_radio_group.hide()
# Test tor settings button
self.connection_type_test_button = QtWidgets.QPushButton(strings._('gui_settings_connection_type_test_button', True))
self.connection_type_test_button.clicked.connect(self.test_tor_clicked)
connection_type_test_button_layout = QtWidgets.QHBoxLayout()
connection_type_test_button_layout.addWidget(self.connection_type_test_button)
connection_type_test_button_layout.addStretch()
# Connection type layout
connection_type_layout = QtWidgets.QVBoxLayout()
connection_type_layout.addWidget(self.connection_type_control_port_extras)
connection_type_layout.addWidget(self.connection_type_socket_file_extras)
connection_type_layout.addWidget(self.connection_type_socks)
connection_type_layout.addWidget(self.authenticate_group)
connection_type_layout.addWidget(self.connection_type_bridges_radio_group)
connection_type_layout.addLayout(connection_type_test_button_layout)
# Buttons
self.save_button = QtWidgets.QPushButton(strings._('gui_settings_button_save', True))
self.save_button.clicked.connect(self.save_clicked)
self.cancel_button = QtWidgets.QPushButton(strings._('gui_settings_button_cancel', True))
self.cancel_button.clicked.connect(self.cancel_clicked)
version_label = QtWidgets.QLabel('OnionShare {0:s}'.format(common.get_version()))
version_label = QtWidgets.QLabel('OnionShare {0:s}'.format(self.common.version))
version_label.setStyleSheet('color: #666666')
self.help_button = QtWidgets.QPushButton(strings._('gui_settings_button_help', True))
self.help_button.clicked.connect(self.help_clicked)
@ -350,8 +356,7 @@ class SettingsDialog(QtWidgets.QDialog):
right_col_layout = QtWidgets.QVBoxLayout()
right_col_layout.addWidget(connection_type_radio_group)
right_col_layout.addWidget(connection_type_group)
right_col_layout.addWidget(self.connection_type_bridges_radio_group)
right_col_layout.addLayout(connection_type_layout)
right_col_layout.addWidget(self.tor_status)
right_col_layout.addStretch()
@ -367,7 +372,7 @@ class SettingsDialog(QtWidgets.QDialog):
self.cancel_button.setFocus()
# Load settings, and fill them in
self.old_settings = Settings(self.config)
self.old_settings = Settings(self.common, self.config)
self.old_settings.load()
close_after_first_download = self.old_settings.get('close_after_first_download')
@ -465,7 +470,7 @@ class SettingsDialog(QtWidgets.QDialog):
"""
Connection type bundled was toggled. If checked, hide authentication fields.
"""
common.log('SettingsDialog', 'connection_type_bundled_toggled')
self.common.log('SettingsDialog', 'connection_type_bundled_toggled')
if checked:
self.authenticate_group.hide()
self.connection_type_socks.hide()
@ -491,6 +496,9 @@ class SettingsDialog(QtWidgets.QDialog):
"""
if checked:
self.tor_bridges_use_custom_textbox_options.hide()
# Alert the user about meek's costliness if it looks like they're turning it on
if not self.old_settings.get('tor_bridges_use_meek_lite_amazon'):
Alert(strings._('gui_settings_meek_lite_expensive_warning', True), QtWidgets.QMessageBox.Warning)
def tor_bridges_use_meek_lite_azure_radio_toggled(self, checked):
"""
@ -498,6 +506,9 @@ class SettingsDialog(QtWidgets.QDialog):
"""
if checked:
self.tor_bridges_use_custom_textbox_options.hide()
# Alert the user about meek's costliness if it looks like they're turning it on
if not self.old_settings.get('tor_bridges_use_meek_lite_azure'):
Alert(strings._('gui_settings_meek_lite_expensive_warning', True), QtWidgets.QMessageBox.Warning)
def tor_bridges_use_custom_radio_toggled(self, checked):
"""
@ -510,7 +521,7 @@ class SettingsDialog(QtWidgets.QDialog):
"""
Connection type automatic was toggled. If checked, hide authentication fields.
"""
common.log('SettingsDialog', 'connection_type_automatic_toggled')
self.common.log('SettingsDialog', 'connection_type_automatic_toggled')
if checked:
self.authenticate_group.hide()
self.connection_type_socks.hide()
@ -521,7 +532,7 @@ class SettingsDialog(QtWidgets.QDialog):
Connection type control port was toggled. If checked, show extra fields
for Tor control address and port. If unchecked, hide those extra fields.
"""
common.log('SettingsDialog', 'connection_type_control_port_toggled')
self.common.log('SettingsDialog', 'connection_type_control_port_toggled')
if checked:
self.authenticate_group.show()
self.connection_type_control_port_extras.show()
@ -536,7 +547,7 @@ class SettingsDialog(QtWidgets.QDialog):
Connection type socket file was toggled. If checked, show extra fields
for socket file. If unchecked, hide those extra fields.
"""
common.log('SettingsDialog', 'connection_type_socket_file_toggled')
self.common.log('SettingsDialog', 'connection_type_socket_file_toggled')
if checked:
self.authenticate_group.show()
self.connection_type_socket_file_extras.show()
@ -549,14 +560,14 @@ class SettingsDialog(QtWidgets.QDialog):
"""
Authentication option no authentication was toggled.
"""
common.log('SettingsDialog', 'authenticate_no_auth_toggled')
self.common.log('SettingsDialog', 'authenticate_no_auth_toggled')
def authenticate_password_toggled(self, checked):
"""
Authentication option password was toggled. If checked, show extra fields
for password auth. If unchecked, hide those extra fields.
"""
common.log('SettingsDialog', 'authenticate_password_toggled')
self.common.log('SettingsDialog', 'authenticate_password_toggled')
if checked:
self.authenticate_password_extras.show()
else:
@ -567,7 +578,7 @@ class SettingsDialog(QtWidgets.QDialog):
Toggle the 'Copy HidServAuth' button
to copy the saved HidServAuth to clipboard.
"""
common.log('SettingsDialog', 'hidservauth_copy_button_clicked', 'HidServAuth was copied to clipboard')
self.common.log('SettingsDialog', 'hidservauth_copy_button_clicked', 'HidServAuth was copied to clipboard')
clipboard = self.qtapp.clipboard()
clipboard.setText(self.old_settings.get('hidservauth_string'))
@ -576,7 +587,7 @@ class SettingsDialog(QtWidgets.QDialog):
Test Tor Settings button clicked. With the given settings, see if we can
successfully connect and authenticate to Tor.
"""
common.log('SettingsDialog', 'test_tor_clicked')
self.common.log('SettingsDialog', 'test_tor_clicked')
settings = self.settings_from_fields()
try:
@ -591,17 +602,17 @@ class SettingsDialog(QtWidgets.QDialog):
else:
tor_status_update_func = None
onion = Onion()
onion.connect(settings=settings, config=self.config, tor_status_update_func=tor_status_update_func)
onion = Onion(self.common)
onion.connect(custom_settings=settings, config=self.config, tor_status_update_func=tor_status_update_func)
# If an exception hasn't been raised yet, the Tor settings work
Alert(strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth))
Alert(self.common, strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth))
# Clean up
onion.cleanup()
except (TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorNotSupported, BundledTorTimeout) as e:
Alert(e.args[0], QtWidgets.QMessageBox.Warning)
Alert(self.common, e.args[0], QtWidgets.QMessageBox.Warning)
if settings.get('connection_type') == 'bundled':
self.tor_status.hide()
self._enable_buttons()
@ -610,14 +621,14 @@ class SettingsDialog(QtWidgets.QDialog):
"""
Check for Updates button clicked. Manually force an update check.
"""
common.log('SettingsDialog', 'check_for_updates')
self.common.log('SettingsDialog', 'check_for_updates')
# Disable buttons
self._disable_buttons()
self.qtapp.processEvents()
def update_timestamp():
# Update the last checked label
settings = Settings(self.config)
settings = Settings(self.common, self.config)
settings.load()
autoupdate_timestamp = settings.get('autoupdate_timestamp')
self._update_autoupdate_timestamp(autoupdate_timestamp)
@ -631,22 +642,22 @@ class SettingsDialog(QtWidgets.QDialog):
# Check for updates
def update_available(update_url, installed_version, latest_version):
Alert(strings._("update_available", True).format(update_url, installed_version, latest_version))
Alert(self.common, strings._("update_available", True).format(update_url, installed_version, latest_version))
close_forced_update_thread()
def update_not_available():
Alert(strings._('update_not_available', True))
Alert(self.common, strings._('update_not_available', True))
close_forced_update_thread()
def update_error():
Alert(strings._('update_error_check_error', True), QtWidgets.QMessageBox.Warning)
Alert(self.common, strings._('update_error_check_error', True), QtWidgets.QMessageBox.Warning)
close_forced_update_thread()
def update_invalid_version():
Alert(strings._('update_error_invalid_latest_version', True).format(e.latest_version), QtWidgets.QMessageBox.Warning)
Alert(self.common, strings._('update_error_invalid_latest_version', True).format(e.latest_version), QtWidgets.QMessageBox.Warning)
close_forced_update_thread()
forced_update_thread = UpdateThread(self.onion, self.config, force=True)
forced_update_thread = UpdateThread(self.common, self.onion, self.config, force=True)
forced_update_thread.update_available.connect(update_available)
forced_update_thread.update_not_available.connect(update_not_available)
forced_update_thread.update_error.connect(update_error)
@ -657,7 +668,7 @@ class SettingsDialog(QtWidgets.QDialog):
"""
Save button clicked. Save current settings to disk.
"""
common.log('SettingsDialog', 'save_clicked')
self.common.log('SettingsDialog', 'save_clicked')
settings = self.settings_from_fields()
if settings:
@ -666,48 +677,52 @@ class SettingsDialog(QtWidgets.QDialog):
# If Tor isn't connected, or if Tor settings have changed, Reinitialize
# the Onion object
reboot_onion = False
if self.onion.is_authenticated():
common.log('SettingsDialog', 'save_clicked', 'Connected to Tor')
def changed(s1, s2, keys):
"""
Compare the Settings objects s1 and s2 and return true if any values
have changed for the given keys.
"""
for key in keys:
if s1.get(key) != s2.get(key):
return True
return False
if not self.local_only:
if self.onion.is_authenticated():
self.common.log('SettingsDialog', 'save_clicked', 'Connected to Tor')
def changed(s1, s2, keys):
"""
Compare the Settings objects s1 and s2 and return true if any values
have changed for the given keys.
"""
for key in keys:
if s1.get(key) != s2.get(key):
return True
return False
if changed(settings, self.old_settings, [
'connection_type', 'control_port_address',
'control_port_port', 'socks_address', 'socks_port',
'socket_file_path', 'auth_type', 'auth_password',
'no_bridges', 'tor_bridges_use_obfs4',
'tor_bridges_use_meek_lite_amazon', 'tor_bridges_use_meek_lite_azure',
'tor_bridges_use_custom_bridges']):
if changed(settings, self.old_settings, [
'connection_type', 'control_port_address',
'control_port_port', 'socks_address', 'socks_port',
'socket_file_path', 'auth_type', 'auth_password',
'no_bridges', 'tor_bridges_use_obfs4',
'tor_bridges_use_meek_lite_amazon', 'tor_bridges_use_meek_lite_azure',
'tor_bridges_use_custom_bridges']):
reboot_onion = True
else:
self.common.log('SettingsDialog', 'save_clicked', 'Not connected to Tor')
# Tor isn't connected, so try connecting
reboot_onion = True
else:
common.log('SettingsDialog', 'save_clicked', 'Not connected to Tor')
# Tor isn't connected, so try connecting
reboot_onion = True
# Do we need to reinitialize Tor?
if reboot_onion:
# Reinitialize the Onion object
self.common.log('SettingsDialog', 'save_clicked', 'rebooting the Onion')
self.onion.cleanup()
# Do we need to reinitialize Tor?
if reboot_onion:
# Reinitialize the Onion object
common.log('SettingsDialog', 'save_clicked', 'rebooting the Onion')
self.onion.cleanup()
tor_con = TorConnectionDialog(self.qtapp, settings, self.onion)
tor_con.start()
tor_con = TorConnectionDialog(self.qtapp, settings, self.onion)
tor_con.start()
self.common.log('SettingsDialog', 'save_clicked', 'Onion done rebooting, connected to Tor: {}'.format(self.onion.connected_to_tor))
common.log('SettingsDialog', 'save_clicked', 'Onion done rebooting, connected to Tor: {}'.format(self.onion.connected_to_tor))
if self.onion.is_authenticated() and not tor_con.wasCanceled():
self.settings_saved.emit()
self.close()
if self.onion.is_authenticated() and not tor_con.wasCanceled():
else:
self.settings_saved.emit()
self.close()
else:
self.settings_saved.emit()
self.close()
@ -716,9 +731,9 @@ class SettingsDialog(QtWidgets.QDialog):
"""
Cancel button clicked.
"""
common.log('SettingsDialog', 'cancel_clicked')
self.common.log('SettingsDialog', 'cancel_clicked')
if not self.onion.is_authenticated():
Alert(strings._('gui_tor_connection_canceled', True), QtWidgets.QMessageBox.Warning)
Alert(self.common, strings._('gui_tor_connection_canceled', True), QtWidgets.QMessageBox.Warning)
sys.exit()
else:
self.close()
@ -727,7 +742,7 @@ class SettingsDialog(QtWidgets.QDialog):
"""
Help button clicked.
"""
common.log('SettingsDialog', 'help_clicked')
self.common.log('SettingsDialog', 'help_clicked')
help_site = 'https://github.com/micahflee/onionshare/wiki'
QtGui.QDesktopServices.openUrl(QtCore.QUrl(help_site))
@ -735,8 +750,8 @@ class SettingsDialog(QtWidgets.QDialog):
"""
Return a Settings object that's full of values from the settings dialog.
"""
common.log('SettingsDialog', 'settings_from_fields')
settings = Settings(self.config)
self.common.log('SettingsDialog', 'settings_from_fields')
settings = Settings(self.common, self.config)
settings.load() # To get the last update timestamp
settings.set('close_after_first_download', self.close_after_first_download_checkbox.isChecked())
@ -839,24 +854,25 @@ class SettingsDialog(QtWidgets.QDialog):
new_bridges = ''.join(new_bridges)
settings.set('tor_bridges_use_custom_bridges', new_bridges)
else:
Alert(strings._('gui_settings_tor_bridges_invalid', True))
Alert(self.common, strings._('gui_settings_tor_bridges_invalid', True))
settings.set('no_bridges', True)
return False
return settings
def closeEvent(self, e):
common.log('SettingsDialog', 'closeEvent')
self.common.log('SettingsDialog', 'closeEvent')
# On close, if Tor isn't connected, then quit OnionShare altogether
if not self.onion.is_authenticated():
common.log('SettingsDialog', 'closeEvent', 'Closing while not connected to Tor')
if not self.local_only:
if not self.onion.is_authenticated():
self.common.log('SettingsDialog', 'closeEvent', 'Closing while not connected to Tor')
# Wait 1ms for the event loop to finish, then quit
QtCore.QTimer.singleShot(1, self.qtapp.quit)
# Wait 1ms for the event loop to finish, then quit
QtCore.QTimer.singleShot(1, self.qtapp.quit)
def _update_autoupdate_timestamp(self, autoupdate_timestamp):
common.log('SettingsDialog', '_update_autoupdate_timestamp')
self.common.log('SettingsDialog', '_update_autoupdate_timestamp')
if autoupdate_timestamp:
dt = datetime.datetime.fromtimestamp(autoupdate_timestamp)
@ -873,7 +889,7 @@ class SettingsDialog(QtWidgets.QDialog):
self._enable_buttons()
def _disable_buttons(self):
common.log('SettingsDialog', '_disable_buttons')
self.common.log('SettingsDialog', '_disable_buttons')
self.check_for_updates_button.setEnabled(False)
self.connection_type_test_button.setEnabled(False)
@ -881,7 +897,7 @@ class SettingsDialog(QtWidgets.QDialog):
self.cancel_button.setEnabled(False)
def _enable_buttons(self):
common.log('SettingsDialog', '_enable_buttons')
self.common.log('SettingsDialog', '_enable_buttons')
# We can't check for updates if we're still not connected to Tor
if not self.onion.connected_to_tor:
self.check_for_updates_button.setEnabled(False)

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings, common
from onionshare import strings
from onionshare.onion import *
from .alert import Alert
@ -30,16 +30,23 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
"""
open_settings = QtCore.pyqtSignal()
def __init__(self, qtapp, settings, onion):
def __init__(self, common, qtapp, onion, custom_settings=False):
super(TorConnectionDialog, self).__init__(None)
common.log('TorConnectionDialog', '__init__')
self.common = common
if custom_settings:
self.settings = custom_settings
else:
self.settings = self.common.settings
self.common.log('TorConnectionDialog', '__init__')
self.qtapp = qtapp
self.settings = settings
self.onion = onion
self.setWindowTitle("OnionShare")
self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png')))
self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png')))
self.setModal(True)
self.setFixedSize(400, 150)
@ -55,9 +62,9 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
self._tor_status_update(0, '')
def start(self):
common.log('TorConnectionDialog', 'start')
self.common.log('TorConnectionDialog', 'start')
t = TorConnectionThread(self, self.settings, self.onion)
t = TorConnectionThread(self.common, self.settings, self, self.onion)
t.tor_status_update.connect(self._tor_status_update)
t.connected_to_tor.connect(self._connected_to_tor)
t.canceled_connecting_to_tor.connect(self._canceled_connecting_to_tor)
@ -77,14 +84,14 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
self.setLabelText("<strong>{}</strong><br>{}".format(strings._('connecting_to_tor', True), summary))
def _connected_to_tor(self):
common.log('TorConnectionDialog', '_connected_to_tor')
self.common.log('TorConnectionDialog', '_connected_to_tor')
self.active = False
# Close the dialog after connecting
self.setValue(self.maximum())
def _canceled_connecting_to_tor(self):
common.log('TorConnectionDialog', '_canceled_connecting_to_tor')
self.common.log('TorConnectionDialog', '_canceled_connecting_to_tor')
self.active = False
self.onion.cleanup()
@ -92,12 +99,12 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
QtCore.QTimer.singleShot(1, self.cancel)
def _error_connecting_to_tor(self, msg):
common.log('TorConnectionDialog', '_error_connecting_to_tor')
self.common.log('TorConnectionDialog', '_error_connecting_to_tor')
self.active = False
def alert_and_open_settings():
# Display the exception in an alert box
Alert("{}\n\n{}".format(msg, strings._('gui_tor_connection_error_settings', True)), QtWidgets.QMessageBox.Warning)
Alert(self.common, "{}\n\n{}".format(msg, strings._('gui_tor_connection_error_settings', True)), QtWidgets.QMessageBox.Warning)
# Open settings
self.open_settings.emit()
@ -113,16 +120,20 @@ class TorConnectionThread(QtCore.QThread):
canceled_connecting_to_tor = QtCore.pyqtSignal()
error_connecting_to_tor = QtCore.pyqtSignal(str)
def __init__(self, dialog, settings, onion):
def __init__(self, common, settings, dialog, onion):
super(TorConnectionThread, self).__init__()
common.log('TorConnectionThread', '__init__')
self.common = common
self.common.log('TorConnectionThread', '__init__')
self.settings = settings
self.dialog = dialog
self.settings = settings
self.onion = onion
def run(self):
common.log('TorConnectionThread', 'run')
self.common.log('TorConnectionThread', 'run')
# Connect to the Onion
try:
@ -133,11 +144,11 @@ class TorConnectionThread(QtCore.QThread):
self.canceled_connecting_to_tor.emit()
except BundledTorCanceled as e:
common.log('TorConnectionThread', 'run', 'caught exception: BundledTorCanceled')
self.common.log('TorConnectionThread', 'run', 'caught exception: BundledTorCanceled')
self.canceled_connecting_to_tor.emit()
except Exception as e:
common.log('TorConnectionThread', 'run', 'caught exception: {}'.format(e.args[0]))
self.common.log('TorConnectionThread', 'run', 'caught exception: {}'.format(e.args[0]))
self.error_connecting_to_tor.emit(str(e.args[0]))
def _tor_status_update(self, progress, summary):

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -25,7 +25,7 @@ from onionshare import socks
from onionshare.settings import Settings
from onionshare.onion import Onion
from . import strings, common
from . import strings
class UpdateCheckerCheckError(Exception):
"""
@ -55,16 +55,19 @@ class UpdateChecker(QtCore.QObject):
update_error = QtCore.pyqtSignal()
update_invalid_version = QtCore.pyqtSignal()
def __init__(self, onion, config=False):
def __init__(self, common, onion, config=False):
super(UpdateChecker, self).__init__()
common.log('UpdateChecker', '__init__')
self.common = common
self.common.log('UpdateChecker', '__init__')
self.onion = onion
self.config = config
def check(self, force=False, config=False):
common.log('UpdateChecker', 'check', 'force={}'.format(force))
self.common.log('UpdateChecker', 'check', 'force={}'.format(force))
# Load the settings
settings = Settings(config)
settings = Settings(self.common, config)
settings.load()
# If force=True, then definitely check
@ -87,11 +90,11 @@ class UpdateChecker(QtCore.QObject):
# Check for updates
if check_for_updates:
common.log('UpdateChecker', 'check', 'checking for updates')
self.common.log('UpdateChecker', 'check', 'checking for updates')
# Download the latest-version file over Tor
try:
# User agent string includes OnionShare version and platform
user_agent = 'OnionShare {}, {}'.format(common.get_version(), platform.system())
user_agent = 'OnionShare {}, {}'.format(self.common.version, self.common.platform)
# If the update is forced, add '?force=1' to the URL, to more
# accurately measure daily users
@ -104,7 +107,7 @@ class UpdateChecker(QtCore.QObject):
else:
onion_domain = 'elx57ue5uyfplgva.onion'
common.log('UpdateChecker', 'check', 'loading http://{}{}'.format(onion_domain, path))
self.common.log('UpdateChecker', 'check', 'loading http://{}{}'.format(onion_domain, path))
(socks_address, socks_port) = self.onion.get_tor_socks_port()
socks.set_default_proxy(socks.SOCKS5, socks_address, socks_port)
@ -122,10 +125,10 @@ class UpdateChecker(QtCore.QObject):
http_response = s.recv(1024)
latest_version = http_response[http_response.find(b'\r\n\r\n'):].strip().decode('utf-8')
common.log('UpdateChecker', 'check', 'latest OnionShare version: {}'.format(latest_version))
self.common.log('UpdateChecker', 'check', 'latest OnionShare version: {}'.format(latest_version))
except Exception as e:
common.log('UpdateChecker', 'check', '{}'.format(e))
self.common.log('UpdateChecker', 'check', '{}'.format(e))
self.update_error.emit()
raise UpdateCheckerCheckError
@ -145,7 +148,7 @@ class UpdateChecker(QtCore.QObject):
# Do we need to update?
update_url = 'https://github.com/micahflee/onionshare/releases/tag/v{}'.format(latest_version)
installed_version = common.get_version()
installed_version = self.common.version
if installed_version < latest_version:
self.update_available.emit(update_url, installed_version, latest_version)
return
@ -159,17 +162,20 @@ class UpdateThread(QtCore.QThread):
update_error = QtCore.pyqtSignal()
update_invalid_version = QtCore.pyqtSignal()
def __init__(self, onion, config=False, force=False):
def __init__(self, common, onion, config=False, force=False):
super(UpdateThread, self).__init__()
common.log('UpdateThread', '__init__')
self.common = common
self.common.log('UpdateThread', '__init__')
self.onion = onion
self.config = config
self.force = force
def run(self):
common.log('UpdateThread', 'run')
self.common.log('UpdateThread', 'run')
u = UpdateChecker(self.onion, self.config)
u = UpdateChecker(self.common, self.onion, self.config)
u.update_available.connect(self._update_available)
u.update_not_available.connect(self._update_not_available)
u.update_error.connect(self._update_error)
@ -179,25 +185,25 @@ class UpdateThread(QtCore.QThread):
u.check(config=self.config,force=self.force)
except Exception as e:
# If update check fails, silently ignore
common.log('UpdateThread', 'run', '{}'.format(e))
self.common.log('UpdateThread', 'run', '{}'.format(e))
pass
def _update_available(self, update_url, installed_version, latest_version):
common.log('UpdateThread', '_update_available')
self.common.log('UpdateThread', '_update_available')
self.active = False
self.update_available.emit(update_url, installed_version, latest_version)
def _update_not_available(self):
common.log('UpdateThread', '_update_not_available')
self.common.log('UpdateThread', '_update_not_available')
self.active = False
self.update_not_available.emit()
def _update_error(self):
common.log('UpdateThread', '_update_error')
self.common.log('UpdateThread', '_update_error')
self.active = False
self.update_error.emit()
def _update_invalid_version(self):
common.log('UpdateThread', '_update_invalid_version')
self.common.log('UpdateThread', '_update_invalid_version')
self.active = False
self.update_invalid_version.emit()

View File

@ -3,7 +3,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -52,7 +52,10 @@ data_files=[
(os.path.join(sys.prefix, 'share/onionshare'), file_list('share')),
(os.path.join(sys.prefix, 'share/onionshare/images'), file_list('share/images')),
(os.path.join(sys.prefix, 'share/onionshare/locale'), file_list('share/locale')),
(os.path.join(sys.prefix, 'share/onionshare/html'), file_list('share/html')),
(os.path.join(sys.prefix, 'share/onionshare/templates'), file_list('share/templates')),
(os.path.join(sys.prefix, 'share/onionshare/static/css'), file_list('share/static/css')),
(os.path.join(sys.prefix, 'share/onionshare/static/img'), file_list('share/static/img')),
(os.path.join(sys.prefix, 'share/onionshare/static/js'), file_list('share/static/js'))
]
if platform.system() != 'OpenBSD':
data_files.append(('/usr/share/nautilus-python/extensions/', ['install/scripts/onionshare-nautilus.py']))

View File

@ -1,16 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Error 404</title>
<link href="data:image/x-icon;base64,{{favicon_b64}}" rel="icon" type="image/x-icon" />
<style type="text/css">
body {
background-color: #FFC4D5;
color: #FF0048;
text-align: center;
font-size: 20em;
}
</style>
</head>
<body>404</body>
</html>

View File

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>OnionShare</title>
<link href="data:image/x-icon;base64,{{favicon_b64}}" rel="icon" type="image/x-icon" />
<style>
body {
background-color: #222222;
color: #ffffff;
text-align: center;
font-family: sans-serif;
padding: 5em 1em;
}
</style>
</head>
<body>
<p>OnionShare download in progress</p>
</body>
</html>

View File

@ -1,208 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>OnionShare</title>
<link href="data:image/x-icon;base64,{{favicon_b64}}" rel="icon" type="image/x-icon" />
<style type="text/css">
.clearfix:after {
content: ".";
display: block;
clear: both;
visibility: hidden;
line-height: 0;
height: 0;
}
body {
margin: 0;
font-family: Helvetica;
}
header {
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
background: #fcfcfc;
background: -webkit-linear-gradient(top, #fcfcfc 0%, #f2f2f2 100%);
padding: 0.8rem;
}
header .logo {
vertical-align: middle;
width: 45px;
height: 45px;
}
header h1 {
display: inline-block;
margin: 0 0 0 0.5rem;
vertical-align: middle;
font-weight: normal;
font-size: 1.5rem;
color: #666666;
}
header .right {
float: right;
font-size: .75rem;
}
header .right ul li {
display: inline;
margin: 0 0 0 .5rem;
font-size: 1rem;
}
header .button {
color: #ffffff;
background-color: #4e064f;
padding: 10px;
border-radius: 5px;
text-decoration: none;
margin-left: 1rem;
cursor: pointer;
}
table.file-list {
width: 100%;
margin: 0 auto;
border-collapse: collapse;
}
table.file-list th {
text-align: left;
text-transform: uppercase;
font-weight: normal;
color: #666666;
padding: 0.5rem;
}
table.file-list tr {
border-bottom: 1px solid #e0e0e0;
}
table.file-list td {
white-space: nowrap;
padding: 0.5rem 10rem 0.5rem 0.8rem;
}
table.file-list td img {
vertical-align: middle;
margin-right: 0.5rem;
}
table.file-list td:last-child {
width: 100%;
}
</style>
<meta name="onionshare-filename" content="{{ filename }}">
<meta name="onionshare-filesize" content="{{ filesize }}">
</head>
<body>
<header class="clearfix">
<div class="right">
<ul>
<li>Total size: <strong>{{ filesize_human }}</strong> (compressed)</li>
<li><a class="button" href='/{{ slug }}/download'>Download Files</a></li>
</ul>
</div>
<img class="logo" src="data:image/png;base64,{{logo_b64}}" title="OnionShare">
<h1>OnionShare</h1>
</header>
<table class="file-list" id="file-list">
<tr>
<th onclick="sortTable(0)">Filename</th>
<th onclick="sortTable(1)">Size</th>
<th></th>
</tr>
{% for info in file_info.dirs %}
<tr>
<td>
<img width="30" height="30" title="" alt="" src="data:image/png;base64,{{ folder_b64 }}" />
{{ info.basename }}
</td>
<td>{{ info.size_human }}</td>
<td></td>
</tr>
{% endfor %}
{% for info in file_info.files %}
<tr>
<td>
<img width="30" height="30" title="" alt="" src="data:image/png;base64,{{ file_b64 }}" />
{{ info.basename }}
</td>
<td>{{ info.size_human }}</td>
<td></td>
</tr>
{% endfor %}
</table>
<script>
// Function to convert human-readable sizes back to bytes, for sorting
function unhumanize(text) {
var powers = {'b': 0, 'k': 1, 'm': 2, 'g': 3, 't': 4};
var regex = /(\d+(?:\.\d+)?)\s?(B|K|M|G|T)?/i;
var res = regex.exec(text);
if(res[2] === undefined) {
// Account for alphabetical words (file/dir names)
return text;
} else {
return res[1] * Math.pow(1024, powers[res[2].toLowerCase()]);
}
}
function sortTable(n) {
var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
table = document.getElementById("file-list");
switching = true;
// Set the sorting direction to ascending:
dir = "asc";
/* Make a loop that will continue until
no switching has been done: */
while (switching) {
// Start by saying: no switching is done:
switching = false;
rows = table.getElementsByTagName("TR");
/* Loop through all table rows (except the
first, which contains table headers): */
for (i = 1; i < (rows.length - 1); i++) {
// Start by saying there should be no switching:
shouldSwitch = false;
/* Get the two elements you want to compare,
one from current row and one from the next: */
x = rows[i].getElementsByTagName("TD")[n];
y = rows[i + 1].getElementsByTagName("TD")[n];
/* Check if the two rows should switch place,
based on the direction, asc or desc: */
if (dir == "asc") {
if (unhumanize(x.innerHTML.toLowerCase()) > unhumanize(y.innerHTML.toLowerCase())) {
// If so, mark as a switch and break the loop:
shouldSwitch= true;
break;
}
} else if (dir == "desc") {
if (unhumanize(x.innerHTML.toLowerCase()) < unhumanize(y.innerHTML.toLowerCase())) {
// If so, mark as a switch and break the loop:
shouldSwitch= true;
break;
}
}
}
if (shouldSwitch) {
/* If a switch has been marked, make the switch
and mark that a switch has been done: */
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
// Each time a switch is done, increase this count by 1:
switchcount ++;
} else {
/* If no switching has been done AND the direction is "asc",
set the direction to "desc" and run the while loop again. */
if (switchcount == 0 && dir == "asc") {
dir = "desc";
switching = true;
}
}
}
}
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 B

View File

@ -2,14 +2,17 @@
"config_onion_service": "Configuring onion service on port {0:d}.",
"preparing_files": "Preparing files to share.",
"wait_for_hs": "Waiting for HS to be ready:",
"wait_for_hs_trying": "Trying...",
"wait_for_hs_trying": "Trying",
"wait_for_hs_nope": "Not ready yet.",
"wait_for_hs_yup": "Ready!",
"give_this_url": "Give this address to the person you're sending the file to:",
"give_this_url_stealth": "Give this address and HidServAuth line to the person you're sending the file to:",
"ctrlc_to_stop": "Press Ctrl-C to stop server",
"give_this_url_receive": "Give this address to the people sending you files:",
"give_this_url_receive_stealth": "Give this address and HidServAuth line to the people sending you files:",
"ctrlc_to_stop": "Press Ctrl+C to stop the server",
"not_a_file": "{0:s} is not a valid file.",
"not_a_readable_file": "{0:s} is not a readable file.",
"no_filenames": "You must specify a list of files to share.",
"no_available_port": "Could not start the Onion service as there was no available port.",
"download_page_loaded": "Download page loaded",
"other_page_loaded": "Address loaded",
@ -17,7 +20,7 @@
"closing_automatically": "Stopped because download finished",
"timeout_download_still_running": "Waiting for download to complete",
"large_filesize": "Warning: Sending large files could take hours",
"error_tails_invalid_port": "Invalid value, port must be an integer",
"error_tails_invalid_port": "Invalid value, port must be a regular number",
"error_tails_unknown_root": "Unknown error with Tails root process",
"systray_menu_exit": "Quit",
"systray_download_started_title": "OnionShare Download Started",
@ -26,11 +29,11 @@
"systray_download_completed_message": "The user finished downloading your files",
"systray_download_canceled_title": "OnionShare Download Canceled",
"systray_download_canceled_message": "The user canceled the download",
"help_local_only": "Do not attempt to use tor: for development only",
"help_local_only": "Do not attempt to use Tor: For development only",
"help_stay_open": "Keep onion service running after download has finished",
"help_shutdown_timeout": "Shut down the onion service after N seconds",
"help_transparent_torification": "My system is transparently torified",
"help_stealth": "Create stealth onion service (advanced)",
"help_receive": "Receive files instead of sending them",
"help_debug": "Log application errors to stdout, and log web errors to disk",
"help_filename": "List of files or folders to share",
"help_config": "Path to a custom JSON config file (optional)",
@ -41,25 +44,27 @@
"gui_start_server": "Start Sharing",
"gui_stop_server": "Stop Sharing",
"gui_stop_server_shutdown_timeout": "Stop Sharing ({}s remaining)",
"gui_stop_server_shutdown_timeout_tooltip": "Share will stop automatically at {}",
"gui_stop_server_shutdown_timeout_tooltip": "Share will expire automatically at {}",
"gui_copy_url": "Copy Address",
"gui_copy_hidservauth": "Copy HidServAuth",
"gui_downloads": "Downloads:",
"gui_downloads": "Download History",
"gui_downloads_window_tooltip": "Show/hide downloads",
"gui_no_downloads": "No downloads yet.",
"gui_canceled": "Canceled",
"gui_copied_url_title": "Copied OnionShare address",
"gui_copied_url": "The OnionShare address has been copied to clipboard",
"gui_copied_hidservauth_title": "Copied HidServAuth",
"gui_copied_hidservauth": "The HidServAuth line has been copied to clipboard",
"gui_starting_server1": "Starting Tor onion service...",
"gui_starting_server2": "Compressing files...",
"gui_please_wait": "Starting... Click to cancel",
"gui_starting_server1": "Starting Tor onion service",
"gui_starting_server2": "Compressing files",
"gui_please_wait": "Starting Click to cancel",
"error_hs_dir_cannot_create": "Cannot create onion service dir {0:s}",
"error_hs_dir_not_writable": "onion service dir {0:s} is not writable",
"using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication",
"gui_download_progress_complete": "%p%, Time Elapsed: {0:s}",
"gui_download_progress_starting": "{0:s}, %p% (Computing ETA)",
"gui_download_progress_eta": "{0:s}, ETA: {1:s}, %p%",
"version_string": "Onionshare {0:s} | https://onionshare.org/",
"version_string": "OnionShare {0:s} | https://onionshare.org/",
"gui_quit_title": "Transfer in Progress",
"gui_quit_warning": "You're in the process of sending files. Are you sure you want to quit OnionShare?",
"gui_quit_warning_quit": "Quit",
@ -71,18 +76,18 @@
"gui_settings_window_title": "Settings",
"gui_settings_stealth_label": "Stealth (advanced)",
"gui_settings_stealth_option": "Create stealth onion services",
"gui_settings_stealth_option_details": "This makes OnionShare more secure, but also more difficult for the recipient to connect to it.<br><a href=\"https://github.com/micahflee/onionshare/wiki/Stealth-Onion-Services\">More information</a>.",
"gui_settings_stealth_option_details": "This makes OnionShare more secure, but also more difficult for the recipient to connect to.<br><a href=\"https://github.com/micahflee/onionshare/wiki/Stealth-Onion-Services\">More information</a>.",
"gui_settings_stealth_hidservauth_string": "You have saved the private key for reuse, so your HidServAuth string is also reused.\nClick below to copy the HidServAuth.",
"gui_settings_autoupdate_label": "Check for updates",
"gui_settings_autoupdate_option": "Notify me when updates are available",
"gui_settings_autoupdate_label": "Check for upgrades",
"gui_settings_autoupdate_option": "Notify me when upgrades are available",
"gui_settings_autoupdate_timestamp": "Last checked: {}",
"gui_settings_autoupdate_timestamp_never": "Never",
"gui_settings_autoupdate_check_button": "Check For Updates",
"gui_settings_autoupdate_check_button": "Check For Upgrades",
"gui_settings_sharing_label": "Sharing options",
"gui_settings_close_after_first_download_option": "Stop sharing after first download",
"gui_settings_systray_notifications": "Show desktop notifications",
"gui_settings_connection_type_label": "How should OnionShare connect to Tor?",
"gui_settings_connection_type_bundled_option": "Use Tor that is bundled with OnionShare",
"gui_settings_connection_type_bundled_option": "Use the Tor version that is bundled with OnionShare",
"gui_settings_connection_type_automatic_option": "Attempt automatic configuration with Tor Browser",
"gui_settings_connection_type_control_port_option": "Connect using control port",
"gui_settings_connection_type_socket_file_option": "Connect using socket file",
@ -104,9 +109,10 @@
"gui_settings_tor_bridges_meek_lite_amazon_radio_option_no_obfs4proxy": "Use built-in meek_lite (Amazon) pluggable transports (requires obfs4proxy)",
"gui_settings_tor_bridges_meek_lite_azure_radio_option": "Use built-in meek_lite (Azure) pluggable transports",
"gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy": "Use built-in meek_lite (Azure) pluggable transports (requires obfs4proxy)",
"gui_settings_meek_lite_expensive_warning": "Warning: the meek_lite bridges are very costly for the Tor Project to run!<br><br>You should only use meek_lite bridges if you are having trouble connecting to Tor directly, via obfs4 transports or other normal bridges.",
"gui_settings_tor_bridges_custom_radio_option": "Use custom bridges",
"gui_settings_tor_bridges_custom_label": "You can get bridges from <a href=\"https://bridges.torproject.org/options\">https://bridges.torproject.org</a>",
"gui_settings_tor_bridges_invalid": "None of the bridges you supplied seem to be valid.\nPlease try again with valid bridges.",
"gui_settings_tor_bridges_invalid": "None of the bridges you supplied seem to work.\nPlease try again by double-checking them or adding other ones.",
"gui_settings_button_save": "Save",
"gui_settings_button_cancel": "Cancel",
"gui_settings_button_help": "Help",
@ -119,15 +125,15 @@
"settings_error_socket_file": "Can't connect to Tor controller using socket file {}.",
"settings_error_auth": "Connected to {}:{}, but can't authenticate. Maybe this isn't a Tor controller?",
"settings_error_missing_password": "Connected to Tor controller, but it requires a password to authenticate.",
"settings_error_unreadable_cookie_file": "Connected to Tor controller, but can't authenticate because your password may be wrong, and your user doesn't have permission to read the cookie file.",
"settings_error_bundled_tor_not_supported": "Bundled Tor is not supported when not using developer mode in Windows or macOS.",
"settings_error_bundled_tor_timeout": "Connecting to Tor is taking too long. Maybe your computer is offline, or your clock isn't accurate.",
"settings_error_unreadable_cookie_file": "Connected to Tor controller, but can't authenticate because your password may be wrong, and your user lacks permission to read the cookie file.",
"settings_error_bundled_tor_not_supported": "Use of the Tor version bundled with OnionShare is not supported when using developer mode on Windows or macOS.",
"settings_error_bundled_tor_timeout": "Connecting to Tor is taking too long. Maybe your computer is offline, or your system clock isn't accurate.",
"settings_error_bundled_tor_canceled": "The Tor process closed before it could finish connecting.",
"settings_error_bundled_tor_broken": "Something is wrong with OnionShare connecting to Tor in the background:\n{}",
"settings_error_bundled_tor_broken": "OnionShare could not connect to Tor in the background:\n{}",
"settings_test_success": "Congratulations, OnionShare can connect to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}\nSupports stealth onion services: {}",
"error_tor_protocol_error": "Error talking to the Tor controller.\nIf you're using Whonix, check out https://www.whonix.org/wiki/onionshare to make OnionShare work.",
"error_tor_protocol_error": "Could not communicate with the Tor controller.\nIf you're using Whonix, check out https://www.whonix.org/wiki/onionshare to make OnionShare work.",
"connecting_to_tor": "Connecting to the Tor network",
"update_available": "There is an OnionShare update available. <a href='{}'>Click here</a> to download it.<br><br>Installed version: {}<br>Latest version: {}",
"update_available": "A new version of OnionShare is available. <a href='{}'>Click here</a> to download it.<br><br>Installed version: {}<br>Latest version: {}",
"update_error_check_error": "Error checking for updates: Maybe you're not connected to Tor, or maybe the OnionShare website is down.",
"update_error_invalid_latest_version": "Error checking for updates: The OnionShare website responded saying the latest version is '{}', but that doesn't appear to be a valid version string.",
"update_not_available": "You are running the latest version of OnionShare.",
@ -135,22 +141,27 @@
"gui_tor_connection_ask_open_settings": "Open Settings",
"gui_tor_connection_ask_quit": "Quit",
"gui_tor_connection_error_settings": "Try adjusting how OnionShare connects to the Tor network in Settings.",
"gui_tor_connection_canceled": "OnionShare cannot connect to Tor.\n\nMake sure you're connected to the internet, then re-open OnionShare to configure the Tor connection.",
"gui_tor_connection_canceled": "OnionShare could not connect to Tor.\n\nMake sure you're connected to the Internet, then re-open OnionShare to set up the Tor connection.",
"gui_tor_connection_lost": "Disconnected from Tor.",
"gui_server_started_after_timeout": "The server started after your chosen auto-timeout.\nPlease start a new share.",
"gui_server_timeout_expired": "The chosen timeout has already expired.\nPlease update the timeout and then you may start sharing.",
"share_via_onionshare": "Share via OnionShare",
"gui_save_private_key_checkbox": "Use a persistent address\n(unchecking will delete any saved address)",
"gui_url_description": "<b>Anyone</b> with this link can <b>download</b> your files using <b>Tor Browser</b>: <img src='{}' />",
"gui_url_label_persistent": "This share will not stop automatically unless a timer is set.<br><br>Every share will have the same address (to use one-time addresses, disable persistence in the Settings)",
"gui_url_label_stay_open": "This share will not stop automatically unless a timer is set.",
"gui_url_label_onetime": "This share will stop after the first download",
"gui_url_label_onetime_and_persistent": "This share will stop after the first download<br><br>Every share will have the same address (to use one-time addresses, disable persistence in the Settings)",
"gui_save_private_key_checkbox": "Use a persistent address\n(unchecking will delete any saved addresses)",
"gui_url_description": "<b>Anyone</b> with this link can <b>download</b> your files using the <b>Tor Browser</b>: <img src='{}' />",
"gui_url_label_persistent": "This share will not expire automatically unless a timer is set.<br><br>Every share will have the same address (to use one-time addresses, disable persistence in Settings)",
"gui_url_label_stay_open": "This share will not expire automatically unless a timer is set.",
"gui_url_label_onetime": "This share will expire after the first download",
"gui_url_label_onetime_and_persistent": "This share will expire after the first download<br><br>Every share will have the same address (to use one-time addresses, disable persistence in the Settings)",
"gui_status_indicator_stopped": "Ready to Share",
"gui_status_indicator_working": "Starting...",
"gui_status_indicator_working": "Starting",
"gui_status_indicator_started": "Sharing",
"gui_file_info": "{} Files, {}",
"gui_file_info_single": "{} File, {}",
"info_in_progress_downloads_tooltip": "{} download(s) in progress",
"info_completed_downloads_tooltip": "{} download(s) completed"
"info_completed_downloads_tooltip": "{} download(s) completed",
"error_cannot_create_downloads_dir": "Error creating downloads folder: {}",
"error_downloads_dir_not_writable": "The downloads folder isn't writable: {}",
"receive_mode_downloads_dir": "Files people send you will appear in this folder: {}",
"receive_mode_warning": "Warning: Some files can hack your computer if you open them! Only open files from people you trust, or if you know what you're doing.",
"receive_mode_received_file": "Received file: {}"
}

144
share/static/css/style.css Normal file
View File

@ -0,0 +1,144 @@
.clearfix:after {
content: ".";
display: block;
clear: both;
visibility: hidden;
line-height: 0;
height: 0;
}
body {
margin: 0;
font-family: Helvetica;
}
header {
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
background: #fcfcfc;
background: -webkit-linear-gradient(top, #fcfcfc 0%, #f2f2f2 100%);
padding: 0.8rem;
}
header .logo {
vertical-align: middle;
width: 45px;
height: 45px;
}
header h1 {
display: inline-block;
margin: 0 0 0 0.5rem;
vertical-align: middle;
font-weight: normal;
font-size: 1.5rem;
color: #666666;
}
header .right {
float: right;
font-size: .75rem;
}
header .right ul li {
display: inline;
margin: 0 0 0 .5rem;
font-size: 1rem;
}
.button {
color: #ffffff;
background-color: #4e064f;
padding: 10px;
border: 0;
border-radius: 5px;
text-decoration: none;
margin-left: 1rem;
cursor: pointer;
}
.close-button {
color: #ffffff;
background-color: #c90c0c;
padding: 10px;
border: 0;
border-radius: 5px;
text-decoration: none;
margin-left: 1rem;
cursor: pointer;
position: absolute;
right: 10px;
bottom: 10px;
}
table.file-list {
width: 100%;
margin: 0 auto;
border-collapse: collapse;
}
table.file-list th {
text-align: left;
text-transform: uppercase;
font-weight: normal;
color: #666666;
padding: 0.5rem;
}
table.file-list tr {
border-bottom: 1px solid #e0e0e0;
}
table.file-list td {
white-space: nowrap;
padding: 0.5rem 10rem 0.5rem 0.8rem;
}
table.file-list td img {
vertical-align: middle;
margin-right: 0.5rem;
}
table.file-list td:last-child {
width: 100%;
}
.upload-wrapper {
display: flex;
align-items: center;
justify-content: center;
min-height: 400px;
}
.upload {
text-align: center;
}
.upload img {
width: 120px;
height: 120px;
}
.upload .upload-header {
font-size: 30px;
font-weight: normal;
color: #666666;
margin: 0 0 10px 0;
}
.upload .upload-description {
color: #666666;
margin: 0 0 20px 0;
}
ul.flashes {
list-style: none;
margin: 0;
padding: 0;
color: #cc0000;
text-align: left;
}
ul.flashes li {
margin: 0;
padding: 10px;
}

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
share/static/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 251 B

After

Width:  |  Height:  |  Size: 251 B

View File

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 338 B

75
share/static/js/send.js Normal file
View File

@ -0,0 +1,75 @@
// Function to convert human-readable sizes back to bytes, for sorting
function unhumanize(text) {
var powers = {'b': 0, 'k': 1, 'm': 2, 'g': 3, 't': 4};
var regex = /(\d+(?:\.\d+)?)\s?(B|K|M|G|T)?/i;
var res = regex.exec(text);
if(res[2] === undefined) {
// Account for alphabetical words (file/dir names)
return text;
} else {
return res[1] * Math.pow(1024, powers[res[2].toLowerCase()]);
}
}
function sortTable(n) {
var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
table = document.getElementById("file-list");
switching = true;
// Set the sorting direction to ascending:
dir = "asc";
/* Make a loop that will continue until
no switching has been done: */
while (switching) {
// Start by saying: no switching is done:
switching = false;
rows = table.getElementsByTagName("TR");
/* Loop through all table rows (except the
first, which contains table headers): */
for (i = 1; i < (rows.length - 1); i++) {
// Start by saying there should be no switching:
shouldSwitch = false;
/* Get the two elements you want to compare,
one from current row and one from the next: */
x = rows[i].getElementsByTagName("TD")[n];
y = rows[i + 1].getElementsByTagName("TD")[n];
/* Check if the two rows should switch place,
based on the direction, asc or desc: */
if (dir == "asc") {
if (unhumanize(x.innerHTML.toLowerCase()) > unhumanize(y.innerHTML.toLowerCase())) {
// If so, mark as a switch and break the loop:
shouldSwitch= true;
break;
}
} else if (dir == "desc") {
if (unhumanize(x.innerHTML.toLowerCase()) < unhumanize(y.innerHTML.toLowerCase())) {
// If so, mark as a switch and break the loop:
shouldSwitch= true;
break;
}
}
}
if (shouldSwitch) {
/* If a switch has been marked, make the switch
and mark that a switch has been done: */
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
// Each time a switch is done, increase this count by 1:
switchcount ++;
} else {
/* If no switching has been done AND the direction is "asc",
set the direction to "desc" and run the while loop again. */
if (switchcount == 0 && dir == "asc") {
dir = "desc";
switching = true;
}
}
}
}
// Set click handlers
document.getElementById("filename-header").addEventListener("click", function(){
sortTable(0);
});
document.getElementById("size-header").addEventListener("click", function(){
sortTable(1);
});

10
share/templates/404.html Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>OnionShare: Error 404</title>
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon" />
</head>
<body>
<p>Error 404: You probably typed the OnionShare address wrong</p>
</body>
</html>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>OnionShare is closed</title>
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon" />
</head>
<body>
<p>Thank you for using OnionShare</p>
</body>
</html>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>OnionShare</title>
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon" />
</head>
<body>
<p>OnionShare download in progress</p>
</body>
</html>

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<title>OnionShare</title>
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon" />
<link href="/static/css/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<header class="clearfix">
<img class="logo" src="/static/img/logo.png" title="OnionShare">
<h1>OnionShare</h1>
</header>
<div class="upload-wrapper">
<div class="upload">
<p><img class="logo" src="/static/img/logo_large.png" title="OnionShare"></p>
<p class="upload-header">Send Files</p>
<p class="upload-description">Select the files you want to send, then click "Send Files"...</p>
<form method="post" enctype="multipart/form-data" action="/{{ slug }}/upload">
<p><input type="file" name="file[]" multiple /></p>
<p><input type="submit" class="button" value="Send Files" /></p>
<div>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class=flashes>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
</div>
</form>
</div>
</div>
<form method="post" action="/{{ slug }}/close">
<input type="submit" class="close-button" value="I'm Finished Uploading" />
</form>
</body>
</html>

52
share/templates/send.html Normal file
View File

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html>
<head>
<title>OnionShare</title>
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon" />
<link href="/static/css/style.css" rel="stylesheet" type="text/css" />
<meta name="onionshare-filename" content="{{ filename }}">
<meta name="onionshare-filesize" content="{{ filesize }}">
</head>
<body>
<header class="clearfix">
<div class="right">
<ul>
<li>Total size: <strong>{{ filesize_human }}</strong> (compressed)</li>
<li><a class="button" href='/{{ slug }}/download'>Download Files</a></li>
</ul>
</div>
<img class="logo" src="/static/img/logo.png" title="OnionShare">
<h1>OnionShare</h1>
</header>
<table class="file-list" id="file-list">
<tr>
<th id="filename-header">Filename</th>
<th id="size-header">Size</th>
<th></th>
</tr>
{% for info in file_info.dirs %}
<tr>
<td>
<img width="30" height="30" title="" alt="" src="/static/img/web_folder.png" />
{{ info.basename }}
</td>
<td>{{ info.size_human }}</td>
<td></td>
</tr>
{% endfor %}
{% for info in file_info.files %}
<tr>
<td>
<img width="30" height="30" title="" alt="" src="/static/img/web_file.png" />
{{ info.basename }}
</td>
<td>{{ info.size_human }}</td>
<td></td>
</tr>
{% endfor %}
</table>
<script src="/static/js/send.js"></script>
</body>
</html>

View File

@ -1 +1 @@
1.3
1.3.1

View File

@ -8,7 +8,7 @@ import tempfile
import pytest
from onionshare import common
from onionshare import common, web
@pytest.fixture
def temp_dir_1024():
@ -64,8 +64,9 @@ def temp_file_1024_delete():
# pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture(scope='session')
def custom_zw():
zw = common.ZipWriter(
zip_filename=common.random_string(4, 6),
zw = web.ZipWriter(
common.Common(),
zip_filename=common.Common.random_string(4, 6),
processed_size_callback=lambda _: 'custom_callback'
)
yield zw
@ -76,7 +77,7 @@ def custom_zw():
# pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture(scope='session')
def default_zw():
zw = common.ZipWriter()
zw = web.ZipWriter(common.Common())
yield zw
zw.close()
tmp_dir = os.path.dirname(zw.zip_filename)
@ -118,16 +119,6 @@ def platform_windows(monkeypatch):
monkeypatch.setattr('platform.system', lambda: 'Windows')
@pytest.fixture
def set_debug_false(monkeypatch):
monkeypatch.setattr('onionshare.common.debug', False)
@pytest.fixture
def set_debug_true(monkeypatch):
monkeypatch.setattr('onionshare.common.debug', True)
@pytest.fixture
def sys_argv_sys_prefix(monkeypatch):
monkeypatch.setattr('sys.argv', [sys.prefix])
@ -157,3 +148,7 @@ def time_time_100(monkeypatch):
@pytest.fixture
def time_strftime(monkeypatch):
monkeypatch.setattr('time.strftime', lambda _: 'Jun 06 2013 11:05:00')
@pytest.fixture
def common_obj():
return common.Common()

View File

@ -1,7 +1,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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

View File

@ -1,7 +1,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -22,6 +22,7 @@ import os
import pytest
from onionshare import OnionShare
from onionshare.common import Common
class MyOnion:
@ -37,7 +38,8 @@ class MyOnion:
@pytest.fixture
def onionshare_obj():
return OnionShare(MyOnion())
common = Common()
return OnionShare(common, MyOnion())
class TestOnionShare:

View File

@ -1,7 +1,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -29,17 +29,16 @@ import zipfile
import pytest
from onionshare import common
DEFAULT_ZW_FILENAME_REGEX = re.compile(r'^onionshare_[a-z2-7]{6}.zip$')
LOG_MSG_REGEX = re.compile(r"""
^\[Jun\ 06\ 2013\ 11:05:00\]
\ TestModule\.<function\ TestLog\.test_output\.<locals>\.dummy_func
\ at\ 0x[a-f0-9]+>(:\ TEST_MSG)?$""", re.VERBOSE)
RANDOM_STR_REGEX = re.compile(r'^[a-z2-7]+$')
SLUG_REGEX = re.compile(r'^([a-z]+)(-[a-z]+)?-([a-z]+)(-[a-z]+)?$')
# TODO: Improve the Common tests to test it all as a single class
class TestBuildSlug:
@pytest.mark.parametrize('test_input,expected', (
# VALID, two lowercase words, separated by a hyphen
@ -79,17 +78,17 @@ class TestBuildSlug:
assert bool(SLUG_REGEX.match(test_input)) == expected
def test_build_slug_unique(self, sys_onionshare_dev_mode):
assert common.build_slug() != common.build_slug()
def test_build_slug_unique(self, common_obj, sys_onionshare_dev_mode):
assert common_obj.build_slug() != common_obj.build_slug()
class TestDirSize:
def test_temp_dir_size(self, temp_dir_1024_delete):
def test_temp_dir_size(self, common_obj, temp_dir_1024_delete):
""" dir_size() should return the total size (in bytes) of all files
in a particular directory.
"""
assert common.dir_size(temp_dir_1024_delete) == 1024
assert common_obj.dir_size(temp_dir_1024_delete) == 1024
class TestEstimatedTimeRemaining:
@ -103,16 +102,16 @@ class TestEstimatedTimeRemaining:
((971, 1009, 83), '1s')
))
def test_estimated_time_remaining(
self, test_input, expected, time_time_100):
assert common.estimated_time_remaining(*test_input) == expected
self, common_obj, test_input, expected, time_time_100):
assert common_obj.estimated_time_remaining(*test_input) == expected
@pytest.mark.parametrize('test_input', (
(10, 20, 100), # if `time_elapsed == 0`
(0, 37, 99) # if `download_rate == 0`
))
def test_raises_zero_division_error(self, test_input, time_time_100):
def test_raises_zero_division_error(self, common_obj, test_input, time_time_100):
with pytest.raises(ZeroDivisionError):
common.estimated_time_remaining(*test_input)
common_obj.estimated_time_remaining(*test_input)
class TestFormatSeconds:
@ -131,16 +130,16 @@ class TestFormatSeconds:
(129674, '1d12h1m14s'),
(56404.12, '15h40m4s')
))
def test_format_seconds(self, test_input, expected):
assert common.format_seconds(test_input) == expected
def test_format_seconds(self, common_obj, test_input, expected):
assert common_obj.format_seconds(test_input) == expected
# TODO: test negative numbers?
@pytest.mark.parametrize('test_input', (
'string', lambda: None, [], {}, set()
))
def test_invalid_input_types(self, test_input):
def test_invalid_input_types(self, common_obj, test_input):
with pytest.raises(TypeError):
common.format_seconds(test_input)
common_obj.format_seconds(test_input)
class TestGetAvailablePort:
@ -148,29 +147,29 @@ class TestGetAvailablePort:
(random.randint(1024, 1500),
random.randint(1800, 2048)) for _ in range(50)
))
def test_returns_an_open_port(self, port_min, port_max):
def test_returns_an_open_port(self, common_obj, port_min, port_max):
""" get_available_port() should return an open port within the range """
port = common.get_available_port(port_min, port_max)
port = common_obj.get_available_port(port_min, port_max)
assert port_min <= port <= port_max
with socket.socket() as tmpsock:
tmpsock.bind(('127.0.0.1', port))
class TestGetPlatform:
def test_darwin(self, platform_darwin):
assert common.get_platform() == 'Darwin'
def test_darwin(self, platform_darwin, common_obj):
assert common_obj.platform == 'Darwin'
def test_linux(self, platform_linux):
assert common.get_platform() == 'Linux'
def test_linux(self, platform_linux, common_obj):
assert common_obj.platform == 'Linux'
def test_windows(self, platform_windows):
assert common.get_platform() == 'Windows'
def test_windows(self, platform_windows, common_obj):
assert common_obj.platform == 'Windows'
# TODO: double-check these tests
class TestGetResourcePath:
def test_onionshare_dev_mode(self, sys_onionshare_dev_mode):
def test_onionshare_dev_mode(self, common_obj, sys_onionshare_dev_mode):
prefix = os.path.join(
os.path.dirname(
os.path.dirname(
@ -178,29 +177,29 @@ class TestGetResourcePath:
inspect.getfile(
inspect.currentframe())))), 'share')
assert (
common.get_resource_path(os.path.join(prefix, 'test_filename')) ==
common_obj.get_resource_path(os.path.join(prefix, 'test_filename')) ==
os.path.join(prefix, 'test_filename'))
def test_linux(self, platform_linux, sys_argv_sys_prefix):
def test_linux(self, common_obj, platform_linux, sys_argv_sys_prefix):
prefix = os.path.join(sys.prefix, 'share/onionshare')
assert (
common.get_resource_path(os.path.join(prefix, 'test_filename')) ==
common_obj.get_resource_path(os.path.join(prefix, 'test_filename')) ==
os.path.join(prefix, 'test_filename'))
def test_frozen_darwin(self, platform_darwin, sys_frozen, sys_meipass):
def test_frozen_darwin(self, common_obj, platform_darwin, sys_frozen, sys_meipass):
prefix = os.path.join(sys._MEIPASS, 'share')
assert (
common.get_resource_path(os.path.join(prefix, 'test_filename')) ==
common_obj.get_resource_path(os.path.join(prefix, 'test_filename')) ==
os.path.join(prefix, 'test_filename'))
class TestGetTorPaths:
# @pytest.mark.skipif(sys.platform != 'Darwin', reason='requires MacOS') ?
def test_get_tor_paths_darwin(self, platform_darwin, sys_frozen, sys_meipass):
def test_get_tor_paths_darwin(self, platform_darwin, common_obj, sys_frozen, sys_meipass):
base_path = os.path.dirname(
os.path.dirname(
os.path.dirname(
common.get_resource_path(''))))
common_obj.get_resource_path(''))))
tor_path = os.path.join(
base_path, 'Resources', 'Tor', 'tor')
tor_geo_ip_file_path = os.path.join(
@ -209,20 +208,20 @@ class TestGetTorPaths:
base_path, 'Resources', 'Tor', 'geoip6')
obfs4proxy_file_path = os.path.join(
base_path, 'Resources', 'Tor', 'obfs4proxy')
assert (common.get_tor_paths() ==
assert (common_obj.get_tor_paths() ==
(tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path))
# @pytest.mark.skipif(sys.platform != 'Linux', reason='requires Linux') ?
def test_get_tor_paths_linux(self, platform_linux):
assert (common.get_tor_paths() ==
def test_get_tor_paths_linux(self, platform_linux, common_obj):
assert (common_obj.get_tor_paths() ==
('/usr/bin/tor', '/usr/share/tor/geoip', '/usr/share/tor/geoip6', '/usr/bin/obfs4proxy'))
# @pytest.mark.skipif(sys.platform != 'Windows', reason='requires Windows') ?
def test_get_tor_paths_windows(self, platform_windows, sys_frozen):
def test_get_tor_paths_windows(self, platform_windows, common_obj, sys_frozen):
base_path = os.path.join(
os.path.dirname(
os.path.dirname(
common.get_resource_path(''))), 'tor')
common_obj.get_resource_path(''))), 'tor')
tor_path = os.path.join(
os.path.join(base_path, 'Tor'), 'tor.exe')
obfs4proxy_file_path = os.path.join(
@ -233,18 +232,10 @@ class TestGetTorPaths:
tor_geo_ipv6_file_path = os.path.join(
os.path.join(
os.path.join(base_path, 'Data'), 'Tor'), 'geoip6')
assert (common.get_tor_paths() ==
assert (common_obj.get_tor_paths() ==
(tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path))
class TestGetVersion:
def test_get_version(self, sys_onionshare_dev_mode):
with open(common.get_resource_path('version.txt')) as f:
version = f.read().strip()
assert version == common.get_version()
class TestHumanReadableFilesize:
@pytest.mark.parametrize('test_input,expected', (
(1024 ** 0, '1.0 B'),
@ -257,8 +248,8 @@ class TestHumanReadableFilesize:
(1024 ** 7, '1.0 ZiB'),
(1024 ** 8, '1.0 YiB')
))
def test_human_readable_filesize(self, test_input, expected):
assert common.human_readable_filesize(test_input) == expected
def test_human_readable_filesize(self, common_obj, test_input, expected):
assert common_obj.human_readable_filesize(test_input) == expected
class TestLog:
@ -273,82 +264,18 @@ class TestLog:
def test_log_msg_regex(self, test_input):
assert bool(LOG_MSG_REGEX.match(test_input))
def test_output(self, set_debug_true, time_strftime):
def test_output(self, common_obj, time_strftime):
def dummy_func():
pass
common_obj.debug = True
# From: https://stackoverflow.com/questions/1218933
with io.StringIO() as buf, contextlib.redirect_stdout(buf):
common.log('TestModule', dummy_func)
common.log('TestModule', dummy_func, 'TEST_MSG')
common_obj.log('TestModule', dummy_func)
common_obj.log('TestModule', dummy_func, 'TEST_MSG')
output = buf.getvalue()
line_one, line_two, _ = output.split('\n')
assert LOG_MSG_REGEX.match(line_one)
assert LOG_MSG_REGEX.match(line_two)
class TestSetDebug:
def test_debug_true(self, set_debug_false):
common.set_debug(True)
assert common.debug is True
def test_debug_false(self, set_debug_true):
common.set_debug(False)
assert common.debug is False
class TestZipWriterDefault:
@pytest.mark.parametrize('test_input', (
'onionshare_{}.zip'.format(''.join(
random.choice('abcdefghijklmnopqrstuvwxyz234567') for _ in range(6)
)) for _ in range(50)
))
def test_default_zw_filename_regex(self, test_input):
assert bool(DEFAULT_ZW_FILENAME_REGEX.match(test_input))
def test_zw_filename(self, default_zw):
zw_filename = os.path.basename(default_zw.zip_filename)
assert bool(DEFAULT_ZW_FILENAME_REGEX.match(zw_filename))
def test_zipfile_filename_matches_zipwriter_filename(self, default_zw):
assert default_zw.z.filename == default_zw.zip_filename
def test_zipfile_allow_zip64(self, default_zw):
assert default_zw.z._allowZip64 is True
def test_zipfile_mode(self, default_zw):
assert default_zw.z.mode == 'w'
def test_callback(self, default_zw):
assert default_zw.processed_size_callback(None) is None
def test_add_file(self, default_zw, temp_file_1024_delete):
default_zw.add_file(temp_file_1024_delete)
zipfile_info = default_zw.z.getinfo(
os.path.basename(temp_file_1024_delete))
assert zipfile_info.compress_type == zipfile.ZIP_DEFLATED
assert zipfile_info.file_size == 1024
def test_add_directory(self, temp_dir_1024_delete, default_zw):
previous_size = default_zw._size # size before adding directory
default_zw.add_dir(temp_dir_1024_delete)
assert default_zw._size == previous_size + 1024
class TestZipWriterCustom:
@pytest.mark.parametrize('test_input', (
common.random_string(
random.randint(2, 50),
random.choice((None, random.randint(2, 50)))
) for _ in range(50)
))
def test_random_string_regex(self, test_input):
assert bool(RANDOM_STR_REGEX.match(test_input))
def test_custom_filename(self, custom_zw):
assert bool(RANDOM_STR_REGEX.match(custom_zw.zip_filename))
def test_custom_callback(self, custom_zw):
assert custom_zw.processed_size_callback(None) == 'custom_callback'

View File

@ -1,7 +1,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -26,19 +26,16 @@ import pytest
from onionshare import common, settings, strings
@pytest.fixture
def custom_version(monkeypatch):
monkeypatch.setattr(common, 'get_version', lambda: 'DUMMY_VERSION_1.2.3')
@pytest.fixture
def os_path_expanduser(monkeypatch):
monkeypatch.setattr('os.path.expanduser', lambda path: path)
@pytest.fixture
def settings_obj(custom_version, sys_onionshare_dev_mode, platform_linux):
return settings.Settings()
def settings_obj(sys_onionshare_dev_mode, platform_linux):
_common = common.Common()
_common.version = 'DUMMY_VERSION_1.2.3'
return settings.Settings(_common)
class TestSettings:
@ -67,7 +64,8 @@ class TestSettings:
'save_private_key': False,
'private_key': '',
'slug': '',
'hidservauth_string': ''
'hidservauth_string': '',
'downloads_dir': os.path.expanduser('~/OnionShare')
}
def test_fill_in_defaults(self, settings_obj):
@ -153,30 +151,27 @@ class TestSettings:
def test_filename_darwin(
self,
custom_version,
monkeypatch,
os_path_expanduser,
platform_darwin):
obj = settings.Settings()
obj = settings.Settings(common.Common())
assert (obj.filename ==
'~/Library/Application Support/OnionShare/onionshare.json')
def test_filename_linux(
self,
custom_version,
monkeypatch,
os_path_expanduser,
platform_linux):
obj = settings.Settings()
obj = settings.Settings(common.Common())
assert obj.filename == '~/.config/onionshare/onionshare.json'
def test_filename_windows(
self,
custom_version,
monkeypatch,
platform_windows):
monkeypatch.setenv('APPDATA', 'C:')
obj = settings.Settings()
obj = settings.Settings(common.Common())
assert obj.filename == 'C:\\OnionShare\\onionshare.json'
def test_set_custom_bridge(self, settings_obj):

View File

@ -2,7 +2,7 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
Copyright (C) 2018 Micah Lee <micah@micahflee.com>
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
@ -22,7 +22,7 @@ import types
import pytest
from onionshare import common, strings
from onionshare import strings
# # Stub get_resource_path so it finds the correct path while running tests
@ -44,28 +44,28 @@ def test_underscore_is_function():
class TestLoadStrings:
def test_load_strings_defaults_to_english(
self, locale_en, sys_onionshare_dev_mode):
self, common_obj, locale_en, sys_onionshare_dev_mode):
""" load_strings() loads English by default """
strings.load_strings(common)
strings.load_strings(common_obj)
assert strings._('wait_for_hs') == "Waiting for HS to be ready:"
def test_load_strings_loads_other_languages(
self, locale_fr, sys_onionshare_dev_mode):
self, common_obj, locale_fr, sys_onionshare_dev_mode):
""" load_strings() loads other languages in different locales """
strings.load_strings(common, "fr")
strings.load_strings(common_obj, "fr")
assert strings._('wait_for_hs') == "En attente du HS:"
def test_load_partial_strings(
self, locale_ru, sys_onionshare_dev_mode):
strings.load_strings(common)
self, common_obj, locale_ru, sys_onionshare_dev_mode):
strings.load_strings(common_obj)
assert strings._("give_this_url") == (
"Отправьте эту ссылку тому человеку, "
"которому вы хотите передать файл:")
assert strings._('wait_for_hs') == "Waiting for HS to be ready:"
def test_load_invalid_locale(
self, locale_invalid, sys_onionshare_dev_mode):
self, common_obj, locale_invalid, sys_onionshare_dev_mode):
""" load_strings() raises a KeyError for an invalid locale """
with pytest.raises(KeyError):
strings.load_strings(common, 'XX')
strings.load_strings(common_obj, 'XX')

View File

@ -0,0 +1,90 @@
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
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/>.
"""
import contextlib
import inspect
import io
import os
import random
import re
import socket
import sys
import zipfile
import pytest
from onionshare.common import Common
DEFAULT_ZW_FILENAME_REGEX = re.compile(r'^onionshare_[a-z2-7]{6}.zip$')
RANDOM_STR_REGEX = re.compile(r'^[a-z2-7]+$')
class TestZipWriterDefault:
@pytest.mark.parametrize('test_input', (
'onionshare_{}.zip'.format(''.join(
random.choice('abcdefghijklmnopqrstuvwxyz234567') for _ in range(6)
)) for _ in range(50)
))
def test_default_zw_filename_regex(self, test_input):
assert bool(DEFAULT_ZW_FILENAME_REGEX.match(test_input))
def test_zw_filename(self, default_zw):
zw_filename = os.path.basename(default_zw.zip_filename)
assert bool(DEFAULT_ZW_FILENAME_REGEX.match(zw_filename))
def test_zipfile_filename_matches_zipwriter_filename(self, default_zw):
assert default_zw.z.filename == default_zw.zip_filename
def test_zipfile_allow_zip64(self, default_zw):
assert default_zw.z._allowZip64 is True
def test_zipfile_mode(self, default_zw):
assert default_zw.z.mode == 'w'
def test_callback(self, default_zw):
assert default_zw.processed_size_callback(None) is None
def test_add_file(self, default_zw, temp_file_1024_delete):
default_zw.add_file(temp_file_1024_delete)
zipfile_info = default_zw.z.getinfo(
os.path.basename(temp_file_1024_delete))
assert zipfile_info.compress_type == zipfile.ZIP_DEFLATED
assert zipfile_info.file_size == 1024
def test_add_directory(self, temp_dir_1024_delete, default_zw):
previous_size = default_zw._size # size before adding directory
default_zw.add_dir(temp_dir_1024_delete)
assert default_zw._size == previous_size + 1024
class TestZipWriterCustom:
@pytest.mark.parametrize('test_input', (
Common.random_string(
random.randint(2, 50),
random.choice((None, random.randint(2, 50)))
) for _ in range(50)
))
def test_random_string_regex(self, test_input):
assert bool(RANDOM_STR_REGEX.match(test_input))
def test_custom_filename(self, custom_zw):
assert bool(RANDOM_STR_REGEX.match(custom_zw.zip_filename))
def test_custom_callback(self, custom_zw):
assert custom_zw.processed_size_callback(None) == 'custom_callback'