#!/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)