1#!/usr/bin/env python
2
3"""Back door shell server
4
5This exposes an shell terminal on a socket.
6
7    --hostname : sets the remote host name to open an ssh connection to.
8    --username : sets the user name to login with
9    --password : (optional) sets the password to login with
10    --port     : set the local port for the server to listen on
11    --watch    : show the virtual screen after each client request
12"""
13
14# Having the password on the command line is not a good idea, but
15# then this entire project is probably not the most security concious thing
16# I've ever built. This should be considered an experimental tool -- at best.
17import pxssh, pexpect, ANSI
18import time, sys, os, getopt, getpass, traceback, threading, socket
19
20def exit_with_usage(exit_code=1):
21
22    print globals()['__doc__']
23    os._exit(exit_code)
24
25class roller (threading.Thread):
26
27    """This runs a function in a loop in a thread."""
28
29    def __init__(self, interval, function, args=[], kwargs={}):
30
31        """The interval parameter defines time between each call to the function.
32        """
33
34        threading.Thread.__init__(self)
35        self.interval = interval
36        self.function = function
37        self.args = args
38        self.kwargs = kwargs
39        self.finished = threading.Event()
40
41    def cancel(self):
42
43        """Stop the roller."""
44
45        self.finished.set()
46
47    def run(self):
48
49        while not self.finished.isSet():
50            # self.finished.wait(self.interval)
51            self.function(*self.args, **self.kwargs)
52
53def endless_poll (child, prompt, screen, refresh_timeout=0.1):
54
55    """This keeps the screen updated with the output of the child. This runs in
56    a separate thread. See roller(). """
57
58    #child.logfile_read = screen
59    try:
60        s = child.read_nonblocking(4000, 0.1)
61        screen.write(s)
62    except:
63        pass
64    #while True:
65    #    #child.prompt (timeout=refresh_timeout)
66    #    try:
67    #        #child.read_nonblocking(1,timeout=refresh_timeout)
68    #        child.read_nonblocking(4000, 0.1)
69    #    except:
70    #        pass
71
72def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
73
74    '''This forks the current process into a daemon. Almost none of this is
75    necessary (or advisable) if your daemon is being started by inetd. In that
76    case, stdin, stdout and stderr are all set up for you to refer to the
77    network connection, and the fork()s and session manipulation should not be
78    done (to avoid confusing inetd). Only the chdir() and umask() steps remain
79    as useful.
80
81    References:
82        UNIX Programming FAQ
83        1.7 How do I get my program to act like a daemon?
84        http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
85
86        Advanced Programming in the Unix Environment
87        W. Richard Stevens, 1992, Addison-Wesley, ISBN 0-201-56317-7.
88
89    The stdin, stdout, and stderr arguments are file names that will be opened
90    and be used to replace the standard file descriptors in sys.stdin,
91    sys.stdout, and sys.stderr. These arguments are optional and default to
92    /dev/null. Note that stderr is opened unbuffered, so if it shares a file
93    with stdout then interleaved output may not appear in the order that you
94    expect. '''
95
96    # Do first fork.
97    try:
98        pid = os.fork()
99        if pid > 0:
100            sys.exit(0)   # Exit first parent.
101    except OSError, e:
102        sys.stderr.write ("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror) )
103        sys.exit(1)
104
105    # Decouple from parent environment.
106    os.chdir("/")
107    os.umask(0)
108    os.setsid()
109
110    # Do second fork.
111    try:
112        pid = os.fork()
113        if pid > 0:
114            sys.exit(0)   # Exit second parent.
115    except OSError, e:
116        sys.stderr.write ("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror) )
117        sys.exit(1)
118
119    # Now I am a daemon!
120
121    # Redirect standard file descriptors.
122    si = open(stdin, 'r')
123    so = open(stdout, 'a+')
124    se = open(stderr, 'a+', 0)
125    os.dup2(si.fileno(), sys.stdin.fileno())
126    os.dup2(so.fileno(), sys.stdout.fileno())
127    os.dup2(se.fileno(), sys.stderr.fileno())
128
129    # I now return as the daemon
130    return 0
131
132def add_cursor_blink (response, row, col):
133
134    i = (row-1) * 80 + col
135    return response[:i]+'<img src="http://www.noah.org/cursor.gif">'+response[i:]
136
137def main ():
138
139    try:
140        optlist, args = getopt.getopt(sys.argv[1:], 'h?d', ['help','h','?', 'hostname=', 'username=', 'password=', 'port=', 'watch'])
141    except Exception, e:
142        print str(e)
143        exit_with_usage()
144
145    command_line_options = dict(optlist)
146    options = dict(optlist)
147    # There are a million ways to cry for help. These are but a few of them.
148    if [elem for elem in command_line_options if elem in ['-h','--h','-?','--?','--help']]:
149        exit_with_usage(0)
150
151    hostname = "127.0.0.1"
152    port = 1664
153    username = os.getenv('USER')
154    password = ""
155    daemon_mode = False
156    if '-d' in options:
157        daemon_mode = True
158    if '--watch' in options:
159        watch_mode = True
160    else:
161        watch_mode = False
162    if '--hostname' in options:
163        hostname = options['--hostname']
164    if '--port' in options:
165        port = int(options['--port'])
166    if '--username' in options:
167        username = options['--username']
168    print "Login for %s@%s:%s" % (username, hostname, port)
169    if '--password' in options:
170        password = options['--password']
171    else:
172        password = getpass.getpass('password: ')
173
174    if daemon_mode:
175        print "daemonizing server"
176        daemonize()
177        #daemonize('/dev/null','/tmp/daemon.log','/tmp/daemon.log')
178
179    sys.stdout.write ('server started with pid %d\n' % os.getpid() )
180
181    virtual_screen = ANSI.ANSI (24,80)
182    child = pxssh.pxssh()
183    child.login (hostname, username, password)
184    print 'created shell. command line prompt is', child.PROMPT
185    #child.sendline ('stty -echo')
186    #child.setecho(False)
187    virtual_screen.write (child.before)
188    virtual_screen.write (child.after)
189
190    if os.path.exists("/tmp/mysock"): os.remove("/tmp/mysock")
191    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
192    localhost = '127.0.0.1'
193    s.bind('/tmp/mysock')
194    os.chmod('/tmp/mysock',0777)
195    print 'Listen'
196    s.listen(1)
197    print 'Accept'
198    #s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
199    #localhost = '127.0.0.1'
200    #s.bind((localhost, port))
201    #print 'Listen'
202    #s.listen(1)
203
204    r = roller (0.01, endless_poll, (child, child.PROMPT, virtual_screen))
205    r.start()
206    print "screen poll updater started in background thread"
207    sys.stdout.flush()
208
209    try:
210        while True:
211            conn, addr = s.accept()
212            print 'Connected by', addr
213            data = conn.recv(1024)
214            if data[0]!=':':
215                cmd = ':sendline'
216                arg = data.strip()
217            else:
218                request = data.split(' ', 1)
219                if len(request)>1:
220                    cmd = request[0].strip()
221                    arg = request[1].strip()
222                else:
223                    cmd = request[0].strip()
224            if cmd == ':exit':
225                r.cancel()
226                break
227            elif cmd == ':sendline':
228                child.sendline (arg)
229                #child.prompt(timeout=2)
230                time.sleep(0.2)
231                shell_window = str(virtual_screen)
232            elif cmd == ':send' or cmd==':xsend':
233                if cmd==':xsend':
234                    arg = arg.decode("hex")
235                child.send (arg)
236                time.sleep(0.2)
237                shell_window = str(virtual_screen)
238            elif cmd == ':cursor':
239                shell_window = '%x%x' % (virtual_screen.cur_r, virtual_screen.cur_c)
240            elif cmd == ':refresh':
241                shell_window = str(virtual_screen)
242
243            response = []
244            response.append (shell_window)
245            #response = add_cursor_blink (response, row, col)
246            sent = conn.send('\n'.join(response))
247            if watch_mode: print '\n'.join(response)
248            if sent < len (response):
249                print "Sent is too short. Some data was cut off."
250            conn.close()
251    finally:
252        r.cancel()
253        print "cleaning up socket"
254        s.close()
255        if os.path.exists("/tmp/mysock"): os.remove("/tmp/mysock")
256        print "done!"
257
258def pretty_box (rows, cols, s):
259
260    """This puts an ASCII text box around the given string, s.
261    """
262
263    top_bot = '+' + '-'*cols + '+\n'
264    return top_bot + '\n'.join(['|'+line+'|' for line in s.split('\n')]) + '\n' + top_bot
265
266def error_response (msg):
267
268    response = []
269    response.append ("""All commands start with :
270:{REQUEST} {ARGUMENT}
271{REQUEST} may be one of the following:
272    :sendline: Run the ARGUMENT followed by a line feed.
273    :send    : send the characters in the ARGUMENT without a line feed.
274    :refresh : Use to catch up the screen with the shell if state gets out of sync.
275Example:
276    :sendline ls -l
277You may also leave off :command and it will be assumed.
278Example:
279    ls -l
280is equivalent to:
281    :sendline ls -l
282""")
283    response.append (msg)
284    return '\n'.join(response)
285
286def parse_host_connect_string (hcs):
287
288    """This parses a host connection string in the form
289    username:password@hostname:port. All fields are options expcet hostname. A
290    dictionary is returned with all four keys. Keys that were not included are
291    set to empty strings ''. Note that if your password has the '@' character
292    then you must backslash escape it. """
293
294    if '@' in hcs:
295        p = re.compile (r'(?P<username>[^@:]*)(:?)(?P<password>.*)(?!\\)@(?P<hostname>[^:]*):?(?P<port>[0-9]*)')
296    else:
297        p = re.compile (r'(?P<username>)(?P<password>)(?P<hostname>[^:]*):?(?P<port>[0-9]*)')
298    m = p.search (hcs)
299    d = m.groupdict()
300    d['password'] = d['password'].replace('\\@','@')
301    return d
302
303if __name__ == "__main__":
304
305    try:
306        start_time = time.time()
307        print time.asctime()
308        main()
309        print time.asctime()
310        print "TOTAL TIME IN MINUTES:",
311        print (time.time() - start_time) / 60.0
312    except Exception, e:
313        print str(e)
314        tb_dump = traceback.format_exc()
315        print str(tb_dump)
316
317