1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-06 12:41:29 +00:00
Files
nmap/zenmap/setup.py

586 lines
22 KiB
Python
Executable File

#!/usr/bin/env python3
# ***********************IMPORTANT NMAP LICENSE TERMS************************
# *
# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap
# * Project"). Nmap is also a registered trademark of the Nmap Project.
# *
# * This program is distributed under the terms of the Nmap Public Source
# * License (NPSL). The exact license text applying to a particular Nmap
# * release or source code control revision is contained in the LICENSE
# * file distributed with that version of Nmap or source code control
# * revision. More Nmap copyright/legal information is available from
# * https://nmap.org/book/man-legal.html, and further information on the
# * NPSL license itself can be found at https://nmap.org/npsl/ . This
# * header summarizes some key points from the Nmap license, but is no
# * substitute for the actual license text.
# *
# * Nmap is generally free for end users to download and use themselves,
# * including commercial use. It is available from https://nmap.org.
# *
# * The Nmap license generally prohibits companies from using and
# * redistributing Nmap in commercial products, but we sell a special Nmap
# * OEM Edition with a more permissive license and special features for
# * this purpose. See https://nmap.org/oem/
# *
# * If you have received a written Nmap license agreement or contract
# * stating terms other than these (such as an Nmap OEM license), you may
# * choose to use and redistribute Nmap under those terms instead.
# *
# * The official Nmap Windows builds include the Npcap software
# * (https://npcap.com) for packet capture and transmission. It is under
# * separate license terms which forbid redistribution without special
# * permission. So the official Nmap Windows builds may not be redistributed
# * without special permission (such as an Nmap OEM license).
# *
# * Source is provided to this software because we believe users have a
# * right to know exactly what a program is going to do before they run it.
# * This also allows you to audit the software for security holes.
# *
# * Source code also allows you to port Nmap to new platforms, fix bugs, and add
# * new features. You are highly encouraged to submit your changes as a Github PR
# * or by email to the dev@nmap.org mailing list for possible incorporation into
# * the main distribution. Unless you specify otherwise, it is understood that
# * you are offering us very broad rights to use your submissions as described in
# * the Nmap Public Source License Contributor Agreement. This is important
# * because we fund the project by selling licenses with various terms, and also
# * because the inability to relicense code has caused devastating problems for
# * other Free Software projects (such as KDE and NASM).
# *
# * The free version of Nmap 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. Warranties,
# * indemnification and commercial support are all available through the
# * Npcap OEM program--see https://nmap.org/oem/
# *
# ***************************************************************************/
import sys
if sys.version_info[0] != 3:
sys.exit("Sorry, Zenmap requires Python 3")
import errno
import os
import os.path
import re
import logging
from setuptools import setup, Command
from setuptools.command.install import install
from glob import glob
from stat import S_IRGRP, S_IROTH, S_IRUSR, S_IRWXU, S_IWUSR, S_IXGRP, S_IXOTH, ST_MODE
from zenmapCore.Version import VERSION
from zenmapCore.Name import APP_NAME, APP_DISPLAY_NAME, APP_WEB_SITE,\
APP_DOWNLOAD_SITE, NMAP_DISPLAY_NAME
# The name of the file used to record the list of installed files, so that the
# uninstall command can remove them.
INSTALLED_FILES_NAME = "INSTALLED_FILES"
# Directories for POSIX operating systems
# These are created after a "install" or "py2exe" command
# These directories are relative to the installation or dist directory
data_dir = os.path.join('share', APP_NAME)
pixmaps_dir = os.path.join(data_dir, 'pixmaps')
locale_dir = os.path.join(data_dir, 'locale')
config_dir = os.path.join(data_dir, 'config')
docs_dir = os.path.join(data_dir, 'docs')
misc_dir = os.path.join(data_dir, 'misc')
# Where to install .desktop files.
desktop_dir = os.path.join('share', 'applications')
def mo_find(result, dirname, fnames):
files = []
for f in fnames:
p = os.path.join(dirname, f)
if os.path.isfile(p) and f.endswith(".mo"):
files.append(p)
if files:
result.append((dirname, files))
###############################################################################
# Installation variables
data_files = [
(pixmaps_dir, glob(os.path.join(pixmaps_dir, '*.gif')) +
glob(os.path.join(pixmaps_dir, '*.png'))),
(os.path.join(pixmaps_dir, "radialnet"),
glob(os.path.join(pixmaps_dir, "radialnet", '*.png'))),
(config_dir, [os.path.join(config_dir, APP_NAME + '.conf'),
os.path.join(config_dir, 'scan_profile.usp'),
os.path.join(config_dir, APP_NAME + '_version')]),
(misc_dir, glob(os.path.join(misc_dir, '*.xml'))),
(docs_dir, [os.path.join(docs_dir, 'help.html')])
]
# Add i18n files to data_files list
os.walk(locale_dir, mo_find, data_files)
# path_startswith and path_strip_prefix are used to deal with the installation
# root (--root option, also known as DESTDIR).
def path_startswith(path, prefix):
"""Returns True if path starts with prefix. It's a little more intelligent
than str.startswith because it normalizes the paths to remove multiple
directory separators and down-up traversals."""
path = os.path.normpath(path)
prefix = os.path.normpath(prefix)
return path.startswith(prefix)
def path_strip_prefix(path, prefix):
"""Return path stripped of its directory prefix if it starts with prefix,
otherwise return path unmodified. This only works correctly with Unix
paths; for example it will not replace the drive letter on a Windows path.
Examples:
>>> path_strip_prefix('/tmp/destdir/usr/bin', '/tmp/destdir')
'/usr/bin'
>>> path_strip_prefix('/tmp/../tmp/destdir/usr/bin', '/tmp///destdir')
'/usr/bin'
>>> path_strip_prefix('/etc', '/tmp/destdir')
'/etc'
>>> path_strip_prefix('/etc', '/')
'/etc'
>>> path_strip_prefix('/etc', '')
'/etc'
"""
absolute = os.path.isabs(path)
path = os.path.normpath(path)
prefix = os.path.normpath(prefix)
if path.startswith(prefix) and prefix != os.sep:
path = path[len(prefix):]
# Absolute paths must remain absolute and relative paths must remain
# relative.
assert os.path.isabs(path) == absolute
return path
###############################################################################
# Distutils subclasses
class my_install(install):
def finalize_options(self):
# Ubuntu's python2.6-2.6.4-0ubuntu3 package changes sys.prefix in
# install.finalize_options when sys.prefix is "/usr/local" (our
# default). Because we need the unchanged value later, remember it
# here.
self.saved_prefix = self.prefix
install.finalize_options(self)
def run(self):
install.run(self)
self.set_perms()
self.set_modules_path()
self.fix_paths()
self.create_uninstaller()
self.write_installed_files()
def get_installed_files(self):
"""Return a list of installed files and directories, each prefixed with
the installation root if given. The list of installed directories
doesn't come from setuptools so it may be incomplete."""
installed_files = self.get_outputs()
for package in self.distribution.packages:
dir = package.replace(".", "/")
installed_files.append(os.path.join(self.install_lib, dir))
# Recursively include all the directories in data_dir (share/zenmap).
# This is mainly for convenience in listing locale directories.
installed_files.append(os.path.join(self.install_data, data_dir))
for dirpath, dirs, files in os.walk(
os.path.join(self.install_data, data_dir)):
for dir in dirs:
installed_files.append(os.path.join(dirpath, dir))
installed_files.append(
os.path.join(self.install_scripts, "uninstall_" + APP_NAME))
return installed_files
def create_uninstaller(self):
uninstaller_filename = os.path.join(
self.install_scripts, "uninstall_" + APP_NAME)
uninstaller = """\
#!/usr/bin/env python3
import errno, os, os.path, sys
print('Uninstall %(name)s %(version)s')
answer = raw_input('Are you sure that you want to uninstall '
'%(name)s %(version)s? (yes/no) ')
if answer != 'yes' and answer != 'y':
print('Not uninstalling.')
sys.exit(0)
""" % {'name': APP_DISPLAY_NAME, 'version': VERSION}
installed_files = []
for output in self.get_installed_files():
if self.root is not None:
# If we have a root (DESTDIR), we need to strip it off the
# front of paths so the uninstaller runs on the target host.
# The path manipulations are tricky, but made easier because
# the uninstaller only has to run on Unix.
if not path_startswith(output, self.root):
# This should never happen (everything gets installed
# inside the root), but if it does, be safe and don't
# delete anything.
uninstaller += ("print('%s was not installed inside "
"the root %s; skipping.')\n" % (output, self.root))
continue
output = path_strip_prefix(output, self.root)
assert os.path.isabs(output)
installed_files.append(output)
uninstaller += """\
INSTALLED_FILES = (
"""
for file in installed_files:
uninstaller += " %s,\n" % repr(file)
uninstaller += """\
)
# Split the list into lists of files and directories.
files = []
dirs = []
for path in INSTALLED_FILES:
if os.path.isfile(path) or os.path.islink(path):
files.append(path)
elif os.path.isdir(path):
dirs.append(path)
# Delete the files.
for file in files:
print("Removing '%s'." % file)
try:
os.remove(file)
except OSError as e:
print(' Error: %s.' % str(e), file=sys.stderr)
# Delete the directories. First reverse-sort the normalized paths by
# length so that child directories are deleted before their parents.
dirs = [os.path.normpath(dir) for dir in dirs]
dirs.sort(key = len, reverse = True)
for dir in dirs:
try:
print("Removing the directory '%s'." % dir)
os.rmdir(dir)
except OSError as e:
if e.errno == errno.ENOTEMPTY:
print("Directory '%s' not empty; not removing." % dir)
else:
print(str(e), file=sys.stderr)
"""
uninstaller_file = open(uninstaller_filename, 'w')
uninstaller_file.write(uninstaller)
uninstaller_file.close()
# Set exec bit for uninstaller
mode = ((os.stat(uninstaller_filename)[ST_MODE]) | 0o555) & 0o7777
os.chmod(uninstaller_filename, mode)
def set_modules_path(self):
app_file_name = os.path.join(self.install_scripts, APP_NAME)
# Find where the modules are installed. setuptools will put them in
# self.install_lib, but that path can contain the root (DESTDIR), so we
# must strip it off if necessary.
modules_dir = self.install_lib
if self.root is not None:
modules_dir = path_strip_prefix(modules_dir, self.root)
app_file = open(app_file_name, "r")
lines = app_file.readlines()
app_file.close()
for i in range(len(lines)):
if re.match(r'^INSTALL_LIB =', lines[i]):
lines[i] = "INSTALL_LIB = %s\n" % repr(modules_dir)
break
else:
raise ValueError(
"INSTALL_LIB replacement not found in %s" % app_file_name)
app_file = open(app_file_name, "w")
app_file.writelines(lines)
app_file.close()
def set_perms(self):
re_bin = re.compile("(bin|\.sh)")
for output in self.get_installed_files():
if re_bin.findall(output):
continue
if os.path.isdir(output):
os.chmod(output, S_IRWXU |
S_IRGRP |
S_IXGRP |
S_IROTH |
S_IXOTH)
else:
os.chmod(output, S_IRUSR |
S_IWUSR |
S_IRGRP |
S_IROTH)
def fix_paths(self):
"""Replace some hardcoded paths to match where files were installed."""
interesting_paths = {
"CONFIG_DIR": os.path.join(self.saved_prefix, config_dir),
"DOCS_DIR": os.path.join(self.saved_prefix, docs_dir),
"LOCALE_DIR": os.path.join(self.saved_prefix, locale_dir),
"MISC_DIR": os.path.join(self.saved_prefix, misc_dir),
"PIXMAPS_DIR": os.path.join(self.saved_prefix, pixmaps_dir),
# See $(nmapdatadir) in nmap/Makefile.in.
"NMAPDATADIR": os.path.join(self.saved_prefix, "share", "nmap")
}
# Find and read the Paths.py file.
pcontent = ""
paths_file = os.path.join("zenmapCore", "Paths.py")
installed_files = self.get_outputs()
for f in installed_files:
if re.findall("(%s)" % re.escape(paths_file), f):
paths_file = f
pf = open(paths_file)
pcontent = pf.read()
pf.close()
break
# Replace the path definitions.
for path, replacement in interesting_paths.items():
pcontent = re.sub("%s\s+=\s+.+" % path,
"%s = %s" % (path, repr(replacement)),
pcontent)
# Write the modified file.
pf = open(paths_file, "w")
pf.write(pcontent)
pf.close()
# Rewrite the zenmap.desktop and zenmap-root.desktop files to point to
# the installed locations of the su-to-zenmap.sh script and application
# icon.
su_filename = os.path.join(
self.saved_prefix, data_dir, "su-to-zenmap.sh")
icon_filename = os.path.join(
self.saved_prefix, pixmaps_dir, "zenmap.png")
desktop_filename = None
root_desktop_filename = None
for f in installed_files:
if re.search("%s$" % re.escape("zenmap-root.desktop"), f):
root_desktop_filename = f
elif re.search("%s$" % re.escape("zenmap.desktop"), f):
desktop_filename = f
if desktop_filename is not None:
df = open(desktop_filename, "r")
dcontent = df.read()
df.close()
regex = re.compile("^(Icon *= *).*$", re.MULTILINE)
dcontent = regex.sub("\\1%s" % icon_filename, dcontent)
df = open(desktop_filename, "w")
df.write(dcontent)
df.close()
if root_desktop_filename is not None:
df = open(root_desktop_filename, "r")
dcontent = df.read()
df.close()
regex = re.compile(
"^((?:Exec|TryExec) *= *).*su-to-zenmap.sh(.*)$",
re.MULTILINE)
dcontent = regex.sub("\\1%s\\2" % su_filename, dcontent)
regex = re.compile("^(Icon *= *).*$", re.MULTILINE)
dcontent = regex.sub("\\1%s" % icon_filename, dcontent)
df = open(root_desktop_filename, "w")
df.write(dcontent)
df.close()
def write_installed_files(self):
"""Write a list of installed files for use by the uninstall command.
This is similar to what happens with the --record option except that it
doesn't strip off the installation root, if any. File names containing
newline characters are not handled."""
if INSTALLED_FILES_NAME == self.record:
logging.warning("warning: installation record is overwriting "
"--record file '%s'." % self.record)
with open(INSTALLED_FILES_NAME, "w") as f:
for output in self.get_installed_files():
assert "\n" not in output
print(output, file=f)
class my_uninstall(Command):
"""A setuptools command that performs uninstallation. It reads the list of
installed files written by the install command."""
command_name = "uninstall"
description = "uninstall installed files recorded in '%s'" % (
INSTALLED_FILES_NAME)
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
# Read the list of installed files.
try:
f = open(INSTALLED_FILES_NAME, "r")
except IOError as e:
if e.errno == errno.ENOENT:
logging.error("Couldn't open the installation record '%s'. "
"Have you installed yet?", INSTALLED_FILES_NAME)
return
installed_files = [file.rstrip("\n") for file in f.readlines()]
f.close()
# Delete the installation record too.
installed_files.append(INSTALLED_FILES_NAME)
# Split the list into lists of files and directories.
files = []
dirs = []
for path in installed_files:
if os.path.isfile(path) or os.path.islink(path):
files.append(path)
elif os.path.isdir(path):
dirs.append(path)
# Delete the files.
for file in files:
logging.info("Removing '%s'.", file)
try:
if not self.dry_run:
os.remove(file)
except OSError as e:
logging.error(str(e))
# Delete the directories. First reverse-sort the normalized paths by
# length so that child directories are deleted before their parents.
dirs = [os.path.normpath(dir) for dir in dirs]
dirs.sort(key=len, reverse=True)
for dir in dirs:
try:
logging.info("Removing the directory '%s'.", dir)
if not self.dry_run:
os.rmdir(dir)
except OSError as e:
if e.errno == errno.ENOTEMPTY:
logging.info("Directory '%s' not empty; not removing.",
dir)
else:
logging.error(str(e))
# setup can be called in different ways depending on what we're doing. (For
# example py2exe needs special handling.) These arguments are common between
# all the operations.
COMMON_SETUP_ARGS = {
'name': APP_NAME,
'license': 'Nmap License (https://nmap.org/book/man-legal.html)',
'url': APP_WEB_SITE,
'download_url': APP_DOWNLOAD_SITE,
'author': 'Nmap Project',
'maintainer': 'Nmap Project',
'description': "%s frontend and results viewer" % NMAP_DISPLAY_NAME,
'long_description': "%s is an %s frontend that is really useful"
"for advanced users and easy to be used by newbies." % (
APP_DISPLAY_NAME, NMAP_DISPLAY_NAME),
'version': VERSION,
'scripts': [APP_NAME],
'packages': ['zenmapCore', 'zenmapGUI', 'zenmapGUI.higwidgets',
'radialnet', 'radialnet.bestwidgets', 'radialnet.core',
'radialnet.gui', 'radialnet.util'],
'data_files': data_files,
}
# All of the arguments to setup are collected in setup_args.
setup_args = {}
setup_args.update(COMMON_SETUP_ARGS)
if 'py2exe' in sys.argv:
# Windows- and py2exe-specific args.
import py2exe
WINDOWS_SETUP_ARGS = {
'zipfile': 'py2exe/library.zip',
'name': APP_NAME,
'windows': [{
"script": APP_NAME,
"icon_resources": [(1, "install_scripts/windows/nmap-eye.ico")]
}],
# On Windows we build Ndiff here in Zenmap's setup.py so the two Python
# programs will share a common runtime.
'py_modules': ["ndiff"],
# override the package search path to let Ndiff be found
'package_dir': {
'zenmapCore': 'zenmapCore',
'zenmapGUI': 'zenmapGUI',
'radialnet': 'radialnet',
'': '../ndiff'
},
'console': [{
"script": "../ndiff/scripts/ndiff",
"description": "Nmap scan comparison tool"
}],
'options': {"py2exe": {
"compressed": 1,
"optimize": 2,
"packages": ["encodings"],
"includes": ["pango", "atk", "gobject", "gio", "pickle", "bz2",
"encodings", "encodings.*", "cairo", "pangocairo"],
"dll_excludes": ["USP10.dll", "NSI.dll", "MSIMG32.dll",
"DNSAPI.dll"],
"custom_boot_script": "install_scripts/windows/boot_script.py",
}
}
}
setup_args.update(WINDOWS_SETUP_ARGS)
elif 'py2app' in sys.argv:
# Args for Mac OS X and py2app.
import py2app
import shutil
# py2app requires a ".py" suffix.
extended_app_name = APP_NAME + ".py"
shutil.copyfile(APP_NAME, extended_app_name)
MACOSX_SETUP_ARGS = {
'app': [extended_app_name],
'options': {"py2app": {
"packages": ["gio", "gobject", "gtk", "cairo"],
"includes": ["atk", "pango", "pangocairo"],
"argv_emulation": True,
"compressed": True,
"plist": "install_scripts/macosx/Info.plist",
"iconfile": "install_scripts/macosx/zenmap.icns"
}}
}
setup_args.update(MACOSX_SETUP_ARGS)
elif 'vanilla' in sys.argv:
# Don't create uninstaller, don't fix paths. Used for bundling on OS X
sys.argv.remove('vanilla')
else:
# Default args.
DEFAULT_SETUP_ARGS = {
'cmdclass': {'install': my_install, 'uninstall': my_uninstall},
}
setup_args.update(DEFAULT_SETUP_ARGS)
data_files = [
(desktop_dir, glob('install_scripts/unix/*.desktop')),
(data_dir, ['install_scripts/unix/su-to-zenmap.sh'])
]
setup_args["data_files"].extend(data_files)
setup(**setup_args)