#!/usr/bin/env python3 """fpaste - a cli frontend for the fpaste.org pastebin.""" # Copyright 2008, 2010 Fedora Unity Project (http://fedoraunity.org) # Author: Jason 'zcat' Farrell # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program 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. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import sys import urllib.request import urllib.parse import urllib.error import subprocess import time import json from optparse import OptionParser, OptionGroup, SUPPRESS_HELP from subprocess import CalledProcessError VERSION = '0.4.0.1' USER_AGENT = 'fpaste/' + VERSION APIKEY = urllib.parse.urlencode({'apikey': '5uZ30dTZE1a5V0WYhNwcMddBRDpk6UzuzMu-APKM38iMHacxdA0n4vCqA34avNyt'}) SERVER_URL = 'https://paste.centos.org' FPASTE_URL = 'https://paste.centos.org/api/create?' + APIKEY def is_text(text, maxCheck=100, pctPrintable=0.75): """ Check to see if a majority of the characters are printable. Returns true if maxCheck evenly distributed chars in text are >= pctPrintable% text chars. """ # e.g.: /bin/* ranges between 19% and 42% printable from string import printable if type(text) == bytes: text = text.decode("utf-8", "replace") nchars = len(text) if nchars == 0: return False ncheck = min(nchars, maxCheck) inc = float(nchars)/ncheck i = 0.0 nprintable = 0 while i < nchars: if text[int(i)] in printable: nprintable += 1 i += inc pct = float(nprintable) / ncheck return (pct >= pctPrintable) def confirm(prompt="OK?"): """Prompt user for yes/no input and return True or False.""" prompt += " [y/N]: " try: ans = input(prompt) except EOFError: # already read sys.stdin and hit EOF # rebind sys.stdin to user tty (unix-only) try: mytty = os.ttyname(sys.stdout.fileno()) sys.stdin = open(mytty) ans = input() except: print( "could not rebind sys.stdin to %s after sys.stdin EOF" % mytty, file=sys.stderr) return False if ans.lower().startswith("y"): return True else: return False def paste(text, options): """Send text to paste server and return the URL.""" if not text: print("No text to send.", file=sys.stderr) return False # if sent data exceeds maxlength, server dies without error returned, so, # we'll truncate the input here, until the server decides to truncate # instead of die data = urllib.parse.urlencode( {'lang': options.lang, 'text': text, 'title': options.title, 'name': options.author, 'private': options.private, 'expire': options.life }) pasteSizeKiB = len(data)/1024.0 # 512KiB appears to be the current hard limit (20110404); old limit was # 16MiB if pasteSizeKiB >= 512: print( "WARNING: your paste size (%.1fKiB) is very large and may be rejected by the server. A pastebin is NOT a file hosting service!" % (pasteSizeKiB), file=sys.stderr) # verify that it's most likely *non-binary* data being sent. if not is_text(text): print( "WARNING: your paste looks a lot like binary data instead of text.", file=sys.stderr) if not confirm("Send binary data anyway?"): return False req = urllib.request.Request( url=FPASTE_URL, data=data.encode('ascii'), headers={ 'User-agent': USER_AGENT}) if options.proxy: if options.debug: print("Using proxy: %s" % options.proxy, file=sys.stderr) req.set_proxy(options.proxy, 'http') print("Uploading (%.1fKiB)..." % pasteSizeKiB, file=sys.stderr) try: f = urllib.request.urlopen(req) except urllib.error.URLError as e: if hasattr(e, 'reason'): print("Error Uploading: %s" % e.reason, file=sys.stderr) elif hasattr(e, 'code'): print("Server Error: %d - %s" % (e.code, e.msg), file=sys.stderr) if options.debug: print(f.read()) return False try: response = f.read().decode("utf-8", "replace") except ValueError as e: print( "Error: Server did not return a correct response", file=sys.stderr) return False return response def sysinfo( show_stderr=False, show_successful_cmds=True, show_failed_cmds=True): """Return commonly requested system info.""" # 'ps' output below has been anonymized: -n for uid vs username, and -c for # short processname # cmd name, command, command2 fallback, command3 fallback, ... cmdlist = [ ('OS Release', '''lsb_release -ds''', '''cat /etc/*-release | uniq''', 'cat /etc/issue', 'cat /etc/motd' ), ('Kernel', '''uname -r ; cat /proc/cmdline''' ), ('Desktop(s) Running', '''ps -eo comm= | grep -E '(gnome-session|startkde|startactive|xfce.?-session|fluxbox|blackbox|hackedbox|ratpoison|enlightenment|icewm-session|od-session|wmaker|wmx|openbox-lxde|openbox-gnome-session|openbox-kde-session|mwm|e16|fvwm|xmonad|sugar-session|mate-session|lxqt-session|cinnamon)' ''' ), ('Desktop(s) Installed', '''ls -m /usr/share/xsessions/ | sed 's/\.desktop//g' ''' ), ('SELinux Status', '''sestatus''', '''/usr/sbin/sestatus''', '''getenforce''', '''grep -v '^#' /etc/sysconfig/selinux''' ), ('SELinux Errors', '''selinuxenabled && journalctl --since yesterday |grep avc: | grep -Eo comm="[^ ]+" | sort |uniq -c |sort -rn''' ), ('CPU Model', '''grep 'model name' /proc/cpuinfo | awk -F: '{print $2}' | uniq -c | sed -re 's/^ +//' ''', '''grep 'model name' /proc/cpuinfo''' ), ('64-bit Support', '''grep -q ' lm ' /proc/cpuinfo && echo Yes || echo No''' ), ('Hardware Virtualization Support', '''grep -Eq '(vmx|svm)' /proc/cpuinfo && echo Yes || echo No''' ), ('Load average', '''uptime'''), ('Memory usage', '''free -m''', 'free'), ('Top 5 CPU hogs', '''ps axuScnh | awk '$2!=''' + str(os.getpid()) + '''' | sort -rnk3 | head -5'''), ('Top 5 Memory hogs', '''ps axuScnh | sort -rnk4 | head -5'''), ('Disk space usage', '''df -hT''', 'df -h', 'df'), ('Block devices', '''blkid''', '''/sbin/blkid'''), ('PCI devices', '''lspci''', '''/sbin/lspci'''), ('USB devices', '''lsusb''', '''/sbin/lsusb'''), ('DRM Information', '''journalctl -k -b | grep -o 'kernel:.*drm.*$' | cut -d ' ' -f 2- ''' ), ('Xorg modules', '''grep LoadModule /var/log/Xorg.0.log ~/.local/share/xorg/Xorg.0.log | cut -d \\" -f 2 | xargs''' ), ('GL Support', '''glxinfo | grep -E "OpenGL version|OpenGL renderer"''' ), ('Xorg errors', '''grep '^\[.*(EE)' /var/log/Xorg.0.log ~/.local/share/xorg/Xorg.0.log | cut -d ':' -f 2- ''' ), ('Kernel buffer tail', '''dmesg | tail'''), ('Last few reboots', '''last -x -n10 reboot runlevel'''), ('DNF Repositories', '''dnf repolist''', '''ls -l /etc/yum.repos.d''', '''grep -v '^#' /etc/yum.conf''' ), ('DNF Extras', '''dnf -C list extras'''), ('Last 20 packages installed', '''rpm -qa --nodigest --nosignature --last | head -20'''), # ('Installed packages', '''rpm -qa --nodigest --nosignature | sort''', '''dpkg -l''') ] ('EFI boot manager output', '''efibootmgr -v''')] si = [] print("Gathering system info", end=' ', file=sys.stderr) for cmds in cmdlist: cmdname = cmds[0] cmd = "" for cmd in cmds[1:]: sys.stderr.write('.') # simple progress feedback p = subprocess.Popen( cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) try: (out, err) = p.communicate(timeout=300) except subprocess.TimeoutExpired: p.kill() (out, err) = p.communicate() if not p.returncode == 0: if show_stderr: if err: print( "sysinfo Error: the cmd \"%s\" returned %d with stderr: %s" % (cmd, p.returncode, err), file=sys.stderr) else: print( "sysinfo Error: the cmd \"%s\" returned %d without errors" % (cmd, p.returncode), file=sys.stderr) print("Trying next fallback cmd...", file=sys.stderr) if p.returncode == 0 and out: break if out: if show_successful_cmds: si.append(('%s (%s)' % (cmdname, cmd), out)) else: si.append(('%s' % cmdname, out)) else: if show_failed_cmds: si.append( ('%s (without results: "%s")' % (cmdname, '" AND "'.join( cmds[ 1:])), out)) else: si.append(('%s' % cmdname, out)) # return in readable indented format sistr = "=== fpaste %s System Information (fpaste --sysinfo) ===\n" % VERSION for cmdname, output in si: sistr += "* %s:\n" % cmdname if not output: sistr += " N/A\n\n" else: for line in output.decode("utf-8", "replace").split('\n'): sistr += " %s\n" % line return sistr def summarize_text(text): # use beginning/middle/end content snippets as a description summary. 120 char limit # "36chars ... 36chars ... 36chars" == 118 chars # TODO: nuking whitespace in huge text files might be expensive; optimize # for b/m/e segments only sniplen = 36 seplen = len(" ... ") tsum = "" text = " ".join(text.split()) # nuke whitespace tlen = len(text) if tlen < sniplen+seplen: tsum += text if tlen >= sniplen+seplen: tsum += text[0:sniplen] + " ..." if tlen >= (sniplen*2)+seplen: tsum += " " + text[tlen/2-(sniplen/2):(tlen/2)+(sniplen/2)] + " ..." if tlen >= (sniplen*3)+(seplen*2): tsum += " " + text[-sniplen:] # print >> sys.stderr, str(len(tsum)) + ": " + tsum return tsum def main(): """Main work function.""" validSyntaxOpts = [ "text", "html5", "css", "javascript", "php", "python", "ruby", "lua", "bash", "erlang", "go", "c", "cpp", "diff", "latex", "sql", "xml", "0", "4cs", "6502acme", "6502kickass", "6502tasm", "68000devpac", "abap", "actionscript", "actionscript3", "ada", "aimms", "algol68", "apache", "applescript", "apt_sources", "arm", "asm", "asymptote", "asp", "autoconf", "autohotkey", "autoit", "avisynth", "awk", "bascomavr", "basic4gl", "bbcode", "bf", "bibtex", "blitzbasic", "bnf", "boo", "c_loadrunner", "c_mac", "c_winapi", "caddcl", "cadlisp", "cfdg", "cfm", "chaiscript", "chapel", "cil", "clojure", "cmake", "cobol", "coffeescript", "cpp-winapi", "csharp", "cuesheet", "d", "dart", "dcs", "dcl", "dcpu16", "delphi", "div", "dos", "dot", "e", "ecmascript", "eiffel", "email", "epc", "euphoria", "ezt", "f1", "falcon", "fo", "fortran", "freebasic", "freeswitch", "fsharp", "gambas", "gdb", "genero", "genie", "gettext", "glsl", "gml", "gnuplot", "groovy", "gwbasic", "haskell", "haxe", "hicest", "hq9plus", "html4strict", "icon", "idl", "ini", "inno", "intercal", "io", "ispfpanel", "j", "java", "java5", "jcl", "jquery", "klonec", "klonecpp", "kotlin", "lb", "ldif", "lisp", "llvm", "locobasic", "logcat", "logtalk", "lolcode", "lotusformulas", "lotusscript", "lscript", "lsl2", "m68k", "magiksf", "make", "mapbasic", "matlab", "mirc", "mmix", "modula2", "modula3", "mpasm", "mxml", "mysql", "nagios", "netrexx", "newlisp", "nginx", "nimrod", "nsis", "oberon2", "objc", "objeck", "ocaml", "octave", "oobas", "oorexx", "oracle11", "oracle8", "oxygene", "oz", "parasail", "parigp", "pascal", "pcre", "per", "perl", "perl6", "pf", "pic16", "pike", "pixelbender", "pli", "plsql", "postgresql", "postscript", "povray", "powerbuilder", "powershell", "proftpd", "progress", "prolog", "properties", "providex", "purebasic", "pys60", "q", "qbasic", "qml", "racket", "rails", "rbs", "rebol", "reg", "rexx", "robots", "rpmspec", "rsplus", "rust", "sas", "scala", "scheme", "scilab", "scl", "sdlbasic", "smalltalk", "smarty", "spark", "sparql", "standardml", "stonescript", "systemverilog", "tcl", "teraterm", "thinbasic", "tsql", "typoscript", "unicon", "uscript", "upc", "urbi", "vala", "vb", "vbnet", "vbscript", "vedit", "verilog", "vhdl", "vim", "visualfoxpro", "visualprolog", "whitespace", "whois", "winbatch", "xbasic", "xorg_conf", "xpp", "yaml", "z80", "zxbasic" ] validClipboardSelectionOpts = ['primary', 'secondary', 'clipboard'] ext2lang_map = { 'sh': 'bash', 'bash': 'bash', 'bib': 'bibtex', 'c': 'c', 'h': 'c', 'hpp': 'cpp', 'cls': 'latex', 'cpp': 'cpp', 'css': 'css', 'diff': 'diff', 'html': 'html5', 'htm': 'html5', 'ini': 'ini', 'java': 'java', 'js': 'javascript', 'jsp': 'html5', 'lua': 'lua', 'm': 'octave', 'mat': 'matlab', 'mbox': 'email', 'pl': 'perl', 'plt': 'gnuplot', 'php': 'php', 'php3': 'php', 'py': 'python', 'rb': 'ruby', 'rhtml': 'html', 'spec': 'rpmspec', 'sql': 'sql', 'sqlite': 'sql', 'sty': 'latex', 'tcl': 'tcl', 'tex': 'latex', 'xml': 'xml', 'yaml': 'yaml' } usage = """\ Usage: %%prog [OPTION]... [FILE]... send text file(s), stdin, or clipboard to the Fedora community pastebin at %s and return the URL. Examples: %%prog file1.txt file2.txt dmesg | %%prog (prog1; prog2; prog3) | fpaste %%prog --sysinfo --confirm %%prog -t "debug output" -l python foo.py""" % SERVER_URL parser = OptionParser(usage=usage, version='%prog '+VERSION) parser.add_option( '', '--debug', dest='debug', help=SUPPRESS_HELP, action="store_true", default=False) parser.add_option('', '--proxy', dest='proxy', help=SUPPRESS_HELP) # pastebin-specific options first fpasteOrg_group = OptionGroup(parser, "fpaste.org Options") fpasteOrg_group.add_option( '-t', '--title', dest='title', help='title of paste; defaults to UNTITLED', metavar='"TITLE"') fpasteOrg_group.add_option( '-a', '--author', dest='author', help='author name; empty by default', metavar='"AUTHOR"') fpasteOrg_group.add_option( '-r', '--private', dest='private', help='make paste private; defaults to 1', metavar='PRIVATE') fpasteOrg_group.add_option( '-l', dest='lang', help='language of content for syntax highlighting; default is "%default"; use "list" to show all ' + str(len(validSyntaxOpts)) + ' supported langs', metavar='"LANGUAGE"') fpasteOrg_group.add_option( '-x', dest='life', help='life of paste in minutes; default is 1 day (maximum)', metavar='LIFE') parser.add_option_group(fpasteOrg_group) # other options fpasteProg_group = OptionGroup(parser, "Input/Output Options") fpasteProg_group.add_option( '-i', '--clipin', dest='clipin', help='read paste text from current X clipboard selection [requires: xsel]', action="store_true", default=False) fpasteProg_group.add_option( '-w', '--wayland-clipin', dest='wclipin', help='read paste text from Wayland selection [requires: wl-clip]', action="store_true", default=False) fpasteProg_group.add_option( '-o', '--clipout', dest='clipout', help='save returned paste URL to all available clipboards', action="store_true", default=False) fpasteProg_group.add_option( '', '--input-selection', dest='selection', help='specify which X clipboard to use. valid options: "primary" (default; middle-mouse-button paste), "secondary" (uncommon), or "clipboard" (ctrl-v paste)', metavar='CLIP') fpasteProg_group.add_option( '', '--fullpath', dest='fullpath', help='use pathname VS basename for file description(s)', action="store_true", default=False) fpasteProg_group.add_option( '', '--pasteself', dest='pasteself', help='paste this script itself', action="store_true", default=False) fpasteProg_group.add_option( '', '--sysinfo', dest='sysinfo', help='paste system information', action="store_true", default=False) fpasteProg_group.add_option( '', '--printonly', dest='printonly', help='print paste, but do not send', action="store_true", default=False) fpasteProg_group.add_option( '', '--confirm', dest='confirm', help='print paste, and prompt for confirmation before sending', action="store_true", default=False) parser.add_option_group(fpasteProg_group) # Let default be anonymous. # p = subprocess.Popen('whoami', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # (out, err) = p.communicate () # if p.returncode == 0 and out: # user = out[0:-1] # else: # print >> sys.stderr, "WARNING Could not run whoami. Posting anonymously." parser.set_defaults( lang='text', life='1440', selection='primary', title="UNTITLED", private="1", author="" ) (options, args) = parser.parse_args() if options.lang.lower() == 'list': print('Valid language syntax options:') for opt in validSyntaxOpts: print(opt) sys.exit(0) if options.clipin: if not os.access('/usr/bin/xsel', os.X_OK): # TODO: try falling back to xclip or dbus parser.error( 'OOPS - the clipboard options currently depend on "/usr/bin/xsel", which does not appear to be installed') if options.wclipin: if not os.access('/usr/bin/wl-paste', os.X_OK): parser.error( 'OOPS - the Wayland clipboard option currently depends on "/usr/bin/wl-paste", which does not appear to be installed') if (options.clipin or options.wclipin) and args: parser.error( "Sending both clipboard contents AND files is not supported. Use -i OR -w OR filename(s)") for optk, optv, opts in [('language', options.lang, validSyntaxOpts), ('clipboard selection', options.selection, validClipboardSelectionOpts)]: if optv not in opts: parser.error( "'%s' is not a valid %s option.\n\tVALID OPTIONS: %s" % (optv, optk, ', '.join(opts))) fileargs = args if options.fullpath: fileargs = [os.path.abspath(x) for x in args] else: # remove potentially non-anonymous path info from file path # descriptions fileargs = [os.path.basename(x) for x in args] # guess lang for some common file extensions, if all file exts similar, # and lang not changed from default if options.lang == 'text': all_exts_similar = False ext_prev = "" for i in range(0, len(args)): all_exts_similar = True ext = os.path.splitext(args[i])[1].lstrip(os.extsep) if i > 0 and ext != ext_prev: all_exts_similar = False break ext_prev = ext if all_exts_similar and ext in list(ext2lang_map.keys()): options.lang = ext2lang_map[ext] # get input from mutually exclusive sources, though they *could* be # combined text = "" if options.clipin: xselcmd = 'xsel -o --%s' % options.selection # text = os.popen(xselcmd).read() p = subprocess.Popen( xselcmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (text, err) = p.communicate() text = text.decode("utf-8", "replace") if p.returncode != 0: if options.debug: print(err, file=sys.stderr) parser.error( "'xsel' failure. this usually means you're not running X") if not text: parser.error("%s clipboard is empty" % options.selection) if options.wclipin: cmd = 'wl-paste' # text = os.popen(xselcmd).read() p = subprocess.Popen( cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (text, err) = p.communicate() text = text.decode("utf-8", "replace") if p.returncode != 0: if options.debug: print(err, file=sys.stderr) parser.error( "'wl-paste' failure. this usually means you're not running Wayland") if not text: parser.error("%s clipboard is empty" % options.selection) elif options.pasteself: text = open(sys.argv[0]).read() options.lang = 'python' elif options.sysinfo: text = sysinfo(options.debug) elif not args: # read from stdin if no file args supplied try: input_text = sys.stdin.buffer.read() text += input_text.decode("utf-8", "replace") except KeyboardInterrupt: print( "\nUSAGE REMINDER:\n fpaste waits for input when run without file arguments.\n Paste your text, then press on a new line to upload.\n Try `fpaste --help' for more information.\nExiting...", file=sys.stderr) sys.exit(1) else: for i, f in enumerate(args): if not os.access(f, os.R_OK): parser.error("file '%s' is not readable" % f) if (len(args) > 1): # separate multiple files with header text += '#' * 78 + '\n' text += '### file %d of %d: %s\n' % (i+1, len(args), fileargs[i]) text += '#' * 78 + '\n' text += open(f).read() if options.debug: print('lang: "%s"' % options.lang) print('text (%d): "%s ..."' % (len(text), text[:80])) if options.printonly or options.confirm: try: if is_text(text): # when piped to less, sometimes fails with [Errno 32] Broken # pipe print(text) else: print("DATA") except IOError: pass if options.printonly: # print only what would be sent, and exit sys.exit(0) elif options.confirm: # print what would be sent, and ask for permission if not confirm("OK to send?"): sys.exit(1) url = paste(text, options) if url: # Try to save URL in clipboard, and warn but don't throw error if options.clipout: if not os.access('/usr/bin/xsel', os.X_OK): print( 'OOPS - the clipboard options currently depend on "/usr/bin/xsel", which does not appear to be installed', file=sys.stderr) else: # Copy the url in all the valid clipboard options for selection in validClipboardSelectionOpts: xselcmd = 'xsel -i --%s' % selection p = subprocess.Popen( xselcmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) (out, err) = p.communicate(input=url.encode('utf-8')) if p.returncode != 0: if options.debug: print(err, file=sys.stderr) print( "WARNING: URL not saved to %s" % selection, file=sys.stderr) # Copy to Wayland clipboard if not os.access('/usr/bin/wl-copy', os.X_OK): print( 'OOPS - the clipboard options currently depend on "/usr/bin/wl-copy", which does not appear to be installed', file=sys.stderr) else: wcmd = ['wl-copy', url] try: subprocess.run(wcmd, check=True) except CalledProcessError as cpe: if options.debug: print("{} errored with return code {}".format( cpe.cmd, cpe.returncode), file=sys.stderr) print("\n" + cpe.stderr.decode(), file=sys.stderr) print( "WARNING: URL not saved to Wayland clipboard", file=sys.stderr) print(url) else: sys.exit(1) if options.pasteself: print( "install fpaste to local ~/bin dir by running: mkdir -p ~/bin; curl " + url + "/raw -o ~/bin/fpaste && chmod +x ~/bin/fpaste", file=sys.stderr) sys.exit(0) if __name__ == '__main__': try: main() except KeyboardInterrupt: print("\ninterrupted.") sys.exit(1)