Merge pull request #823 from micahflee/406_osx_sandbox

Enable macOS sandbox
This commit is contained in:
Micah Lee 2018-12-19 13:39:41 -08:00 committed by GitHub
commit 80becc73fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 185 additions and 80 deletions

View File

@ -1,5 +1,15 @@
# OnionShare Changelog # OnionShare Changelog
## 2.0
* New feature: Receiver mode allows you to receive files with OnionShare, instead of only sending files
* New feature: macOS sandbox is enabled
* New feature: Support for next generation onion services (TODO waiting on Tor release)
* New feature: If you're sharing a single file, don't zip it up
* New feature: Allow selecting your language from a dropdown
* New translations: (TODO fill in for final release)
* Several bugfixes
## 1.3.1 ## 1.3.1
* Updated Tor to 0.2.3.10 * Updated Tor to 0.2.3.10

View File

@ -23,9 +23,12 @@ if [ "$1" = "--release" ]; then
PKG_PATH="$ROOT/dist/OnionShare.pkg" PKG_PATH="$ROOT/dist/OnionShare.pkg"
IDENTITY_NAME_APPLICATION="Developer ID Application: Micah Lee" IDENTITY_NAME_APPLICATION="Developer ID Application: Micah Lee"
IDENTITY_NAME_INSTALLER="Developer ID Installer: Micah Lee" IDENTITY_NAME_INSTALLER="Developer ID Installer: Micah Lee"
ENTITLEMENTS_CHILD_PATH="$ROOT/install/macos_sandbox/child.plist"
ENTITLEMENTS_PARENT_PATH="$ROOT/install/macos_sandbox/parent.plist"
echo "Codesigning the app bundle" echo "Codesigning the app bundle"
codesign --deep -s "$IDENTITY_NAME_APPLICATION" "$APP_PATH" codesign --deep -s "$IDENTITY_NAME_APPLICATION" -f --entitlements "$ENTITLEMENTS_CHILD_PATH" "$APP_PATH"
codesign -s "$IDENTITY_NAME_APPLICATION" -f --entitlements "$ENTITLEMENTS_PARENT_PATH" "$APP_PATH"
echo "Creating an installer" echo "Creating an installer"
productbuild --sign "$IDENTITY_NAME_INSTALLER" --component "$APP_PATH" /Applications "$PKG_PATH" productbuild --sign "$IDENTITY_NAME_INSTALLER" --component "$APP_PATH" /Applications "$PKG_PATH"

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.inherit</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Enable app sandbox -->
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- Both OnionShare and Tor need network server and client -->
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<!-- In share mode, users need to be able to select files, and in receive mode,
users need to be able to choose a folder to save files to -->
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<!-- Flask needs to read this mime.types file when starting an HTTP server -->
<key>com.apple.security.temporary-exception.files.absolute-path.read-only</key>
<array>
<string>/private/etc/apache2/mime.types</string>
</array>
<!-- For OnionShare to be able to connect to Tor Browser's tor control port,
it needs to read it's control_auth_cookie file -->
<key>com.apple.security.temporary-exception.files.home-relative-path.read-only</key>
<array>
<string>/Library/Application Support/TorBrowser-Data/Tor/control_auth_cookie</string>
</array>
<!-- In receive mode, OnionShare needs to be able to write to ~/OnionShare -->
<key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
<array>
<string>/OnionShare/</string>
</array>
</dict>
</plist>

View File

@ -3,10 +3,10 @@
!define ABOUTURL "https:\\onionshare.org\" !define ABOUTURL "https:\\onionshare.org\"
# change these with each release # change these with each release
!define INSTALLSIZE 66537 !define INSTALLSIZE 115186
!define VERSIONMAJOR 1 !define VERSIONMAJOR 2
!define VERSIONMINOR 3 !define VERSIONMINOR 0
!define VERSIONSTRING "1.3.1" !define VERSIONSTRING "2.0"
RequestExecutionLevel admin RequestExecutionLevel admin

View File

@ -15,7 +15,6 @@ a = Analysis(
('../share/torrc_template', 'share'), ('../share/torrc_template', 'share'),
('../share/torrc_template-obfs4', 'share'), ('../share/torrc_template-obfs4', 'share'),
('../share/torrc_template-meek_lite_azure', 'share'), ('../share/torrc_template-meek_lite_azure', 'share'),
('../share/torrc_template-windows', 'share'),
('../share/images/*', 'share/images'), ('../share/images/*', 'share/images'),
('../share/locale/*', 'share/locale'), ('../share/locale/*', 'share/locale'),
('../share/static/*', 'share/static'), ('../share/static/*', 'share/static'),

View File

@ -123,6 +123,23 @@ class Common(object):
return (tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path) return (tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path)
def build_data_dir(self):
"""
Returns the path of the OnionShare data directory.
"""
if self.platform == 'Windows':
try:
appdata = os.environ['APPDATA']
return '{}\\OnionShare'.format(appdata)
except:
# If for some reason we don't have the 'APPDATA' environment variable
# (like running tests in Linux while pretending to be in Windows)
return os.path.expanduser('~/.config/onionshare')
elif self.platform == 'Darwin':
return os.path.expanduser('~/Library/Application Support/OnionShare')
else:
return os.path.expanduser('~/.config/onionshare')
def build_slug(self): def build_slug(self):
""" """
Returns a random string made from two words from the wordlist, such as "deter-trig". Returns a random string made from two words from the wordlist, such as "deter-trig".

View File

@ -169,34 +169,35 @@ class Onion(object):
raise BundledTorNotSupported(strings._('settings_error_bundled_tor_not_supported')) raise BundledTorNotSupported(strings._('settings_error_bundled_tor_not_supported'))
# Create a torrc for this session # Create a torrc for this session
self.tor_data_directory = tempfile.TemporaryDirectory() self.tor_data_directory = tempfile.TemporaryDirectory(dir=self.common.build_data_dir())
self.common.log('Onion', 'connect', 'tor_data_directory={}'.format(self.tor_data_directory.name))
if self.common.platform == 'Windows': # Create the torrc
# Windows needs to use network ports, doesn't support unix sockets with open(self.common.get_resource_path('torrc_template')) as f:
torrc_template = open(self.common.get_resource_path('torrc_template-windows')).read() torrc_template = f.read()
self.tor_cookie_auth_file = os.path.join(self.tor_data_directory.name, 'cookie')
try:
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')
if self.common.platform == 'Windows' or self.common.platform == "Darwin":
# Windows doesn't support unix sockets, so it must use a network port.
# macOS can't use unix sockets either because socket filenames are limited to
# 100 chars, and the macOS sandbox forces us to put the socket file in a place
# with a really long path.
torrc_template += 'ControlPort {{control_port}}\n'
try: try:
self.tor_control_port = self.common.get_available_port(1000, 65535) self.tor_control_port = self.common.get_available_port(1000, 65535)
except: except:
raise OSError(strings._('no_available_port')) raise OSError(strings._('no_available_port'))
self.tor_control_socket = None self.tor_control_socket = None
self.tor_cookie_auth_file = os.path.join(self.tor_data_directory.name, 'cookie')
try:
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: else:
# Linux, Mac and BSD can use unix sockets # Linux and BSD can use unix sockets
with open(self.common.get_resource_path('torrc_template')) as f: torrc_template += 'ControlSocket {{control_socket}}\n'
torrc_template = f.read()
self.tor_control_port = None self.tor_control_port = None
self.tor_control_socket = os.path.join(self.tor_data_directory.name, 'control_socket') 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 = 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')
torrc_template = torrc_template.replace('{{data_directory}}', self.tor_data_directory.name) torrc_template = torrc_template.replace('{{data_directory}}', self.tor_data_directory.name)
torrc_template = torrc_template.replace('{{control_port}}', str(self.tor_control_port)) torrc_template = torrc_template.replace('{{control_port}}', str(self.tor_control_port))
@ -205,6 +206,7 @@ class Onion(object):
torrc_template = torrc_template.replace('{{geo_ip_file}}', self.tor_geo_ip_file_path) torrc_template = torrc_template.replace('{{geo_ip_file}}', self.tor_geo_ip_file_path)
torrc_template = torrc_template.replace('{{geo_ipv6_file}}', self.tor_geo_ipv6_file_path) torrc_template = torrc_template.replace('{{geo_ipv6_file}}', self.tor_geo_ipv6_file_path)
torrc_template = torrc_template.replace('{{socks_port}}', str(self.tor_socks_port)) torrc_template = torrc_template.replace('{{socks_port}}', str(self.tor_socks_port))
with open(self.tor_torrc, 'w') as f: with open(self.tor_torrc, 'w') as f:
f.write(torrc_template) f.write(torrc_template)
@ -243,7 +245,7 @@ class Onion(object):
# Connect to the controller # Connect to the controller
try: try:
if self.common.platform == 'Windows': if self.common.platform == 'Windows' or self.common.platform == "Darwin":
self.c = Controller.from_port(port=self.tor_control_port) self.c = Controller.from_port(port=self.tor_control_port)
self.c.authenticate() self.c.authenticate()
else: else:

View File

@ -23,6 +23,12 @@ import os
import platform import platform
import locale import locale
try:
# We only need pwd module in macOS, and it's not available in Windows
import pwd
except:
pass
from . import strings from . import strings
@ -132,30 +138,24 @@ class Settings(object):
""" """
Returns the path of the settings file. Returns the path of the settings file.
""" """
p = platform.system() return os.path.join(self.common.build_data_dir(), 'onionshare.json')
if p == 'Windows':
try:
appdata = os.environ['APPDATA']
return '{}\\OnionShare\\onionshare.json'.format(appdata)
except:
# If for some reason we don't have the 'APPDATA' environment variable
# (like running tests in Linux while pretending to be in Windows)
return os.path.expanduser('~/.config/onionshare/onionshare.json')
elif p == 'Darwin':
return os.path.expanduser('~/Library/Application Support/OnionShare/onionshare.json')
else:
return os.path.expanduser('~/.config/onionshare/onionshare.json')
def build_default_downloads_dir(self): def build_default_downloads_dir(self):
""" """
Returns the path of the default Downloads directory for receive mode. Returns the path of the default Downloads directory for receive mode.
""" """
# On Windows, os.path.expanduser() needs to use backslash, or else it
# retains the forward slash, which breaks opening the folder in explorer. if self.common.platform == "Darwin":
p = platform.system() # We can't use os.path.expanduser() in macOS because in the sandbox it
if p == 'Windows': # returns the path to the sandboxed homedir
real_homedir = pwd.getpwuid(os.getuid()).pw_dir
return os.path.join(real_homedir, 'OnionShare')
elif self.common.platform == "Windows":
# On Windows, os.path.expanduser() needs to use backslash, or else it
# retains the forward slash, which breaks opening the folder in explorer.
return os.path.expanduser('~\OnionShare') return os.path.expanduser('~\OnionShare')
else: else:
# All other OSes
return os.path.expanduser('~/OnionShare') return os.path.expanduser('~/OnionShare')
def load(self): def load(self):
@ -174,16 +174,18 @@ class Settings(object):
except: except:
pass pass
# Make sure downloads_dir exists
try:
os.makedirs(self.get('downloads_dir'), exist_ok=True)
except:
pass
def save(self): def save(self):
""" """
Save settings to file. Save settings to file.
""" """
self.common.log('Settings', 'save') self.common.log('Settings', 'save')
os.makedirs(os.path.dirname(self.filename), exist_ok=True)
try:
os.makedirs(os.path.dirname(self.filename))
except:
pass
open(self.filename, 'w').write(json.dumps(self._settings)) open(self.filename, 'w').write(json.dumps(self._settings))
self.common.log('Settings', 'save', 'Settings saved in {}'.format(self.filename)) self.common.log('Settings', 'save', 'Settings saved in {}'.format(self.filename))

View File

@ -184,19 +184,7 @@ class Web(object):
""" """
Turn on debugging mode, which will log flask errors to a debug file. Turn on debugging mode, which will log flask errors to a debug file.
""" """
if self.common.platform == 'Windows': flask_debug_filename = os.path.join(self.common.build_data_dir(), 'flask_debug.log')
try:
appdata = os.environ['APPDATA']
flask_debug_filename = '{}\\OnionShare\\flask_debug.log'.format(appdata)
except:
# If for some reason we don't have the 'APPDATA' environment variable
# (like running tests in Linux while pretending to be in Windows)
flask_debug_filename = os.path.expanduser('~/.config/onionshare/flask_debug.log')
elif self.common.platform == 'Darwin':
flask_debug_filename = os.path.expanduser('~/Library/Application Support/OnionShare/flask_debug.log')
else:
flask_debug_filename = os.path.expanduser('~/.config/onionshare/flask_debug.log')
log_handler = logging.FileHandler(flask_debug_filename) log_handler = logging.FileHandler(flask_debug_filename)
log_handler.setLevel(logging.WARNING) log_handler.setLevel(logging.WARNING)
self.app.logger.addHandler(log_handler) self.app.logger.addHandler(log_handler)

View File

@ -47,7 +47,7 @@ class ShareMode(Mode):
self.web = Web(self.common, True, 'share') self.web = Web(self.common, True, 'share')
# File selection # File selection
self.file_selection = FileSelection(self.common) self.file_selection = FileSelection(self.common, self)
if self.filenames: if self.filenames:
for filename in self.filenames: for filename in self.filenames:
self.file_selection.file_list.add_file(filename) self.file_selection.file_list.add_file(filename)

View File

@ -288,10 +288,11 @@ class FileSelection(QtWidgets.QVBoxLayout):
The list of files and folders in the GUI, as well as buttons to add and The list of files and folders in the GUI, as well as buttons to add and
delete the files and folders. delete the files and folders.
""" """
def __init__(self, common): def __init__(self, common, parent):
super(FileSelection, self).__init__() super(FileSelection, self).__init__()
self.common = common self.common = common
self.parent = parent
self.server_on = False self.server_on = False
@ -302,13 +303,25 @@ class FileSelection(QtWidgets.QVBoxLayout):
self.file_list.files_updated.connect(self.update) self.file_list.files_updated.connect(self.update)
# Buttons # Buttons
self.add_button = QtWidgets.QPushButton(strings._('gui_add')) if self.common.platform == 'Darwin':
self.add_button.clicked.connect(self.add) # The macOS sandbox makes it so the Mac version needs separate add files
# and folders buttons, in order to use native file selection dialogs
self.add_files_button = QtWidgets.QPushButton(strings._('gui_add_files'))
self.add_files_button.clicked.connect(self.add_files)
self.add_folder_button = QtWidgets.QPushButton(strings._('gui_add_folder'))
self.add_folder_button.clicked.connect(self.add_folder)
else:
self.add_button = QtWidgets.QPushButton(strings._('gui_add'))
self.add_button.clicked.connect(self.add)
self.delete_button = QtWidgets.QPushButton(strings._('gui_delete')) self.delete_button = QtWidgets.QPushButton(strings._('gui_delete'))
self.delete_button.clicked.connect(self.delete) self.delete_button.clicked.connect(self.delete)
button_layout = QtWidgets.QHBoxLayout() button_layout = QtWidgets.QHBoxLayout()
button_layout.addStretch() button_layout.addStretch()
button_layout.addWidget(self.add_button) if self.common.platform == 'Darwin':
button_layout.addWidget(self.add_files_button)
button_layout.addWidget(self.add_folder_button)
else:
button_layout.addWidget(self.add_button)
button_layout.addWidget(self.delete_button) button_layout.addWidget(self.delete_button)
# Add the widgets # Add the widgets
@ -323,10 +336,18 @@ class FileSelection(QtWidgets.QVBoxLayout):
""" """
# All buttons should be hidden if the server is on # All buttons should be hidden if the server is on
if self.server_on: if self.server_on:
self.add_button.hide() if self.common.platform == 'Darwin':
self.add_files_button.hide()
self.add_folder_button.hide()
else:
self.add_button.hide()
self.delete_button.hide() self.delete_button.hide()
else: else:
self.add_button.show() if self.common.platform == 'Darwin':
self.add_files_button.show()
self.add_folder_button.show()
else:
self.add_button.show()
# Delete button should be hidden if item isn't selected # Delete button should be hidden if item isn't selected
if len(self.file_list.selectedItems()) == 0: if len(self.file_list.selectedItems()) == 0:
@ -349,6 +370,24 @@ class FileSelection(QtWidgets.QVBoxLayout):
self.file_list.setCurrentItem(None) self.file_list.setCurrentItem(None)
self.update() self.update()
def add_files(self):
"""
Add files button clicked.
"""
files = QtWidgets.QFileDialog.getOpenFileNames(self.parent, caption=strings._('gui_choose_items'))
filenames = files[0]
for filename in filenames:
self.file_list.add_file(filename)
def add_folder(self):
"""
Add folder button clicked.
"""
filename = QtWidgets.QFileDialog.getExistingDirectory(self.parent,
caption=strings._('gui_choose_items'),
options=QtWidgets.QFileDialog.ShowDirsOnly)
self.file_list.add_file(filename)
def delete(self): def delete(self):
""" """
Delete button clicked Delete button clicked

View File

@ -44,6 +44,10 @@ class AddFileDialog(QtWidgets.QFileDialog):
""" """
Overridden version of QFileDialog which allows us to select folders as well Overridden version of QFileDialog which allows us to select folders as well
as, or instead of, files. For adding files/folders to share. as, or instead of, files. For adding files/folders to share.
Note that this dialog can't be used in macOS, only in Windows, Linux, and BSD.
This is because the macOS sandbox requires native dialogs, and this is a Qt5
dialog.
""" """
def __init__(self, common, *args, **kwargs): def __init__(self, common, *args, **kwargs):
QtWidgets.QFileDialog.__init__(self, *args, **kwargs) QtWidgets.QFileDialog.__init__(self, *args, **kwargs)

View File

@ -34,6 +34,8 @@
"help_config": "Custom JSON config file location (optional)", "help_config": "Custom JSON config file location (optional)",
"gui_drag_and_drop": "Drag and drop files and folders\nto start sharing", "gui_drag_and_drop": "Drag and drop files and folders\nto start sharing",
"gui_add": "Add", "gui_add": "Add",
"gui_add_files": "Add Files",
"gui_add_folder": "Add Folder",
"gui_delete": "Delete", "gui_delete": "Delete",
"gui_choose_items": "Choose", "gui_choose_items": "Choose",
"gui_share_start_server": "Start sharing", "gui_share_start_server": "Start sharing",

View File

@ -1,6 +1,5 @@
DataDirectory {{data_directory}} DataDirectory {{data_directory}}
SocksPort {{socks_port}} SocksPort {{socks_port}}
ControlSocket {{control_socket}}
CookieAuthentication 1 CookieAuthentication 1
CookieAuthFile {{cookie_auth_file}} CookieAuthFile {{cookie_auth_file}}
AvoidDiskWrites 1 AvoidDiskWrites 1

View File

@ -1,9 +0,0 @@
DataDirectory {{data_directory}}
SocksPort {{socks_port}}
ControlPort {{control_port}}
CookieAuthentication 1
CookieAuthFile {{cookie_auth_file}}
AvoidDiskWrites 1
Log notice stdout
GeoIPFile {{geo_ip_file}}
GeoIPv6File {{geo_ipv6_file}}

View File

@ -1 +1 @@
2.0.dev 2.0.dev1

View File

@ -175,7 +175,7 @@ class TestSettings:
platform_windows): platform_windows):
monkeypatch.setenv('APPDATA', 'C:') monkeypatch.setenv('APPDATA', 'C:')
obj = settings.Settings(common.Common()) obj = settings.Settings(common.Common())
assert obj.filename == 'C:\\OnionShare\\onionshare.json' assert obj.filename.replace('/', '\\') == 'C:\\OnionShare\\onionshare.json'
def test_set_custom_bridge(self, settings_obj): def test_set_custom_bridge(self, settings_obj):
settings_obj.set('tor_bridges_use_custom_bridges', 'Bridge 45.3.20.65:9050 21300AD88890A49C429A6CB9959CFD44490A8F6E') settings_obj.set('tor_bridges_use_custom_bridges', 'Bridge 45.3.20.65:9050 21300AD88890A49C429A6CB9959CFD44490A8F6E')