58 Commits

Author SHA1 Message Date
Florian Mounier
da79ffe04b Changelog 2018-09-12 10:23:06 +02:00
Florian Mounier
c348e1f285 Bump 3.2.5 2018-09-12 10:22:02 +02:00
Florian Mounier
91d52ed6ae Fix coffee as per #179 2018-09-12 10:20:10 +02:00
Mounier Florian
06751c68f9 Merge pull request #179 from GrahamDumpleton/uri-root-path-coffee
Apply uri-root-path change to coffee/sass files and regenerate.
2018-09-12 10:16:49 +02:00
Graham Dumpleton
a9854e9136 Apply uri-root-path change to coffee/sass scripts and regenerate. 2018-09-12 11:16:12 +10:00
Florian Mounier
039c730409 Bump 3.2.4 2018-09-03 14:41:39 +02:00
Florian Mounier
82676862ca Fix one-shot auto-open url when uri-root-path is used. 2018-09-03 11:54:38 +02:00
Mounier Florian
5b6b61286d Merge pull request #173 from GrahamDumpleton/uri-root-path
Fix up --uri-root-path so behaves as one would expect for this.
2018-09-03 11:42:36 +02:00
Mounier Florian
f32cb4d358 Merge pull request #172 from ZoomerAnalytics/fix-keepalive-ping
added missing keepalive_timer.start()
2018-09-03 11:27:55 +02:00
Graham Dumpleton
ad155f1f17 Only create default conf file after options are parsed. 2018-08-30 12:30:22 +10:00
Felix Zumstein
9e1045de9b added missing keepalive_timer.start() 2018-08-26 22:54:53 +02:00
Graham Dumpleton
db3d37f6fe Fix up generation of URLs with prefix. 2018-08-23 13:26:16 +10:00
Graham Dumpleton
611f2e30d6 Add uri root path before all routes. 2018-08-23 11:53:38 +10:00
Florian Mounier
1984e4b869 Fix compare tags in Changelog 2018-06-04 11:20:09 +02:00
Florian Mounier
f58ea904b3 Merge branch 'master' of github.com:paradoxxxzero/butterfly 2018-06-04 11:15:47 +02:00
Florian Mounier
af0f4d20fe Update Changelog 2018-06-04 11:15:24 +02:00
Mounier Florian
10b5ce3bcc Merge pull request #161 from k4pu77/master
Updated docker baseimage
2018-06-04 11:12:33 +02:00
Florian Mounier
a0287946d9 Bump 3.2.3 2018-06-04 11:05:07 +02:00
Florian Mounier
fbd71d55ef Fix lint 2018-06-04 11:03:06 +02:00
Peter Cai
0ac8437387 term: fix password input on Chrome for Android
1. Also force focus on inputHelper on keyup on Android
2. Clear the inputHelper immediately upon receiving input
2018-06-03 20:58:39 +08:00
Peter Cai
866b56b682 term: bring back touch simulation of special keys on mobile
and also fixed it. The original version did not work because it tried
to change read-only fields of the event, which is not allowed.

The last commit removed support of touch simulation of Ctrl and Alt
by removing the `virtual_input.coffee` file. This commit brings it back
with a better implementation.
2018-06-03 20:10:24 +08:00
Peter Cai
4d87059872 remove unneeded virtual_input
We have already introduced a virtual textarea for every platform.
This one seems redundant.

However, some features may still not work perfectly on a mobile browser
2018-06-03 18:03:23 +08:00
Peter Cai
5bbe456496 term: remove redundant events of inputHelper and redundant contentEditable
We do not need to listen for keydown and keypress for inputHelper because these events will propagate through the parent.
Listening them will be redundant and will cause some shortcut key combinations to stop working.

Since we now have a hidden `textarea`, there is no longer need to set anything to contentEditable
2018-06-03 17:51:16 +08:00
Peter Cai
5b9cc257a8 term: do not re-focus on keyup when on mobile
Doing this will mess up the mobile browsers.
Although we still don't support mobile browsers very well, this can at least make it usable on them.
2018-06-03 12:19:43 +08:00
Peter Cai
34b6287e0c term: complete support for IME & CJK rendering
this fixes #75 and #47, two bugs originated long long ago.

1. Added support for IME events `compositionstart` `compositionupdate` and `compositionend`.
2. Refactored some code to receive input events from a hidden textarea just as how `xterm.js` now does. This removes the need to set `contentEditable` on the body in order to receive IME compistion events, and also guides the IME input box correctly following the cursor.
3. Fixed CJK rendering. Forces "forceWidth" mode with double width on those known CJK ranges in Unicode. Corrected the placeholder logic of the force width mode. Note that some rare halfwidth CJK characters will still not render correctly without `force-unicode-width` enabled. If you see any issue, please enable the `--force-unicode-width` option.
4. Miscallaneous fixes for some problems after introducing the above change

Tested on Firefox Nightly 62 on Linux and Chromium 67 on Linux, with `fcitx` as input method.
2018-06-03 10:27:49 +08:00
Peter Cai
41ee5fb843 update grunt-sass
the old grunt-sass no longer works with newer node.js
2018-06-03 10:12:19 +08:00
Christoph Christen
ae6b36fa89 Updated docker baseimage 2018-01-02 21:05:49 +01:00
Mounier Florian
cfda54a724 Merge pull request #158 from brentley/master
updating setuptools
2017-12-19 18:02:48 +01:00
Brent Langston
033169ab08 updating setuptools 2017-12-19 10:56:53 -06:00
Florian Mounier
920c435b00 Bump 3.2.2 2017-11-23 14:57:08 +01:00
Florian Mounier
27e6aa8a5d Update changelog 2017-11-23 14:56:56 +01:00
Florian Mounier
92633f52ce Fix unescaping entities when linkifying 2017-11-23 14:56:18 +01:00
Florian Mounier
f5f854964b Bump 3.2.1 2017-09-27 11:55:15 +02:00
Florian Mounier
55528fdf91 Issue correct X.509 v3 certificates (you will need to re-generate your certs) 2017-09-27 11:54:58 +02:00
Florian Mounier
9eae13486e Use X509 v4. 2017-09-27 11:34:41 +02:00
Florian Mounier
79bd074dae Bump 3.2.0 2017-09-27 10:59:19 +02:00
Mounier Florian
7b0ba2bfe7 Merge pull request #147 from 3ch01c/master
updated cert generation to v3 to comply with new browser standards
2017-09-21 17:58:55 +02:00
Mounier Florian
db17b9d8ac Merge pull request #152 from f0ma/master
Fix problem with ignoring --shell option in python2
2017-09-21 17:58:11 +02:00
Stanislav Ivanov
b5de82bfcf Fix problem with ignoring --shell option in python2 2017-09-21 18:04:03 +03:00
Jack Miner
13dbe0434c updated cert generation to v3 to comply with new browser standards 2017-07-24 17:52:14 -06:00
Florian Mounier
ef0057c23f Bump 3.1.5 2017-05-29 10:32:28 +02:00
Mounier Florian
6bc8e1438f Merge pull request #146 from warpkwd/fix_i_dont_options
fix i-hereby-...-whatsoever option
2017-05-29 10:21:39 +02:00
Yukihiro KAWADA
8856ea9dc4 fix i-hereby-...-whatsoever option
i-hereby-...-whatsoever revise to i_hereby_whatsoever
2017-05-24 12:59:18 +09:00
Florian Mounier
4edb2d269f Bump 3.1.4 2017-05-15 15:32:50 +02:00
Florian Mounier
272891470c Add --i-hereby-declare-i-dont-want-any-security-whatsoever option. Fix #143 2017-05-15 15:32:39 +02:00
Florian Mounier
574b3dc74b Bump 3.1.3 2017-05-15 11:31:07 +02:00
Florian Mounier
269dd2b618 Fix crash on lsof on python3 2017-05-15 11:30:59 +02:00
Florian Mounier
0625e05cbb Actually fix white-space on folded lines. 2017-05-10 16:01:40 +02:00
Florian Mounier
6b1101bc45 Fix white-space on folded lines. 2017-05-10 15:59:42 +02:00
Florian Mounier
3e6d0b203f Bump 3.1.2 2017-05-03 10:27:40 +02:00
Florian Mounier
8189598dd6 Add __about__ __all__ 2017-05-03 10:27:30 +02:00
Florian Mounier
4a8b5f2147 Add yarn.lock 2017-05-02 18:17:46 +02:00
Florian Mounier
f9a1ff4dea Bump 3.1.1 2017-05-02 18:12:36 +02:00
Florian Mounier
96d88a5e91 Bump 3.1.0 2017-05-02 18:00:24 +02:00
Florian Mounier
bdc1c7a80d Add a Makefile. Lint code. Fix butterfly open. Add a CHANGELOG.md 2017-05-02 17:59:52 +02:00
Florian Mounier
eacfdcd52f Fix huge performance loss on extended lines 2017-04-04 18:14:27 +02:00
Florian Mounier
ed347e2bd0 Use a __about__ file 2017-03-30 10:20:25 +02:00
Florian Mounier
3228e8c204 Integrate new themes 2017-03-30 10:14:03 +02:00
43 changed files with 2551 additions and 348 deletions

3
.gitignore vendored
View File

@@ -8,3 +8,6 @@ node_modules/
sass/scss
*.egg-info/
build/
.cache/
.env*
.pytest_cache

2
.isort.cfg Normal file
View File

@@ -0,0 +1,2 @@
[settings]
multi_line_output=4

46
CHANGELOG.md Normal file
View File

@@ -0,0 +1,46 @@
[3.2.5](https://github.com/paradoxxxzero/butterfly/compare/3.2.4...3.2.5)
=====
* Fix #155 again (PR #179)
[3.2.4](https://github.com/paradoxxxzero/butterfly/compare/3.2.3...3.2.4)
=====
* Fix up --uri-root-path so behaves as one would expect for this. Fix #155 (PR #173 thanks @GrahamDumpleton)
* Fix websocket keepalive. Fix #167 (PR #172 thanks @fzumstein)
[3.2.3](https://github.com/paradoxxxzero/butterfly/compare/3.2.2...3.2.3)
=====
* Complete support for IME & CJK rendering (#168 thanks @PeterCxy)
3.2.2
=====
* Fix unescaping entities when linkifying
3.2.1
=====
* Issue correct X.509 v3 certificates (you will need to re-generate your certs)
3.1.5
=====
* Fix new option in older tornado version. (#146 thanks @warpkwd)
3.1.4
=====
* Add --i-hereby-declare-i-dont-want-any-security-whatsoever option (#143)
3.1.3
=====
* Fix lsof parsing crash on python 2
3.1.0
=====
* Start a changelog

View File

@@ -1,4 +1,4 @@
FROM ubuntu:14.04
FROM ubuntu:16.04
RUN apt-get update \
&& apt-get install -y -q --no-install-recommends \
@@ -7,6 +7,9 @@ RUN apt-get update \
libssl-dev \
python-dev \
python-setuptools \
ca-certificates \
&& easy_install pip \
&& pip install --upgrade setuptools \
&& apt-get clean \
&& rm -r /var/lib/apt/lists/*

37
Makefile Normal file
View File

@@ -0,0 +1,37 @@
include Makefile.config
-include Makefile.custom.config
all: install lint check-outdated run-debug
install:
test -d $(VENV) || virtualenv $(VENV) -p $(PYTHON_VERSION)
$(PIP) install --upgrade --no-cache pip setuptools -e .[lint,themes] devcore
$(NPM) install
clean:
rm -fr $(NODE_MODULES)
rm -fr $(VENV)
rm -fr *.egg-info
lint:
$(PYTEST) --flake8 -m flake8 $(PROJECT_NAME)
$(PYTEST) --isort -m isort $(PROJECT_NAME)
check-outdated:
$(PIP) list --outdated --format=columns
ARGS ?= --port=1212 --unsecure --debug
run-debug:
$(PYTHON) ./butterfly.server.py $(ARGS)
build-coffee:
$(NODE_MODULES)/.bin/grunt
release: build-coffee
git pull
$(eval VERSION := $(shell PROJECT_NAME=$(PROJECT_NAME) $(VENV)/bin/devcore bump $(LEVEL)))
git commit -am "Bump $(VERSION)"
git tag $(VERSION)
$(PYTHON) setup.py sdist bdist_wheel upload
git push
git push --tags

10
Makefile.config Normal file
View File

@@ -0,0 +1,10 @@
PROJECT_NAME = butterfly
# Python env
PYTHON_VERSION ?= python
VENV = $(PWD)/.env$(if $(filter $(PYTHON_VERSION),python),,-$(PYTHON_VERSION))
PIP = $(VENV)/bin/pip
PYTHON = $(VENV)/bin/python
PYTEST = $(VENV)/bin/py.test
NODE_MODULES = $(PWD)/node_modules
NPM = yarn

View File

@@ -12,7 +12,7 @@ Butterfly is a xterm compatible terminal that runs in your browser.
* xterm compatible (support a lot of unused features!)
* Native browser scroll and search
* Theming in css / sass [(18 preset themes)](https://github.com/paradoxxxzero/butterfly-themes) endless possibilities!
* Theming in css / sass [(20 preset themes)](https://github.com/paradoxxxzero/butterfly-themes) endless possibilities!
* HTML in your terminal! cat images and use <table>
* Multiple sessions support (à la screen -x) to simultaneously access a terminal from several places on the planet!
* Secure authentication with X509 certificates!

View File

@@ -45,7 +45,8 @@ tornado.options.define("unminified", default=False,
tornado.options.define("host", default='localhost', help="Server host")
tornado.options.define("port", default=57575, type=int, help="Server port")
tornado.options.define("keepalive_interval", default=30, type=int,
help="Interval between ping packets sent from server to client (in seconds)")
help="Interval between ping packets sent from server "
"to client (in seconds)")
tornado.options.define("one_shot", default=False,
help="Run a one-shot instance. Quit at term close")
tornado.options.define("shell", help="Shell to execute at login")
@@ -54,10 +55,18 @@ tornado.options.define("cmd",
help="Command to run instead of shell, f.i.: 'ls -l'")
tornado.options.define("unsecure", default=False,
help="Don't use ssl not recommended")
tornado.options.define("i_hereby_declare_i_dont_want_any_security_whatsoever",
default=False,
help="Remove all security and warnings. There are some "
"use cases for that. Use this if you really know what "
"you are doing.")
tornado.options.define("login", default=False,
help="Use login screen at start")
tornado.options.define("pam_profile", default="", type=str,
help="When --login=True provided and running as ROOT, use PAM with the specified PAM profile for authentication and then execute the user's default shell. Will override --shell.")
help="When --login=True provided and running as ROOT, "
"use PAM with the specified PAM profile for "
"authentication and then execute the user's default "
"shell. Will override --shell.")
tornado.options.define("force_unicode_width",
default=False,
help="Force all unicode characters to the same width."
@@ -76,6 +85,7 @@ tornado.options.define("uri_root_path", default='',
help="Sets the servier root path: "
"example.com/<uri_root_path>/static/")
if os.getuid() == 0:
ev = os.getenv('XDG_CONFIG_DIRS', '/etc')
else:
@@ -88,17 +98,6 @@ butterfly_dir = os.path.join(ev, 'butterfly')
conf_file = os.path.join(butterfly_dir, 'butterfly.conf')
ssl_dir = os.path.join(butterfly_dir, 'ssl')
if not os.path.exists(conf_file):
try:
import butterfly
shutil.copy(
os.path.join(
os.path.abspath(os.path.dirname(butterfly.__file__)),
'butterfly.conf.default'), conf_file)
print('butterfly.conf installed in %s' % conf_file)
except:
pass
tornado.options.define("conf", default=conf_file,
help="Butterfly configuration file. "
"Contains the same options as command line.")
@@ -115,6 +114,21 @@ if os.path.exists(tornado.options.options.conf):
# Do it again to overwrite conf with args
tornado.options.parse_command_line()
# For next time, create them a conf file from template.
# Need to do this after parsing options so we do not trigger
# code import for butterfly module, in case that code is
# dependent on the set of parsed options.
if not os.path.exists(conf_file):
try:
import butterfly
shutil.copy(
os.path.join(
os.path.abspath(os.path.dirname(butterfly.__file__)),
'butterfly.conf.default'), conf_file)
print('butterfly.conf installed in %s' % conf_file)
except:
pass
options = tornado.options.options
for logger in ('tornado.access', 'tornado.application',
@@ -131,6 +145,9 @@ log = logging.getLogger('butterfly')
host = options.host
port = options.port
if options.i_hereby_declare_i_dont_want_any_security_whatsoever:
options.unsecure = True
if not os.path.exists(options.ssl_dir):
os.makedirs(options.ssl_dir)
@@ -165,6 +182,9 @@ def read(file):
with open(file, 'rb') as fd:
return fd.read()
def b(s):
return s.encode('utf-8')
if options.generate_certs:
from OpenSSL import crypto
@@ -175,6 +195,7 @@ if options.generate_certs:
ca_pk = crypto.PKey()
ca_pk.generate_key(crypto.TYPE_RSA, 2048)
ca_cert = crypto.X509()
ca_cert.set_version(2)
ca_cert.get_subject().CN = 'Butterfly CA on %s' % socket.gethostname()
fill_fields(ca_cert.get_subject())
ca_cert.set_serial_number(uuid.uuid4().int)
@@ -182,6 +203,21 @@ if options.generate_certs:
ca_cert.gmtime_adj_notAfter(315360000) # to 10y
ca_cert.set_issuer(ca_cert.get_subject()) # Self signed
ca_cert.set_pubkey(ca_pk)
ca_cert.add_extensions([
crypto.X509Extension(
b('basicConstraints'), True, b('CA:TRUE, pathlen:0')),
crypto.X509Extension(
b('keyUsage'), True, b('keyCertSign, cRLSign')),
crypto.X509Extension(
b('subjectKeyIdentifier'), False, b('hash'), subject=ca_cert),
])
ca_cert.add_extensions([
crypto.X509Extension(
b('authorityKeyIdentifier'), False,
b('issuer:always, keyid:always'),
issuer=ca_cert, subject=ca_cert
)
])
ca_cert.sign(ca_pk, 'sha512')
write(ca, crypto.dump_certificate(crypto.FILETYPE_PEM, ca_cert))
@@ -195,12 +231,23 @@ if options.generate_certs:
server_pk = crypto.PKey()
server_pk.generate_key(crypto.TYPE_RSA, 2048)
server_cert = crypto.X509()
server_cert.set_version(2)
server_cert.get_subject().CN = host
alt = 'subjectAltName'
value = 'DNS:%s' % host
server_cert.add_extensions([crypto.X509Extension(
alt.encode('utf-8'), False, value.encode('utf-8'))])
server_cert.add_extensions([
crypto.X509Extension(
b('basicConstraints'), False, b('CA:FALSE')),
crypto.X509Extension(
b('subjectKeyIdentifier'), False, b('hash'), subject=server_cert),
crypto.X509Extension(
b('subjectAltName'), False, b('DNS:%s' % host)),
])
server_cert.add_extensions([
crypto.X509Extension(
b('authorityKeyIdentifier'), False,
b('issuer:always, keyid:always'),
issuer=ca_cert, subject=ca_cert
)
])
fill_fields(server_cert.get_subject())
server_cert.set_serial_number(uuid.uuid4().int)
server_cert.gmtime_adj_notBefore(0) # From now
@@ -250,6 +297,7 @@ if (options.generate_current_user_pkcs or
client_pk.generate_key(crypto.TYPE_RSA, 2048)
client_cert = crypto.X509()
client_cert.set_version(2)
client_cert.get_subject().CN = user
fill_fields(client_cert.get_subject())
client_cert.set_serial_number(uuid.uuid4().int)
@@ -327,8 +375,10 @@ ioloop = tornado.ioloop.IOLoop.instance()
if port == 0:
port = list(http_server._sockets.values())[0].getsockname()[1]
url = "http%s://%s:%d/" % (
"s" if not options.unsecure else "", host, port)
url = "http%s://%s:%d/%s" % (
"s" if not options.unsecure else "", host, port,
(options.uri_root_path.strip('/') + '/') if options.uri_root_path else ''
)
if not options.one_shot or not webbrowser.open(url):
log.warn('Butterfly is ready, open your browser to: %s' % url)

15
butterfly/__about__.py Normal file
View File

@@ -0,0 +1,15 @@
__title__ = "butterfly"
__version__ = "3.2.5"
__summary__ = "A sleek web based terminal emulator"
__uri__ = "https://github.com/paradoxxxzero/butterfly"
__author__ = "Florian Mounier"
__email__ = "paradoxxx.zero@gmail.com"
__license__ = "GPLv3"
__copyright__ = "Copyright 2017 %s" % __author__
__all__ = [
'__title__', '__version__', '__summary__', '__uri__', '__author__',
'__email__', '__license__', '__copyright__'
]

View File

@@ -14,21 +14,14 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .__about__ import * # noqa: F401,F403
import os
import tornado.web
import tornado.options
import tornado.web
from logging import getLogger
try:
import pkg_resources
except ImportError:
__version__ = "pkg_resources not found on PYTHON_PATH"
else:
try:
__version__ = pkg_resources.require('butterfly')[0].version
except pkg_resources.DistributionNotFound:
__version__ = "butterfly is not installed"
log = getLogger('butterfly')
@@ -38,10 +31,15 @@ class url(object):
self.url = url
def __call__(self, cls):
if tornado.options.options.uri_root_path:
url = '/' + tornado.options.options.uri_root_path.strip('/') + self.url
else:
url = self.url
application.add_handlers(
r'.*$',
(tornado.web.url(self.url, cls, name=cls.__name__),)
(tornado.web.url(url, cls, name=cls.__name__),)
)
return cls
@@ -80,8 +78,8 @@ if hasattr(tornado.options.options, 'debug'):
template_path=os.path.join(os.path.dirname(__file__), "templates"),
debug=tornado.options.options.debug,
static_url_prefix='%s/static/' % (
'/%s' % tornado.options.options.uri_root_path
'/%s' % tornado.options.options.uri_root_path.strip('/')
if tornado.options.options.uri_root_path else '')
)
import butterfly.routes
import butterfly.routes # noqa: F401

View File

@@ -1,11 +1,12 @@
#!/usr/bin/env python
import sys
import os
import argparse
import base64
import mimetypes
import os
import subprocess
import sys
from butterfly.escapes import image
import argparse
parser = argparse.ArgumentParser(description='Butterfly cat wrapper.')
parser.add_argument('-o', action="store_true",

View File

@@ -2,7 +2,8 @@
import argparse
import sys
parser = argparse.ArgumentParser(description='Butterfly terminal color tester.')
parser = argparse.ArgumentParser(
description='Butterfly terminal color tester.')
parser.add_argument(
'--colors',
default='16',

View File

@@ -1,11 +1,11 @@
#!/usr/bin/env python
import base64
import os
import subprocess
import butterfly
from butterfly.escapes import image
from butterfly.utils import ansi_colors
import os
import butterfly
import base64
import shutil
import subprocess
print(ansi_colors.white + "Welcome to the butterfly help." + ansi_colors.reset)
path = os.getenv('BUTTERFLY_PATH')
@@ -58,7 +58,6 @@ Butterfly is a xterm compliant terminal built with python and javascript.
code=ansi_colors.light_yellow,
comment=ansi_colors.light_magenta,
reset=ansi_colors.reset,
rcol=int(subprocess.check_output(['stty','size']).split()[1]) - 31,
rcol=int(subprocess.check_output(['stty', 'size']).split()[1]) - 31,
main=os.path.normpath(os.path.join(
os.path.abspath(os.path.dirname(butterfly.__file__)),
'sass'))))
os.path.abspath(os.path.dirname(butterfly.__file__)), 'sass'))))

View File

@@ -1,9 +1,10 @@
#!/usr/bin/env python
from butterfly.escapes import html
import argparse
import fileinput
import sys
from butterfly.escapes import html
parser = argparse.ArgumentParser(
description="Butterfly html converter.\n\n"
"Output in html standard input.\n"

View File

@@ -1,7 +1,14 @@
#!/usr/bin/env python
import argparse
import os
import webbrowser
import argparse
try:
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
except ImportError:
from urlparse import urlparse, parse_qs, urlunparse
from urllib import urlencode
parser = argparse.ArgumentParser(description='Butterfly tab opener.')
parser.add_argument(
@@ -11,6 +18,10 @@ parser.add_argument(
help='Directory to open the new tab in. (Defaults to current)')
args = parser.parse_args()
url = '%swd%s' % (os.getenv('LOCATION', '/'), os.path.abspath(args.location))
url_parts = urlparse(os.getenv('LOCATION', '/'))
query = parse_qs(url_parts.query)
query['path'] = os.path.abspath(args.location)
url = urlunparse(url_parts._replace(path='')._replace(query=urlencode(query)))
if not webbrowser.open(url):
print('Unable to open browser, please go to %s' % url)

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
import argparse
import os
import webbrowser
import argparse
parser = argparse.ArgumentParser(description='Butterfly session opener.')
parser.add_argument(

View File

@@ -1,8 +1,9 @@
from contextlib import contextmanager
from butterfly.utils import ansi_colors as colors
import sys
import termios
import tty
from contextlib import contextmanager
from butterfly.utils import ansi_colors as colors # noqa: F401
@contextmanager
@@ -60,7 +61,7 @@ def geolocation():
rv = sys.stdin.read(1)
if rv != 'R':
loc += rv
except:
except Exception:
return
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

View File

@@ -133,9 +133,9 @@ class PAM():
if isinstance(service, str):
service = service.encode(encoding)
else:
if isinstance(username, unicode):
if isinstance(username, unicode): # noqa: F821
username = username.encode(encoding)
if isinstance(service, unicode):
if isinstance(service, unicode): # noqa: F821
service = service.encode(encoding)
if b'\x00' in username or b'\x00' in service:

View File

@@ -30,7 +30,8 @@ import tornado.options
import tornado.process
import tornado.web
import tornado.websocket
from butterfly import Route, __version__, url, utils
from butterfly import Route, url, utils
from butterfly.terminal import Terminal
@@ -138,6 +139,7 @@ class KeptAliveWebSocketHandler(tornado.websocket.WebSocketHandler):
def open(self, *args, **kwargs):
self.keepalive_timer = tornado.ioloop.PeriodicCallback(
self.send_ping, tornado.options.options.keepalive_interval * 1000)
self.keepalive_timer.start()
def send_ping(self):
t = int(time.time())

View File

@@ -23,7 +23,7 @@ $weights: (ExtraLight 100) (Light 300) (Regular 400) (Medium 500) (Semibold 600)
@font-face
font-family: "SourceCodePro"
src: url("/static/fonts/SourceCodePro-#{$weight_name}.otf") format("woff")
src: url("fonts/SourceCodePro-#{$weight_name}.otf") format("woff")
font-weight: nth($weight, 2)
body

View File

@@ -36,11 +36,12 @@ body
background-color: $active-bg
.line.extended
overflow-x: auto
overflow-x: overlay
cursor: zoom-in
background-image: linear-gradient(90deg, rgba(darken($bg, 3%), 0), 95%, darken($bg, 3%))
.extra
display: none
&:not(.expanded):hover
background-color: lighten($bg, 2%)
@@ -50,19 +51,9 @@ body
.extra
display: block
white-space: pre-line
white-space: pre-wrap
word-break: break-all
&::-webkit-scrollbar
background: rgba($scroll-bg, .1)
height: 0
&::-webkit-scrollbar-thumb
background: rgba($scroll-fg, .1)
&::-webkit-scrollbar-thumb:hover
background: rgba($scroll-fg-hover, .1)
&::-webkit-scrollbar
background: $scroll-bg
width: $scroll-width
@@ -102,5 +93,20 @@ body
padding: .5em
font-size: .75em
#input-view
position: fixed
z-index: 100
padding: 0
margin: 0
text-decoration: underline
#input-helper
position: fixed
z-index: -100
opacity: 0
white-space: nowrap
overflow: hidden
resize: none
.terminal
outline: none

View File

@@ -1,5 +1,5 @@
(function() {
var Popup, Selection, _set_theme_href, _theme, alt, cancel, clean_ansi, copy, ctrl, first, histSize, linkify, maybePack, nextLeaf, packSize, popup, previousLeaf, selection, setAlarm, tid, virtualInput, walk,
var Popup, Selection, _set_theme_href, _theme, alt, cancel, clean_ansi, copy, ctrl, escape, histSize, linkify, maybePack, nextLeaf, packSize, popup, previousLeaf, selection, setAlarm, tags, tid, walk,
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
clean_ansi = function(data) {
@@ -121,13 +121,14 @@
});
addEventListener('copy', copy = function(e) {
var data, end, j, len1, line, ref, sel;
var data, end, j, len, line, ref, sel;
document.getElementsByTagName('body')[0].contentEditable = false;
butterfly.bell("copied");
e.clipboardData.clearData();
sel = getSelection().toString().replace(/\u00A0/g, ' ').replace(/\u2007/g, ' ');
data = '';
ref = sel.split('\n');
for (j = 0, len1 = ref.length; j < len1; j++) {
for (j = 0, len = ref.length; j < len; j++) {
line = ref[j];
if (line.slice(-1) === '\u23CE') {
end = '';
@@ -143,6 +144,7 @@
addEventListener('paste', function(e) {
var data, send, size;
document.getElementsByTagName('body')[0].contentEditable = false;
butterfly.bell("pasted");
data = e.clipboardData.getData('text/plain');
data = data.replace(/\r\n/g, '\n').replace(/\n/g, '\r');
@@ -183,10 +185,10 @@
});
walk = function(node, callback) {
var child, j, len1, ref, results;
var child, j, len, ref, results;
ref = node.childNodes;
results = [];
for (j = 0, len1 = ref.length; j < len1; j++) {
for (j = 0, len = ref.length; j < len; j++) {
child = ref[j];
callback.call(child);
results.push(walk(child, callback));
@@ -202,12 +204,25 @@
return text.replace(urlPattern, '<a href="$&">$&</a>').replace(pseudoUrlPattern, '$1<a href="http://$2">$2</a>').replace(emailAddressPattern, '<a href="mailto:$&">$&</a>');
};
tags = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
};
escape = function(s) {
return s.replace(/[&<>]/g, function(tag) {
return tags[tag] || tag;
});
};
Terminal.on('change', function(line) {
return walk(line, function() {
var linkified, newNode;
var linkified, newNode, val;
if (this.nodeType === 3) {
linkified = linkify(this.nodeValue);
if (linkified !== this.nodeValue) {
val = this.nodeValue;
linkified = linkify(escape(val));
if (linkified !== val) {
newNode = document.createElement('span');
newNode.innerHTML = linkified;
this.parentElement.replaceChild(newNode, this);
@@ -217,11 +232,51 @@
});
});
ctrl = false;
alt = false;
addEventListener('touchstart', function(e) {
if (e.touches.length === 2) {
return ctrl = true;
} else if (e.touches.length === 3) {
ctrl = false;
return alt = true;
} else if (e.touches.length === 4) {
ctrl = true;
return alt = true;
}
});
window.mobileKeydown = function(e) {
var _altKey, _ctrlKey, _keyCode;
if (ctrl || alt) {
_ctrlKey = ctrl;
_altKey = alt;
_keyCode = e.keyCode;
if (e.keyCode >= 97 && e.keyCode <= 122) {
_keyCode -= 32;
}
e = new KeyboardEvent('keydown', {
ctrlKey: _ctrlKey,
altKey: _altKey,
keyCode: _keyCode
});
ctrl = alt = false;
setTimeout(function() {
return window.dispatchEvent(e);
}, 0);
return true;
} else {
return false;
}
};
document.addEventListener('keydown', function(e) {
if (!(e.altKey && e.keyCode === 79)) {
return true;
}
open(location.href);
open(location.origin);
return cancel(e);
});
@@ -276,9 +331,6 @@
Popup.prototype.open = function(html) {
this.el.innerHTML = html;
this.el.classList.remove('hidden');
if (typeof InstallTrigger !== "undefined") {
document.body.contentEditable = 'false';
}
addEventListener('click', this.bound_click_maybe_close);
return addEventListener('keydown', this.bound_key_maybe_close);
};
@@ -286,9 +338,6 @@
Popup.prototype.close = function() {
removeEventListener('click', this.bound_click_maybe_close);
removeEventListener('keydown', this.bound_key_maybe_close);
if (typeof InstallTrigger !== "undefined") {
document.body.contentEditable = 'true';
}
this.el.classList.add('hidden');
return this.el.innerHTML = '';
};
@@ -652,7 +701,7 @@
}
oReq = new XMLHttpRequest();
oReq.addEventListener('load', function() {
var j, len1, out, ref, response, session;
var j, len, out, ref, response, session;
response = JSON.parse(this.responseText);
out = '<div>';
out += '<h2>Session list</h2>';
@@ -661,7 +710,7 @@
} else {
out += '<ul>';
ref = response.sessions;
for (j = 0, len1 = ref.length; j < len1; j++) {
for (j = 0, len = ref.length; j < len; j++) {
session = ref[j];
out += "<li><a href=\"/session/" + session + "\">" + session + "</a></li>";
}
@@ -716,7 +765,7 @@
}
oReq = new XMLHttpRequest();
oReq.addEventListener('load', function() {
var builtin_themes, inner, j, k, len1, len2, option, response, theme, theme_list, themes, url;
var builtin_themes, inner, j, k, len, len1, option, response, theme, theme_list, themes, url;
response = JSON.parse(this.responseText);
builtin_themes = response.builtin_themes;
themes = response.themes;
@@ -733,7 +782,7 @@
option("/static/main.css", 'default');
if (themes.length) {
inner += '<optgroup label="Local themes">';
for (j = 0, len1 = themes.length; j < len1; j++) {
for (j = 0, len = themes.length; j < len; j++) {
theme = themes[j];
url = "/theme/" + theme + "/style.css";
option(url, theme);
@@ -741,7 +790,7 @@
inner += '</optgroup>';
}
inner += '<optgroup label="Built-in themes">';
for (k = 0, len2 = builtin_themes.length; k < len2; k++) {
for (k = 0, len1 = builtin_themes.length; k < len1; k++) {
theme = builtin_themes[k];
url = "/theme/" + theme + "/style.css";
option(url, theme.slice('built-in-'.length));
@@ -759,74 +808,6 @@
return cancel(e);
});
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
ctrl = false;
alt = false;
first = true;
virtualInput = document.createElement('input');
virtualInput.type = 'password';
virtualInput.style.position = 'fixed';
virtualInput.style.top = 0;
virtualInput.style.left = 0;
virtualInput.style.border = 'none';
virtualInput.style.outline = 'none';
virtualInput.style.opacity = 0;
virtualInput.value = '0';
document.body.appendChild(virtualInput);
virtualInput.addEventListener('blur', function() {
return setTimeout(((function(_this) {
return function() {
return _this.focus();
};
})(this)), 10);
});
addEventListener('click', function() {
return virtualInput.focus();
});
addEventListener('touchstart', function(e) {
if (e.touches.length === 2) {
return ctrl = true;
} else if (e.touches.length === 3) {
ctrl = false;
return alt = true;
} else if (e.touches.length === 4) {
ctrl = true;
return alt = true;
}
});
virtualInput.addEventListener('keydown', function(e) {
butterfly.keyDown(e);
return true;
});
virtualInput.addEventListener('input', function(e) {
var len;
len = this.value.length;
if (len === 0) {
e.keyCode = 8;
butterfly.keyDown(e);
this.value = '0';
return true;
}
e.keyCode = this.value.charAt(1).charCodeAt(0);
if ((ctrl || alt) && !first) {
e.keyCode = this.value.charAt(1).charCodeAt(0);
e.ctrlKey = ctrl;
e.altKey = alt;
if (e.keyCode >= 97 && e.keyCode <= 122) {
e.keyCode -= 32;
}
butterfly.keyDown(e);
this.value = '0';
ctrl = alt = false;
return true;
}
butterfly.keyPress(e);
first = false;
this.value = '0';
return true;
});
}
}).call(this);
//# sourceMappingURL=ext.js.map

File diff suppressed because one or more lines are too long

View File

@@ -70,37 +70,37 @@
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
@font-face {
font-family: "SourceCodePro";
src: url("/static/fonts/SourceCodePro-ExtraLight.otf") format("woff");
src: url("fonts/SourceCodePro-ExtraLight.otf") format("woff");
font-weight: 100; }
@font-face {
font-family: "SourceCodePro";
src: url("/static/fonts/SourceCodePro-Light.otf") format("woff");
src: url("fonts/SourceCodePro-Light.otf") format("woff");
font-weight: 300; }
@font-face {
font-family: "SourceCodePro";
src: url("/static/fonts/SourceCodePro-Regular.otf") format("woff");
src: url("fonts/SourceCodePro-Regular.otf") format("woff");
font-weight: 400; }
@font-face {
font-family: "SourceCodePro";
src: url("/static/fonts/SourceCodePro-Medium.otf") format("woff");
src: url("fonts/SourceCodePro-Medium.otf") format("woff");
font-weight: 500; }
@font-face {
font-family: "SourceCodePro";
src: url("/static/fonts/SourceCodePro-Semibold.otf") format("woff");
src: url("fonts/SourceCodePro-Semibold.otf") format("woff");
font-weight: 600; }
@font-face {
font-family: "SourceCodePro";
src: url("/static/fonts/SourceCodePro-Bold.otf") format("woff");
src: url("fonts/SourceCodePro-Bold.otf") format("woff");
font-weight: 700; }
@font-face {
font-family: "SourceCodePro";
src: url("/static/fonts/SourceCodePro-Black.otf") format("woff");
src: url("fonts/SourceCodePro-Black.otf") format("woff");
font-weight: 900; }
body {
@@ -2817,10 +2817,10 @@ body {
body .line.active {
background-color: transparent; }
body .line.extended {
overflow-x: auto;
overflow-x: overlay;
cursor: zoom-in;
background-image: linear-gradient(90deg, rgba(9, 8, 10, 0), 95%, #09080a); }
body .line.extended .extra {
display: none; }
body .line.extended:not(.expanded):hover {
background-color: #161419; }
body .line.extended.expanded {
@@ -2828,15 +2828,8 @@ body {
background-color: #09080a; }
body .line.extended.expanded .extra {
display: block;
white-space: pre-line;
white-space: pre-wrap;
word-break: break-all; }
body .line.extended::-webkit-scrollbar {
background: rgba(17, 15, 19, 0.1);
height: 0; }
body .line.extended::-webkit-scrollbar-thumb {
background: rgba(244, 234, 213, 0.1); }
body .line.extended::-webkit-scrollbar-thumb:hover {
background: rgba(244, 234, 213, 0.1); }
body::-webkit-scrollbar {
background: #110f13;
width: 0.75em; }
@@ -2868,6 +2861,19 @@ body {
display: block;
padding: .5em;
font-size: .75em; }
body #input-view {
position: fixed;
z-index: 100;
padding: 0;
margin: 0;
text-decoration: underline; }
body #input-helper {
position: fixed;
z-index: -100;
opacity: 0;
white-space: nowrap;
overflow: hidden;
resize: none; }
.terminal {
outline: none; }

View File

@@ -1,5 +1,5 @@
(function() {
var $, State, Terminal, cancel, cols, openTs, quit, rows, s, ws,
var $, State, Terminal, cancel, cols, isMobile, openTs, quit, rows, s, ws,
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
cols = rows = null;
@@ -24,11 +24,12 @@
wsUrl = 'ws://';
}
rootPath = document.body.getAttribute('data-root-path');
rootPath = rootPath.replace(/^\/+|\/+$/g, '');
if (rootPath.length) {
rootPath = "/" + rootPath;
}
wsUrl += document.location.host + rootPath;
path = location.pathname;
path = '/';
if (path.indexOf('/session') < 0) {
path += "session/" + (document.body.getAttribute('data-session-token'));
}
@@ -131,6 +132,10 @@
return false;
};
isMobile = function() {
return /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
};
s = 0;
State = {
@@ -170,10 +175,14 @@
this.body = this.document.getElementsByTagName('body')[0];
this.term = this.document.getElementById('term');
this.forceWidth = this.body.getAttribute('data-force-unicode-width') === 'yes';
this.inputHelper = this.document.getElementById('input-helper');
this.inputView = this.document.getElementById('input-view');
this.body.className = 'terminal focus';
this.body.style.outline = 'none';
this.body.setAttribute('tabindex', 0);
this.body.setAttribute('spellcheck', 'false');
this.inputHelper.setAttribute('tabindex', 0);
this.inputHelper.setAttribute('spellcheck', 'false');
div = this.document.createElement('div');
div.className = 'line';
this.term.appendChild(div);
@@ -185,11 +194,28 @@
this.termName = 'xterm';
this.cursorBlink = true;
this.cursorState = 0;
this.inComposition = false;
this.compositionText = "";
this.resetVars();
this.focus();
this.startBlink();
this.inputHelper.addEventListener('compositionstart', this.compositionStart.bind(this));
this.inputHelper.addEventListener('compositionupdate', this.compositionUpdate.bind(this));
this.inputHelper.addEventListener('compositionend', this.compositionEnd.bind(this));
addEventListener('keydown', this.keyDown.bind(this));
addEventListener('keypress', this.keyPress.bind(this));
addEventListener('keyup', (function(_this) {
return function() {
return _this.inputHelper.focus();
};
})(this));
if (isMobile()) {
addEventListener('click', (function(_this) {
return function() {
return _this.inputHelper.focus();
};
})(this));
}
addEventListener('focus', this.focus.bind(this));
addEventListener('blur', this.blur.bind(this));
addEventListener('resize', (function(_this) {
@@ -202,9 +228,6 @@
return _this.nativeScrollTo();
};
})(this), true);
if (typeof InstallTrigger !== "undefined") {
this.body.contentEditable = 'true';
}
this.initmouse();
addEventListener('load', (function(_this) {
return function() {
@@ -248,7 +271,8 @@
invisible: a.invisible,
italic: a.italic,
faint: a.faint,
crossed: a.crossed
crossed: a.crossed,
placeholder: false
};
};
@@ -256,12 +280,18 @@
return a.bg === b.bg && a.fg === b.fg && a.bold === b.bold && a.underline === b.underline && a.blink === b.blink && a.inverse === b.inverse && a.invisible === b.invisible && a.italic === b.italic && a.faint === b.faint && a.crossed === b.crossed;
};
Terminal.prototype.putChar = function(c) {
Terminal.prototype.putChar = function(c, placeholder) {
var newChar;
if (placeholder == null) {
placeholder = false;
}
newChar = this.cloneAttr(this.curAttr, c);
newChar.placeholder = placeholder;
if (this.insertMode) {
this.screen[this.y + this.shift].chars.splice(this.x, 0, this.cloneAttr(this.curAttr, c));
this.screen[this.y + this.shift].chars.splice(this.x, 0, newChar);
this.screen[this.y + this.shift].chars.pop();
} else {
this.screen[this.y + this.shift].chars[this.x] = this.cloneAttr(this.curAttr, c);
this.screen[this.y + this.shift].chars[this.x] = newChar;
}
return this.screen[this.y + this.shift].dirty = true;
};
@@ -297,7 +327,8 @@
invisible: false,
italic: false,
faint: false,
crossed: false
crossed: false,
placeholder: false
};
this.curAttr = this.cloneAttr(this.defAttr);
this.params = [];
@@ -342,6 +373,7 @@
this.showCursor();
this.body.classList.add('focus');
this.body.classList.remove('blur');
this.inputHelper.focus();
this.resize();
return this.scrollLock = old_sl;
};
@@ -581,8 +613,15 @@
return [classes, styles];
};
Terminal.prototype.isCJK = function(ch) {
return ("\u4e00" <= ch && ch <= "\u9fff") || ("\u3040" <= ch && ch <= "\u30ff") || ("\u31f0" <= ch && ch <= "\u31ff") || ("\u3190" <= ch && ch <= "\u319f") || ("\u3301" <= ch && ch <= "\u3356") || ("\uac00" <= ch && ch <= "\ud7ff") || ("\u3000" <= ch && ch <= "\u303f") || ("\uff00" <= ch && ch <= "\uff60") || ("\uffe0" <= ch && ch <= "\uffe6");
};
Terminal.prototype.charToDom = function(data, attr, cursor) {
var ch, char, classes, ref, styles;
if (data.placeholder) {
return;
}
if (data.html) {
return data.html;
}
@@ -621,12 +660,12 @@
default:
if (ch <= " ") {
char += "&nbsp;";
} else if (!this.forceWidth) {
} else if (!(this.forceWidth || this.isCJK(ch))) {
char += ch;
} else {
if (ch <= "~") {
char += ch;
} else if (("\uff00" < ch && ch < "\uffef")) {
} else if (this.isCJK(ch)) {
char += "<span style=\"display: inline-block; width: " + (2 * this.charSize.width) + "px\">" + ch + "</span>";
} else {
char += "<span style=\"display: inline-block; width: " + this.charSize.width + "px\">" + ch + "</span>";
@@ -733,6 +772,7 @@
dom = this.screenToDom(force);
this.writeDom(dom);
this.nativeScrollTo();
this.updateInputViews();
return this.emit('refresh');
};
@@ -912,12 +952,8 @@
}
this.putChar(ch);
this.x++;
if (this.forceWidth && ("\uff00" < ch && ch < "\uffef")) {
if (this.cols < 2 || this.x >= this.cols) {
this.putChar(" ");
break;
}
this.putChar(" ");
if (this.isCJK(ch)) {
this.putChar(" ", true);
this.x++;
}
}
@@ -1409,11 +1445,93 @@
return this.write(data + "\r\n");
};
Terminal.prototype.updateInputViews = function() {
var cursorPos;
cursorPos = this.cursor.getBoundingClientRect();
this.inputView.style['left'] = cursorPos.left + "px";
this.inputView.style['top'] = cursorPos.top + "px";
this.inputHelper.style['left'] = cursorPos.left + "px";
this.inputHelper.style['top'] = cursorPos.top + "px";
return this.inputHelper.value = "";
};
Terminal.prototype.compositionStart = function(ev) {
ev.preventDefault();
ev.stopPropagation();
this.updateInputViews();
this.inputView.className = "";
this.inputView.innerText = "";
this.cursor.style['visibility'] = "hidden";
this.inComposition = true;
this.compositionText = "";
return false;
};
Terminal.prototype.compositionUpdate = function(ev) {
ev.preventDefault();
ev.stopPropagation();
this.compositionText = ev.data;
this.inputView.innerText = this.compositionText;
return false;
};
Terminal.prototype.compositionEnd = function(ev) {
ev.preventDefault();
ev.stopPropagation();
this.finishComposition();
return false;
};
Terminal.prototype.finishComposition = function() {
this.inComposition = false;
this.showCursor();
this.inputHelper.value = "";
this.inputView.className = "hidden";
this.send(this.compositionText);
this.compositionText = "";
return this.inputHelper.focus();
};
Terminal.prototype.keyDown = function(ev) {
var key, ref;
if (this.inComposition) {
if (ev.keyCode === 229) {
return false;
} else if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) {
return false;
}
this.finishComposition();
}
if (ev.keyCode === 229) {
ev.preventDefault();
ev.stopPropagation();
setTimeout((function(_this) {
return function() {
var char, e, val;
if (!(_this.inComposition || _this.inputHelper.value.length > 1)) {
val = _this.inputHelper.value;
_this.inputHelper.value = "";
char = val.toUpperCase().charCodeAt(0);
if ((65 <= char && char <= 90)) {
e = new KeyboardEvent('keydown', {
keyCode: char
});
if (window.mobileKeydown(e)) {
return;
}
}
return _this.send(val);
}
};
})(this), 0);
return false;
}
if (ev.keyCode > 15 && ev.keyCode < 19) {
return true;
}
if (window.mobileKeydown(ev)) {
return true;
}
if (ev.keyCode === 19) {
this.body.classList.add('stopped');
this.out('\x03');
@@ -1424,6 +1542,7 @@
return true;
}
if ((ev.shiftKey && ev.ctrlKey) && ((ref = ev.keyCode) === 67 || ref === 86)) {
this.body.contentEditable = true;
return true;
}
if (ev.altKey && ev.keyCode === 90 && !this.skipNextKey) {

File diff suppressed because one or more lines are too long

View File

@@ -18,6 +18,10 @@
data-force-unicode-width="{{ 'yes' if options.force_unicode_width else 'no' }}"
data-root-path="{{ options.uri_root_path }}"
data-session-token={{ session }}>
<textarea id="input-helper">
</textarea>
<div id="input-view" class="hidden">
</div>
<div id="popup" class="hidden">
</div>
<script src="{{ static_url('html-sanitizer.js') }}"></script>

View File

@@ -19,7 +19,7 @@
! ! {{ colors.red if opts.unsecure else colors.green }}{{ butterfly.socket.remote_addr }}:{{ butterfly.socket.remote_port }}{{ colors.reset }}
For more information type: {{ colors.white }}$ {{ colors.green }}butterfly help{{ colors.reset }}
{% if opts.unsecure %}{{ colors.light_red + '\x1b[5m' }}/!\{{ colors.reset }} {{ colors.red }}This session is UNSECURE everyone can access you terminal at:
{% if opts.unsecure and not opts.i_hereby_declare_i_dont_want_any_security_whatsoever %}{{ colors.light_red + '\x1b[5m' }}/!\{{ colors.reset }} {{ colors.red }}This session is UNSECURE everyone can access you terminal at:
{{ uri }}
{% else %}You can share your session with the following uri:
{{ uri }}

View File

@@ -25,13 +25,15 @@ import string
import struct
import sys
import termios
from logging import getLogger
import tornado.ioloop
import tornado.options
import tornado.process
import tornado.web
import tornado.websocket
from logging import getLogger
from butterfly import utils, __version__
from butterfly import __version__, utils
log = getLogger('butterfly')
ioloop = tornado.ioloop.IOLoop.instance()
@@ -194,19 +196,19 @@ class Terminal(object):
tty, os.getpid(),
self.callee.name, self.uri)
if not tornado.options.options.unsecure or (
self.socket.local and
self.caller == self.callee and
server == self.callee
) and not tornado.options.options.login:
local_login = (
self.socket.local and self.caller == self.callee and
server == self.callee)
secure = not tornado.options.options.unsecure
force_login = tornado.options.options.login
ignore_security = (
tornado.options.options.
i_hereby_declare_i_dont_want_any_security_whatsoever)
if not force_login and (ignore_security or secure or local_login):
# User has been auth with ssl or is the same user as server
# or login is explicitly turned off
if (
not tornado.options.options.unsecure and not (
self.socket.local and
self.caller == self.callee and
server == self.callee
)):
if secure and not local_login:
# User is authed by ssl, setting groups
try:
os.initgroups(self.callee.name, self.callee.gid)
@@ -262,7 +264,7 @@ class Terminal(object):
args = ['/bin/su']
args.append('-l')
if sys.platform == 'linux' and tornado.options.options.shell:
if sys.platform.startswith('linux') and tornado.options.options.shell:
args.append('-s')
args.append(tornado.options.options.shell)
args.append(self.callee.name)

View File

@@ -18,13 +18,13 @@
import os
import pwd
import time
import sys
import struct
from logging import getLogger
from collections import namedtuple
import subprocess
import re
import struct
import subprocess
import sys
import time
from collections import namedtuple
from logging import getLogger
log = getLogger('butterfly')
@@ -168,7 +168,7 @@ def get_lsof_socket_line(addr, port):
# May want to make this into a dictionary in the future...
regex = "\w+\s+(?P<pid>\d+)\s+(?P<user>\w+).*\s" \
"(?P<laddr>.*?):(?P<lport>\d+)->(?P<raddr>.*?):(?P<rport>\d+)"
output = subprocess.check_output(['lsof', '-Pni'])
output = subprocess.check_output(['lsof', '-Pni']).decode('utf-8')
lines = output.split('\n')
for line in lines:
# Look for local address with peer port
@@ -294,11 +294,12 @@ def get_wtmp_file():
if os.path.exists(file):
return file
UTmp = namedtuple(
'UTmp',
['type', 'pid', 'line', 'id', 'user', 'host',
'exit0', 'exit1', 'session',
'sec', 'usec', 'addr0', 'addr1', 'addr2', 'addr3', 'unused'])
'UTmp',
['type', 'pid', 'line', 'id', 'user', 'host',
'exit0', 'exit1', 'session',
'sec', 'usec', 'addr0', 'addr1', 'addr2', 'addr3', 'unused'])
def utmp_line(id, type, pid, fd, user, host, ts):

View File

@@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
addEventListener 'copy', copy = (e) ->
document.getElementsByTagName('body')[0].contentEditable = false
butterfly.bell "copied"
e.clipboardData.clearData()
sel = getSelection().toString().replace(
@@ -35,6 +36,7 @@ addEventListener 'copy', copy = (e) ->
addEventListener 'paste', (e) ->
document.getElementsByTagName('body')[0].contentEditable = false
butterfly.bell "pasted"
data = e.clipboardData.getData 'text/plain'
data = data.replace(/\r\n/g, '\n').replace(/\n/g, '\r')

View File

@@ -14,11 +14,19 @@ linkify = (text) ->
.replace(pseudoUrlPattern, '$1<a href="http://$2">$2</a>')
.replace(emailAddressPattern, '<a href="mailto:$&">$&</a>')
tags =
'&': '&amp;'
'<': '&lt;'
'>': '&gt;'
escape = (s) -> s.replace(/[&<>]/g, (tag) -> tags[tag] or tag)
Terminal.on 'change', (line) ->
walk line, ->
if @nodeType is 3
linkified = linkify @nodeValue
if linkified isnt @nodeValue
val = @nodeValue
linkified = linkify escape(val)
if linkified isnt val
newNode = document.createElement('span')
newNode.innerHTML = linkified
@parentElement.replaceChild newNode, @

52
coffees/ext/mobile.coffee Normal file
View File

@@ -0,0 +1,52 @@
# *-* coding: utf-8 *-*
# This file is part of butterfly
#
# butterfly Copyright(C) 2015-2017 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/>.
ctrl = false
alt = false
addEventListener 'touchstart', (e) ->
if e.touches.length == 2
ctrl = true
else if e.touches.length == 3
ctrl = false
alt = true
else if e.touches.length == 4
ctrl = true
alt = true
# Dispatch a new event if the current event need to
# be modified with ctrlKey and altKey from touch events
# If so, this function will return true and dispatch the new event.
# The caller should return immediately upon receiving true.
window.mobileKeydown = (e) ->
if ctrl or alt
_ctrlKey = ctrl
_altKey = alt
_keyCode = e.keyCode
if e.keyCode >= 97 && e.keyCode <= 122
_keyCode -= 32
e = new KeyboardEvent 'keydown',
ctrlKey: _ctrlKey,
altKey: _altKey,
keyCode: _keyCode
ctrl = alt = false
setTimeout ->
window.dispatchEvent e
, 0
return true
else
return false

View File

@@ -1,4 +1,4 @@
document.addEventListener 'keydown', (e) ->
return true unless e.altKey and e.keyCode is 79
open(location.href)
open(location.origin)
cancel e

View File

@@ -9,10 +9,6 @@ class Popup
@el.innerHTML = html
@el.classList.remove 'hidden'
# ff glorious hack
if typeof InstallTrigger isnt "undefined"
document.body.contentEditable = 'false'
addEventListener 'click', @bound_click_maybe_close
addEventListener 'keydown', @bound_key_maybe_close
@@ -20,10 +16,6 @@ class Popup
removeEventListener 'click', @bound_click_maybe_close
removeEventListener 'keydown', @bound_key_maybe_close
# ff glorious hack
if typeof InstallTrigger isnt "undefined"
document.body.contentEditable = 'true'
@el.classList.add 'hidden'
@el.innerHTML = ''

View File

@@ -1,80 +0,0 @@
# *-* coding: utf-8 *-*
# This file is part of butterfly
#
# butterfly Copyright(C) 2015-2017 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/>.
if /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
.test navigator.userAgent
ctrl = false
alt = false
first = true
virtualInput = document.createElement 'input'
virtualInput.type = 'password'
virtualInput.style.position = 'fixed'
virtualInput.style.top = 0
virtualInput.style.left = 0
virtualInput.style.border = 'none'
virtualInput.style.outline = 'none'
virtualInput.style.opacity = 0
virtualInput.value = '0'
document.body.appendChild virtualInput
virtualInput.addEventListener 'blur', ->
setTimeout((=> @focus()), 10)
addEventListener 'click', ->
virtualInput.focus()
addEventListener 'touchstart', (e) ->
if e.touches.length == 2
ctrl = true
else if e.touches.length == 3
ctrl = false
alt = true
else if e.touches.length == 4
ctrl = true
alt = true
virtualInput.addEventListener 'keydown', (e) ->
butterfly.keyDown(e)
return true
virtualInput.addEventListener 'input', (e) ->
len = @value.length
if len == 0
e.keyCode = 8
butterfly.keyDown e
@value = '0'
return true
e.keyCode = @value.charAt(1).charCodeAt(0)
if (ctrl or alt) and not first
e.keyCode = @value.charAt(1).charCodeAt(0)
e.ctrlKey = ctrl
e.altKey = alt
if e.keyCode >= 97 && e.keyCode <= 122
e.keyCode -= 32
butterfly.keyDown e
@value = '0'
ctrl = alt = false
return true
butterfly.keyPress e
first = false
@value = '0'
true

View File

@@ -34,11 +34,12 @@ document.addEventListener 'DOMContentLoaded', ->
wsUrl = 'ws://'
rootPath = document.body.getAttribute('data-root-path')
rootPath = rootPath.replace(/^\/+|\/+$/g, '')
if rootPath.length
rootPath = "/#{rootPath}"
wsUrl += document.location.host + rootPath
path = location.pathname
path = '/'
if path.indexOf('/session') < 0
path += "session/#{document.body.getAttribute('data-session-token')}"

View File

@@ -34,6 +34,9 @@ cancel = (ev) ->
ev.cancelBubble = true
false
isMobile = ->
/iPhone|iPad|iPod|Android/i.test navigator.userAgent
s = 0
State =
normal: s++
@@ -67,11 +70,23 @@ class Terminal
@forceWidth = @body.getAttribute(
'data-force-unicode-width') is 'yes'
# A hidden textarea to capture all input events
# This allows the body to receive IME composition events
# without being `contentEditable`, which will mess up
# the layout
@inputHelper = @document.getElementById('input-helper')
# A simple div to take place of the IME input preview
# which is now hidden due to the textarea
@inputView = @document.getElementById('input-view')
# Main terminal element
@body.className = 'terminal focus'
@body.style.outline = 'none'
@body.setAttribute 'tabindex', 0
@body.setAttribute 'spellcheck', 'false'
@inputHelper.setAttribute 'tabindex', 0
@inputHelper.setAttribute 'spellcheck', 'false'
# Adding one line to compute char size
div = @document.createElement('div')
@@ -87,14 +102,29 @@ class Terminal
@termName = 'xterm'
@cursorBlink = true
@cursorState = 0
@inComposition = false
@compositionText = ""
@resetVars()
@focus()
@startBlink()
# IME Events should be registered to the textarea
# The textarea helps guiding the IME to pop up
# at the correct position
@inputHelper.addEventListener 'compositionstart',
@compositionStart.bind(@)
@inputHelper.addEventListener 'compositionupdate',
@compositionUpdate.bind(@)
@inputHelper.addEventListener 'compositionend',
@compositionEnd.bind(@)
addEventListener 'keydown', @keyDown.bind(@)
addEventListener 'keypress', @keyPress.bind(@)
# Always focus on the inputHelper textarea
addEventListener 'keyup', => @inputHelper.focus()
if isMobile()
addEventListener 'click', => @inputHelper.focus()
addEventListener 'focus', @focus.bind(@)
addEventListener 'blur', @blur.bind(@)
addEventListener 'resize', => @resize()
@@ -102,10 +132,6 @@ class Terminal
@nativeScrollTo()
, true
# # Horrible Firefox paste workaround
if typeof InstallTrigger isnt "undefined"
@body.contentEditable = 'true'
@initmouse()
addEventListener 'load', => @resize()
@emit 'load'
@@ -131,6 +157,7 @@ class Terminal
italic: a.italic
faint: a.faint
crossed: a.crossed
placeholder: false
equalAttr: (a, b) ->
# Not testing char
@@ -140,12 +167,14 @@ class Terminal
a.italic is b.italic and a.faint is b.faint and
a.crossed is b.crossed)
putChar: (c) ->
putChar: (c, placeholder = false) ->
newChar = @cloneAttr @curAttr, c
newChar.placeholder = placeholder
if @insertMode
@screen[@y + @shift].chars.splice(@x, 0, @cloneAttr @curAttr, c)
@screen[@y + @shift].chars.splice(@x, 0, newChar)
@screen[@y + @shift].chars.pop()
else
@screen[@y + @shift].chars[@x] = @cloneAttr @curAttr, c
@screen[@y + @shift].chars[@x] = newChar
@screen[@y + @shift].dirty = true
@@ -187,6 +216,7 @@ class Terminal
italic: false
faint: false
crossed: false
placeholder: false
@curAttr = @cloneAttr @defAttr
@params = []
@@ -222,6 +252,7 @@ class Terminal
@showCursor()
@body.classList.add('focus')
@body.classList.remove('blur')
@inputHelper.focus() # Always focus on the textarea
@resize()
@scrollLock = old_sl
@@ -450,7 +481,21 @@ class Terminal
[classes, styles]
# Fullwidth (CJK) character ranges
isCJK: (ch) ->
"\u4e00" <= ch <= "\u9fff" or # CJK Unified Ideographs
"\u3040" <= ch <= "\u30ff" or # Japanese Hiragana and Katakana
"\u31f0" <= ch <= "\u31ff" or # Japanese Katakana Phonetic Extensions
"\u3190" <= ch <= "\u319f" or # Japanese Kanbun symbols
"\u3301" <= ch <= "\u3356" or # Japanese compound characters Kumimoji (組文字)
"\uac00" <= ch <= "\ud7ff" or # Hangul precomposed syllables
"\u3000" <= ch <= "\u303f" or # CJK Punctuations and Symbols
"\uff00" <= ch <= "\uff60" or # Fullwidth forms
"\uffe0" <= ch <= "\uffe6" # Fullwidth forms
charToDom: (data, attr, cursor) ->
# Just do not render if we see any placeholder characters
return if data.placeholder
return data.html if data.html
attr = attr or @cloneAttr @defAttr
ch = data.ch
@@ -478,12 +523,13 @@ class Terminal
else
if ch <= " "
char += "&nbsp;"
else unless @forceWidth
# CJK characters should always be forced to be fullwidth
else unless @forceWidth or @isCJK ch
char += ch
else
if ch <= "~" # Ascii chars
char += ch
else if "\uff00" < ch < "\uffef"
else if @isCJK ch # CJK always fullwidth
char += "<span style=\"display: inline-block; width: #{
2 * @charSize.width}px\">#{ch}</span>"
else
@@ -544,6 +590,7 @@ class Terminal
dom = @screenToDom(force)
@writeDom dom
@nativeScrollTo()
@updateInputViews()
@emit 'refresh'
_cursorBlink: ->
@@ -692,12 +739,15 @@ class Terminal
@x = 0
@putChar ch
@x++
if @forceWidth and "\uff00" < ch < "\uffef"
if @cols < 2 or @x >= @cols
@putChar " "
break
@putChar " "
if @isCJK ch
# Add a dummy, placeholder character
# for double-width, CJK characters
# In order to fix counting of characters
# when calculating for remaining cols
# They are always considered to be
# @forceWidth because otherwise they
# do not render properly at all
@putChar " ", true
@x++
when State.escaped
@@ -1251,11 +1301,96 @@ class Terminal
writeln: (data) ->
@write "#{data}\r\n"
updateInputViews: ->
# Re-position the textarea and the preview box
# to the current position of the cursor
cursorPos = @cursor.getBoundingClientRect()
@inputView.style['left'] = cursorPos.left + "px"
@inputView.style['top'] = cursorPos.top + "px"
@inputHelper.style['left'] = cursorPos.left + "px"
@inputHelper.style['top'] = cursorPos.top + "px"
# Clear the textarea as often as possible
@inputHelper.value = ""
compositionStart: (ev) ->
ev.preventDefault()
ev.stopPropagation()
@updateInputViews()
# Show the preview box
@inputView.className = ""
@inputView.innerText = ""
# Hide the blinking cursor
@cursor.style['visibility'] = "hidden"
@inComposition = true
@compositionText = ""
return false
compositionUpdate: (ev) ->
ev.preventDefault()
ev.stopPropagation()
# Update the composition text
@compositionText = ev.data
@inputView.innerText = @compositionText
return false
compositionEnd: (ev) ->
ev.preventDefault()
ev.stopPropagation()
@finishComposition()
return false
finishComposition: ->
@inComposition = false
@showCursor()
@inputHelper.value = ""
@inputView.className = "hidden"
@send @compositionText
@compositionText = ""
# Force focus on the inputHelper
@inputHelper.focus()
keyDown: (ev) ->
if @inComposition
# Continue IME composition if the character is
# composition key or modifier key
if ev.keyCode is 229
return false
else if ev.keyCode is 16 || ev.keyCode is 17 || ev.keyCode is 18
return false
# Otherwise, if we receive a keyDown, abort the composition
@finishComposition()
if ev.keyCode is 229
ev.preventDefault()
ev.stopPropagation()
# If the composition key is sent while IME not active
# it means that some character have been input while IME
# enabled, i.e. punctuations
# in which case we just fetch it from the text area
setTimeout =>
unless @inComposition || @inputHelper.value.length > 1
val = @inputHelper.value
@inputHelper.value = "" # Clear the value immediately
char = val.toUpperCase().charCodeAt(0)
if 65 <= char <= 90
# If the character sent here is a letter
# allow it to be overridden on mobile
# Combinations like "Ctrl+C" won't work without this
# on Chrome for Android
e = new KeyboardEvent 'keydown', keyCode: char
return if window.mobileKeydown e
@send val
, 0
return false
# Key Resources:
# https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
# Don't handle modifiers alone
return true if ev.keyCode > 15 and ev.keyCode < 19
return true if window.mobileKeydown ev
if ev.keyCode is 19 # Pause break
@body.classList.add 'stopped'
@@ -1268,7 +1403,11 @@ class Terminal
return true if (ev.shiftKey or ev.ctrlKey) and ev.keyCode is 45
# Let the ctrl+shift+c, ctrl+shift+v go through to handle native copy paste
return true if (ev.shiftKey and ev.ctrlKey) and ev.keyCode in [67, 86]
if (ev.shiftKey and ev.ctrlKey) and ev.keyCode in [67, 86]
# Make the content temporarily ediatble, to allow the paste event
# to propagate (this does not work for the textarea if not set like this)
@body.contentEditable = true
return true
# Alt-z works as an escape to relay the following keys to the browser.
# usefull to trigger browser shortcuts, i.e.: Alt+Z F5 to reload

View File

@@ -20,6 +20,6 @@
"grunt-contrib-cssmin": "^1.0.1",
"grunt-contrib-uglify": "^1.0.1",
"grunt-contrib-watch": "^1.0.0",
"grunt-sass": "^1.2.0"
"grunt-sass": "^2.1.0"
}
}

View File

@@ -1,2 +1,7 @@
[bdist_wheel]
universal = 1
[tool:pytest]
flake8-ignore =
*.py E731 E402
butterfly/bin/help.py E501

View File

@@ -4,26 +4,31 @@
"""
Butterfly - A sleek web based terminal emulator
"""
import os
from setuptools import setup
__version__ = '3.0.1'
about = {}
with open(os.path.join(
os.path.dirname(__file__), "butterfly", "__about__.py")) as f:
exec(f.read(), about)
options = dict(
name="butterfly",
version=__version__,
description="A sleek web based terminal emulator",
long_description="See http://github.com/paradoxxxzero/butterfly",
author="Florian Mounier",
author_email="paradoxxx.zero@gmail.com",
url="http://github.com/paradoxxxzero/butterfly",
license="GPLv3",
setup(
name=about['__title__'],
version=about['__version__'],
description=about['__summary__'],
url=about['__uri__'],
author=about['__author__'],
author_email=about['__email__'],
license=about['__license__'],
platforms="Any",
scripts=['butterfly.server.py', 'scripts/butterfly', 'scripts/b'],
packages=['butterfly'],
install_requires=["tornado>=3.2", "pyOpenSSL"],
extras_require={
'themes': ["libsass"],
'systemd': ['tornado_systemd']
'systemd': ['tornado_systemd'],
'lint': ['pytest', 'pytest-flake8', 'pytest-isort']
},
package_data={
'butterfly': [
@@ -43,12 +48,10 @@ options = dict(
]
},
classifiers=[
"Development Status :: 4 - Beta",
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
"Topic :: Terminals"])
setup(**options)

1782
yarn.lock Normal file

File diff suppressed because it is too large Load Diff