Refactoring + better inline html integration

This commit is contained in:
Florian Mounier
2014-02-06 18:00:51 +01:00
parent 421a15a05f
commit d4bbdec781
11 changed files with 1388 additions and 1225 deletions

View File

@@ -9,7 +9,6 @@ import io
print('\x1b]99;')
out = ''
i = 0
for f in os.listdir(os.getcwd()):
mime = mimetypes.guess_type(f)[0]
@@ -24,9 +23,6 @@ for f in os.listdir(os.getcwd()):
mime,
base64.b64encode(buf.read()).decode('ascii'),
f)
i += 1
if i % 5 == 0:
out += '\n'
except:
pass

13
bin/month Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env python
from calendar import LocaleHTMLCalendar
from datetime import datetime
import locale
now = datetime.now()
calendar = LocaleHTMLCalendar(locale=locale.getlocale())
calendar_table = calendar.formatmonth(now.year, now.month)
calendar_table = calendar_table.replace('border="0"', 'border="1"')
print('\x1b]99;')
print(calendar_table)
print('\x07')

View File

@@ -24,7 +24,7 @@ tornado.options.define("secret", default='secret', help="Secret")
tornado.options.define("debug", default=False, help="Debug mode")
tornado.options.define("host", default='127.0.0.1', help="Server host")
tornado.options.define("port", default=57575, type=int, help="Server port")
tornado.options.define("command", help="Command to execute at login")
tornado.options.define("shell", help="Shell to execute at login")
tornado.options.parse_command_line()

View File

@@ -25,13 +25,6 @@ from logging import getLogger
log = getLogger('butterfly')
application = tornado.web.Application(
debug=tornado.options.options.debug,
cookie_secret=tornado.options.options.secret,
static_path=os.path.join(os.path.dirname(__file__), "static"),
template_path=os.path.join(os.path.dirname(__file__), "templates")
)
class url(object):
def __init__(self, url):
@@ -50,5 +43,19 @@ class Route(tornado.web.RequestHandler):
def log(self):
return log
if hasattr(tornado.options.options, 'debug'):
opts = dict(
debug=tornado.options.options.debug,
cookie_secret=tornado.options.options.secret)
else:
opts = {}
application = tornado.web.Application(
static_path=os.path.join(os.path.dirname(__file__), "static"),
template_path=os.path.join(os.path.dirname(__file__), "templates"),
**opts
)
import butterfly.routes

View File

@@ -15,7 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pwd
import pty
import os
import io
@@ -26,10 +26,51 @@ import tornado.websocket
import tornado.process
import tornado.ioloop
import tornado.options
from butterfly import url, Route
import sys
from butterfly import url, Route, utils
ioloop = tornado.ioloop.IOLoop.instance()
server = utils.User()
daemon = utils.User(name='daemon')
def motd(socket, caller, callee):
return (
'''
B ` '
;,,, ` ' ,,,;
`Y888888bo. : : .od888888Y'
8888888888b. : : .d8888888888 AWelcome to RbutterflyB
88888Y' `Y8b. ` ' .d8Y' `Y88888
j88888 R.db.B Yb. ' ' .dY R.db.B 88888k AServer runnging as G%rB
`888 RY88YB `b ( ) d' RY88YB 888'
888b R'"B ,', R"'B d888 AConnecting to:B
j888888bd8gf"' ':' `"?g8bd888888k AHost: G%sB
R'Y'B .8' d' 'b '8. R'Y'X AUser: G%rB
R!B .8' RdbB d'; ;`b RdbB '8. R!B
d88 R`'B 8 ; ; 8 R`'B 88b AFrom:B
d888b .g8 ',' 8g. d888b AHost: G%sB
:888888888Y' 'Y888888888: AUser: G%rB
'! 8888888' `8888888 !'
'8Y R`Y Y'B Y8'
R Y Y
! !X
'''
.replace('B', '\x1b[34;1m')
.replace('G', '\x1b[32;1m')
.replace('R', '\x1b[37;1m')
.replace('A', '\x1b[37;0m')
.replace('X', '\x1b[0m')
.replace('\n', '\r\n')
% (
server,
'%s:%d' % (socket.remote_addr, socket.remote_port),
callee,
'%s:%d' % (socket.local_addr, socket.local_port),
caller or '?'))
@url(r'/(?:user/(.+))?/?(?:wd/(.+))?')
class Index(Route):
@@ -43,184 +84,121 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
def pty(self):
self.pid, self.fd = pty.fork()
if self.pid == 0:
# Child
try:
os.closerange(3, 256)
except:
self.log.error('closerange failed', exc_info=True)
if not self.is_local and not self.user:
while self.user is None:
user = input('%s login:' % self.bind)
try:
pwd.getpwnam(user)
except:
self.user = None
print('User %s not found' % user)
else:
self.user = user
butterfly_dir = os.getcwd()
try:
os.chdir(self.path or self.pw.pw_dir)
except:
pass
env = os.environ
if self.is_local and os.getuid() == 0:
try:
env = self.socket_opener_environ
except:
pass
env["TERM"] = "xterm-256color"
env["COLORTERM"] = "butterfly"
env["LOCATION"] = "http://%s:%d/" % (
tornado.options.options.host, tornado.options.options.port)
env["BUTTERFLY_DIR"] = butterfly_dir
env["SHELL"] = self.pw.pw_shell or '/bin/sh'
env["PATH"] = '%s:%s' % (os.path.abspath(os.path.join(
os.path.dirname(__file__), '..', 'bin')), env.get("PATH"))
shell = tornado.options.options.command or self.pw.pw_shell
args = ['butterfly', '-i', '-l']
# All users are the same -> launch shell
if self.is_local and (
self.uid == self.pw.pw_uid and self.uid == os.getuid()):
os.execvpe(shell, args, env)
if not (self.is_local and os.getuid() == 0 and
self.uid == self.pw.pw_uid):
# If user is not the same, get a password prompt
# (setuid to daemon user before su)
try:
os.setuid(2)
except PermissionError:
pass
args = ['butterfly', '-p']
if tornado.options.options.command:
args.append('-s')
args.append('%s' % tornado.options.options.command)
args.append(self.pw.pw_name)
print('Logging: %s@%s' % (self.pw.pw_name, self.bind))
os.execvpe('/bin/su', args, env)
self.shell()
else:
self.log.debug('Adding handler')
fcntl.fcntl(self.fd, fcntl.F_SETFL, os.O_NONBLOCK)
self.communicate()
# Set the size of the terminal window:
s = struct.pack("HHHH", 80, 80, 0, 0)
fcntl.ioctl(self.fd, termios.TIOCSWINSZ, s)
def utf8_error(e):
self.log.error(e)
self.reader = io.open(
self.fd,
'rb',
buffering=0,
closefd=False
)
self.writer = io.open(
self.fd,
'wt',
encoding='utf-8',
closefd=False
)
ioloop.add_handler(self.fd, self.shell, ioloop.READ | ioloop.ERROR)
@property
def is_local(self):
return self.bind in ['127.0.0.1', '::1']
@property
def pw(self):
if self.user:
return pwd.getpwnam(self.user)
if self.uid and self.is_local:
return pwd.getpwuid(self.uid)
@property
def uid(self):
try:
return int(self.socket_line[7])
except:
self.log.error('getting socket uid fail', exc_info=True)
@property
def socket_opener(self):
inode = self.socket_line[9]
for pid in os.listdir("/proc/"):
if not pid.isdigit():
continue
for fd in os.listdir("/proc/%s/fd/" % pid):
lnk = "/proc/%s/fd/%s" % (pid, fd)
if not os.path.islink(lnk):
continue
if 'socket:[%s]' % inode == os.readlink(lnk):
return pid
@property
def socket_opener_parent(self):
opener = self.socket_opener
if opener is None:
return
# Get parent pid
with open('/proc/%s/status' % opener) as s:
for line in s.readlines():
if line.startswith('PPid:'):
return line[len('PPid:'):].strip()
@property
def socket_opener_environ(self):
parent = self.socket_opener_parent
if parent is None:
return
with open('/proc/%s/environ' % parent) as e:
keyvals = e.read().split('\x00')
env = {}
for keyval in keyvals:
if '=' in keyval:
key, val = keyval.split('=', 1)
env[key] = val
return env
@property
def socket_line(self):
try:
with open('/proc/net/tcp') as k:
lines = k.readlines()
for line in lines:
# Look for local address with peer port
if line.split()[1] == '0100007F:%X' % self.port:
# We got the socket
return line.split()
except:
self.log.error('getting socket inet4 line fail', exc_info=True)
def shell(self):
while self.callee is None:
user = input('login: ')
try:
self.callee = utils.User(name=user)
except:
print('User %s not found' % user)
try:
with open('/proc/net/tcp6') as k:
lines = k.readlines()
for line in lines:
# Look for local address with peer port
if line.split()[1] == (
'00000000000000000000000001000000:%X' % self.port):
# We got the socket
return line.split()
os.chdir(self.path or self.callee.dir)
except:
self.log.error('getting socket inet6 line fail', exc_info=True)
pass
env = os.environ
env.update(self.socket.env)
env["TERM"] = "xterm-256color"
env["COLORTERM"] = "butterfly"
env["HOME"] = self.callee.dir
env["SHELL"] = self.callee.shell
env["LOCATION"] = "http://%s:%d/" % (
tornado.options.options.host, tornado.options.options.port)
env["PATH"] = '%s:%s' % (os.path.abspath(os.path.join(
os.path.dirname(__file__), '..', 'bin')), env.get("PATH"))
args = ['butterfly']
if self.socket.local:
# All users are the same -> launch shell
if self.caller == self.callee and server == self.callee:
os.execvpe(
tornado.options.options.shell or self.callee.shell,
args, env)
# This process has been replaced
return
if server.root:
if self.callee != self.caller:
# Force password prompt by dropping rights
# to the daemon user
os.setuid(daemon.uid)
else:
# We are not local so we should always get a password prompt
if server.root:
if self.callee == daemon:
# No logging from daemon
sys.exit(1)
os.setuid(daemon.uid)
args.append('-p')
if tornado.options.options.shell:
args.append('-s')
args.append(tornado.options.options.shell)
args.append(self.callee.name)
os.execvpe('/bin/su', args, env)
def communicate(self):
self.log.debug('Adding handler')
fcntl.fcntl(self.fd, fcntl.F_SETFL, os.O_NONBLOCK)
# Set the size of the terminal window:
s = struct.pack("HHHH", 80, 80, 0, 0)
fcntl.ioctl(self.fd, termios.TIOCSWINSZ, s)
def utf8_error(e):
self.log.error(e)
self.reader = io.open(
self.fd,
'rb',
buffering=0,
closefd=False
)
self.writer = io.open(
self.fd,
'wt',
encoding='utf-8',
closefd=False
)
ioloop.add_handler(
self.fd, self.shell_handler, ioloop.READ | ioloop.ERROR)
def open(self, user, path):
self.bind, self.port = (
self.ws_connection.stream.socket.getpeername()[:2])
self.log.info('Websocket opened for %s:%d' % (self.bind, self.port))
self.socket = utils.Socket(self.ws_connection.stream.socket)
self.set_nodelay(True)
self.user = user.decode('utf-8') if user else None
self.log.info('Websocket opened %r' % self.socket)
self.path = path
self.user = user.decode('utf-8') if user else None
self.caller = self.callee = None
if self.socket.local:
self.caller = utils.User(uid=self.socket.uid)
else:
# We don't know uid is on the other machine
pass
if self.user:
try:
self.callee = utils.User(name=self.user)
except LookupError:
print('User %s not found' % self.user)
self.callee = None
# If no user where given and we are local, keep the same user
# as the one who opened the socket
# ie: the one openning a terminal in borwser
if not self.callee and not self.user and self.socket.local:
self.callee = self.caller
self.write_message(motd(self.socket, self.caller, self.callee))
self.pty()
def on_message(self, message):
@@ -236,7 +214,7 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
self.writer.write(message)
self.writer.flush()
def shell(self, fd, events):
def shell_handler(self, fd, events):
if events & ioloop.READ:
try:
read = self.reader.read()

View File

@@ -129,4 +129,7 @@ addEventListener 'resize', resize = ->
console.log "Computed #{cols} cols and #{rows} rows from ", main_bb, ew, eh
term.resize cols, rows
for div in $('.terminal div')
div.style.height = eh + 'px'
ws.send "RS|#{cols},#{rows}"

View File

@@ -130,7 +130,7 @@ addEventListener('beforeunload', function() {
});
addEventListener('resize', resize = function() {
var eh, ew, fake_term, fake_term_div, fake_term_line, main, main_bb;
var div, eh, ew, fake_term, fake_term_div, fake_term_line, main, main_bb, _i, _len, _ref;
main = $('main')[0];
fake_term = document.createElement('div');
fake_term.className = 'terminal test';
@@ -148,5 +148,10 @@ addEventListener('resize', resize = function() {
rows = Math.floor(main_bb.height / eh);
console.log("Computed " + cols + " cols and " + rows + " rows from ", main_bb, ew, eh);
term.resize(cols, rows);
_ref = $('.terminal div');
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
div = _ref[_i];
div.style.height = eh + 'px';
}
return ws.send("RS|" + cols + "," + rows);
});

View File

@@ -38,9 +38,17 @@ body
color: $fg
text-shadow: 0 0 $shadow rgba($fg, $shadow-alpha)
transition: 50ms
&.bell
-webkit-filter: blur(2px)
div
overflow: visible
.inline-html
white-space: normal
.focus .cursor
transition: 300ms

File diff suppressed because it is too large Load Diff

145
butterfly/utils.py Normal file
View File

@@ -0,0 +1,145 @@
# *-* coding: utf-8 *-*
# This file is part of butterfly
#
# butterfly Copyright (C) 2014 Florian Mounier
# 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 <http://www.gnu.org/licenses/>.
import os
import pwd
from logging import getLogger
log = getLogger('butterfly')
class User(object):
def __init__(self, uid=None, name=None):
if not uid and not name:
uid = os.getuid()
if uid is not None:
self.pw = pwd.getpwuid(uid)
else:
self.pw = pwd.getpwnam(name)
if self.pw is None:
raise LookupError('Unknown user')
@property
def uid(self):
return self.pw.pw_uid
@property
def name(self):
return self.pw.pw_name
@property
def dir(self):
return self.pw.pw_dir
@property
def shell(self):
return self.pw.pw_shell
@property
def root(self):
return self.uid == 0
def __eq__(self, other):
return self.uid == other.uid
def __repr__(self):
return "%s [%r]" % (self.name, self.uid)
def get_socket_line(port):
try:
with open('/proc/net/tcp') as k:
lines = k.readlines()
for line in lines:
# Look for local address with peer port
if line.split()[1] == '0100007F:%X' % port:
# We got the socket
return line.split()
except:
log.error('getting socket inet4 line fail', exc_info=True)
try:
with open('/proc/net/tcp6') as k:
lines = k.readlines()
for line in lines:
# Look for local address with peer port
if line.split()[1] == (
'00000000000000000000000001000000:%X' % port):
# We got the socket
return line.split()
except:
log.error('getting socket inet6 line fail', exc_info=True)
def get_env(inode):
for pid in os.listdir("/proc/"):
if not pid.isdigit():
continue
for fd in os.listdir("/proc/%s/fd/" % pid):
lnk = "/proc/%s/fd/%s" % (pid, fd)
if not os.path.islink(lnk):
continue
if 'socket:[%s]' % inode == os.readlink(lnk):
with open('/proc/%s/status' % pid) as s:
for line in s.readlines():
if line.startswith('PPid:'):
with open('/proc/%s/environ' %
line[len('PPid:'):].strip()) as e:
keyvals = e.read().split('\x00')
env = {}
for keyval in keyvals:
if '=' in keyval:
key, val = keyval.split('=', 1)
env[key] = val
return env
class Socket(object):
def __init__(self, socket):
sn = socket.getsockname()
self.local_addr = sn[0]
self.local_port = sn[1]
pn = socket.getpeername()
self.remote_addr = pn[0]
self.remote_port = pn[1]
line = get_socket_line(self.remote_port)
if line:
self.uid = int(line[7])
self.inode = line[9]
else:
self.uid = None
self.inode = None
self.env = {}
if self.local:
try:
self.env = get_env(self.inode)
except:
log.warning('Unable to get env', exc_info=True)
@property
def local(self):
return self.remote_addr in ['127.0.0.1', '::1']
def __repr__(self):
return '<Socket L: %s:%d R: %s:%d Uid: %r Inode: %s %d>' % (
self.local_addr, self.local_port,
self.remote_addr, self.remote_port,
self.uid, self.inode, len(self.env))