Files
frida-frida-python/setup.py
T
2022-09-12 20:21:45 +03:00

312 lines
12 KiB
Python
Executable File

import sys
import os
import platform
import re
import zipfile
import shutil
import struct
import codecs
import subprocess
import hashlib
from collections import namedtuple
from functools import partial
from io import BytesIO
from urllib.request import urlopen
from urllib.parse import urljoin, urlparse, urlunparse
from html.parser import HTMLParser
from setuptools import setup
from setuptools.command.build_ext import build_ext
from setuptools.extension import Extension
DEFAULT_INDEX_URL = "https://pypi.org/simple/"
python_version = sys.version_info[0:2]
python_major_version = python_version[0]
package_dir = os.path.dirname(os.path.realpath(__file__))
pkg_info = os.path.join(package_dir, "PKG-INFO")
in_source_package = os.path.isfile(pkg_info)
if in_source_package:
with codecs.open(pkg_info, "r", 'utf-8') as f:
version_line = [line.rstrip("\r") for line in f.read().split("\n") if line.startswith("Version: ")][0]
frida_version = version_line[9:]
long_description = None
else:
frida_version = os.environ.get('FRIDA_VERSION', None)
long_description = codecs.open(os.path.join(package_dir, "README.md"), "r", 'utf-8').read()
frida_extension = os.environ.get('FRIDA_EXTENSION', None)
index_url_pip_configs = ("global.index-url", "global.extra-index-url")
Tag = namedtuple("Tag", ["tagname", "attrs"])
ParsedUrlInfo = namedtuple("ParsedUrlInfo",
["url", "filename", "major", "minor", "micro"])
class FridaPrebuiltExt(build_ext):
def build_extension(self, ext):
target = self.get_ext_fullpath(ext.name)
target_extension = os.path.splitext(target)[1]
target_dir = os.path.dirname(target)
try:
os.makedirs(target_dir)
except:
pass
if in_source_package:
system = platform.system()
arch = struct.calcsize('P') * 8
if system == 'Windows':
os_version = "win-amd64" if arch == 64 else "win32"
elif system == 'Darwin':
if platform.machine() == 'x86_64':
os_version = "macosx-10.9-x86_64"
elif python_major_version == 2:
os_version = "macosx-11.0-fat64"
else:
os_version = "macosx-11.0-arm64"
elif system == 'Linux':
os_name = 'android' if subprocess.check_output(["uname", "-o"]).decode('utf-8').rstrip() == 'Android' else 'linux'
machine = platform.machine()
if machine == "" or "86" in machine:
arch_name = "x86_64" if arch == 64 else "i686"
elif os_name == 'android' and machine.startswith("armv"):
arch_name = 'armv7l'
else:
arch_name = machine
os_version = "{}-{}".format(os_name, arch_name)
elif system == 'FreeBSD':
os_version = "freebsd-" + platform.machine()
else:
raise NotImplementedError("unsupported OS")
egg_path = os.path.expanduser(
"~{}frida-{}-py{}.{}-{}.egg".format(os.sep, frida_version, python_version[0], python_version[1], os_version))
print("looking for prebuilt extension in home directory, i.e.", egg_path)
try:
with open(egg_path, "rb") as cache:
egg_data = cache.read()
except:
egg_data = None
if egg_data is None:
print("prebuilt extension not found in home directory, will try downloading it")
print("querying pypi for available prebuilds")
# index_url is a url compatible with PEP 503
index_url = get_index_url().strip()
index_url = normalize_url(index_url)
frida_url = urljoin(index_url, "frida/") # slash is necessary here
timeout = 20
errmsg = (
"unable to download it within {} seconds; "
"please download it manually to {}"
).format("{}", egg_path)
print("downloading package list from", frida_url)
try:
links_html = urlopen(frida_url, timeout=timeout).read().decode("utf-8")
except Exception:
print(errmsg.format(timeout))
raise
parser = PEP503PageParser("frida", frida_version, os_version)
parser.feed(links_html)
urls = [url for url in parser.urls if url.major == python_major_version]
if len(urls) == 0:
raise NotImplementedError("could not find prebuilt Frida extension; "
"prebuilds only provided for Python 2.7 and 3.4+")
url = urls[0]
egg_url = urljoin(frida_url, url.url)
try:
print("downloading prebuilt extension from", egg_url)
timeout = 120 # We'll assume the user has at least 200 kB/s transfer speed.
egg_data = urlopen(egg_url, timeout=timeout).read()
except Exception:
print(errmsg.format(timeout))
raise
else:
egg_url = None
egg_file = BytesIO(egg_data)
if egg_url is not None:
print("checking hash")
check_pep503_hash(egg_file, egg_url)
print("extracting prebuilt extension")
egg_zip = zipfile.ZipFile(egg_file)
extension_member = [info for info in egg_zip.infolist() if info.filename.endswith(target_extension)][0]
extension_data = egg_zip.read(extension_member)
if system == 'Windows' and python_major_version >= 3:
trailer = b"\x00" if python_version[1] >= 10 else b"\x00\x00"
extension_data = re.sub(b"python[3-9][0-9][0-9]\\.dll\x00",
"python{0}{1}.dll".format(*python_version).encode('utf-8') + trailer,
extension_data)
with open(target, 'wb') as f:
f.write(extension_data)
else:
shutil.copyfile(frida_extension, target)
def get_index_url():
"""get `index-url` from environment or pip
Use FRIDA_INDEX_URL environment variable to customize index-url compatible
with PEP 503.
"""
index_url = os.environ.get("FRIDA_INDEX_URL", None)
if index_url is not None:
return index_url
for config_name in index_url_pip_configs:
try:
index_url = get_index_url_from_pip(config_name)
except (subprocess.CalledProcessError, OSError):
pass
else:
return index_url
print("using default index URL: {}".format(DEFAULT_INDEX_URL))
return DEFAULT_INDEX_URL
def get_index_url_from_pip(config_name):
assert config_name in index_url_pip_configs
return subprocess.check_output([sys.executable, "-m", "pip", "config", "get", config_name],
stderr=subprocess.PIPE).decode("utf-8")
def normalize_url(url):
parse_result = urlparse(url)
path = parse_result.path
if not path.endswith("/"):
path += "/"
return urlunparse((
parse_result.scheme, parse_result.netloc, path,
parse_result.params, parse_result.query, parse_result.fragment,
))
class PEP503PageParser(HTMLParser):
def __init__(self, name, version, os_version):
HTMLParser.__init__(self)
filename_pattern = (
r"^{}\-{}\-py(?P<major>\d+)\.(?P<minor>\d+)(\.(?P<micro>\d+))?-{}.egg$"
).format(*map(re.escape, [name, version, os_version]))
if python_major_version == 2:
filename_pattern = filename_pattern.decode("utf-8")
self._filename_pattern = re.compile(filename_pattern)
def reset(self):
HTMLParser.reset(self)
self._path = []
self.urls = []
def handle_starttag(self, tag, attrs):
self._path.append(Tag(tag, dict(attrs)))
def handle_endtag(self, tag):
if tag == u"a":
while True:
if self._path.pop().tagname == tag:
break
else:
if len(self._path) > 0 and self._path[-1].tagname == tag:
self._path.pop()
def handle_data(self, data):
if not (len(self._path) > 0
and self._path[-1].tagname == u"a"
and self._path[-1].attrs.get("href")):
return
match = self._filename_pattern.match(data)
if match is not None:
self.urls.append(ParsedUrlInfo(
self._path[-1].attrs["href"],
data,
*map(
lambda g: int(g) if g else None,
map(match.group, ["major", "minor", "micro"])
)
))
def check_pep503_hash(bytes_io, url):
parse_result = urlparse(url)
fragment = parse_result.fragment
if fragment == "":
return
hashname, hashvalue = fragment.split("=")
if hashname not in {"md5", "sha1", "sha224", "sha256", "sha348", "sha512"}:
raise ValueError("Unsupported hash algorithm: {}, hashvalue={}".format(
hashname, hashvalue,
))
h = hashlib.new(hashname)
for block in iter(partial(bytes_io.read, 4096), b""): # iterate until EOF
h.update(block)
digest = h.hexdigest()
bytes_io.seek(0) # reset offset
if digest == hashvalue:
return
else:
raise ValueError(
"`{}` hash checking failed! Expected: {}, but got: {}".format(
hashname, hashvalue, digest)
)
if __name__ == "__main__":
setup(
name="frida",
version=frida_version,
description="Dynamic instrumentation toolkit for developers, reverse-engineers, and security researchers",
long_description=long_description,
long_description_content_type="text/markdown",
author="Frida Developers",
author_email="oleavr@frida.re",
url="https://frida.re",
install_requires=["setuptools"],
python_requires=">=3.7",
license="wxWindows Library Licence, Version 3.1",
keywords="frida debugger dynamic instrumentation inject javascript windows macos linux ios iphone ipad android qnx",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Environment :: MacOS X",
"Environment :: Win32 (MS Windows)",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"License :: OSI Approved",
"Natural Language :: English",
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: JavaScript",
"Topic :: Software Development :: Debuggers",
"Topic :: Software Development :: Libraries :: Python Modules"
],
packages=['frida'],
ext_modules=[Extension('_frida', [])],
cmdclass={
'build_ext': FridaPrebuiltExt
},
zip_safe=False
)