2021-10-18 02:17:47 -04:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
|
|
OnionShare | https://onionshare.org/
|
|
|
|
|
2022-01-16 19:15:49 -05:00
|
|
|
Copyright (C) 2014-2022 Micah Lee, et al. <micah@micahflee.com>
|
2021-10-18 02:17:47 -04:00
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
"""
|
|
|
|
import requests
|
|
|
|
|
2021-10-18 20:36:03 -04:00
|
|
|
|
2021-12-19 22:50:09 -05:00
|
|
|
class CensorshipCircumventionError(Exception):
|
|
|
|
"""
|
|
|
|
There was a problem connecting to the Tor CensorshipCircumvention API.
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
2021-10-18 20:36:03 -04:00
|
|
|
class CensorshipCircumvention(object):
|
2021-10-18 02:17:47 -04:00
|
|
|
"""
|
2021-10-18 20:36:03 -04:00
|
|
|
Connect to the Tor Moat APIs to retrieve censorship
|
2021-11-26 18:35:25 -05:00
|
|
|
circumvention recommendations or the latest bridges.
|
|
|
|
|
|
|
|
We support reaching this API over Tor, or Meek
|
|
|
|
(domain fronting) if Tor is not connected.
|
2021-10-18 02:17:47 -04:00
|
|
|
"""
|
|
|
|
|
2021-11-26 18:35:25 -05:00
|
|
|
def __init__(self, common, meek=None, onion=None):
|
2021-10-18 20:36:03 -04:00
|
|
|
"""
|
|
|
|
Set up the CensorshipCircumvention object to hold
|
|
|
|
common and meek objects.
|
|
|
|
"""
|
2021-10-18 02:17:47 -04:00
|
|
|
self.common = common
|
|
|
|
self.common.log("CensorshipCircumvention", "__init__")
|
2021-11-26 18:35:25 -05:00
|
|
|
self.api_proxies = {}
|
|
|
|
if meek:
|
|
|
|
self.meek = meek
|
2022-03-21 23:22:16 -04:00
|
|
|
self.common.log(
|
|
|
|
"CensorshipCircumvention",
|
|
|
|
"__init__",
|
|
|
|
"Using Meek with CensorshipCircumvention API",
|
|
|
|
)
|
|
|
|
self.api_proxies = self.meek.meek_proxies
|
2021-11-26 18:35:25 -05:00
|
|
|
if onion:
|
|
|
|
self.onion = onion
|
|
|
|
if not self.onion.is_authenticated:
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
self.common.log(
|
|
|
|
"CensorshipCircumvention",
|
|
|
|
"__init__",
|
2021-12-13 22:32:00 -05:00
|
|
|
"Using Tor with CensorshipCircumvention API",
|
2021-11-26 18:35:25 -05:00
|
|
|
)
|
|
|
|
(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}",
|
|
|
|
}
|
2021-10-18 02:17:47 -04:00
|
|
|
|
2021-10-18 20:36:03 -04:00
|
|
|
def request_map(self, country=False):
|
2021-10-18 02:17:47 -04:00
|
|
|
"""
|
|
|
|
Retrieves the Circumvention map from Tor Project and store it
|
|
|
|
locally for further look-ups if required.
|
|
|
|
|
|
|
|
Optionally pass a country code in order to get recommended settings
|
|
|
|
just for that country.
|
|
|
|
|
|
|
|
Note that this API endpoint doesn't return actual bridges,
|
|
|
|
it just returns the recommended bridge type countries.
|
|
|
|
"""
|
2022-05-30 17:26:46 -04:00
|
|
|
self.common.log("CensorshipCircumvention", "request_map", f"country={country}")
|
2021-11-26 18:35:25 -05:00
|
|
|
if not self.api_proxies:
|
|
|
|
return False
|
2021-10-18 02:17:47 -04:00
|
|
|
endpoint = "https://bridges.torproject.org/moat/circumvention/map"
|
|
|
|
data = {}
|
|
|
|
if country:
|
|
|
|
data = {"country": country}
|
|
|
|
|
2021-12-19 22:50:09 -05:00
|
|
|
try:
|
|
|
|
r = requests.post(
|
|
|
|
endpoint,
|
|
|
|
json=data,
|
|
|
|
headers={"Content-Type": "application/vnd.api+json"},
|
|
|
|
proxies=self.api_proxies,
|
2021-10-18 02:17:47 -04:00
|
|
|
)
|
2021-12-19 22:50:09 -05:00
|
|
|
if r.status_code != 200:
|
|
|
|
self.common.log(
|
|
|
|
"CensorshipCircumvention",
|
2022-03-27 21:14:02 -04:00
|
|
|
"request_map",
|
2021-12-19 22:50:09 -05:00
|
|
|
f"status_code={r.status_code}",
|
|
|
|
)
|
|
|
|
return False
|
2021-10-18 02:17:47 -04:00
|
|
|
|
2021-12-19 22:50:09 -05:00
|
|
|
result = r.json()
|
2021-10-18 02:17:47 -04:00
|
|
|
|
2021-12-19 22:50:09 -05:00
|
|
|
if "errors" in result:
|
|
|
|
self.common.log(
|
|
|
|
"CensorshipCircumvention",
|
2022-03-27 21:14:02 -04:00
|
|
|
"request_map",
|
2021-12-19 22:50:09 -05:00
|
|
|
f"errors={result['errors']}",
|
|
|
|
)
|
|
|
|
return False
|
2021-10-18 02:17:47 -04:00
|
|
|
|
2021-12-19 22:50:09 -05:00
|
|
|
return result
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
raise CensorshipCircumventionError(e)
|
2021-10-18 02:17:47 -04:00
|
|
|
|
2021-10-18 20:36:03 -04:00
|
|
|
def request_settings(self, country=False, transports=False):
|
2021-10-18 02:17:47 -04:00
|
|
|
"""
|
|
|
|
Retrieves the Circumvention Settings from Tor Project, which
|
|
|
|
will return recommended settings based on the country code of
|
|
|
|
the requesting IP.
|
|
|
|
|
|
|
|
Optionally, a country code can be specified in order to override
|
|
|
|
the IP detection.
|
|
|
|
|
|
|
|
Optionally, a list of transports can be specified in order to
|
|
|
|
return recommended settings for just that transport type.
|
|
|
|
"""
|
2022-05-30 17:26:46 -04:00
|
|
|
self.common.log(
|
|
|
|
"CensorshipCircumvention",
|
|
|
|
"request_settings",
|
|
|
|
f"country={country}, transports={transports}",
|
|
|
|
)
|
2021-11-26 18:35:25 -05:00
|
|
|
if not self.api_proxies:
|
|
|
|
return False
|
2021-10-18 02:17:47 -04:00
|
|
|
endpoint = "https://bridges.torproject.org/moat/circumvention/settings"
|
|
|
|
data = {}
|
|
|
|
if country:
|
2021-12-13 18:48:47 -05:00
|
|
|
self.common.log(
|
|
|
|
"CensorshipCircumvention",
|
2022-03-27 21:14:02 -04:00
|
|
|
"request_settings",
|
2021-12-13 18:48:47 -05:00
|
|
|
f"Trying to obtain bridges for country={country}",
|
|
|
|
)
|
2021-10-18 02:17:47 -04:00
|
|
|
data = {"country": country}
|
|
|
|
if transports:
|
|
|
|
data.append({"transports": transports})
|
2021-12-19 22:50:09 -05:00
|
|
|
try:
|
|
|
|
r = requests.post(
|
|
|
|
endpoint,
|
|
|
|
json=data,
|
|
|
|
headers={"Content-Type": "application/vnd.api+json"},
|
|
|
|
proxies=self.api_proxies,
|
2021-10-18 02:17:47 -04:00
|
|
|
)
|
2021-12-19 22:50:09 -05:00
|
|
|
if r.status_code != 200:
|
|
|
|
self.common.log(
|
|
|
|
"CensorshipCircumvention",
|
2022-03-27 21:14:02 -04:00
|
|
|
"request_settings",
|
2021-12-19 22:50:09 -05:00
|
|
|
f"status_code={r.status_code}",
|
|
|
|
)
|
|
|
|
return False
|
2021-10-18 02:17:47 -04:00
|
|
|
|
2021-12-19 22:50:09 -05:00
|
|
|
result = r.json()
|
2022-05-30 17:26:46 -04:00
|
|
|
self.common.log(
|
|
|
|
"CensorshipCircumvention",
|
|
|
|
"request_settings",
|
|
|
|
f"result={result}",
|
|
|
|
)
|
2021-10-18 02:17:47 -04:00
|
|
|
|
2021-12-19 22:50:09 -05:00
|
|
|
if "errors" in result:
|
|
|
|
self.common.log(
|
|
|
|
"CensorshipCircumvention",
|
2022-03-27 21:14:02 -04:00
|
|
|
"request_settings",
|
2021-12-19 22:50:09 -05:00
|
|
|
f"errors={result['errors']}",
|
|
|
|
)
|
|
|
|
return False
|
2021-10-18 02:17:47 -04:00
|
|
|
|
2021-12-19 22:50:09 -05:00
|
|
|
# There are no settings - perhaps this country doesn't require censorship circumvention?
|
|
|
|
# This is not really an error, so we can just check if False and assume direct Tor
|
|
|
|
# connection will work.
|
2022-05-30 17:26:46 -04:00
|
|
|
if not "settings" in result or result["settings"] is None:
|
2021-12-19 22:50:09 -05:00
|
|
|
self.common.log(
|
|
|
|
"CensorshipCircumvention",
|
2022-03-27 21:14:02 -04:00
|
|
|
"request_settings",
|
2021-12-19 22:50:09 -05:00
|
|
|
"No settings found for this country",
|
|
|
|
)
|
|
|
|
return False
|
2021-10-18 02:17:47 -04:00
|
|
|
|
2021-12-19 22:50:09 -05:00
|
|
|
return result
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
raise CensorshipCircumventionError(e)
|
2021-10-18 02:17:47 -04:00
|
|
|
|
2021-10-18 20:36:03 -04:00
|
|
|
def request_builtin_bridges(self):
|
2021-10-18 02:17:47 -04:00
|
|
|
"""
|
|
|
|
Retrieves the list of built-in bridges from the Tor Project.
|
|
|
|
"""
|
2021-11-26 18:35:25 -05:00
|
|
|
if not self.api_proxies:
|
|
|
|
return False
|
2021-10-18 02:17:47 -04:00
|
|
|
endpoint = "https://bridges.torproject.org/moat/circumvention/builtin"
|
2021-12-19 22:50:09 -05:00
|
|
|
try:
|
|
|
|
r = requests.post(
|
|
|
|
endpoint,
|
|
|
|
headers={"Content-Type": "application/vnd.api+json"},
|
|
|
|
proxies=self.api_proxies,
|
2021-10-18 02:17:47 -04:00
|
|
|
)
|
2021-12-19 22:50:09 -05:00
|
|
|
if r.status_code != 200:
|
|
|
|
self.common.log(
|
|
|
|
"CensorshipCircumvention",
|
2022-03-27 21:14:02 -04:00
|
|
|
"request_builtin_bridges",
|
2021-12-19 22:50:09 -05:00
|
|
|
f"status_code={r.status_code}",
|
|
|
|
)
|
|
|
|
return False
|
2021-10-18 02:17:47 -04:00
|
|
|
|
2021-12-19 22:50:09 -05:00
|
|
|
result = r.json()
|
2021-10-18 02:17:47 -04:00
|
|
|
|
2021-12-19 22:50:09 -05:00
|
|
|
if "errors" in result:
|
|
|
|
self.common.log(
|
|
|
|
"CensorshipCircumvention",
|
2022-03-27 21:14:02 -04:00
|
|
|
"request_builtin_bridges",
|
2021-12-19 22:50:09 -05:00
|
|
|
f"errors={result['errors']}",
|
|
|
|
)
|
|
|
|
return False
|
2021-10-18 02:17:47 -04:00
|
|
|
|
2021-12-19 22:50:09 -05:00
|
|
|
return result
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
raise CensorshipCircumventionError(e)
|
2021-11-29 18:50:47 -05:00
|
|
|
|
|
|
|
def save_settings(self, settings, bridge_settings):
|
|
|
|
"""
|
|
|
|
Checks the bridges and saves them in settings.
|
|
|
|
"""
|
2022-05-30 17:26:46 -04:00
|
|
|
self.common.log(
|
|
|
|
"CensorshipCircumvention",
|
|
|
|
"save_settings",
|
|
|
|
f"bridge_settings: {bridge_settings}",
|
|
|
|
)
|
|
|
|
|
2021-11-29 18:50:47 -05:00
|
|
|
bridges_ok = False
|
|
|
|
self.settings = settings
|
|
|
|
|
|
|
|
# @TODO there might be several bridge types recommended.
|
|
|
|
# Should we attempt to iterate over each type if one of them fails to connect?
|
|
|
|
# But if so, how to stop it starting 3 separate Tor connection threads?
|
|
|
|
# for bridges in request_bridges["settings"]:
|
|
|
|
bridges = bridge_settings["settings"][0]["bridges"]
|
|
|
|
bridge_strings = bridges["bridge_strings"]
|
|
|
|
|
2022-03-27 21:14:02 -04:00
|
|
|
self.settings.set("bridges_type", "custom")
|
|
|
|
|
|
|
|
# Sanity check the bridges provided from the Tor API before saving
|
|
|
|
bridges_checked = self.common.check_bridges_valid(bridge_strings)
|
2021-11-29 18:50:47 -05:00
|
|
|
|
2022-03-27 21:14:02 -04:00
|
|
|
if bridges_checked:
|
|
|
|
self.settings.set("bridges_custom", "\n".join(bridges_checked))
|
|
|
|
bridges_ok = True
|
2021-11-29 18:50:47 -05:00
|
|
|
|
|
|
|
# If we got any good bridges, save them to settings and return.
|
|
|
|
if bridges_ok:
|
|
|
|
self.common.log(
|
|
|
|
"CensorshipCircumvention",
|
|
|
|
"save_settings",
|
|
|
|
"Saving settings with automatically-obtained bridges",
|
|
|
|
)
|
2021-12-13 22:32:00 -05:00
|
|
|
self.settings.set("bridges_enabled", True)
|
2021-11-29 18:50:47 -05:00
|
|
|
self.settings.save()
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
self.common.log(
|
|
|
|
"CensorshipCircumvention",
|
|
|
|
"save_settings",
|
|
|
|
"Could not use any of the obtained bridges.",
|
|
|
|
)
|
|
|
|
return False
|
2022-03-27 21:14:02 -04:00
|
|
|
|
|
|
|
def request_default_bridges(self):
|
|
|
|
"""
|
|
|
|
Retrieves the list of default fall-back bridges from the Tor Project.
|
|
|
|
|
|
|
|
These are intended for when no censorship settings were found for a
|
|
|
|
specific country, but maybe there was some connection issue anyway.
|
|
|
|
"""
|
|
|
|
if not self.api_proxies:
|
|
|
|
return False
|
|
|
|
endpoint = "https://bridges.torproject.org/moat/circumvention/defaults"
|
|
|
|
try:
|
|
|
|
r = requests.get(
|
|
|
|
endpoint,
|
|
|
|
headers={"Content-Type": "application/vnd.api+json"},
|
|
|
|
proxies=self.api_proxies,
|
|
|
|
)
|
|
|
|
if r.status_code != 200:
|
|
|
|
self.common.log(
|
|
|
|
"CensorshipCircumvention",
|
|
|
|
"request_default_bridges",
|
|
|
|
f"status_code={r.status_code}",
|
|
|
|
)
|
|
|
|
return False
|
|
|
|
|
|
|
|
result = r.json()
|
|
|
|
|
|
|
|
if "errors" in result:
|
|
|
|
self.common.log(
|
|
|
|
"CensorshipCircumvention",
|
|
|
|
"request_default_bridges",
|
|
|
|
f"errors={result['errors']}",
|
|
|
|
)
|
|
|
|
return False
|
|
|
|
|
|
|
|
return result
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
raise CensorshipCircumventionError(e)
|