#!/usr/bin/env python """ This file is part of Shell-Sink. Copyright Joshua Cronemeyer 2008, 2009 Shell-Sink 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. Shell-Sink 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 v3 for more details. You should have received a copy of the GNU General Public License along with Shell-Sink. If not, see . """ #import requests # very slow - moved after os.fork() import urllib import socket import getopt import sys import os import re import logging, logging.handlers SOCKET_TIMEOUT=10 BASE_URL="http://history.shellsink.com" class Client: def __init__(self): verify_environment() self.config = os.environ['HOME'] + "/.shellsink" self.config_file = os.environ['HOME'] + "/.shellsink/config" self.disable_slug = os.environ['HOME'] + "/.shellsink/disable_slug" self.id = os.environ['SHELL_SINK_ID'] self.tags = os.environ['SHELL_SINK_TAGS'] self.base_url = os.environ.get("SHELL_SINK_URL", BASE_URL) self.send_url = self.base_url + "/addCommand" self.pull_url = self.base_url + "/pull" self.verbose = False self.history = HistoryFile() self.status = False def url_with_send_both(self, tags): params = {'hash' : self.id, 'command' : self.history.latest()} if self.status: params['status'] = self.status data = urllib.urlencode(params) for tag in tags: data += '&tags[]=' + urllib.quote_plus(tag) return data def inline_tags(self, command): #Regexes that ignore quoted and escaped comments.. #Remove problem groups problems = re.compile(r"""(\\'|\\"|\\#+)""", re.I) stripped = problems.sub('', command) #Remove quoted areas quotes = re.compile(r"""("[^"]*?"|'[^']*?')""", re.I) clean = quotes.sub('', stripped) #Find comments comment = re.compile('#(.*?)') match_result = comment.split(clean, 1) if (len(match_result) == 3): return match_result[2].strip().split(":") def url_with_pull_command(self): params = {'hash': self.id} if self.tag: params['tag'] = self.tag if self.keyword: params['keyword'] = self.keyword data = urllib.urlencode(params) return data def async_sending_of_command_and_tags(self, tags): the_tags = tags.split(":") the_tags.append(os.getcwd()) if self.inline_tags(self.history.latest()): the_tags.extend(self.inline_tags(self.history.latest())) self.http_post(self.send_url, self.url_with_send_both(the_tags)) def http_post(self, url, data): import requests # Verify SSL certificates try: import urllib3.contrib.pyopenssl urllib3.contrib.pyopenssl.inject_into_urllib3() except ImportError: pass if self.verbose: logger.info("Attempt HTTP POST with " + url) hdr = {'User-Agent': 'Shell-Sink/1.0a', 'content-type': 'application/x-www-form-urlencoded'} response = None try: response = requests.post(url, data=data, headers=hdr) if response.status_code != requests.codes.ok: logger.error("Shellsink recieved an error from the server: %d" % response.status_code) if response.status_code == 404: logger.error("Ensure the env variable SHELL_SINK_ID is set to the unique id found here: %s%s" % (self.base_url, "/preferences")) except requests.exceptions.ConnectionError: logger.error("Shellsink cannot establish HTTP connection. Your message was not sent.") return response def send_command(self): if self.history.has_new_command(): response = self.spawn_process(self.async_sending_of_command_and_tags, self.tags) def get_history(self): response = self.http_post(self.pull_url, self.url_with_pull_command()) commands = response.text if commands.find('') < 0: print "Is your SHELL_SINK_ID env variable correct?" exit(1) commands = commands.split('') if len(commands) < 2: print "No commands matched your query." exit(0) commands = commands[1] return commands.split('')[0].lstrip().rstrip() def spawn_process(self, func, arg): pid = os.fork() if pid > 0: sys.exit(0) os.setsid() pid = os.fork() if pid > 0: sys.exit(0) func(arg) def enable(self): if os.path.exists(self.disable_slug): os.remove(self.disable_slug) def disable(self): file = open(self.disable_slug, "w") file.close() def is_enabled(self): return not os.path.exists(self.disable_slug) def conf(self): base = ["""#shellsink-client, a client for remote archiving your shell history"""] if not os.path.exists(self.config): os.mkdir(self.config) if not os.path.exists(self.config_file): file = open(self.config_file,"w") file.writelines(base) file.close() file = open(self.config_file, "r") self.config = file.readlines() file.close() def pull(self): history_content = self.get_history() if self.verbose: print 'ShellSink Commands Pulled From Server:' print history_content print '' self.history.add(history_content) class HistoryFile: def __init__(self): if os.environ.has_key('HISTFILE'): self.history_file = os.environ['HISTFILE'] self.history_timestamp = os.environ['HISTFILE'] + '_timestamp' else: self.history_file = os.environ['HOME'] + "/.bash_history" self.history_timestamp = os.environ['HOME'] + "/.bash_history_timestamp" def has_new_command(self): if os.path.isfile(os.environ['HOME'] + "/.shellsink_new_login"): os.remove(os.environ['HOME'] + "/.shellsink_new_login") return False new_history_timestamp = self.history_file_timestamp() last_recorded_history_timestamp = self.last_recorded_history_timestamp() if not last_recorded_history_timestamp: last_recorded_history_timestamp = new_history_timestamp - 1 self.record_new_last_recorded_history_timestamp(new_history_timestamp) if abs(new_history_timestamp - last_recorded_history_timestamp) < 0.01: return False return True def history_file_timestamp(self): return os.path.getmtime(self.history_file) def last_recorded_history_timestamp(self): try: file = open(self.history_timestamp,"r") return float(file.readline()) except: return None def record_new_last_recorded_history_timestamp(self, timestamp): file = open(self.history_timestamp,"w") file.writelines([str(timestamp)]) file.close() def latest(self): try: file = open(self.history_file, "r") latest = file.readlines()[-1] finally: file.close() return latest def add(self, commands): try: file = open(self.history_file, "a") file.writelines(commands + "\n") finally: file.close() def get_tag(opts): for opt in opts: if opt[0] in ["-t", "--tag"]: return opt[1] return None def get_keyword(opts): for opt in opts: if opt[0] in ["-k", "--keyword"]: return opt[1] return None def verify_environment(): if not os.environ.has_key('HOME'): raise Exception, "HOME environment variable must be set" if not os.environ.has_key('SHELL_SINK_ID'): raise Exception, "SHELL_SINK_ID environment variable must be set" if not os.environ.has_key('SHELL_SINK_TAGS'): raise Exception, "SHELL_SINK_TAGS can be empty but must exist" def logger(): if sys.platform == "darwin": address = "/var/run/syslog" else: address = "/dev/log" hdlr = logging.handlers.SysLogHandler(address) logger = logging.getLogger("shellsink") formatter = logging.Formatter('%(filename)s: %(levelname)s: %(message)s') hdlr.setFormatter(formatter) logger.addHandler(hdlr) return logger def usage(): print """usage: shellsink-client [ -v|-h|-e|-d|-s exit_status|-p [ -t TAG|-k KEYWORD ] ] | [ --verbose|--help|--enable|--disable|--status exit_status|--pull [ --tag TAG|--keyword KEYWORD ] ] The pull option pulls the most recent commands from the server into your history. Specifying a tag or keyword along with the pull command will pull the most recent commands matching that tag or keyword into your history. You cannot combine tags and keywords in your search. The verbose option will output commands that were returned by the pull operation to standard out.""" def main(): try: opts, args = getopt.getopt(sys.argv[1:], "vheds:pt:k:", ["verbose", "help", "enable", "disable", "status", "pull", "tag=", "keyword="]) except getopt.GetoptError, err: # print help information and exit: print str(err) # will print something like "option -a not recognized" usage() sys.exit(2) client = Client() client.conf() for o, a in opts: if o in ("-v", "--verbose"): client.verbose = True elif o in ("-h", "--help"): usage() sys.exit() elif o in ("-e", "--enable"): client.enable() sys.exit(0) elif o in ("-d", "--disable"): client.disable() sys.exit(0) elif o in ("-s", "--status"): client.status = a elif o in ("-p", "--pull"): client.tag = get_tag(opts) client.keyword = get_keyword(opts) client.pull() print "History file updated. Execute 'history -r' to add the commands to your current bash session." sys.exit(0) else: assert False, "unhandled option" socket.setdefaulttimeout(SOCKET_TIMEOUT) if client.is_enabled(): client.send_command() if __name__== '__main__': logger = logger() try: main() except SystemExit: pass except Exception, e: logger.error(e)