mirror of
https://github.com/seemoo-lab/opendrop.git
synced 2026-09-14 23:26:11 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11fe7ba786 | ||
|
|
9075732230 | ||
|
|
ae5ac821fb | ||
|
|
cbc7ca46a7 | ||
|
|
df221b0e2b | ||
|
|
4f1849cea2 | ||
|
|
8bacbe97b0 | ||
|
|
bc7813bf91 | ||
|
|
8a9bd1f4d5 | ||
|
|
ba550bb468 | ||
|
|
17db506f04 | ||
|
|
6960a97ab8 | ||
|
|
fe99dab664 | ||
|
|
d4f3df70e7 | ||
|
|
d96fa1d7ef | ||
|
|
bf7cea785c | ||
|
|
7db266f089 | ||
|
|
db34b3e349 | ||
|
|
368a8afc8c | ||
|
|
adb658d04d | ||
|
|
91e204f8a2 |
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 John’s iPhone
|
Found index 0 ID eccb2f2dcfe7 name John’s iPhone
|
||||||
Found index 1 ID e63138ac6ba8 name Jane’s MacBook Pro
|
Found index 1 ID e63138ac6ba8 name Jane’s 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 14–16, 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 14–16, 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 14–16, 2019, Santa Clara, CA, USA. [Paper](https://www.usenix.org/conference/usenixsecurity19/presentation/stute)
|
||||||
|
|
||||||
|
|
||||||
## Authors
|
## Authors
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
black
|
black
|
||||||
flake8
|
flake8
|
||||||
flake8-bugbear
|
flake8-bugbear
|
||||||
|
isort
|
||||||
|
pylint
|
||||||
pytest
|
pytest
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user