From 4956995f9ac6547cffccfb310f019c9ea4076978 Mon Sep 17 00:00:00 2001 From: Krytarik Raido Date: Sat, 27 May 2017 04:45:04 +0200 Subject: [PATCH] PackageInfo: Various improvements (by Valentin Lorentz) * Add depends command. * Improve handling of find and info. * Fix crash by double reloading. * Fix shell code injection vulnerability. --- PackageInfo/__init__.py | 2 +- PackageInfo/packages.py | 70 +++++++++++++++++++++++++++++++---------- PackageInfo/plugin.py | 50 +++++++++++++++++++++++++++-- 3 files changed, 102 insertions(+), 20 deletions(-) diff --git a/PackageInfo/__init__.py b/PackageInfo/__init__.py index baf795c..1823845 100644 --- a/PackageInfo/__init__.py +++ b/PackageInfo/__init__.py @@ -22,7 +22,7 @@ import supybot import supybot.world as world from imp import reload -__version__ = "1.0.0" +__version__ = "1.1.0" __author__ = supybot.Author("Krytarik Raido", "krytarik", "krytarik@tuxgarage.com") __contributors__ = { supybot.Author("Dennis Kaarsemaker", "Seveas", "dennis@kaarsemaker.net"): ['Original Concept'], diff --git a/PackageInfo/packages.py b/PackageInfo/packages.py index ed7c631..a66d475 100644 --- a/PackageInfo/packages.py +++ b/PackageInfo/packages.py @@ -35,6 +35,23 @@ def description(pkg): return pkg['Description'].split('\n')[0] return None +def apt_cache(aptdir, distro, extra): + return subprocess.check_output(['apt-cache', + '-oDir::State::Lists=%s/%s' % (aptdir, distro), + '-oDir::State::Status=%s/%s.status' % (aptdir, distro), + '-oDir::Etc::SourceList=%s/%s.list' % (aptdir, distro), + '-oDir::Etc::SourceParts=""', + '-oDir::Cache=%s/cache' % aptdir, + '-oAPT::Architecture=i386'] + + extra).decode('utf8') + +def apt_file(aptdir, distro, pkg): + return subprocess.check_output(['apt-file', + '-s', '%s/%s.list' % (aptdir, distro), + '-c', '%s/apt-file/%s' % (aptdir, distro), + '-l', '-a', 'i386', + 'search', pkg]).decode('utf8') + class Apt: def __init__(self, plugin): self.aptdir = plugin.registryValue('aptdir') @@ -44,15 +61,6 @@ class Apt: os.environ["LANG"] = "C" if self.aptdir: self.distros = sorted([x[:-5] for x in os.listdir(self.aptdir) if x.endswith('.list')]) - self.aptcommand = """apt-cache\\ - -o"Dir::State::Lists=%s/%%s"\\ - -o"Dir::etc::sourcelist=%s/%%s.list"\\ - -o"Dir::etc::SourceParts=%s/%%s.list.d"\\ - -o"Dir::State::status=%s/%%s.status"\\ - -o"Dir::Cache=%s/cache"\\ - -o"APT::Architecture=i386"\\ - %%s %%s""" % tuple([self.aptdir]*5) - self.aptfilecommand = """apt-file -s %s/%%s.list -c %s/apt-file/%%s -l -a i386 search %%s""" % (self.aptdir, self.aptdir) def find(self, pkg, chkdistro, filelookup=True): _pkg = ''.join([x for x in pkg.strip().split(None,1)[0] if x.isalnum() or x in '.-_+/']) @@ -65,10 +73,16 @@ class Apt: return "%s is not a valid distribution: %s" % (distro, ", ".join(self.distros)) pkg = _pkg - data = subprocess.getoutput(self.aptcommand % (distro, distro, distro, distro, 'search -n', pkg)) + try: + data = apt_cache(self.aptdir, distro, ['search', '-n', pkg]) + except subprocess.CalledProcessError as e: + data = e.output if not data: if filelookup: - data = subprocess.getoutput(self.aptfilecommand % (distro, distro, pkg)).split() + try: + data = apt_file(self.aptdir, distro, pkg).split() + except subprocess.CalledProcessError as e: + data = e.output if data: if data[0] == 'sh:': # apt-file isn't installed self.log.error("PackageInfo/packages: apt-file is not installed") @@ -84,13 +98,13 @@ class Apt: return "File %s found in %s" % (pkg, ', '.join(data)) return 'Package/file %s does not exist in %s' % (pkg, distro) return "No packages matching '%s' could be found" % pkg - pkgs = [x.split()[0] for x in data.split('\n')] + pkgs = [x.split()[0] for x in data.split('\n') if x] if len(pkgs) > 10: return "Found: %s (and %d others) http://packages.ubuntu.com/search?keywords=%s&searchon=names&suite=%s§ion=all" % (', '.join(pkgs[:10]), len(pkgs)-10, utils.web.urlquote(pkg), distro) else: return "Found: %s" % ', '.join(pkgs[:5]) - def info(self, pkg, chkdistro): + def raw_info(self, pkg, chkdistro): if not pkg.strip(): return '' _pkg = ''.join([x for x in pkg.strip().split(None,1)[0] if x.isalnum() or x in '.-_+']) @@ -102,8 +116,14 @@ class Apt: pkg = _pkg - data = subprocess.getoutput(self.aptcommand % (distro, distro, distro, distro, 'show', pkg)) - data2 = subprocess.getoutput(self.aptcommand % (distro, distro, distro, distro, 'showsrc', pkg)) + try: + data = apt_cache(self.aptdir, distro, ['show', pkg]) + except subprocess.CalledProcessError as e: + data = e.output + try: + data2 = apt_cache(self.aptdir, distro, ['showsrc', pkg]) + except subprocess.CalledProcessError as e: + data2 = e.output if not data or 'E: No packages found' in data: return 'Package %s does not exist in %s' % (pkg, distro) maxp = {'Version': '0~'} @@ -151,10 +171,24 @@ class Apt: if archs: archs = ' (Only available for %s)' % '; '.join(archs) - maxp["Distrobution"] = distro + maxp["Distribution"] = distro + maxp["Architectures"] = archs + return maxp + + def info(self, pkg, chkdistro): + maxp = self.raw_info(pkg, chkdistro) + if isinstance(maxp, str): + return maxp return("%s (source: %s): %s. In component %s, is %s. Version %s (%s), package size %s kB, installed size %s kB%s" % (maxp['Package'], maxp['Source'] or maxp['Package'], description(maxp), component(maxp['Section']), - maxp['Priority'], maxp['Version'], distro, int(maxp['Size'])/1024, maxp['Installed-Size'], archs)) + maxp['Priority'], maxp['Version'], maxp["Distribution"], int(maxp['Size'])/1024, maxp['Installed-Size'], maxp["Architectures"])) + + def depends(self, pkg, chkdistro): + maxp = self.raw_info(pkg, chkdistro) + if isinstance(maxp, str): + return maxp + return("%s (version %s in %s) depends on: %s" % + (maxp['Package'], maxp["Version"], maxp["Distribution"], maxp["Depends"])) # Simple test if __name__ == "__main__": @@ -189,6 +223,8 @@ if __name__ == "__main__": aptlookup = Apt(plugin) if command == "find": print(aptlookup.find(lookup, dists)) + elif command == "depends": + print(aptlookup.depends(lookup, dists)) else: print(aptlookup.info(lookup, dists)) diff --git a/PackageInfo/plugin.py b/PackageInfo/plugin.py index bdc1998..a7e4982 100644 --- a/PackageInfo/plugin.py +++ b/PackageInfo/plugin.py @@ -26,9 +26,7 @@ import supybot.conf as conf import os import re import time -from imp import reload from . import packages -reload(packages) def get_user(msg): try: @@ -155,6 +153,50 @@ class PackageInfo(callbacks.Plugin): info = wrap(real_info, ['anything', optional('text')]) + def real_depends(self, irc, msg, args, package, release): + """ [] + + Lookup dependencies for , optionally in + """ + channel = self.__getChannel(msg.args[0]) + reply_target = ircutils.replyTo(msg) + (release, rest) = self.__getRelease(irc, release, channel) + if not release: + return + reply = self.Apt.depends(package, release) + if rest: + if rest[0] == '|': + try: + target = rest + while target[0] == '|': + target = target[1:].strip() + if target.lower() == "me": + target = msg.nick + queue(irc, reply_target, "%s: %s" % (target, reply)) + return + except Exception as e: + self.log.info("PackageInfo: (depends) Exception in pipe: %r" % e) + pass + elif rest[0] == '>': + try: + while rest[0] == '>': + rest = rest[1:].strip() + targets = [_ for _ in rest.split() if _] # Split and discard empty parts + target = stripNick(targets[0]) # Take the first "nick" and strip off bad chars + if target.lower() == "me": + target = msg.nick # redirect + if not target: # Throw error + raise Exception('No target') + queue(irc, target, "<%s> wants you to know: %s" % (msg.nick, reply)) + return + except Exception as e: + self.log.info("PackageInfo: (depends) Exception in redirect: %r" % e) + pass + + queue(irc, reply_target, reply) + + depends = wrap(real_depends, ['anything', optional('text')]) + def real_find(self, irc, msg, args, package, release): """ [] @@ -216,6 +258,8 @@ class PackageInfo(callbacks.Plugin): (term, rest) = (rest.split(' ', 1) + [None])[:2] if cmd == "find": self.real_find(irc, msg, [], term, rest) + elif cmd == "depends": + self.real_depends(irc, msg, [], term, rest) else: self.real_info(irc, msg, [], term, rest) @@ -234,6 +278,8 @@ class PackageInfo(callbacks.Plugin): (term, rest) = (rest.split(' ', 1) + [None])[:2] if cmd == "find": self.real_find(irc, msg, [], term, rest) + elif cmd == "depends": + self.real_depends(irc, msg, [], term, rest) else: self.real_info(irc, msg, [], term, rest)