aboutsummaryrefslogtreecommitdiff
path: root/cli/onionshare_cli
diff options
context:
space:
mode:
Diffstat (limited to 'cli/onionshare_cli')
-rw-r--r--cli/onionshare_cli/censorship.py51
-rw-r--r--cli/onionshare_cli/common.py44
-rw-r--r--cli/onionshare_cli/meek.py52
-rw-r--r--cli/onionshare_cli/onion.py179
-rw-r--r--cli/onionshare_cli/onionshare.py7
-rw-r--r--cli/onionshare_cli/resources/static/css/style.css12
-rw-r--r--cli/onionshare_cli/resources/static/js/chat.js2
-rw-r--r--cli/onionshare_cli/resources/static/js/send.js24
-rw-r--r--cli/onionshare_cli/resources/templates/403.html2
-rw-r--r--cli/onionshare_cli/resources/templates/404.html2
-rw-r--r--cli/onionshare_cli/resources/templates/405.html2
-rw-r--r--cli/onionshare_cli/resources/templates/500.html2
-rw-r--r--cli/onionshare_cli/resources/templates/send.html14
-rw-r--r--cli/onionshare_cli/resources/torrc_template-meek_lite_azure5
-rw-r--r--cli/onionshare_cli/resources/torrc_template-obfs419
-rw-r--r--cli/onionshare_cli/resources/torrc_template-snowflake3
-rw-r--r--cli/onionshare_cli/settings.py1
-rw-r--r--cli/onionshare_cli/web/send_base_mode.py18
-rw-r--r--cli/onionshare_cli/web/share_mode.py50
-rw-r--r--cli/onionshare_cli/web/web.py55
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>&mdash;</div>
+ <div class="cell-data">&mdash;</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 = []