21 Commits
Author SHA1 Message Date
eickeh 11fe7ba786 Update README.md
Removed link to the deprecated owlink website as it's no longer associated with this project.
2026-08-17 14:18:16 +02:00
eickeh 9075732230 Update setup.py
Removed link to the deprecated owlink website as it's no longer associated with this project.
2026-08-17 14:16:23 +02:00
Alexander Heinrich ae5ac821fb Adding CITATION.cff file 2024-06-11 12:41:13 +02:00
Milan Stute cbc7ca46a7 Bump version 2021-04-29 14:59:52 +02:00
Milan Stute df221b0e2b Fixup: autoformat 2021-04-29 14:55:39 +02:00
Milan Stute 4f1849cea2 Add sending support for URLs (fixes #67) 2021-04-29 14:55:04 +02:00
Milan Stute 8bacbe97b0 Add PrivateDrop reference 2021-04-29 12:47:38 +02:00
Milan Stute bc7813bf91 Bump version 2020-12-03 16:01:18 +01:00
Milan Stute 8a9bd1f4d5 Ignore LGTM false positive (again) 2020-12-02 16:33:51 +01:00
Milan Stute ba550bb468 Don't busy wait 2020-12-02 16:33:51 +01:00
Milan Stute 17db506f04 Check output of openssl 2020-12-02 16:18:57 +01:00
Milan Stute 6960a97ab8 Don't redefine built-in types 2020-12-02 16:18:57 +01:00
Milan Stute fe99dab664 Unused variables 2020-12-02 16:18:57 +01:00
Milan Stute d4f3df70e7 Don't use logger.warn 2020-12-02 16:18:57 +01:00
Milan Stute d96fa1d7ef Use f-strings everywhere else 2020-12-02 16:18:57 +01:00
Milan Stute bf7cea785c Use f-strings in logging statements 2020-12-02 16:18:57 +01:00
Milan Stute 7db266f089 Rename variable 2020-12-02 16:18:57 +01:00
Milan Stute db34b3e349 Silence pylint false positives 2020-12-02 16:18:57 +01:00
Milan Stute 368a8afc8c Remove unused imports 2020-12-02 16:18:57 +01:00
Milan Stute adb658d04d Add pylint to CI pipeline 2020-12-02 16:18:57 +01:00
Milan Stute 91e204f8a2 Add isort to CI pipeline 2020-12-02 16:18:57 +01:00
13 changed files with 178 additions and 121 deletions
+8
View File
@@ -22,6 +22,10 @@ jobs:
- name: Install package - name: Install package
run: | run: |
pip install -e . pip install -e .
- name: Check format with isort
run: |
pip install isort
isort -c opendrop/**.py
- name: Check format with black - name: Check format with black
run: | run: |
pip install black pip install black
@@ -30,6 +34,10 @@ jobs:
run: | run: |
pip install flake8 pip install flake8
flake8 . --count --show-source --statistics flake8 . --count --show-source --statistics
- name: Lint with pylint
run: |
pip install pylint
pylint --rcfile=setup.cfg opendrop
- name: Test with pytest - name: Test with pytest
run: | run: |
pip install pytest pip install pytest
+31
View File
@@ -0,0 +1,31 @@
# This CITATION.cff file was generated with cffinit.
# Visit https://bit.ly/cffinit to generate yours today!
cff-version: 1.2.0
title: 'OpenDrop: an Open Source AirDrop Implementation'
message: 'If you use this software, please cite it as below.'
type: software
authors:
- given-names: Alexander
family-names: Heinrich
affiliation: 'SEEMOO, TU Darmstadt'
orcid: 'https://orcid.org/0000-0002-1150-1922'
- given-names: Milan
family-names: Stute
affiliation: 'SEEMOO, TU Darmstadt'
orcid: 'https://orcid.org/0000-0003-4921-8476'
- given-names: Matthias
family-names: Hollick
affiliation: 'SEEMOO, TU Darmstadt'
orcid: 'https://orcid.org/0000-0002-9163-5989'
repository-code: 'https://github.com/seemoo-lab/opendrop'
abstract: >-
OpenDrop is a command-line tool that allows sharing files
between devices directly over Wi-Fi. Its unique feature is
that it is protocol-compatible with Apple AirDrop which
allows to share files with Apple devices running iOS and
macOS.
license: GPL-3.0
commit: cbc7ca46a75bb2da4af9d406732ddf2192578388
version: '0.13'
date-released: '2021-04-29'
+9 -2
View File
@@ -1,9 +1,9 @@
.PHONY: ci checkformat lint test autoformat .PHONY: ci checkformat isort lint pylint test autoformat
VENV=venv VENV=venv
PYTHON=$(VENV)/bin/python3 PYTHON=$(VENV)/bin/python3
ci: checkformat lint test ci: isort checkformat lint pylint test
$(VENV): $(VENV)/bin/activate $(VENV): $(VENV)/bin/activate
@@ -23,8 +23,15 @@ checkformat: $(VENV)
lint: $(VENV) lint: $(VENV)
$(PYTHON) -m flake8 . --count --show-source --statistics --exclude $(VENV) $(PYTHON) -m flake8 . --count --show-source --statistics --exclude $(VENV)
pylint: $(VENV)
$(PYTHON) -m pylint --rcfile=setup.cfg opendrop
isort: $(VENV)
$(PYTHON) -m isort -c opendrop/**.py
test: $(VENV) test: $(VENV)
$(PYTHON) -m pytest $(PYTHON) -m pytest
autoformat: $(VENV) autoformat: $(VENV)
$(PYTHON) -m isort opendrop/**.py
$(PYTHON) -m black . --exclude $(VENV) $(PYTHON) -m black . --exclude $(VENV)
+12 -4
View File
@@ -9,7 +9,7 @@ We support contacts-only devices by using extracted AirDrop credentials (keys an
## Disclaimer ## Disclaimer
OpenDrop is experimental software and is the result of reverse engineering efforts by the [Open Wireless Link](https://owlink.org) project. OpenDrop is experimental software and is the result of reverse engineering efforts by the [Open Wireless Link](<<DISCLAIMER: The former owlink website is no longer associated with this project, please disregard it.>>) project.
Therefore, it does not support all features of AirDrop or might be incompatible with future AirDrop versions. Therefore, it does not support all features of AirDrop or might be incompatible with future AirDrop versions.
OpenDrop is not affiliated with or endorsed by Apple Inc. Use this code at your own risk. OpenDrop is not affiliated with or endorsed by Apple Inc. Use this code at your own risk.
@@ -52,7 +52,7 @@ pip3 install ./opendrop
We briefly explain how to send and receive files using `opendrop`. We briefly explain how to send and receive files using `opendrop`.
To see all command line options, run `opendrop -h`. To see all command line options, run `opendrop -h`.
### Sending a File ### Sending a File or a Link
Sending a file is typically a two-step procedure. You first discover devices in proximity using the `find` command. Sending a file is typically a two-step procedure. You first discover devices in proximity using the `find` command.
Stop the process once you have found the receiver. Stop the process once you have found the receiver.
@@ -62,7 +62,7 @@ Looking for receivers. Press Ctrl+C to stop ...
Found index 0 ID eccb2f2dcfe7 name Johns iPhone Found index 0 ID eccb2f2dcfe7 name Johns iPhone
Found index 1 ID e63138ac6ba8 name Janes MacBook Pro Found index 1 ID e63138ac6ba8 name Janes MacBook Pro
``` ```
You can then `send` a file using You can then `send` a file (or link, see below) using
``` ```
$ opendrop send -r 0 -f /path/to/some/file $ opendrop send -r 0 -f /path/to/some/file
Asking receiver to accept ... Asking receiver to accept ...
@@ -73,6 +73,13 @@ Uploading has been successful
Instead of the `index`, you can also use `ID` or `name`. Instead of the `index`, you can also use `ID` or `name`.
OpenDrop will try to interpret the input in the order (1) `index`, (2) `ID`, and (3) `name` and fail if no match was found. OpenDrop will try to interpret the input in the order (1) `index`, (2) `ID`, and (3) `name` and fail if no match was found.
**Sending a web link.** Since v0.13, OpenDrop supports sending web links, i.e., URLs, so that receiving Apple devices will immediately open their browser upon accepting.
(Note that OpenDrop _receivers_ still only support receiving regular files.)
```
$ opendrop send -r 0 -f <<DISCLAIMER: The former owlink website is no longer associated with this project, please disregard it.>> --url
```
### Receiving Files ### Receiving Files
Receiving is much easier. Simply use the `receive` command. OpenDrop will accept all incoming files automatically and put received files in the current directory. Receiving is much easier. Simply use the `receive` command. OpenDrop will accept all incoming files automatically and put received files in the current directory.
@@ -94,7 +101,8 @@ OpenDrop is the result of a research project and, thus, has several limitations
## Our Papers ## Our Papers
* Milan Stute, Sashank Narain, Alex Mariotto, Alexander Heinrich, David Kreitschmann, Guevara Noubir, and Matthias Hollick. **A Billion Open Interfaces for Eve and Mallory: MitM, DoS, and Tracking Attacks on iOS and macOS Through Apple Wireless Direct Link.** *28th USENIX Security Symposium (USENIX Security 19)*, August 1416, 2019, Santa Clara, CA, USA. [Link](https://www.usenix.org/conference/usenixsecurity19/presentation/stute) * Alexander Heinrich, Matthias Hollick, Thomas Schneider, Milan Stute, and Christian Weinert. **PrivateDrop: Practical Privacy-Preserving Authentication for Apple AirDrop.** *30th USENIX Security Symposium (USENIX Security 21)*, August 1416, 2019, virtual Event. [Paper](https://www.usenix.org/conference/usenixsecurity21/presentation/heinrich) [Website](https://privatedrop.github.io) [Code](https://github.com/seemoo-lab/privatedrop)
* Milan Stute, Sashank Narain, Alex Mariotto, Alexander Heinrich, David Kreitschmann, Guevara Noubir, and Matthias Hollick. **A Billion Open Interfaces for Eve and Mallory: MitM, DoS, and Tracking Attacks on iOS and macOS Through Apple Wireless Direct Link.** *28th USENIX Security Symposium (USENIX Security 19)*, August 1416, 2019, Santa Clara, CA, USA. [Paper](https://www.usenix.org/conference/usenixsecurity19/presentation/stute)
## Authors ## Authors
+2 -2
View File
@@ -21,11 +21,11 @@ import logging
import os import os
import platform import platform
__version__ = "0.12.2" __version__ = "0.13.0"
if platform.system() == "Darwin": if platform.system() == "Darwin":
dyld_path = os.environ.get("DYLD_LIBRARY_PATH", "") # save old path dyld_path = os.environ.get("DYLD_LIBRARY_PATH", "") # save old path
archive_path = "/usr/local/opt/libarchive/lib" archive_path = "/usr/local/opt/libarchive/lib"
os.environ["DYLD_LIBRARY_PATH"] = "{}:{}".format(dyld_path, archive_path) os.environ["DYLD_LIBRARY_PATH"] = f"{dyld_path}:{archive_path}"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+16 -22
View File
@@ -41,6 +41,9 @@ class AirDropCli:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("action", choices=["receive", "find", "send"]) parser.add_argument("action", choices=["receive", "find", "send"])
parser.add_argument("-f", "--file", help="File to be sent") parser.add_argument("-f", "--file", help="File to be sent")
parser.add_argument(
"-u", "--url", help="'-f,--file is a URL", action="store_true"
)
parser.add_argument( parser.add_argument(
"-r", "-r",
"--receiver", "--receiver",
@@ -99,9 +102,10 @@ class AirDropCli:
else: # args.action == 'send' else: # args.action == 'send'
if args.file is None: if args.file is None:
parser.error("Need -f,--file when using send") parser.error("Need -f,--file when using send")
if not os.path.isfile(args.file): if not os.path.isfile(args.file) and not args.url:
parser.error("File in -f,--file not found") parser.error("File in -f,--file not found")
self.file = args.file self.file = args.file
self.is_url = args.url
if args.receiver is None: if args.receiver is None:
parser.error("Need -r,--receiver when using send") parser.error("Need -r,--receiver when using send")
self.receiver = args.receiver self.receiver = args.receiver
@@ -117,15 +121,12 @@ class AirDropCli:
self.browser = AirDropBrowser(self.config) self.browser = AirDropBrowser(self.config)
self.browser.start(callback_add=self._found_receiver) self.browser.start(callback_add=self._found_receiver)
try: try:
while True: threading.Event().wait()
pass
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
finally: finally:
self.browser.stop() self.browser.stop()
logger.debug( logger.debug(f"Save discovery results to {self.config.discovery_report}")
"Save discovery results to {}".format(self.config.discovery_report)
)
with open(self.config.discovery_report, "w") as f: with open(self.config.discovery_report, "w") as f:
json.dump(self.discover, f) json.dump(self.discover, f)
@@ -137,16 +138,12 @@ class AirDropCli:
try: try:
address = info.parsed_addresses()[0] # there should only be one address address = info.parsed_addresses()[0] # there should only be one address
except IndexError: except IndexError:
logger.warn("Ignoring receiver with missing address {}".format(info)) logger.warning(f"Ignoring receiver with missing address {info}")
return return
id = info.name.split(".")[0] identifier = info.name.split(".")[0]
hostname = info.server hostname = info.server
port = int(info.port) port = int(info.port)
logger.debug( logger.debug(f"AirDrop service found: {hostname}, {address}:{port}, ID {id}")
"AirDrop service found: {}, {}:{}, ID {}".format(
hostname, address, port, id
)
)
client = AirDropClient(self.config, (address, int(port))) client = AirDropClient(self.config, (address, int(port)))
try: try:
flags = int(info.properties[b"flags"]) flags = int(info.properties[b"flags"])
@@ -168,18 +165,16 @@ class AirDropCli:
"name": receiver_name, "name": receiver_name,
"address": address, "address": address,
"port": port, "port": port,
"id": id, "id": identifier,
"flags": flags, "flags": flags,
"discoverable": discoverable, "discoverable": discoverable,
} }
self.lock.acquire() self.lock.acquire()
self.discover.append(node_info) self.discover.append(node_info)
if discoverable: if discoverable:
logger.info( logger.info(f"Found index {index} ID {identifier} name {receiver_name}")
"Found index {} ID {} name {}".format(index, id, receiver_name)
)
else: else:
logger.debug("Receiver ID {} is not discoverable".format(id)) logger.debug(f"Receiver ID {identifier} is not discoverable")
self.lock.release() self.lock.release()
def receive(self): def receive(self):
@@ -193,12 +188,12 @@ class AirDropCli:
return return
self.client = AirDropClient(self.config, (info["address"], info["port"])) self.client = AirDropClient(self.config, (info["address"], info["port"]))
logger.info("Asking receiver to accept ...") logger.info("Asking receiver to accept ...")
if not self.client.send_ask(self.file): if not self.client.send_ask(self.file, is_url=self.is_url):
logger.warning("Receiver declined") logger.warning("Receiver declined")
return return
logger.info("Receiver accepted") logger.info("Receiver accepted")
logger.info("Uploading file ...") logger.info("Uploading file ...")
if not self.client.send_upload(self.file): if not self.client.send_upload(self.file, is_url=self.is_url):
logger.warning("Uploading has failed") logger.warning("Uploading has failed")
return return
logger.info("Uploading has been successful") logger.info("Uploading has been successful")
@@ -210,8 +205,7 @@ class AirDropCli:
age = time.time() - os.path.getmtime(self.config.discovery_report) age = time.time() - os.path.getmtime(self.config.discovery_report)
if age > 60: # warn if report is older than a minute if age > 60: # warn if report is older than a minute
logger.warning( logger.warning(
"Old discovery report (%.1f seconds), consider running 'opendrop find' again", f"Old discovery report ({age:.1f} seconds), consider running 'opendrop find' again"
age,
) )
with open(self.config.discovery_report, "r") as f: with open(self.config.discovery_report, "r") as f:
infos = json.load(f) infos = json.load(f)
+44 -39
View File
@@ -28,9 +28,9 @@ from http.client import HTTPSConnection
import fleep import fleep
import libarchive import libarchive
from zeroconf import IPVersion, ServiceBrowser, Zeroconf
from .util import AirDropUtil, AbsArchiveWrite from .util import AbsArchiveWrite, AirDropUtil
from zeroconf import ServiceBrowser, Zeroconf, IPVersion
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -41,14 +41,11 @@ class AirDropBrowser:
if self.ip_addr is None: if self.ip_addr is None:
if config.interface == "awdl0": if config.interface == "awdl0":
raise RuntimeError( raise RuntimeError(
"Interface {} does not have an IPv6 address. " f"Interface {config.interface} does not have an IPv6 address. Make sure that `owl` is running."
"Make sure that `owl` is running.".format(config.interface)
) )
else: else:
raise RuntimeError( raise RuntimeError(
"Interface {} does not have an IPv6 address".format( f"Interface {config.interface} does not have an IPv6 address"
config.interface
)
) )
self.zeroconf = Zeroconf( self.zeroconf = Zeroconf(
@@ -76,15 +73,15 @@ class AirDropBrowser:
self.browser = None self.browser = None
self.zeroconf.close() self.zeroconf.close()
def add_service(self, zeroconf, type, name): def add_service(self, zeroconf, service_type, name):
info = zeroconf.get_service_info(type, name) info = zeroconf.get_service_info(service_type, name)
logger.debug("Add service {}".format(name)) logger.debug(f"Add service {name}")
if self.callback_add is not None: if self.callback_add is not None:
self.callback_add(info) self.callback_add(info)
def remove_service(self, zeroconf, type, name): def remove_service(self, zeroconf, service_type, name):
info = zeroconf.get_service_info(type, name) info = zeroconf.get_service_info(service_type, name)
logger.debug("Remove service {}".format(name)) logger.debug(f"Remove service {name}")
if self.callback_remove is not None: if self.callback_remove is not None:
self.callback_remove(info) self.callback_remove(info)
@@ -97,10 +94,10 @@ class AirDropClient:
self.http_conn = None self.http_conn = None
def send_POST(self, url, body, headers=None): def send_POST(self, url, body, headers=None):
logger.debug("Send {} request".format(url)) logger.debug(f"Send {url} request")
AirDropUtil.write_debug( AirDropUtil.write_debug(
self.config, body, "send_{}_request.plist".format(url.lower().strip("/")) self.config, body, f"send_{url.lower().strip('/')}_request.plist"
) )
_headers = self._get_headers() _headers = self._get_headers()
@@ -122,15 +119,15 @@ class AirDropClient:
AirDropUtil.write_debug( AirDropUtil.write_debug(
self.config, self.config,
response_bytes, response_bytes,
"send_{}_response.plist".format(url.lower().strip("/")), f"send_{url.lower().strip('/')}_response.plist",
) )
if http_resp.status != 200: if http_resp.status != 200:
status = False status = False
logger.debug("{} request failed: {}".format(url, http_resp.status)) logger.debug(f"{url} request failed: {http_resp.status}")
else: else:
status = True status = True
logger.debug("{} request successful".format(url)) logger.debug(f"{url} request successful")
return status, response_bytes return status, response_bytes
def send_discover(self): def send_discover(self):
@@ -138,14 +135,16 @@ class AirDropClient:
if self.config.record_data: if self.config.record_data:
discover_body["SenderRecordData"] = self.config.record_data discover_body["SenderRecordData"] = self.config.record_data
discover_plist_binary = plistlib.dumps(discover_body, fmt=plistlib.FMT_BINARY) discover_plist_binary = plistlib.dumps(
success, response_bytes = self.send_POST("/Discover", discover_plist_binary) discover_body, fmt=plistlib.FMT_BINARY # pylint: disable=no-member
)
_, response_bytes = self.send_POST("/Discover", discover_plist_binary)
response = plistlib.loads(response_bytes) response = plistlib.loads(response_bytes)
# if name is returned, then receiver is discoverable # if name is returned, then receiver is discoverable
return response.get("ReceiverComputerName") return response.get("ReceiverComputerName")
def send_ask(self, file_path, icon=None): def send_ask(self, file_path, is_url=False, icon=None):
ask_body = { ask_body = {
"SenderComputerName": self.config.computer_name, "SenderComputerName": self.config.computer_name,
"BundleID": "com.apple.finder", "BundleID": "com.apple.finder",
@@ -156,18 +155,6 @@ class AirDropClient:
if self.config.record_data: if self.config.record_data:
ask_body["SenderRecordData"] = self.config.record_data ask_body["SenderRecordData"] = self.config.record_data
if isinstance(file_path, str):
file_path = [file_path]
# generate icon for first file
with open(file_path[0], "rb") as f:
file_header = f.read(128)
flp = fleep.get(file_header)
if not icon and len(flp.mime) > 0 and "image" in flp.mime[0]:
icon = AirDropUtil.generate_file_icon(f.name)
if icon:
ask_body["FileIcon"] = icon
def file_entries(files): def file_entries(files):
for file in files: for file in files:
file_name = os.path.basename(file) file_name = os.path.basename(file)
@@ -180,18 +167,36 @@ class AirDropClient:
} }
yield file_entry yield file_entry
ask_body["Files"] = [e for e in file_entries(file_path)] if isinstance(file_path, str):
ask_body["Items"] = [] file_path = [file_path]
if is_url:
ask_body["Items"] = file_path
else:
# generate icon for first file
with open(file_path[0], "rb") as f:
file_header = f.read(128)
flp = fleep.get(file_header)
if not icon and len(flp.mime) > 0 and "image" in flp.mime[0]:
icon = AirDropUtil.generate_file_icon(f.name)
ask_body["Files"] = [e for e in file_entries(file_path)]
if icon:
ask_body["FileIcon"] = icon
ask_binary = plistlib.dumps(ask_body, fmt=plistlib.FMT_BINARY) ask_binary = plistlib.dumps(
ask_body, fmt=plistlib.FMT_BINARY # pylint: disable=no-member
)
success, _ = self.send_POST("/Ask", ask_binary) success, _ = self.send_POST("/Ask", ask_binary)
return success return success
def send_upload(self, file_path): def send_upload(self, file_path, is_url=False):
""" """
Send a file to a receiver. Send a file to a receiver.
""" """
# Don't send an upload request if we just sent a link
if is_url:
return
headers = { headers = {
"Content-Type": "application/x-cpio", "Content-Type": "application/x-cpio",
} }
@@ -247,7 +252,7 @@ class HTTPSConnectionAWDL(HTTPSConnection):
*, *,
context=None, context=None,
check_hostname=None, check_hostname=None,
interface_name=None interface_name=None,
): ):
if interface_name is not None: if interface_name is not None:
@@ -290,7 +295,7 @@ class HTTPSConnectionAWDL(HTTPSConnection):
host, port = address host, port = address
err = None err = None
for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res af, socktype, proto, _, sa = res
sock = None sock = None
try: try:
sock = socket.socket(af, socktype, proto) sock = socket.socket(af, socktype, proto)
+8 -8
View File
@@ -81,9 +81,7 @@ class AirDropConfig:
self.port = server_port self.port = server_port
if service_id is None: if service_id is None:
service_id = "{0:0{1}x}".format( service_id = f"{random.randint(0, 0xFFFFFFFFFFFF):012x}" # random 6-byte string in base16
random.randint(0, 0xFFFFFFFFFFFF), 12
) # random 6-byte string in base16
self.service_id = service_id self.service_id = service_id
self.debug = debug self.debug = debug
@@ -109,7 +107,7 @@ class AirDropConfig:
self.root_ca_file = resource_filename("opendrop", "certs/apple_root_ca.pem") self.root_ca_file = resource_filename("opendrop", "certs/apple_root_ca.pem")
if not os.path.exists(self.root_ca_file): if not os.path.exists(self.root_ca_file):
raise FileNotFoundError( raise FileNotFoundError(
"Need Apple root CA certificate: {}".format(self.root_ca_file) f"Need Apple root CA certificate: {self.root_ca_file}"
) )
self.key_dir = os.path.join(self.airdrop_dir, "keys") self.key_dir = os.path.join(self.airdrop_dir, "keys")
@@ -130,7 +128,7 @@ class AirDropConfig:
logger.debug("No Apple ID Validation Record found") logger.debug("No Apple ID Validation Record found")
def create_default_key(self): def create_default_key(self):
logger.info("Create new self-signed certificate in {}".format(self.key_dir)) logger.info(f"Create new self-signed certificate in {self.key_dir}")
if not os.path.exists(self.key_dir): if not os.path.exists(self.key_dir):
os.makedirs(self.key_dir) os.makedirs(self.key_dir)
subprocess.run( subprocess.run(
@@ -148,17 +146,19 @@ class AirDropConfig:
"-out", "-out",
"certificate.pem", "certificate.pem",
"-subj", "-subj",
"/CN={}".format(self.computer_name), f"/CN={self.computer_name}",
], ],
cwd=self.key_dir, cwd=self.key_dir,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
check=True,
) )
def get_ssl_context(self): def get_ssl_context(self):
ctx = ssl.SSLContext(
ctx = ssl.SSLContext( # lgtm[py/insecure-protocol], TODO see https://github.com/Semmle/ql/issues/2554
ssl.PROTOCOL_TLS ssl.PROTOCOL_TLS
) # lgtm[py/insecure-protocol], TODO see https://github.com/Semmle/ql/issues/2554 )
ctx.options |= ssl.OP_NO_TLSv1 # TLSv1.0 is insecure ctx.options |= ssl.OP_NO_TLSv1 # TLSv1.0 is insecure
ctx.load_cert_chain(self.cert_file, keyfile=self.key_file) ctx.load_cert_chain(self.cert_file, keyfile=self.key_file)
ctx.load_verify_locations(cafile=self.root_ca_file) ctx.load_verify_locations(cafile=self.root_ca_file)
+17 -23
View File
@@ -23,14 +23,14 @@ import platform
import plistlib import plistlib
import socket import socket
import time import time
from http.server import HTTPServer, BaseHTTPRequestHandler from http.server import BaseHTTPRequestHandler, HTTPServer
import libarchive import libarchive
import libarchive.extract import libarchive.extract
import libarchive.read import libarchive.read
from zeroconf import IPVersion, ServiceInfo, Zeroconf
from .util import AirDropUtil from .util import AirDropUtil
from zeroconf import Zeroconf, ServiceInfo, IPVersion
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -54,14 +54,11 @@ class AirDropServer:
if self.ip_addr is None: if self.ip_addr is None:
if self.config.interface == "awdl0": if self.config.interface == "awdl0":
raise RuntimeError( raise RuntimeError(
"Interface {} does not have an IPv6 address. " f"Interface {self.config.interface} does not have an IPv6 address. Make sure that `owl` is running."
"Make sure that `owl` is running.".format(self.config.interface)
) )
else: else:
raise RuntimeError( raise RuntimeError(
"Interface {} does not have an IPv6 address".format( f"Interface {self.config.interface} does not have an IPv6 address"
self.config.interface
)
) )
self.Handler = AirDropServerHandler self.Handler = AirDropServerHandler
@@ -92,9 +89,7 @@ class AirDropServer:
def start_service(self): def start_service(self):
logger.info( logger.info(
"Announcing service: host {}, address {}, port {}".format( f"Announcing service: host {self.config.host_name}, address {self.ip_addr}, port {self.config.port}"
self.config.host_name, self.ip_addr, self.config.port
)
) )
self.zeroconf.register_service(self.service_info) self.zeroconf.register_service(self.service_info)
@@ -162,7 +157,7 @@ class AirDropServerHandler(BaseHTTPRequestHandler):
""" """
Answer get requests Answer get requests
""" """
logger.debug("GET request at {}".format(self.path)) logger.debug(f"GET request at {self.path}")
body = "\n".encode("utf-8") body = "\n".encode("utf-8")
self._set_response(len(body)) self._set_response(len(body))
self.wfile.write(body) self.wfile.write(body)
@@ -215,7 +210,7 @@ class AirDropServerHandler(BaseHTTPRequestHandler):
discover_answer["ReceiverRecordData"] = self.config.record_data discover_answer["ReceiverRecordData"] = self.config.record_data
discover_answer_binary = plistlib.dumps( discover_answer_binary = plistlib.dumps(
discover_answer, fmt=plistlib.FMT_BINARY discover_answer, fmt=plistlib.FMT_BINARY # pylint: disable=no-member
) )
AirDropUtil.write_debug( AirDropUtil.write_debug(
@@ -236,7 +231,9 @@ class AirDropServerHandler(BaseHTTPRequestHandler):
"ReceiverModelName": self.config.computer_model, "ReceiverModelName": self.config.computer_model,
"ReceiverComputerName": self.config.computer_name, "ReceiverComputerName": self.config.computer_name,
} }
ask_resp_binary = plistlib.dumps(ask_response, fmt=plistlib.FMT_BINARY) ask_resp_binary = plistlib.dumps(
ask_response, fmt=plistlib.FMT_BINARY # pylint: disable=no-member
)
AirDropUtil.write_debug( AirDropUtil.write_debug(
self.config, ask_resp_binary, "receive_ask_response.plist" self.config, ask_resp_binary, "receive_ask_response.plist"
@@ -248,7 +245,7 @@ class AirDropServerHandler(BaseHTTPRequestHandler):
def handle_upload(self): def handle_upload(self):
if self.headers.get("content-type", "").lower() != "application/x-cpio": if self.headers.get("content-type", "").lower() != "application/x-cpio":
logger.warning( logger.warning(
"Unsupported content-type: {}".format(self.headers.get("content-type")) f"Unsupported content-type: {self.headers.get('content-type')}"
) )
self.send_response(406) # Unprocessable Entity self.send_response(406) # Unprocessable Entity
self.send_header("Content-Type", "application/x-cpio") self.send_header("Content-Type", "application/x-cpio")
@@ -309,9 +306,7 @@ class AirDropServerHandler(BaseHTTPRequestHandler):
transferred = reader.total / 1024.0 / 1024.0 transferred = reader.total / 1024.0 / 1024.0
speed = transferred / (time.time() - start) speed = transferred / (time.time() - start)
logger.info( logger.info(
"File(s) received (size {:.02f} MB, speed {:.02f} MB/s)".format( f"File(s) received (size {transferred:.02f} MB, speed {speed:.02f} MB/s)"
transferred, speed
)
) )
self.send_response(200) self.send_response(200)
@@ -324,8 +319,8 @@ class AirDropServerHandler(BaseHTTPRequestHandler):
Handle post requests Handle post requests
""" """
logger.debug("POST request at {}".format(self.path)) logger.debug(f"POST request at {self.path}")
logger.debug("Headers\n{}".format(self.headers)) logger.debug(f"Headers\n{self.headers}")
if self.path == "/Discover": if self.path == "/Discover":
self.handle_discover() self.handle_discover()
@@ -334,14 +329,13 @@ class AirDropServerHandler(BaseHTTPRequestHandler):
elif self.path == "/Upload": elif self.path == "/Upload":
self.handle_upload() self.handle_upload()
else: else:
logger.debug("POST request at {}".format(self.path)) logger.debug(f"POST request at {self.path}")
self.send_response(400) self.send_response(400)
self.send_header("Content-Length", 0) self.send_header("Content-Length", 0)
self.end_headers() self.end_headers()
def log_message(self, format, *args): def log_message(self, format, *args):
# pylint: disable=redefined-builtin
logger.debug( logger.debug(
"{} - - [{}] {}".format( f"{self.client_address[0]} - - [{self.log_date_time_string()}] {format % args}"
self.client_address[0], self.log_date_time_string(), format % args
)
) )
+15 -19
View File
@@ -17,29 +17,25 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import base64
import datetime
import hashlib
import io import io
import ipaddress import ipaddress
import os import os
import plistlib
import ifaddr import ifaddr
from PIL import Image, ExifTags from libarchive.entry import ArchiveEntry, new_archive_entry
from libarchive import ffi from libarchive.ffi import ( # pylint: disable=no-name-in-module
from libarchive.entry import new_archive_entry, ArchiveEntry
from libarchive.ffi import (
ARCHIVE_EOF, ARCHIVE_EOF,
entry_sourcepath,
entry_clear, entry_clear,
read_next_header2, entry_sourcepath,
read_disk_descend, read_disk_descend,
write_header, read_next_header2,
write_data, write_data,
write_finish_entry, write_finish_entry,
write_get_bytes_per_block,
write_header,
) )
from libarchive.write import ArchiveWrite, new_archive_read_disk from libarchive.write import ArchiveWrite, new_archive_read_disk
from PIL import ExifTags, Image
class AirDropUtil: class AirDropUtil:
@@ -104,7 +100,7 @@ class AirDropUtil:
try: try:
exif = dict( exif = dict(
(ExifTags.TAGS[k], v) (ExifTags.TAGS[k], v)
for k, v in im._getexif().items() for k, v in im._getexif().items() # pylint: disable=protected-access
if k in ExifTags.TAGS if k in ExifTags.TAGS
) )
angles = {3: 180, 6: 270, 8: 90} angles = {3: 180, 6: 270, 8: 90}
@@ -116,15 +112,15 @@ class AirDropUtil:
# Big image # Big image
im.thumbnail((540, 540), Image.ANTIALIAS) im.thumbnail((540, 540), Image.ANTIALIAS)
imgByteArr = io.BytesIO() img_bytes = io.BytesIO()
im.save(imgByteArr, format="JPEG2000") im.save(img_bytes, format="JPEG2000")
file_icon = imgByteArr.getvalue() file_icon = img_bytes.getvalue()
# Small image # Small image
# im.thumbnail((64, 64), Image.ANTIALIAS) # im.thumbnail((64, 64), Image.ANTIALIAS)
# imgByteArr = io.BytesIO() # img_bytes = io.BytesIO()
# im.save(imgByteArr, format='JPEG2000') # im.save(img_bytes, format='JPEG2000')
# small_file_icon = imgByteArr.getvalue() # small_file_icon = img_bytes.getvalue()
return file_icon return file_icon
@@ -180,7 +176,7 @@ class AbsArchiveWrite(ArchiveWrite):
""" """
write_p = self._pointer write_p = self._pointer
block_size = ffi.write_get_bytes_per_block(write_p) block_size = write_get_bytes_per_block(write_p)
if block_size <= 0: if block_size <= 0:
block_size = 10240 # pragma: no cover block_size = 10240 # pragma: no cover
+2
View File
@@ -1,4 +1,6 @@
black black
flake8 flake8
flake8-bugbear flake8-bugbear
isort
pylint
pytest pytest
+13 -1
View File
@@ -2,4 +2,16 @@
extend-ignore = E203, E501 extend-ignore = E203, E501
max-line-length = 80 max-line-length = 80
max-complexity = 18 max-complexity = 18
select = B9 select = B9
[isort]
multi_line_output = 3
include_trailing_comma = True
force_grid_wrap = 0
use_parentheses = True
ensure_newline_before_comments = True
line_length = 88
[pylint]
disable = C, R, W0511, W1203
max-line-length = 88
+1 -1
View File
@@ -14,7 +14,7 @@ setup(
description="An open Apple AirDrop implementation", description="An open Apple AirDrop implementation",
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
url="https://owlink.org", url="<<DISCLAIMER: The former owlink website is no longer associated with this project, please disregard it.>>",
project_urls={ project_urls={
"Source": "https://github.com/seemoo-lab/opendrop", "Source": "https://github.com/seemoo-lab/opendrop",
"Research Paper": "https://usenix.org/conference/usenixsecurity19/presentation/stute", "Research Paper": "https://usenix.org/conference/usenixsecurity19/presentation/stute",