diff options
Diffstat (limited to 'cli/onionshare_cli')
20 files changed, 355 insertions, 189 deletions
diff --git a/cli/onionshare_cli/censorship.py b/cli/onionshare_cli/censorship.py index f84b1058..9f41d61c 100644 --- a/cli/onionshare_cli/censorship.py +++ b/cli/onionshare_cli/censorship.py @@ -25,21 +25,46 @@ from .meek import MeekNotRunning class CensorshipCircumvention(object): """ Connect to the Tor Moat APIs to retrieve censorship - circumvention recommendations, over the Meek client. + circumvention recommendations or the latest bridges. + + We support reaching this API over Tor, or Meek + (domain fronting) if Tor is not connected. """ - def __init__(self, common, meek, domain_fronting=True): + def __init__(self, common, meek=None, onion=None): """ Set up the CensorshipCircumvention object to hold common and meek objects. """ self.common = common - self.meek = meek self.common.log("CensorshipCircumvention", "__init__") - - # Bail out if we requested domain fronting but we can't use meek - if domain_fronting and not self.meek.meek_proxies: - raise MeekNotRunning() + self.api_proxies = {} + if meek: + self.meek = meek + if not self.meek.meek_proxies: + raise MeekNotRunning() + else: + self.common.log( + "CensorshipCircumvention", + "__init__", + "Using Meek with CensorShipCircumvention API", + ) + self.api_proxies = self.meek.meek_proxies + if onion: + self.onion = onion + if not self.onion.is_authenticated: + return False + else: + self.common.log( + "CensorshipCircumvention", + "__init__", + "Using Tor with CensorShipCircumvention API", + ) + (socks_address, socks_port) = self.onion.get_tor_socks_port() + self.api_proxies = { + "http": f"socks5h://{socks_address}:{socks_port}", + "https": f"socks5h://{socks_address}:{socks_port}", + } def request_map(self, country=False): """ @@ -52,6 +77,8 @@ class CensorshipCircumvention(object): Note that this API endpoint doesn't return actual bridges, it just returns the recommended bridge type countries. """ + if not self.api_proxies: + return False endpoint = "https://bridges.torproject.org/moat/circumvention/map" data = {} if country: @@ -61,7 +88,7 @@ class CensorshipCircumvention(object): endpoint, json=data, headers={"Content-Type": "application/vnd.api+json"}, - proxies=self.meek.meek_proxies, + proxies=self.api_proxies, ) if r.status_code != 200: self.common.log( @@ -95,6 +122,8 @@ class CensorshipCircumvention(object): Optionally, a list of transports can be specified in order to return recommended settings for just that transport type. """ + if not self.api_proxies: + return False endpoint = "https://bridges.torproject.org/moat/circumvention/settings" data = {} if country: @@ -105,7 +134,7 @@ class CensorshipCircumvention(object): endpoint, json=data, headers={"Content-Type": "application/vnd.api+json"}, - proxies=self.meek.meek_proxies, + proxies=self.api_proxies, ) if r.status_code != 200: self.common.log( @@ -142,11 +171,13 @@ class CensorshipCircumvention(object): """ Retrieves the list of built-in bridges from the Tor Project. """ + if not self.api_proxies: + return False endpoint = "https://bridges.torproject.org/moat/circumvention/builtin" r = requests.post( endpoint, headers={"Content-Type": "application/vnd.api+json"}, - proxies=self.meek.meek_proxies, + proxies=self.api_proxies, ) if r.status_code != 200: self.common.log( diff --git a/cli/onionshare_cli/common.py b/cli/onionshare_cli/common.py index a8e32411..272d2860 100644 --- a/cli/onionshare_cli/common.py +++ b/cli/onionshare_cli/common.py @@ -329,23 +329,49 @@ class Common: tor_geo_ip_file_path = os.path.join(prefix, "share/tor/geoip") tor_geo_ipv6_file_path = os.path.join(prefix, "share/tor/geoip6") elif self.platform == "Windows": + # In Windows, the Tor binaries are in the onionshare package, not the onionshare_cli package base_path = self.get_resource_path("tor") + base_path = base_path.replace("onionshare_cli", "onionshare") tor_path = os.path.join(base_path, "Tor", "tor.exe") + + # If tor.exe isn't there, mayber we're running from the source tree + if not os.path.exists(tor_path): + base_path = os.path.join(os.getcwd(), "onionshare", "resources", "tor") + + tor_path = os.path.join(base_path, "Tor", "tor.exe") + if not os.path.exists(tor_path): + raise CannotFindTor() + obfs4proxy_file_path = os.path.join(base_path, "Tor", "obfs4proxy.exe") snowflake_file_path = os.path.join(base_path, "Tor", "snowflake-client.exe") meek_client_file_path = os.path.join(base_path, "Tor", "meek-client.exe") tor_geo_ip_file_path = os.path.join(base_path, "Data", "Tor", "geoip") tor_geo_ipv6_file_path = os.path.join(base_path, "Data", "Tor", "geoip6") + elif self.platform == "Darwin": - tor_path = shutil.which("tor") - if not tor_path: - raise CannotFindTor() - obfs4proxy_file_path = shutil.which("obfs4proxy") - snowflake_file_path = shutil.which("snowflake-client") - meek_client_file_path = shutil.which("meek-client") - prefix = os.path.dirname(os.path.dirname(tor_path)) - tor_geo_ip_file_path = os.path.join(prefix, "share/tor/geoip") - tor_geo_ipv6_file_path = os.path.join(prefix, "share/tor/geoip6") + # Let's see if we have tor binaries in the onionshare GUI package + base_path = self.get_resource_path("tor") + base_path = base_path.replace("onionshare_cli", "onionshare") + tor_path = os.path.join(base_path, "tor") + if os.path.exists(tor_path): + obfs4proxy_file_path = os.path.join(base_path, "obfs4proxy") + snowflake_file_path = os.path.join(base_path, "snowflake-client") + meek_client_file_path = os.path.join(base_path, "meek-client") + tor_geo_ip_file_path = os.path.join(base_path, "geoip") + tor_geo_ipv6_file_path = os.path.join(base_path, "geoip6") + else: + # Fallback to looking in the path + tor_path = shutil.which("tor") + if not os.path.exists(tor_path): + raise CannotFindTor() + + obfs4proxy_file_path = shutil.which("obfs4proxy") + snowflake_file_path = shutil.which("snowflake-client") + meek_client_file_path = shutil.which("meek-client") + prefix = os.path.dirname(os.path.dirname(tor_path)) + tor_geo_ip_file_path = os.path.join(prefix, "share/tor/geoip") + tor_geo_ipv6_file_path = os.path.join(prefix, "share/tor/geoip6") + elif self.platform == "BSD": tor_path = "/usr/local/bin/tor" tor_geo_ip_file_path = "/usr/local/share/tor/geoip" diff --git a/cli/onionshare_cli/meek.py b/cli/onionshare_cli/meek.py index dffbad83..3ada19c7 100644 --- a/cli/onionshare_cli/meek.py +++ b/cli/onionshare_cli/meek.py @@ -20,8 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. import os import subprocess import time -from queue import Queue, Empty -from threading import Thread class Meek(object): @@ -67,14 +65,6 @@ class Meek(object): Start the Meek Client and populate the SOCKS proxies dict for use with requests to the Tor Moat API. """ - # Small method to read stdout from the subprocess. - # We use this to obtain the random port that Meek - # started on - def enqueue_output(out, queue): - for line in iter(out.readline, b""): - queue.put(line) - out.close() - # Abort early if we can't find the Meek client if self.meek_client_file_path is None or not os.path.exists( self.meek_client_file_path @@ -124,34 +114,22 @@ class Meek(object): universal_newlines=True, ) - # Queue up the stdout from the subprocess for polling later - q = Queue() - t = Thread(target=enqueue_output, args=(self.meek_proc.stdout, q)) - t.daemon = True # thread dies with the program - t.start() - - while True: - # read stdout without blocking - try: - line = q.get_nowait() - self.common.log("Meek", "start", line.strip()) - except Empty: - # no stdout yet? - pass - else: # we got stdout - if "CMETHOD meek socks5" in line: - self.meek_host = line.split(" ")[3].split(":")[0] - self.meek_port = line.split(" ")[3].split(":")[1] - self.common.log( - "Meek", - "start", - f"Meek running on {self.meek_host}:{self.meek_port}", - ) - break + # Obtain the host and port that meek is running on + for line in iter(self.meek_proc.stdout.readline, b""): + if "CMETHOD meek socks5" in line: + self.meek_host = line.split(" ")[3].split(":")[0] + self.meek_port = line.split(" ")[3].split(":")[1] + self.common.log( + "Meek", + "start", + f"Meek running on {self.meek_host}:{self.meek_port}", + ) + break - if "CMETHOD-ERROR" in line: - self.cleanup() - raise MeekNotRunning() + if "CMETHOD-ERROR" in line: + self.cleanup() + raise MeekNotRunning() + break if self.meek_port: self.meek_proxies = { diff --git a/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py index 5ac669b8..76deea80 100644 --- a/cli/onionshare_cli/onion.py +++ b/cli/onionshare_cli/onion.py @@ -18,17 +18,19 @@ You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. """ +from .censorship import CensorshipCircumvention +from .meek import Meek from stem.control import Controller from stem import ProtocolError, SocketClosed from stem.connection import MissingPassword, UnreadableCookieFile, AuthenticationFailure import base64 import nacl.public import os -import tempfile +import psutil +import shlex import subprocess +import tempfile import time -import shlex -import psutil import traceback from distutils.version import LooseVersion as Version @@ -258,9 +260,7 @@ class Onion(object): and cmdline[2] == self.tor_torrc ): self.common.log( - "Onion", - "connect", - "found a stale tor process, killing it", + "Onion", "connect", "found a stale tor process, killing it" ) proc.terminate() proc.wait() @@ -317,49 +317,75 @@ class Onion(object): ) with open(self.tor_torrc, "w") as f: + self.common.log("Onion", "connect", "Writing torrc template file") f.write(torrc_template) # Bridge support if self.settings.get("bridges_enabled"): + f.write("\nUseBridges 1\n") if self.settings.get("bridges_type") == "built-in": - if self.settings.get("bridges_builtin_pt") == "obfs4": - with open( - self.common.get_resource_path("torrc_template-obfs4") - ) as o: - f.write(o.read()) - elif self.settings.get("bridges_builtin_pt") == "meek-azure": - with open( - self.common.get_resource_path( - "torrc_template-meek_lite_azure" - ) - ) as o: - f.write(o.read()) - elif self.settings.get("bridges_builtin_pt") == "snowflake": - with open( - self.common.get_resource_path( - "torrc_template-snowflake" + use_torrc_bridge_templates = False + builtin_bridge_type = self.settings.get("bridges_builtin_pt") + # Use built-inbridges stored in settings, if they are there already. + # They are probably newer than that of our hardcoded copies. + if self.settings.get("bridges_builtin"): + try: + for line in self.settings.get("bridges_builtin")[ + builtin_bridge_type + ]: + if line.strip() != "": + f.write(f"Bridge {line}\n") + self.common.log( + "Onion", + "connect", + "Wrote in the built-in bridges from OnionShare settings", ) - ) as o: - f.write(o.read()) - + except KeyError: + # Somehow we had built-in bridges in our settings, but + # not for this bridge type. Fall back to using the hard- + # coded templates. + use_torrc_bridge_templates = True + else: + use_torrc_bridge_templates = True + if use_torrc_bridge_templates: + if builtin_bridge_type == "obfs4": + with open( + self.common.get_resource_path( + "torrc_template-obfs4" + ) + ) as o: + f.write(o.read()) + elif builtin_bridge_type == "meek-azure": + with open( + self.common.get_resource_path( + "torrc_template-meek_lite_azure" + ) + ) as o: + f.write(o.read()) + elif builtin_bridge_type == "snowflake": + with open( + self.common.get_resource_path( + "torrc_template-snowflake" + ) + ) as o: + f.write(o.read()) + self.common.log( + "Onion", + "connect", + "Wrote in the built-in bridges from torrc templates", + ) elif self.settings.get("bridges_type") == "moat": for line in self.settings.get("bridges_moat").split("\n"): if line.strip() != "": f.write(f"Bridge {line}\n") - f.write("\nUseBridges 1\n") elif self.settings.get("bridges_type") == "custom": for line in self.settings.get("bridges_custom").split("\n"): if line.strip() != "": f.write(f"Bridge {line}\n") - f.write("\nUseBridges 1\n") # Execute a tor subprocess - self.common.log( - "Onion", - "connect", - f"starting {self.tor_path} subprocess", - ) + self.common.log("Onion", "connect", f"starting {self.tor_path} subprocess") start_ts = time.time() if self.common.platform == "Windows": # In Windows, hide console window when opening tor.exe subprocess @@ -385,19 +411,15 @@ class Onion(object): ) # Wait for the tor controller to start - self.common.log( - "Onion", - "connect", - f"tor pid: {self.tor_proc.pid}", - ) + self.common.log("Onion", "connect", f"tor pid: {self.tor_proc.pid}") time.sleep(2) + return_code = self.tor_proc.poll() + if return_code != None: + self.common.log("Onion", "connect", f"tor process has terminated early: {return_code}") + # Connect to the controller - self.common.log( - "Onion", - "connect", - "authenticating to tor controller", - ) + self.common.log("Onion", "connect", "authenticating to tor controller") try: if ( self.common.platform == "Windows" @@ -638,6 +660,14 @@ class Onion(object): # https://trac.torproject.org/projects/tor/ticket/28619 self.supports_v3_onions = self.tor_version >= Version("0.3.5.7") + # Now that we are connected to Tor, if we are using built-in bridges, + # update them with the latest copy available from the Tor API + if ( + self.settings.get("bridges_enabled") + and self.settings.get("bridges_type") == "built-in" + ): + self.update_builtin_bridges() + def is_authenticated(self): """ Returns True if the Tor connection is still working, or False otherwise. @@ -881,3 +911,68 @@ class Onion(object): return ("127.0.0.1", 9150) else: return (self.settings.get("socks_address"), self.settings.get("socks_port")) + + def update_builtin_bridges(self): + """ + Use the CensorshipCircumvention API to fetch the latest built-in bridges + and update them in settings. + """ + builtin_bridges = False + meek = None + # Try obtaining bridges over Tor, if we're connected to it. + if self.is_authenticated: + self.common.log( + "Onion", + "update_builtin_bridges", + "Updating the built-in bridges. Trying over Tor first", + ) + self.censorship_circumvention = CensorshipCircumvention( + self.common, None, self + ) + builtin_bridges = self.censorship_circumvention.request_builtin_bridges() + + if not builtin_bridges: + # Tor was not running or it failed to hit the Tor API. + # Fall back to using Meek (domain-fronting). + self.common.log( + "Onion", + "update_builtin_bridges", + "Updating the built-in bridges. Trying via Meek (no Tor)", + ) + meek = Meek(self.common) + meek.start() + self.censorship_circumvention = CensorshipCircumvention( + self.common, meek, None + ) + builtin_bridges = self.censorship_circumvention.request_builtin_bridges() + meek.cleanup() + + if builtin_bridges: + # If we got to this point, we have bridges + self.common.log( + "Onion", + "update_builtin_bridges", + f"Obtained bridges: {builtin_bridges}", + ) + if builtin_bridges["meek"]: + # Meek bridge needs to be defined as "meek_lite", not "meek", + # for it to work with obfs4proxy. + # We also refer to this bridge type as 'meek-azure' in our settings. + # So first, rename the key in the dict + builtin_bridges["meek-azure"] = builtin_bridges.pop("meek") + new_meek_bridges = [] + # Now replace the values. They also need the url/front params appended + for item in builtin_bridges["meek-azure"]: + newline = item.replace("meek", "meek_lite") + new_meek_bridges.append( + f"{newline} url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com" + ) + builtin_bridges["meek-azure"] = new_meek_bridges + # Save the new settings + self.settings.set("bridges_builtin", builtin_bridges) + self.settings.save() + else: + self.common.log( + "Onion", "update_builtin_bridges", "Error getting built-in bridges" + ) + return False diff --git a/cli/onionshare_cli/onionshare.py b/cli/onionshare_cli/onionshare.py index c2711b89..2bb22296 100644 --- a/cli/onionshare_cli/onionshare.py +++ b/cli/onionshare_cli/onionshare.py @@ -40,9 +40,6 @@ class OnionShare(object): self.onion_host = None self.port = None - # files and dirs to delete on shutdown - self.cleanup_filenames = [] - # do not use tor -- for development self.local_only = local_only @@ -75,7 +72,9 @@ class OnionShare(object): if self.local_only: self.onion_host = f"127.0.0.1:{self.port}" if not mode_settings.get("general", "public"): - self.auth_string = "E2GOT5LTUTP3OAMRCRXO4GSH6VKJEUOXZQUC336SRKAHTTT5OVSA" + self.auth_string = ( + "E2GOT5LTUTP3OAMRCRXO4GSH6VKJEUOXZQUC336SRKAHTTT5OVSA" + ) return self.onion_host = self.onion.start_onion_service( diff --git a/cli/onionshare_cli/resources/static/css/style.css b/cli/onionshare_cli/resources/static/css/style.css index 7cec9738..79be31d0 100644 --- a/cli/onionshare_cli/resources/static/css/style.css +++ b/cli/onionshare_cli/resources/static/css/style.css @@ -320,15 +320,15 @@ div#uploads .upload .upload-status { } div#uploads .upload input.cancel { - color: #d0011b; + color: #d0011b; border: 0; background: none; box-shadow: none; border-radius: 0px; - cursor: pointer; - font-family: sans-serif; - font-size: 12px; - text-decoration: none; + cursor: pointer; + font-family: sans-serif; + font-size: 12px; + text-decoration: none; display: inline-block; float:right; } @@ -398,4 +398,4 @@ a { a:visited { color: #601ca0; -}
\ No newline at end of file +} diff --git a/cli/onionshare_cli/resources/static/js/chat.js b/cli/onionshare_cli/resources/static/js/chat.js index 21f00ca6..88abf4f4 100644 --- a/cli/onionshare_cli/resources/static/js/chat.js +++ b/cli/onionshare_cli/resources/static/js/chat.js @@ -154,7 +154,7 @@ var getScrollDiffBefore = function () { var scrollBottomMaybe = function (scrollDiff) { // Scrolls to bottom if the user is scrolled at bottom - // if the user has scrolled upp, it wont scroll at bottom. + // if the user has scrolled up, it won't scroll at bottom. // Note: when a user themselves send a message, it will still // scroll to the bottom even if they had scrolled up before. if (scrollDiff > 0) { diff --git a/cli/onionshare_cli/resources/static/js/send.js b/cli/onionshare_cli/resources/static/js/send.js index 43e9892d..22844ab9 100644 --- a/cli/onionshare_cli/resources/static/js/send.js +++ b/cli/onionshare_cli/resources/static/js/send.js @@ -11,7 +11,7 @@ function unhumanize(text) { } } function sortTable(n) { - var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0; + var table, rows, switching, i, x, y, valX, valY, shouldSwitch, dir, switchcount = 0; table = document.getElementById("file-list"); switching = true; // Set the sorting direction to ascending: @@ -21,7 +21,7 @@ function sortTable(n) { while (switching) { // Start by saying: no switching is done: switching = false; - rows = table.getElementsByTagName("TR"); + rows = table.getElementsByClassName("row"); /* Loop through all table rows (except the first, which contains table headers): */ for (i = 1; i < (rows.length - 1); i++) { @@ -29,18 +29,22 @@ function sortTable(n) { 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]; + x = rows[i].getElementsByClassName("cell-data")[n]; + y = rows[i + 1].getElementsByClassName("cell-data")[n]; + + valX = x.classList.contains("size") ? unhumanize(x.innerHTML.toLowerCase()) : x.innerHTML; + valY = y.classList.contains("size") ? unhumanize(y.innerHTML.toLowerCase()) : y.innerHTML; + /* 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; - } + if (valX > valY) { + // 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 (valX < valY) { // If so, mark as a switch and break the loop: shouldSwitch= true; break; diff --git a/cli/onionshare_cli/resources/templates/403.html b/cli/onionshare_cli/resources/templates/403.html index c9d28eea..eff250e6 100644 --- a/cli/onionshare_cli/resources/templates/403.html +++ b/cli/onionshare_cli/resources/templates/403.html @@ -4,7 +4,7 @@ <head> <title>OnionShare: 403 Forbidden</title> <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon" /> <link rel="stylesheet" rel="subresource" type="text/css" href="{{ static_url_path }}/css/style.css" media="all"> </head> diff --git a/cli/onionshare_cli/resources/templates/404.html b/cli/onionshare_cli/resources/templates/404.html index e816f2c4..c921aa3e 100644 --- a/cli/onionshare_cli/resources/templates/404.html +++ b/cli/onionshare_cli/resources/templates/404.html @@ -4,7 +4,7 @@ <head> <title>OnionShare: 404 Not Found</title> <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon"> <link rel="stylesheet" rel="subresource" type="text/css" href="{{ static_url_path }}/css/style.css" media="all"> </head> diff --git a/cli/onionshare_cli/resources/templates/405.html b/cli/onionshare_cli/resources/templates/405.html index 63888004..76c32c19 100644 --- a/cli/onionshare_cli/resources/templates/405.html +++ b/cli/onionshare_cli/resources/templates/405.html @@ -4,7 +4,7 @@ <head> <title>OnionShare: 405 Method Not Allowed</title> <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon"> <link rel="stylesheet" rel="subresource" type="text/css" href="{{ static_url_path }}/css/style.css" media="all"> </head> diff --git a/cli/onionshare_cli/resources/templates/500.html b/cli/onionshare_cli/resources/templates/500.html index 9f6727d2..f6501a21 100644 --- a/cli/onionshare_cli/resources/templates/500.html +++ b/cli/onionshare_cli/resources/templates/500.html @@ -4,7 +4,7 @@ <head> <title>OnionShare: An error occurred</title> <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon"> <link rel="stylesheet" rel="subresource" type="text/css" href="{{ static_url_path }}/css/style.css" media="all"> </head> diff --git a/cli/onionshare_cli/resources/templates/send.html b/cli/onionshare_cli/resources/templates/send.html index 5fc1ba1f..b1532cec 100644 --- a/cli/onionshare_cli/resources/templates/send.html +++ b/cli/onionshare_cli/resources/templates/send.html @@ -32,7 +32,7 @@ {% endif %} <div class="file-list" id="file-list"> - <div class="d-flex"> + <div class="d-flex row"> <div id="filename-header" class="heading">Filename</div> <div id="size-header" class="heading">Size</div> </div> @@ -41,26 +41,26 @@ <div> <img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_folder.png" /> <a href="{{ info.link }}"> - <span>{{ info.basename }}</span> + <span class="cell-data">{{ info.basename }}</span> </a> </div> - <div>—</div> + <div class="cell-data">—</div> </div> {% endfor %} {% for info in files %} - <div class="d-flex"> + <div class="d-flex row"> <div> <img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_file.png" /> {% if download_individual_files %} <a href="{{ info.link }}"> - <span>{{ info.basename }}</span> + <span class="cell-data">{{ info.basename }}</span> </a> {% else %} - <span>{{ info.basename }}</span> + <span class="cell-data">{{ info.basename }}</span> {% endif %} </div> - <div>{{ info.size_human }}</div> + <div class="cell-data size">{{ info.size_human }}</div> </div> {% endfor %} </div> diff --git a/cli/onionshare_cli/resources/torrc_template-meek_lite_azure b/cli/onionshare_cli/resources/torrc_template-meek_lite_azure index 6f601681..ff67f518 100644 --- a/cli/onionshare_cli/resources/torrc_template-meek_lite_azure +++ b/cli/onionshare_cli/resources/torrc_template-meek_lite_azure @@ -1,3 +1,2 @@ -# Enable built-in meek-azure bridge -Bridge meek_lite 0.0.2.0:3 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com -UseBridges 1 +# Enable built-in meek bridge +Bridge meek_lite 0.0.2.0:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com diff --git a/cli/onionshare_cli/resources/torrc_template-obfs4 b/cli/onionshare_cli/resources/torrc_template-obfs4 index 720cc28c..adf343de 100644 --- a/cli/onionshare_cli/resources/torrc_template-obfs4 +++ b/cli/onionshare_cli/resources/torrc_template-obfs4 @@ -1,17 +1,16 @@ -# Enable built-in obfs4-bridge -Bridge obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1 -Bridge obfs4 38.229.1.78:80 C8CBDB2464FC9804A69531437BCF2BE31FDD2EE4 cert=Hmyfd2ev46gGY7NoVxA9ngrPF2zCZtzskRTzoWXbxNkzeVnGFPWmrTtILRyqCTjHR+s9dg iat-mode=1 -Bridge obfs4 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1OpbAH0wNqOT6H6BmRQ iat-mode=1 -Bridge obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0 -Bridge obfs4 85.31.186.98:443 011F2599C0E9B27EE74B353155E244813763C3E5 cert=ayq0XzCwhpdysn5o0EyDUbmSOx3X/oTEbzDMvczHOdBJKlvIdHHLJGkZARtT4dcBFArPPg iat-mode=0 -Bridge obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0 +# Enable built-in obfs4 bridge Bridge obfs4 144.217.20.138:80 FB70B257C162BF1038CA669D568D76F5B7F0BABB cert=vYIV5MgrghGQvZPIi1tJwnzorMgqgmlKaB77Y3Z9Q/v94wZBOAXkW+fdx4aSxLVnKO+xNw iat-mode=0 +Bridge obfs4 146.57.248.225:22 10A6CD36A537FCE513A322361547444B393989F0 cert=K1gDtDAIcUfeLqbstggjIw2rtgIKqdIhUlHp82XRqNSq/mtAjp1BIC9vHKJ2FAEpGssTPw iat-mode=0 +Bridge obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1 Bridge obfs4 193.11.166.194:27015 2D82C2E354D531A68469ADF7F878FA6060C6BACA cert=4TLQPJrTSaDffMK7Nbao6LC7G9OW/NHkUwIdjLSS3KYf0Nv4/nQiiI8dY2TcsQx01NniOg iat-mode=0 Bridge obfs4 193.11.166.194:27020 86AC7B8D430DAC4117E9F42C9EAED18133863AAF cert=0LDeJH4JzMDtkJJrFphJCiPqKx7loozKN7VNfuukMGfHO0Z8OGdzHVkhVAOfo1mUdv9cMg iat-mode=0 Bridge obfs4 193.11.166.194:27025 1AE2C08904527FEA90C4C4F8C1083EA59FBC6FAF cert=ItvYZzW5tn6v3G4UnQa6Qz04Npro6e81AP70YujmK/KXwDFPTs3aHXcHp4n8Vt6w/bv8cA iat-mode=0 Bridge obfs4 209.148.46.65:443 74FAD13168806246602538555B5521A0383A1875 cert=ssH+9rP8dG2NLDN2XuFw63hIO/9MNNinLmxQDpVa+7kTOa9/m+tGWT1SmSYpQ9uTBGa6Hw iat-mode=0 -Bridge obfs4 146.57.248.225:22 10A6CD36A537FCE513A322361547444B393989F0 cert=K1gDtDAIcUfeLqbstggjIw2rtgIKqdIhUlHp82XRqNSq/mtAjp1BIC9vHKJ2FAEpGssTPw iat-mode=0 +Bridge obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0 +Bridge obfs4 38.229.1.78:80 C8CBDB2464FC9804A69531437BCF2BE31FDD2EE4 cert=Hmyfd2ev46gGY7NoVxA9ngrPF2zCZtzskRTzoWXbxNkzeVnGFPWmrTtILRyqCTjHR+s9dg iat-mode=1 +Bridge obfs4 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1OpbAH0wNqOT6H6BmRQ iat-mode=1 Bridge obfs4 45.145.95.6:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0 -Bridge obfs4 [2a0c:4d80:42:702::1]:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0 Bridge obfs4 51.222.13.177:80 5EDAC3B810E12B01F6FD8050D2FD3E277B289A08 cert=2uplIpLQ0q9+0qMFrK5pkaYRDOe460LL9WHBvatgkuRr/SL31wBOEupaMMJ6koRE6Ld0ew iat-mode=0 -UseBridges 1 +Bridge obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0 +Bridge obfs4 85.31.186.98:443 011F2599C0E9B27EE74B353155E244813763C3E5 cert=ayq0XzCwhpdysn5o0EyDUbmSOx3X/oTEbzDMvczHOdBJKlvIdHHLJGkZARtT4dcBFArPPg iat-mode=0 +Bridge obfs4 [2a0c:4d80:42:702::1]:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0 diff --git a/cli/onionshare_cli/resources/torrc_template-snowflake b/cli/onionshare_cli/resources/torrc_template-snowflake index 4100d3be..06cb2734 100644 --- a/cli/onionshare_cli/resources/torrc_template-snowflake +++ b/cli/onionshare_cli/resources/torrc_template-snowflake @@ -1,3 +1,2 @@ # Enable built-in snowflake bridge -Bridge snowflake 192.0.2.3:1 2B280B23E1107BB62ABFC40DDCC8824814F80A72 -UseBridges 1 +Bridge snowflake 0.0.3.0:1 2B280B23E1107BB62ABFC40DDCC8824814F80A72 diff --git a/cli/onionshare_cli/settings.py b/cli/onionshare_cli/settings.py index c7d74a70..8a4a9939 100644 --- a/cli/onionshare_cli/settings.py +++ b/cli/onionshare_cli/settings.py @@ -110,6 +110,7 @@ class Settings(object): "bridges_builtin_pt": "obfs4", # "obfs4", "meek-azure", or "snowflake" "bridges_moat": "", "bridges_custom": "", + "bridges_builtin": {}, "persistent_tabs": [], "locale": None, # this gets defined in fill_in_defaults() "theme": 0, diff --git a/cli/onionshare_cli/web/send_base_mode.py b/cli/onionshare_cli/web/send_base_mode.py index 27de598a..d0fccf06 100644 --- a/cli/onionshare_cli/web/send_base_mode.py +++ b/cli/onionshare_cli/web/send_base_mode.py @@ -42,10 +42,11 @@ class SendBaseModeWeb: self.is_zipped = False self.download_filename = None self.download_filesize = None - self.gzip_filename = None - self.gzip_filesize = None self.zip_writer = None + # Store the tempfile objects here, so when they're garbage collected the files are deleted + self.gzip_files = [] + # If autostop_sharing, only allow one download at a time self.download_in_progress = False @@ -192,12 +193,15 @@ class SendBaseModeWeb: # gzip compress the individual file, if it hasn't already been compressed if use_gzip: if filesystem_path not in self.gzip_individual_files: - gzip_filename = tempfile.mkstemp("wb+")[1] - self._gzip_compress(filesystem_path, gzip_filename, 6, None) - self.gzip_individual_files[filesystem_path] = gzip_filename + self.gzip_files.append( + tempfile.NamedTemporaryFile("wb+", dir=self.common.build_tmp_dir()) + ) + gzip_file = self.gzip_files[-1] + self._gzip_compress(filesystem_path, gzip_file.name, 6, None) + self.gzip_individual_files[filesystem_path] = gzip_file.name - # Make sure the gzip file gets cleaned up when onionshare stops - self.web.cleanup_filenames.append(gzip_filename) + # Cleanup this temp file + self.web.cleanup_tempfiles.append(gzip_file) file_to_download = self.gzip_individual_files[filesystem_path] filesize = os.path.getsize(self.gzip_individual_files[filesystem_path]) diff --git a/cli/onionshare_cli/web/share_mode.py b/cli/onionshare_cli/web/share_mode.py index 92a4c9af..9be3a89b 100644 --- a/cli/onionshare_cli/web/share_mode.py +++ b/cli/onionshare_cli/web/share_mode.py @@ -134,8 +134,12 @@ class ShareModeWeb(SendBaseModeWeb): The web app routes for sharing files """ - @self.web.app.route("/", defaults={"path": ""}, methods=["GET"], provide_automatic_options=False) - @self.web.app.route("/<path:path>", methods=["GET"], provide_automatic_options=False) + @self.web.app.route( + "/", defaults={"path": ""}, methods=["GET"], provide_automatic_options=False + ) + @self.web.app.route( + "/<path:path>", methods=["GET"], provide_automatic_options=False + ) def index(path): """ Render the template for the onionshare landing page. @@ -159,7 +163,9 @@ class ShareModeWeb(SendBaseModeWeb): return self.render_logic(path) - @self.web.app.route("/download", methods=["GET"], provide_automatic_options=False) + @self.web.app.route( + "/download", methods=["GET"], provide_automatic_options=False + ) def download(): """ Download the zip file. @@ -286,7 +292,9 @@ class ShareModeWeb(SendBaseModeWeb): if if_unmod: if_date = parse_date(if_unmod) if if_date and not if_date.tzinfo: - if_date = if_date.replace(tzinfo=timezone.utc) # Compatible with Flask < 2.0.0 + if_date = if_date.replace( + tzinfo=timezone.utc + ) # Compatible with Flask < 2.0.0 if if_date and if_date > last_modified: abort(412) elif range_header is None: @@ -459,7 +467,7 @@ class ShareModeWeb(SendBaseModeWeb): return self.web.error404(history_id) def build_zipfile_list(self, filenames, processed_size_callback=None): - self.common.log("ShareModeWeb", "build_zipfile_list") + self.common.log("ShareModeWeb", "build_zipfile_list", f"filenames={filenames}") for filename in filenames: info = { "filename": filename, @@ -484,7 +492,10 @@ class ShareModeWeb(SendBaseModeWeb): self.download_etag = make_etag(f) # Compress the file with gzip now, so we don't have to do it on each request - self.gzip_filename = tempfile.mkstemp("wb+")[1] + self.gzip_tmp_dir = tempfile.TemporaryDirectory( + dir=self.common.build_tmp_dir() + ) + self.gzip_filename = os.path.join(self.gzip_tmp_dir.name, "file.gz") self._gzip_compress( self.download_filename, self.gzip_filename, 6, processed_size_callback ) @@ -492,15 +503,15 @@ class ShareModeWeb(SendBaseModeWeb): with open(self.gzip_filename, "rb") as f: self.gzip_etag = make_etag(f) - # Make sure the gzip file gets cleaned up when onionshare stops - self.web.cleanup_filenames.append(self.gzip_filename) - self.is_zipped = False + # Cleanup this tempfile + self.web.cleanup_tempdirs.append(self.gzip_tmp_dir) + else: # Zip up the files and folders self.zip_writer = ZipWriter( - self.common, processed_size_callback=processed_size_callback + self.common, self.web, processed_size_callback=processed_size_callback ) self.download_filename = self.zip_writer.zip_filename for info in self.file_info["files"]: @@ -519,10 +530,6 @@ class ShareModeWeb(SendBaseModeWeb): with open(self.download_filename, "rb") as f: self.download_etag = make_etag(f) - # Make sure the zip file gets cleaned up when onionshare stops - self.web.cleanup_filenames.append(self.zip_writer.zip_filename) - self.web.cleanup_filenames.append(self.zip_writer.zip_temp_dir) - self.is_zipped = True return True @@ -535,17 +542,24 @@ class ZipWriter(object): filename. """ - def __init__(self, common, zip_filename=None, processed_size_callback=None): + def __init__( + self, common, web=None, zip_filename=None, processed_size_callback=None + ): self.common = common + self.web = web self.cancel_compression = False if zip_filename: self.zip_filename = zip_filename else: - self.zip_temp_dir = tempfile.mkdtemp() - self.zip_filename = ( - f"{self.zip_temp_dir}/onionshare_{self.common.random_string(4, 6)}.zip" + self.zip_temp_dir = tempfile.TemporaryDirectory( + dir=self.common.build_tmp_dir() ) + self.zip_filename = f"{self.zip_temp_dir.name}/onionshare_{self.common.random_string(4, 6)}.zip" + + # Cleanup this temp dir + if self.web: + self.web.cleanup_tempdirs.append(self.zip_temp_dir) self.z = zipfile.ZipFile(self.zip_filename, "w", allowZip64=True) self.processed_size_callback = processed_size_callback diff --git a/cli/onionshare_cli/web/web.py b/cli/onionshare_cli/web/web.py index e0cf97f3..0fc55eb4 100644 --- a/cli/onionshare_cli/web/web.py +++ b/cli/onionshare_cli/web/web.py @@ -18,6 +18,7 @@ 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 logging +import mimetypes import os import queue import requests @@ -80,6 +81,16 @@ class Web: self.settings = mode_settings + # Flask guesses the MIME type of files from a database on the operating + # system. + # Some operating systems, or applications that can modify the database + # (such as the Windows Registry) can treat .js files as text/plain, + # which breaks the chat app due to X-Content-Type-Options: nosniff. + # + # It's probably #notourbug but we can fix it by forcing the mimetype. + # https://github.com/onionshare/onionshare/issues/1443 + mimetypes.add_type("text/javascript", ".js") + # The flask app self.app = Flask( __name__, @@ -151,11 +162,17 @@ class Web: elif self.mode == "website": self.website_mode = WebsiteModeWeb(self.common, self) elif self.mode == "chat": - self.socketio = SocketIO() + if self.common.verbose: + self.socketio = SocketIO( + async_mode="gevent", logger=True, engineio_logger=True + ) + else: + self.socketio = SocketIO(async_mode="gevent") self.socketio.init_app(self.app) self.chat_mode = ChatModeWeb(self.common, self) - self.cleanup_filenames = [] + self.cleanup_tempfiles = [] + self.cleanup_tempdirs = [] def get_mode(self): if self.mode == "share": @@ -198,18 +215,19 @@ class Web: """ for header, value in self.security_headers: r.headers.set(header, value) + # Set a CSP header unless in website mode and the user has disabled it default_csp = "default-src 'self'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; img-src 'self' data:;" - if self.mode != "website" or (not self.settings.get("website", "disable_csp") and not self.settings.get("website", "custom_csp")): - r.headers.set( - "Content-Security-Policy", - default_csp - ) + if self.mode != "website" or ( + not self.settings.get("website", "disable_csp") + and not self.settings.get("website", "custom_csp") + ): + r.headers.set("Content-Security-Policy", default_csp) else: if self.settings.get("website", "custom_csp"): r.headers.set( "Content-Security-Policy", - self.settings.get("website", "custom_csp") + self.settings.get("website", "custom_csp"), ) return r @@ -387,14 +405,13 @@ class Web: """ self.common.log("Web", "cleanup") - # Cleanup files - try: - for filename in self.cleanup_filenames: - if os.path.isfile(filename): - os.remove(filename) - elif os.path.isdir(filename): - shutil.rmtree(filename) - except Exception: - # Don't crash if file is still in use - pass - self.cleanup_filenames = [] + # Close all of the tempfile.NamedTemporaryFile + for file in self.cleanup_tempfiles: + file.close() + + # Clean up the tempfile.NamedTemporaryDirectory objects + for dir in self.cleanup_tempdirs: + dir.cleanup() + + self.cleanup_tempfiles = [] + self.cleanup_tempdirs = [] |