mirror of
https://github.com/paradoxxxzero/butterfly.git
synced 2026-06-10 06:14:39 +00:00
Compare commits
142 Commits
2.0.0-beta
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da79ffe04b | ||
|
|
c348e1f285 | ||
|
|
91d52ed6ae | ||
|
|
06751c68f9 | ||
|
|
a9854e9136 | ||
|
|
039c730409 | ||
|
|
82676862ca | ||
|
|
5b6b61286d | ||
|
|
f32cb4d358 | ||
|
|
ad155f1f17 | ||
|
|
9e1045de9b | ||
|
|
db3d37f6fe | ||
|
|
611f2e30d6 | ||
|
|
1984e4b869 | ||
|
|
f58ea904b3 | ||
|
|
af0f4d20fe | ||
|
|
10b5ce3bcc | ||
|
|
a0287946d9 | ||
|
|
fbd71d55ef | ||
|
|
0ac8437387 | ||
|
|
866b56b682 | ||
|
|
4d87059872 | ||
|
|
5bbe456496 | ||
|
|
5b9cc257a8 | ||
|
|
34b6287e0c | ||
|
|
41ee5fb843 | ||
|
|
ae6b36fa89 | ||
|
|
cfda54a724 | ||
|
|
033169ab08 | ||
|
|
920c435b00 | ||
|
|
27e6aa8a5d | ||
|
|
92633f52ce | ||
|
|
f5f854964b | ||
|
|
55528fdf91 | ||
|
|
9eae13486e | ||
|
|
79bd074dae | ||
|
|
7b0ba2bfe7 | ||
|
|
db17b9d8ac | ||
|
|
b5de82bfcf | ||
|
|
13dbe0434c | ||
|
|
ef0057c23f | ||
|
|
6bc8e1438f | ||
|
|
8856ea9dc4 | ||
|
|
4edb2d269f | ||
|
|
272891470c | ||
|
|
574b3dc74b | ||
|
|
269dd2b618 | ||
|
|
0625e05cbb | ||
|
|
6b1101bc45 | ||
|
|
3e6d0b203f | ||
|
|
8189598dd6 | ||
|
|
4a8b5f2147 | ||
|
|
f9a1ff4dea | ||
|
|
96d88a5e91 | ||
|
|
bdc1c7a80d | ||
|
|
eacfdcd52f | ||
|
|
ed347e2bd0 | ||
|
|
3228e8c204 | ||
|
|
b9c991e3b6 | ||
|
|
8ad12c2379 | ||
|
|
2aa237ef12 | ||
|
|
40496eb9d1 | ||
|
|
ffd19b8162 | ||
|
|
6663568500 | ||
|
|
3a09c47ef0 | ||
|
|
41ab0f36ff | ||
|
|
70e00ac696 | ||
|
|
70369a0b32 | ||
|
|
8c20ffb943 | ||
|
|
729c768dc2 | ||
|
|
17f8c1d1c9 | ||
|
|
964fd07143 | ||
|
|
8553bbd0cb | ||
|
|
f494541652 | ||
|
|
dd6c917462 | ||
|
|
9e03e24764 | ||
|
|
6b5f3ac76f | ||
|
|
a36579bb12 | ||
|
|
e4ce69a967 | ||
|
|
b0e1f37cac | ||
|
|
da659b7526 | ||
|
|
08ecb4d0d2 | ||
|
|
3624962d3c | ||
|
|
b9f1727f1e | ||
|
|
5a7c4da0b1 | ||
|
|
fa2b9d2bee | ||
|
|
3bb6da1eae | ||
|
|
6c827206f7 | ||
|
|
fdeba5a5d4 | ||
|
|
d0eb37765a | ||
|
|
8dffb02980 | ||
|
|
15ebdf6907 | ||
|
|
6e29c702e3 | ||
|
|
c3ad2f342a | ||
|
|
7d7f05e164 | ||
|
|
64a8480938 | ||
|
|
0142ec0a16 | ||
|
|
97d435ce18 | ||
|
|
4b3a5e1ae6 | ||
|
|
9fcc156257 | ||
|
|
2887f6e25a | ||
|
|
ffe8945c09 | ||
|
|
a3e78112a6 | ||
|
|
e5eb7050e8 | ||
|
|
b72da2e4ef | ||
|
|
2d3bed2fef | ||
|
|
cc510500a5 | ||
|
|
1ec50810f9 | ||
|
|
524e578fca | ||
|
|
bce9f99b0b | ||
|
|
9bcc989149 | ||
|
|
1d324ed243 | ||
|
|
3c2bf35b09 | ||
|
|
fe258f44f8 | ||
|
|
1f9d263ad7 | ||
|
|
fe01ffb2b4 | ||
|
|
ac7e9bef8e | ||
|
|
503de38429 | ||
|
|
7ebb122221 | ||
|
|
ec25edb657 | ||
|
|
52714d81ab | ||
|
|
c048f1a4e6 | ||
|
|
c0e2d8959b | ||
|
|
5c054ca290 | ||
|
|
9168878d92 | ||
|
|
056fbc02b1 | ||
|
|
571f07946d | ||
|
|
e09bab810c | ||
|
|
efb019ed00 | ||
|
|
34d2711aa1 | ||
|
|
115190446b | ||
|
|
c8931c6135 | ||
|
|
ab7880779d | ||
|
|
f5724cc39d | ||
|
|
33d4051fca | ||
|
|
28ebf9d8a2 | ||
|
|
5714b97c77 | ||
|
|
a9c35d91f1 | ||
|
|
573b4f1c1b | ||
|
|
856aac2bcb | ||
|
|
e789622b7e | ||
|
|
84c4ff9414 |
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
.gitignore
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
README.md
|
||||
butterfly.png
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -7,3 +7,7 @@ node_modules/
|
||||
*.map
|
||||
sass/scss
|
||||
*.egg-info/
|
||||
build/
|
||||
.cache/
|
||||
.env*
|
||||
.pytest_cache
|
||||
|
||||
2
.isort.cfg
Normal file
2
.isort.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
multi_line_output=4
|
||||
46
CHANGELOG.md
Normal file
46
CHANGELOG.md
Normal 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
|
||||
24
Dockerfile
24
Dockerfile
@@ -1,18 +1,28 @@
|
||||
FROM ubuntu:14.04.1
|
||||
FROM ubuntu:16.04
|
||||
|
||||
RUN apt-get update -y
|
||||
RUN apt-get install -y python-setuptools python-dev build-essential libffi-dev libssl-dev
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y -q --no-install-recommends \
|
||||
build-essential \
|
||||
libffi-dev \
|
||||
libssl-dev \
|
||||
python-dev \
|
||||
python-setuptools \
|
||||
ca-certificates \
|
||||
&& easy_install pip \
|
||||
&& pip install --upgrade setuptools \
|
||||
&& apt-get clean \
|
||||
&& rm -r /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /opt
|
||||
ADD . /opt/app
|
||||
WORKDIR /opt/app
|
||||
|
||||
RUN python setup.py build
|
||||
RUN python setup.py install
|
||||
RUN python setup.py build \
|
||||
&& python setup.py install
|
||||
|
||||
ADD docker/run.sh /opt/run.sh
|
||||
RUN chmod 777 /opt/run.sh
|
||||
|
||||
EXPOSE 57575
|
||||
|
||||
CMD ["/opt/run.sh"]
|
||||
CMD ["butterfly.server.py", "--unsecure", "--host=0.0.0.0"]
|
||||
ENTRYPOINT ["docker/run.sh"]
|
||||
|
||||
@@ -36,7 +36,7 @@ module.exports = (grunt) ->
|
||||
|
||||
coffeelint:
|
||||
butterfly:
|
||||
'coffees/*.coffee'
|
||||
'coffees/**/*.coffee'
|
||||
|
||||
watch:
|
||||
options:
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
butterfly Copyright (C) 2015 Florian Mounier, Kozea
|
||||
butterfly Copyright(C) 2015-2017 Florian Mounier, Kozea
|
||||
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
|
||||
|
||||
37
Makefile
Normal file
37
Makefile
Normal 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
10
Makefile.config
Normal 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
|
||||
129
README.md
129
README.md
@@ -1,36 +1,83 @@
|
||||
# ƸӜƷ butterfly
|
||||
# ƸӜƷ butterfly 3.0
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## Description
|
||||
|
||||
Butterfly is a tornado web server written in python which powers a full featured web terminal.
|
||||
Butterfly is a xterm compatible terminal that runs in your browser.
|
||||
|
||||
The js part is heavily based on [term.js](https://github.com/chjj/term.js/) which is heavily based on [jslinux](http://bellard.org/jslinux/).
|
||||
|
||||
## Features
|
||||
|
||||
* xterm compatible (support a lot of unused features!)
|
||||
* Native browser scroll and search
|
||||
* 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!
|
||||
* 16,777,216 colors support!
|
||||
* Keyboard text selection!
|
||||
* Desktop notifications on terminal output!
|
||||
* Geolocation from browser!
|
||||
* May work on firefox too!
|
||||
|
||||
## Try it
|
||||
|
||||
```bash
|
||||
$ pip install butterfly
|
||||
$ butterfly.server.py
|
||||
``` bash
|
||||
$ pip install butterfly
|
||||
$ pip install butterfly[themes] # If you want to use themes
|
||||
$ pip install butterfly[systemd] # If you want to use systemd
|
||||
$ butterfly
|
||||
```
|
||||
|
||||
Then open [localhost:57575](http://localhost:57575) in your favorite browser and done.
|
||||
A new tab should appear in your browser. Then type
|
||||
|
||||
``` bash
|
||||
$ butterfly help
|
||||
```
|
||||
|
||||
To get an overview of butterfly features.
|
||||
|
||||
|
||||
## Run it as a server
|
||||
|
||||
``` bash
|
||||
$ butterfly.server.py --host=myhost --port=57575
|
||||
```
|
||||
|
||||
Or with login prompt
|
||||
|
||||
```bash
|
||||
$ butterfly.server.py --host=myhost --port=57575 --login
|
||||
```
|
||||
|
||||
Or with PAM authentication (ROOT required)
|
||||
|
||||
```bash
|
||||
# butterfly.server.py --host=myhost --port=57575 --login --pam_profile=sshd
|
||||
```
|
||||
|
||||
You can change `sshd` to your preferred PAM profile.
|
||||
|
||||
The first time it will ask you to generate the certificates (see: [here](http://paradoxxxzero.github.io/2014/03/21/butterfly-with-ssl-auth.html))
|
||||
|
||||
|
||||
## Run it with systemd (linux)
|
||||
|
||||
Systemd provides a way to automatically activate daemons when needed (socket activation):
|
||||
|
||||
```bash
|
||||
$ cd /etc/systemd/system
|
||||
# curl -O https://raw.githubusercontent.com/paradoxxxzero/butterfly/master/butterfly.service
|
||||
# curl -O https://raw.githubusercontent.com/paradoxxxzero/butterfly/master/butterfly.socket
|
||||
# systemctl enable butterfly.socket
|
||||
# systemctl start butterfly.socket
|
||||
``` bash
|
||||
$ cd /etc/systemd/system
|
||||
$ curl -O https://raw.githubusercontent.com/paradoxxxzero/butterfly/master/butterfly.service
|
||||
$ curl -O https://raw.githubusercontent.com/paradoxxxzero/butterfly/master/butterfly.socket
|
||||
$ systemctl enable butterfly.socket
|
||||
$ systemctl start butterfly.socket
|
||||
```
|
||||
|
||||
Don't forget to update the /etc/butterfly/butterfly.conf file with your server options (host, port, shell, ...) and to install butterfly with the [systemd] flag.
|
||||
|
||||
|
||||
## Contribute
|
||||
|
||||
and make the world better (or just butterfly).
|
||||
@@ -41,42 +88,54 @@ If you don't know what to do go to the github issues and pick one you like.
|
||||
|
||||
If you want to motivate me to continue working on this project you can tip me, see: http://paradoxxxzero.github.io/about/
|
||||
|
||||
The dev requirements are coffee script and compass for the client side.
|
||||
Run `python dev.py --debug --port=12345` and you are set (yes you can launch it from your regular butterfly instance)
|
||||
Client side development use [grunt](http://gruntjs.com/) and [bower](http://bower.io/).
|
||||
|
||||
## Credits
|
||||
|
||||
The js part is based on [term.js](https://github.com/chjj/term.js/) which is based on [jslinux](http://bellard.org/jslinux/).
|
||||
## Author
|
||||
|
||||
[Florian Mounier](http://paradoxxxzero.github.io/)
|
||||
|
||||
|
||||
## License
|
||||
|
||||
```
|
||||
butterfly Copyright (C) 2015 Florian Mounier
|
||||
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 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.
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
```
|
||||
|
||||
## Docker Usage
|
||||
## Docker
|
||||
There is a docker repository created for this project that is set to automatically rebuild when there is a push
|
||||
into this repository: https://registry.hub.docker.com/u/garland/butterfly/
|
||||
|
||||
### Starting
|
||||
### Example usage
|
||||
|
||||
docker run \
|
||||
--env PASSWORD=password \
|
||||
--env PORT=57575 \
|
||||
-p 57575:57575 \
|
||||
-d garland/butterfly
|
||||
Starting with login and password
|
||||
|
||||
``` bash
|
||||
docker run --env PASSWORD=password -d garland/butterfly --login
|
||||
```
|
||||
|
||||
Starting with no password
|
||||
|
||||
``` bash
|
||||
docker run -d -p 57575:57575 garland/butterfly
|
||||
```
|
||||
|
||||
Starting with a different port
|
||||
|
||||
``` bash
|
||||
docker run -d -p 12345:12345 garland/butterfly --port=12345
|
||||
```
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2015 Florian Mounier
|
||||
# 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
|
||||
@@ -20,7 +20,11 @@
|
||||
import tornado.options
|
||||
import tornado.ioloop
|
||||
import tornado.httpserver
|
||||
import tornado_systemd
|
||||
try:
|
||||
from tornado_systemd import SystemdHTTPServer as HTTPServer
|
||||
except ImportError:
|
||||
from tornado.httpserver import HTTPServer
|
||||
|
||||
import logging
|
||||
import webbrowser
|
||||
import uuid
|
||||
@@ -40,6 +44,9 @@ 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)")
|
||||
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")
|
||||
@@ -48,8 +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("login", default=True,
|
||||
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.")
|
||||
tornado.options.define("force_unicode_width",
|
||||
default=False,
|
||||
help="Force all unicode characters to the same width."
|
||||
@@ -64,6 +81,10 @@ tornado.options.define("generate_current_user_pkcs", default=False,
|
||||
tornado.options.define("generate_user_pkcs", default='',
|
||||
help="Generate user pfx for client authentication "
|
||||
"(Must be root to create for another user)")
|
||||
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')
|
||||
@@ -77,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:
|
||||
shutil.copy(
|
||||
os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'butterfly',
|
||||
'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.")
|
||||
@@ -104,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',
|
||||
@@ -120,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)
|
||||
@@ -128,6 +156,7 @@ if not os.path.exists(options.ssl_dir):
|
||||
def to_abs(file):
|
||||
return os.path.join(options.ssl_dir, file)
|
||||
|
||||
|
||||
ca, ca_key, cert, cert_key, pkcs12 = map(to_abs, [
|
||||
'butterfly_ca.crt', 'butterfly_ca.key',
|
||||
'butterfly_%s.crt', 'butterfly_%s.key',
|
||||
@@ -153,6 +182,10 @@ 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
|
||||
print('Generating certificates for %s (change it with --host)\n' % host)
|
||||
@@ -162,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)
|
||||
@@ -169,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))
|
||||
@@ -182,7 +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
|
||||
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
|
||||
@@ -232,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)
|
||||
@@ -295,11 +361,10 @@ else:
|
||||
from butterfly import application
|
||||
application.butterfly_dir = butterfly_dir
|
||||
log.info('Starting server')
|
||||
http_server = tornado_systemd.SystemdHTTPServer(
|
||||
application, ssl_options=ssl_opts)
|
||||
http_server = HTTPServer(application, ssl_options=ssl_opts)
|
||||
http_server.listen(port, address=host)
|
||||
|
||||
if http_server.systemd:
|
||||
if getattr(http_server, 'systemd', False):
|
||||
os.environ.pop('LISTEN_PID')
|
||||
os.environ.pop('LISTEN_FDS')
|
||||
|
||||
@@ -310,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
15
butterfly/__about__.py
Normal 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__'
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2015 Florian Mounier
|
||||
# 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
|
||||
@@ -14,8 +14,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/>.
|
||||
__version__ = '2.0.0-beta2'
|
||||
|
||||
from .__about__ import * # noqa: F401,F403
|
||||
|
||||
import os
|
||||
import tornado.web
|
||||
@@ -23,6 +22,7 @@ import tornado.options
|
||||
import tornado.web
|
||||
from logging import getLogger
|
||||
|
||||
|
||||
log = getLogger('butterfly')
|
||||
|
||||
|
||||
@@ -31,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
|
||||
|
||||
|
||||
@@ -46,13 +51,18 @@ class Route(tornado.web.RequestHandler):
|
||||
@property
|
||||
def builtin_themes_dir(self):
|
||||
return os.path.join(
|
||||
os.path.dirname(__file__), 'themes')
|
||||
os.path.dirname(__file__), 'themes')
|
||||
|
||||
@property
|
||||
def themes_dir(self):
|
||||
return os.path.join(
|
||||
self.application.butterfly_dir, 'themes')
|
||||
|
||||
@property
|
||||
def local_js_dir(self):
|
||||
return os.path.join(
|
||||
self.application.butterfly_dir, 'js')
|
||||
|
||||
def get_theme_dir(self, theme):
|
||||
if theme.startswith('built-in-'):
|
||||
return os.path.join(
|
||||
@@ -66,7 +76,10 @@ if hasattr(tornado.options.options, 'debug'):
|
||||
application = tornado.web.Application(
|
||||
static_path=os.path.join(os.path.dirname(__file__), "static"),
|
||||
template_path=os.path.join(os.path.dirname(__file__), "templates"),
|
||||
debug=tornado.options.options.debug
|
||||
debug=tornado.options.options.debug,
|
||||
static_url_prefix='%s/static/' % (
|
||||
'/%s' % tornado.options.options.uri_root_path.strip('/')
|
||||
if tornado.options.options.uri_root_path else '')
|
||||
)
|
||||
|
||||
import butterfly.routes
|
||||
import butterfly.routes # noqa: F401
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/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('\x1bP;HTML|')
|
||||
print(calendar_table)
|
||||
print('\x1bP')
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,9 +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 base64
|
||||
import shutil
|
||||
|
||||
print(ansi_colors.white + "Welcome to the butterfly help." + ansi_colors.reset)
|
||||
path = os.getenv('BUTTERFLY_PATH')
|
||||
@@ -30,13 +32,14 @@ Butterfly is a xterm compliant terminal built with python and javascript.
|
||||
|
||||
|
||||
{title}Butterfly programs:{reset}
|
||||
{strong}b : {reset}Alias for {strong}butterfly{reset} executable. Takes a comand in parameter or launch a butterfly server for one shot use (if outside butterfly).
|
||||
{strong}b cat : {reset}A wrapper around cat allowing to display images as <img> instead of binary.
|
||||
{strong}b open : {reset}Open a new terminal at specified location.
|
||||
{strong}b session : {reset}Open or rattach a butterfly session. Multiplexing is supported.
|
||||
{strong}b colors : {reset}Test the terminal colors (16, 256 and 16777216 colors)
|
||||
{strong}b hr : {reset}Put a html hr. This is a test for html output.
|
||||
{strong}b calendar : {reset}Display current month using html. This is also a test for html output.
|
||||
{strong}b : {reset}Alias for {strong}butterfly{reset} executable. Takes a comand in parameter or launch a butterfly server for one shot use (if outside butterfly).
|
||||
{strong}b cat : {reset}A wrapper around cat allowing to display images as <img> instead of binary.
|
||||
{strong}b open : {reset}Open a new terminal at specified location.
|
||||
{strong}b session : {reset}Open or rattach a butterfly session. Multiplexing is supported.
|
||||
{strong}b colors : {reset}Test the terminal colors (16, 256 and 16777216 colors)
|
||||
{strong}b html : {reset}Output in html standard input.
|
||||
|
||||
For more butterfly programs check out: https://github.com/paradoxxxzero/butterfly-demos
|
||||
|
||||
|
||||
{title}Styling butterfly:{reset}
|
||||
@@ -55,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=shutil.get_terminal_size()[0] - 31,
|
||||
rcol=int(subprocess.check_output(['stty', 'size']).split()[1]) - 31,
|
||||
main=os.path.normpath(os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'../sass/'))))
|
||||
os.path.abspath(os.path.dirname(butterfly.__file__)), 'sass'))))
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
sys.stdout.write('\x1bP;HTML|<hr />\x1bP')
|
||||
sys.stdout.flush()
|
||||
@@ -1,8 +1,19 @@
|
||||
#!/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"
|
||||
"Example: $ echo \"<b>Bold</b>\" | b html",
|
||||
formatter_class=argparse.RawTextHelpFormatter)
|
||||
|
||||
parser.parse_known_args()
|
||||
|
||||
|
||||
with html():
|
||||
for line in fileinput.input():
|
||||
sys.stdout.write(line)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
w = sys.stdout.write
|
||||
print('Image injection test')
|
||||
injection = 'R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" onload="alert(\'pwnd\')" /><img src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
|
||||
w('\x1bP;IMAGE|image/gif;%s' % injection)
|
||||
w('\x1bP')
|
||||
|
||||
|
||||
print('HTML script execution test')
|
||||
w('\x1bP;HTML|<img src="https://imgs.xkcd.com/comics/hack.png" onload="alert(\'pwnd\')" />')
|
||||
w('\x1bP')
|
||||
@@ -1,5 +1,9 @@
|
||||
from contextlib import contextmanager
|
||||
import sys
|
||||
import termios
|
||||
import tty
|
||||
from contextlib import contextmanager
|
||||
|
||||
from butterfly.utils import ansi_colors as colors # noqa: F401
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -34,9 +38,33 @@ def text():
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def sass():
|
||||
sys.stdout.write('\x1bP;SASS|')
|
||||
yield
|
||||
sys.stdout.write('\x1bP')
|
||||
def geolocation():
|
||||
sys.stdout.write('\x1b[?99n')
|
||||
sys.stdout.flush()
|
||||
|
||||
fd = sys.stdin.fileno()
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
rv = sys.stdin.read(1)
|
||||
if rv != '\x1b':
|
||||
raise
|
||||
rv = sys.stdin.read(1)
|
||||
if rv != '[':
|
||||
raise
|
||||
rv = sys.stdin.read(1)
|
||||
if rv != '?':
|
||||
raise
|
||||
|
||||
loc = ''
|
||||
while rv != 'R':
|
||||
rv = sys.stdin.read(1)
|
||||
if rv != 'R':
|
||||
loc += rv
|
||||
except Exception:
|
||||
return
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
if not loc or ';' not in loc:
|
||||
return
|
||||
return tuple(map(float, loc.split(';')))
|
||||
|
||||
192
butterfly/pam.py
Normal file
192
butterfly/pam.py
Normal file
@@ -0,0 +1,192 @@
|
||||
# (c) 2007 Chris AtLee <chris@atlee.ca>
|
||||
# Licensed under the MIT license:
|
||||
# http://www.opensource.org/licenses/mit-license.php
|
||||
#
|
||||
# Original author: Chris AtLee
|
||||
#
|
||||
# Modified by David Ford, 2011-12-6
|
||||
# added py3 support and encoding
|
||||
# added pam_end
|
||||
# added pam_setcred to reset credentials after seeing Leon Walker's remarks
|
||||
# added byref as well
|
||||
# use readline to prestuff the getuser input
|
||||
# Modified by Peter Cai, 2017-02-10
|
||||
# interactive login for Butterfly
|
||||
|
||||
'''
|
||||
PAM module for python
|
||||
Provides an authenticate function that will allow the caller to authenticate
|
||||
a user against the Pluggable Authentication Modules (PAM) on the system.
|
||||
Implemented using ctypes, so no compilation is necessary.
|
||||
'''
|
||||
|
||||
import os
|
||||
import sys
|
||||
from ctypes import (
|
||||
CDLL, CFUNCTYPE, POINTER, Structure, byref, c_char_p, c_int, c_size_t,
|
||||
c_void_p)
|
||||
from ctypes.util import find_library
|
||||
|
||||
|
||||
class PamHandle(Structure):
|
||||
"""wrapper class for pam_handle_t pointer"""
|
||||
_fields_ = [("handle", c_void_p)]
|
||||
|
||||
def __init__(self):
|
||||
Structure.__init__(self)
|
||||
self.handle = 0
|
||||
|
||||
|
||||
class PamMessage(Structure):
|
||||
"""wrapper class for pam_message structure"""
|
||||
_fields_ = [("msg_style", c_int), ("msg", c_char_p)]
|
||||
|
||||
def __repr__(self):
|
||||
return "<PamMessage %i '%s'>" % (self.msg_style, self.msg)
|
||||
|
||||
|
||||
class PamResponse(Structure):
|
||||
"""wrapper class for pam_response structure"""
|
||||
_fields_ = [("resp", c_char_p), ("resp_retcode", c_int)]
|
||||
|
||||
def __repr__(self):
|
||||
return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp)
|
||||
|
||||
|
||||
conv_func = CFUNCTYPE(
|
||||
c_int, c_int, POINTER(POINTER(PamMessage)),
|
||||
POINTER(POINTER(PamResponse)), c_void_p)
|
||||
|
||||
|
||||
class PamConv(Structure):
|
||||
"""wrapper class for pam_conv structure"""
|
||||
_fields_ = [("conv", conv_func), ("appdata_ptr", c_void_p)]
|
||||
|
||||
|
||||
# Various constants
|
||||
PAM_PROMPT_ECHO_OFF = 1
|
||||
PAM_PROMPT_ECHO_ON = 2
|
||||
PAM_ERROR_MSG = 3
|
||||
PAM_TEXT_INFO = 4
|
||||
PAM_REINITIALIZE_CRED = 8
|
||||
|
||||
libc = CDLL(find_library("c"))
|
||||
libpam = CDLL(find_library("pam"))
|
||||
libpam_misc = CDLL(find_library("pam_misc"))
|
||||
|
||||
calloc = libc.calloc
|
||||
calloc.restype = c_void_p
|
||||
calloc.argtypes = [c_size_t, c_size_t]
|
||||
|
||||
pam_end = libpam.pam_end
|
||||
pam_end.restype = c_int
|
||||
pam_end.argtypes = [PamHandle, c_int]
|
||||
|
||||
pam_start = libpam.pam_start
|
||||
pam_start.restype = c_int
|
||||
pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv), POINTER(PamHandle)]
|
||||
|
||||
pam_setcred = libpam.pam_setcred
|
||||
pam_setcred.restype = c_int
|
||||
pam_setcred.argtypes = [PamHandle, c_int]
|
||||
|
||||
pam_strerror = libpam.pam_strerror
|
||||
pam_strerror.restype = c_char_p
|
||||
pam_strerror.argtypes = [PamHandle, c_int]
|
||||
|
||||
pam_authenticate = libpam.pam_authenticate
|
||||
pam_authenticate.restype = c_int
|
||||
pam_authenticate.argtypes = [PamHandle, c_int]
|
||||
|
||||
misc_conv = libpam_misc.misc_conv
|
||||
|
||||
|
||||
class PAM():
|
||||
code = 0
|
||||
reason = None
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def authenticate(
|
||||
self, username,
|
||||
service='login', encoding='utf-8', resetcreds=True):
|
||||
"""PAM authentication through standard input for the given service.
|
||||
Returns True for success, or False for failure.
|
||||
self.code (integer) and self.reason (string) are always stored
|
||||
and may be referenced for the reason why authentication failed.
|
||||
0/'Success' will be stored for success.
|
||||
Python3 expects bytes() for ctypes inputs. This function will make
|
||||
necessary conversions using the supplied encoding.
|
||||
Inputs:
|
||||
username: username to authenticate
|
||||
service: PAM service to authenticate against, defaults to 'login'
|
||||
Returns:
|
||||
success: True
|
||||
failure: False
|
||||
"""
|
||||
|
||||
# python3 ctypes prefers bytes
|
||||
if sys.version_info >= (3,):
|
||||
if isinstance(username, str):
|
||||
username = username.encode(encoding)
|
||||
if isinstance(service, str):
|
||||
service = service.encode(encoding)
|
||||
else:
|
||||
if isinstance(username, unicode): # noqa: F821
|
||||
username = username.encode(encoding)
|
||||
if isinstance(service, unicode): # noqa: F821
|
||||
service = service.encode(encoding)
|
||||
|
||||
if b'\x00' in username or b'\x00' in service:
|
||||
self.code = 4 # PAM_SYSTEM_ERR in Linux-PAM
|
||||
self.reason = 'strings may not contain NUL'
|
||||
return False
|
||||
|
||||
handle = PamHandle()
|
||||
conv = PamConv(conv_func(misc_conv), 0)
|
||||
retval = pam_start(service, username, byref(conv), byref(handle))
|
||||
|
||||
if retval != 0:
|
||||
# This is not an authentication error,
|
||||
# something has gone wrong starting up PAM
|
||||
self.code = retval
|
||||
self.reason = "pam_start() failed"
|
||||
return False
|
||||
|
||||
retval = pam_authenticate(handle, 0)
|
||||
auth_success = retval == 0
|
||||
|
||||
if auth_success and resetcreds:
|
||||
retval = pam_setcred(handle, PAM_REINITIALIZE_CRED)
|
||||
|
||||
# store information to inform the caller why we failed
|
||||
self.code = retval
|
||||
self.reason = pam_strerror(handle, retval)
|
||||
if sys.version_info >= (3,):
|
||||
self.reason = self.reason.decode(encoding)
|
||||
|
||||
pam_end(handle, retval)
|
||||
|
||||
return auth_success
|
||||
|
||||
|
||||
def login_prompt(username, profile, env):
|
||||
pam = PAM()
|
||||
|
||||
success = pam.authenticate(username, profile)
|
||||
print('{} {}'.format(pam.code, pam.reason))
|
||||
|
||||
if success:
|
||||
su = '/usr/bin/su'
|
||||
if not os.path.exists(su):
|
||||
su = '/bin/su'
|
||||
os.execvpe(su, [su, '-l', username], env)
|
||||
return success
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if login_prompt(sys.argv[1], sys.argv[2], os.environ):
|
||||
exit(0)
|
||||
else:
|
||||
exit(1)
|
||||
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2015 Florian Mounier
|
||||
# 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
|
||||
@@ -16,16 +16,22 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from mimetypes import guess_type
|
||||
from uuid import uuid4
|
||||
|
||||
import tornado.escape
|
||||
import tornado.options
|
||||
import tornado.process
|
||||
import tornado.escape
|
||||
import tornado.web
|
||||
import tornado.websocket
|
||||
from mimetypes import guess_type
|
||||
from collections import defaultdict
|
||||
from butterfly import url, Route, utils, __version__
|
||||
|
||||
from butterfly import Route, url, utils
|
||||
from butterfly.terminal import Terminal
|
||||
|
||||
|
||||
@@ -35,12 +41,15 @@ def u(s):
|
||||
return s
|
||||
|
||||
|
||||
@url(r'/(?:user/(.+))?/?(?:wd/(.+))?/?(?:session/(.+))?')
|
||||
@url(r'/(?:session/(?P<session>[^/]+)/?)?')
|
||||
class Index(Route):
|
||||
def get(self, user, path, session):
|
||||
def get(self, session):
|
||||
user = self.request.query_arguments.get(
|
||||
'user', [b''])[0].decode('utf-8')
|
||||
if not tornado.options.options.unsecure and user:
|
||||
raise tornado.web.HTTPError(400)
|
||||
return self.render('index.html')
|
||||
return self.render(
|
||||
'index.html', session=session or str(uuid4()))
|
||||
|
||||
|
||||
@url(r'/theme/([^/]+)/style.css')
|
||||
@@ -89,7 +98,6 @@ class Theme(Route):
|
||||
|
||||
@url(r'/theme/([^/]+)/(.+)')
|
||||
class ThemeStatic(Route):
|
||||
|
||||
def get(self, theme, name):
|
||||
if '..' in name:
|
||||
raise tornado.web.HTTPError(403)
|
||||
@@ -101,7 +109,19 @@ class ThemeStatic(Route):
|
||||
raise tornado.web.HTTPError(403)
|
||||
|
||||
if os.path.exists(fn):
|
||||
self.set_header("Content-Type", guess_type(fn)[0])
|
||||
type = guess_type(fn)[0]
|
||||
if type is None:
|
||||
# Fallback if there's no mimetypes on the system
|
||||
type = {
|
||||
'png': 'image/png',
|
||||
'jpg': 'image/jpeg',
|
||||
'jpeg': 'image/jpeg',
|
||||
'gif': 'image/gif',
|
||||
'woff': 'application/font-woff',
|
||||
'ttf': 'application/x-font-ttf'
|
||||
}.get(fn.split('.')[-1], 'text/plain')
|
||||
|
||||
self.set_header("Content-Type", type)
|
||||
with open(fn, 'rb') as s:
|
||||
while True:
|
||||
data = s.read(16384)
|
||||
@@ -113,51 +133,48 @@ class ThemeStatic(Route):
|
||||
raise tornado.web.HTTPError(404)
|
||||
|
||||
|
||||
@url(r'/ws'
|
||||
'(?:/user/(?P<user>[^/]+))?/?'
|
||||
'(?:session/(?P<session>[^/]+))?/?'
|
||||
'(?:/wd/(?P<path>.+))?')
|
||||
class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
|
||||
session_history_size = 50000
|
||||
# List of websockets per session per user
|
||||
# dict: user -> dict: session -> [TermWebSocket]
|
||||
sessions = defaultdict(dict)
|
||||
class KeptAliveWebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||
keepalive_timer = None
|
||||
|
||||
# Terminal for session per user
|
||||
# dict: user -> dict: session -> Terminal
|
||||
terminals = defaultdict(dict)
|
||||
def open(self, *args, **kwargs):
|
||||
self.keepalive_timer = tornado.ioloop.PeriodicCallback(
|
||||
self.send_ping, tornado.options.options.keepalive_interval * 1000)
|
||||
self.keepalive_timer.start()
|
||||
|
||||
# All terminals sockets for systemd socket deactivation
|
||||
sockets = []
|
||||
def send_ping(self):
|
||||
t = int(time.time())
|
||||
frame = struct.pack('<I', t) # A ping frame based on time
|
||||
self.log.info("Sending ping frame %s" % t)
|
||||
try:
|
||||
self.ping(frame)
|
||||
except tornado.websocket.WebSocketClosedError:
|
||||
self.keepalive_timer.stop()
|
||||
|
||||
# Session history
|
||||
history = {}
|
||||
def on_close(self):
|
||||
if self.keepalive_timer is not None:
|
||||
self.keepalive_timer.stop()
|
||||
|
||||
def open(self, user, path, session):
|
||||
|
||||
@url(r'/ctl/session/(?P<session>[^/]+)')
|
||||
class TermCtlWebSocket(Route, KeptAliveWebSocketHandler):
|
||||
sessions = defaultdict(list)
|
||||
sessions_secure_users = {}
|
||||
|
||||
def open(self, session):
|
||||
super(TermCtlWebSocket, self).open(session)
|
||||
self.session = session
|
||||
self.closed = False
|
||||
self.secure_user = None
|
||||
|
||||
# Prevent cross domain
|
||||
if self.request.headers['Origin'] not in (
|
||||
'http://%s' % self.request.headers['Host'],
|
||||
'https://%s' % self.request.headers['Host']):
|
||||
self.log.warning(
|
||||
'Unauthorized connection attempt: from : %s to: %s' % (
|
||||
self.request.headers['Origin'],
|
||||
self.request.headers['Host']))
|
||||
self.close()
|
||||
return
|
||||
|
||||
TermWebSocket.sockets.append(self)
|
||||
|
||||
self.log.info('Websocket opened %r' % self)
|
||||
self.set_nodelay(True)
|
||||
self.log.info('Websocket /ctl opened %r' % self)
|
||||
|
||||
def create_terminal(self):
|
||||
socket = utils.Socket(self.ws_connection.stream.socket)
|
||||
opts = tornado.options.options
|
||||
user = self.request.query_arguments.get(
|
||||
'user', [b''])[0].decode('utf-8')
|
||||
path = self.request.query_arguments.get(
|
||||
'path', [b''])[0].decode('utf-8')
|
||||
secure_user = None
|
||||
|
||||
if not opts.unsecure:
|
||||
if not tornado.options.options.unsecure:
|
||||
user = utils.parse_cert(
|
||||
self.ws_connection.stream.socket.getpeercert())
|
||||
assert user, 'No user in certificate'
|
||||
@@ -167,137 +184,148 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
|
||||
raise Exception('Invalid user in certificate')
|
||||
|
||||
# Certificate authed user
|
||||
self.secure_user = user
|
||||
secure_user = user
|
||||
|
||||
elif socket.local and socket.user == utils.User():
|
||||
elif socket.local and socket.user == utils.User() and not user:
|
||||
# Local to local returning browser user
|
||||
self.secure_user = socket.user
|
||||
secure_user = socket.user
|
||||
elif user:
|
||||
try:
|
||||
user = utils.User(name=user)
|
||||
except LookupError:
|
||||
raise Exception('Invalid user')
|
||||
|
||||
# Handling terminal session
|
||||
if session:
|
||||
if session in self.user_sessions:
|
||||
# Session already here, registering websocket
|
||||
self.user_sessions[session].append(self)
|
||||
self.write_message('S' + TermWebSocket.history[session])
|
||||
# And returning, we don't want another terminal
|
||||
return
|
||||
if secure_user:
|
||||
user = secure_user
|
||||
if self.session in self.sessions and self.session in (
|
||||
self.sessions_secure_users):
|
||||
if user.name != self.sessions_secure_users[self.session]:
|
||||
# Restrict to authorized users
|
||||
raise tornado.web.HTTPError(403)
|
||||
else:
|
||||
# New session, opening terminal
|
||||
self.user_sessions[session] = [self]
|
||||
TermWebSocket.history[session] = ''
|
||||
self.sessions_secure_users[self.session] = user.name
|
||||
|
||||
self.sessions[self.session].append(self)
|
||||
|
||||
terminal = Terminal.sessions.get(self.session)
|
||||
# Handling terminal session
|
||||
if terminal:
|
||||
TermWebSocket.last.write_message(terminal.history)
|
||||
# And returning, we don't want another terminal
|
||||
return
|
||||
|
||||
# New session, opening terminal
|
||||
terminal = Terminal(
|
||||
user, path, session, socket,
|
||||
self.request.headers['Host'], self.render_string, self.write)
|
||||
user, path, self.session, socket,
|
||||
self.request.full_url().replace('/ctl/', '/'), self.render_string,
|
||||
TermWebSocket.broadcast)
|
||||
|
||||
terminal.pty()
|
||||
|
||||
if session:
|
||||
if not self.secure_user:
|
||||
self.log.error(
|
||||
'No terminal session without secure authenticated user'
|
||||
'or local user.')
|
||||
self._terminal = terminal
|
||||
self.session = None
|
||||
else:
|
||||
self.log.info('Openning session %s for secure user %r' % (
|
||||
session, self.secure_user))
|
||||
self.user_terminals[session] = terminal
|
||||
else:
|
||||
self._terminal = terminal
|
||||
|
||||
@property
|
||||
def user_sessions(self):
|
||||
"""Return the dict session of socket lists"""
|
||||
if not self.secure_user:
|
||||
return {}
|
||||
return TermWebSocket.sessions[self.secure_user.name]
|
||||
|
||||
@property
|
||||
def user_terminals(self):
|
||||
"""Return the dict session of terminal"""
|
||||
if not self.secure_user:
|
||||
return {}
|
||||
return TermWebSocket.terminals[self.secure_user.name]
|
||||
self.log.info('Openning session %s for secure user %r' % (
|
||||
self.session, user))
|
||||
|
||||
@classmethod
|
||||
def close_all(cls, session, user):
|
||||
terminals = TermWebSocket.terminals.get(user.name)
|
||||
del terminals[session]
|
||||
|
||||
sessions = TermWebSocket.sessions.get(user.name)
|
||||
if sessions:
|
||||
sockets = sessions[session]
|
||||
for socket in sockets[:]:
|
||||
socket.on_close()
|
||||
socket.close()
|
||||
del sessions[session]
|
||||
|
||||
@classmethod
|
||||
def broadcast(cls, session, message, user, emitter=None):
|
||||
if message[0] == 'S':
|
||||
cls.history[session] += message[1:]
|
||||
if len(cls.history[session]) > cls.session_history_size:
|
||||
cls.history[session] = cls.history[session][
|
||||
-cls.session_history_size:]
|
||||
sessions = cls.sessions.get(user.name, [])
|
||||
|
||||
for session in sessions[session]:
|
||||
def broadcast(cls, session, message, emitter=None):
|
||||
for wsocket in cls.sessions[session]:
|
||||
try:
|
||||
if session != emitter:
|
||||
session.write_message(message)
|
||||
if wsocket != emitter:
|
||||
wsocket.write_message(message)
|
||||
except Exception:
|
||||
session.log.exception('Error on broadcast')
|
||||
session.close()
|
||||
|
||||
def write(self, message):
|
||||
if self.session and self.secure_user:
|
||||
if message is None:
|
||||
TermWebSocket.close_all(self.session, self.secure_user)
|
||||
else:
|
||||
TermWebSocket.broadcast(
|
||||
self.session, message, self.secure_user)
|
||||
else:
|
||||
if message is None:
|
||||
self.on_close()
|
||||
self.close()
|
||||
else:
|
||||
self.write_message(message)
|
||||
wsocket.log.exception('Error on broadcast')
|
||||
wsocket.close()
|
||||
|
||||
def on_message(self, message):
|
||||
if self.session and self.secure_user:
|
||||
term = self.user_terminals.get(self.session)
|
||||
term and term.write(message)
|
||||
if message[0] == 'R':
|
||||
# Broadcast resize
|
||||
TermWebSocket.broadcast(
|
||||
self.session, message, self.secure_user, self)
|
||||
cmd = json.loads(message)
|
||||
if cmd['cmd'] == 'open':
|
||||
self.create_terminal()
|
||||
else:
|
||||
self._terminal.write(message)
|
||||
try:
|
||||
Terminal.sessions[self.session].ctl(cmd)
|
||||
except Exception:
|
||||
# FF strange bug
|
||||
pass
|
||||
self.broadcast(self.session, message, self)
|
||||
|
||||
def on_close(self):
|
||||
super(TermCtlWebSocket, self).on_close()
|
||||
if self.closed:
|
||||
return
|
||||
self.closed = True
|
||||
self.log.info('Websocket closed %r' % self)
|
||||
TermWebSocket.sockets.remove(self)
|
||||
if self.session:
|
||||
self.user_sessions[self.session].remove(self)
|
||||
elif hasattr(self, '_terminal'):
|
||||
self._terminal.close()
|
||||
else:
|
||||
self.log.error(
|
||||
'Socket with neither session nor terminal %r' % self)
|
||||
opts = tornado.options.options
|
||||
if opts.one_shot or (
|
||||
self.application.systemd and
|
||||
not len(TermWebSocket.sockets) and
|
||||
self.log.info('Websocket /ctl closed %r' % self)
|
||||
if self in self.sessions[self.session]:
|
||||
self.sessions[self.session].remove(self)
|
||||
|
||||
if tornado.options.options.one_shot or (
|
||||
getattr(self.application, 'systemd', False) and
|
||||
not sum([
|
||||
len(sessions)
|
||||
for user, sessions in TermWebSocket.terminals.items()])):
|
||||
len(wsockets)
|
||||
for session, wsockets in self.sessions.items()])):
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
@url(r'/ws/session/(?P<session>[^/]+)')
|
||||
class TermWebSocket(Route, KeptAliveWebSocketHandler):
|
||||
# List of websockets per session
|
||||
sessions = defaultdict(list)
|
||||
|
||||
# Last is kept for session shared history send
|
||||
last = None
|
||||
|
||||
# Session history
|
||||
history = {}
|
||||
|
||||
def open(self, session):
|
||||
super(TermWebSocket, self).open(session)
|
||||
self.set_nodelay(True)
|
||||
self.session = session
|
||||
self.closed = False
|
||||
self.sessions[session].append(self)
|
||||
self.__class__.last = self
|
||||
self.log.info('Websocket /ws opened %r' % self)
|
||||
|
||||
@classmethod
|
||||
def close_session(cls, session):
|
||||
wsockets = (cls.sessions.get(session, []) +
|
||||
TermCtlWebSocket.sessions.get(session, []))
|
||||
for wsocket in wsockets:
|
||||
wsocket.on_close()
|
||||
|
||||
wsocket.close()
|
||||
|
||||
if session in cls.sessions:
|
||||
del cls.sessions[session]
|
||||
if session in TermCtlWebSocket.sessions_secure_users:
|
||||
del TermCtlWebSocket.sessions_secure_users[session]
|
||||
if session in TermCtlWebSocket.sessions:
|
||||
del TermCtlWebSocket.sessions[session]
|
||||
|
||||
@classmethod
|
||||
def broadcast(cls, session, message, emitter=None):
|
||||
if message is None:
|
||||
cls.close_session(session)
|
||||
return
|
||||
|
||||
wsockets = cls.sessions.get(session)
|
||||
for wsocket in wsockets:
|
||||
try:
|
||||
if wsocket != emitter:
|
||||
wsocket.write_message(message)
|
||||
except Exception:
|
||||
wsocket.log.exception('Error on broadcast')
|
||||
wsocket.close()
|
||||
|
||||
def on_message(self, message):
|
||||
Terminal.sessions[self.session].write(message)
|
||||
|
||||
def on_close(self):
|
||||
super(TermWebSocket, self).on_close()
|
||||
if self.closed:
|
||||
return
|
||||
self.closed = True
|
||||
self.log.info('Websocket /ws closed %r' % self)
|
||||
self.sessions[self.session].remove(self)
|
||||
|
||||
|
||||
@url(r'/sessions/list.json')
|
||||
class SessionsList(Route):
|
||||
"""Get the theme list"""
|
||||
@@ -315,7 +343,7 @@ class SessionsList(Route):
|
||||
self.set_header('Content-Type', 'application/json')
|
||||
self.write(tornado.escape.json_encode({
|
||||
'sessions': sorted(
|
||||
TermWebSocket.sessions.get(user, [])),
|
||||
TermWebSocket.sessions),
|
||||
'user': user
|
||||
}))
|
||||
|
||||
@@ -340,7 +368,7 @@ class ThemesList(Route):
|
||||
'built-in-%s' % theme
|
||||
for theme in os.listdir(self.builtin_themes_dir)
|
||||
if os.path.isdir(os.path.join(
|
||||
self.builtin_themes_dir, theme)) and
|
||||
self.builtin_themes_dir, theme)) and
|
||||
not theme.startswith('.')]
|
||||
else:
|
||||
builtin_themes = []
|
||||
@@ -351,3 +379,22 @@ class ThemesList(Route):
|
||||
'builtin_themes': sorted(builtin_themes),
|
||||
'dir': self.themes_dir
|
||||
}))
|
||||
|
||||
|
||||
@url('/local.js')
|
||||
class LocalJsStatic(Route):
|
||||
def get(self):
|
||||
self.set_header("Content-Type", 'application/javascript')
|
||||
if os.path.exists(self.local_js_dir):
|
||||
for fn in os.listdir(self.local_js_dir):
|
||||
if not fn.endswith('.js'):
|
||||
continue
|
||||
with open(os.path.join(self.local_js_dir, fn), 'rb') as s:
|
||||
while True:
|
||||
data = s.read(16384)
|
||||
if data:
|
||||
self.write(data)
|
||||
else:
|
||||
self.write(';')
|
||||
break
|
||||
self.finish()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
@@ -22,13 +22,38 @@ html, body
|
||||
color: $fg
|
||||
|
||||
body
|
||||
padding-bottom: .5em
|
||||
white-space: nowrap
|
||||
overflow-x: hidden
|
||||
overflow-y: scroll
|
||||
a
|
||||
text-decoration: underline rgba($fg, .2)
|
||||
transition: text-decoration-color 500ms
|
||||
&:hover
|
||||
text-decoration: underline
|
||||
|
||||
.line.active
|
||||
background-color: $active-bg
|
||||
|
||||
.line.extended
|
||||
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%)
|
||||
|
||||
&.expanded
|
||||
cursor: zoom-out
|
||||
background-color: darken($bg, 3%)
|
||||
|
||||
.extra
|
||||
display: block
|
||||
white-space: pre-wrap
|
||||
word-break: break-all
|
||||
|
||||
&::-webkit-scrollbar
|
||||
background: $scroll-bg
|
||||
width: $scroll-width
|
||||
@@ -68,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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
@@ -16,27 +16,22 @@
|
||||
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
|
||||
body
|
||||
transition: 200ms
|
||||
transition: filter 200ms
|
||||
transform-origin: bottom
|
||||
|
||||
&.bell
|
||||
-webkit-filter: blur(2px)
|
||||
filter: blur(2px)
|
||||
|
||||
&.skip
|
||||
-webkit-filter: sepia(1)
|
||||
filter: sepia(1)
|
||||
|
||||
&.selection
|
||||
-webkit-filter: saturate(2)
|
||||
filter: saturate(2)
|
||||
|
||||
&.alarm
|
||||
-webkit-filter: hue-rotate(150deg)
|
||||
filter: hue-rotate(150deg)
|
||||
|
||||
&.dead
|
||||
-webkit-filter: grayscale(1)
|
||||
filter: grayscale(1)
|
||||
|
||||
&:after
|
||||
@@ -55,7 +50,6 @@ body
|
||||
font-weight: 900
|
||||
|
||||
&.stopped
|
||||
-webkit-filter: brightness(50%)
|
||||
filter: brightness(50%)
|
||||
|
||||
&.locked
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
(function() {
|
||||
var Popup, Selection, _set_theme_href, _theme, alt, cancel, clean_ansi, copy, ctrl, first, nextLeaf, popup, previousLeaf, selection, setAlarm, virtualInput,
|
||||
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) {
|
||||
@@ -62,8 +62,7 @@
|
||||
var alarm;
|
||||
alarm = function(data) {
|
||||
var message, note, notif;
|
||||
message = clean_ansi(data.data);
|
||||
console.log(message);
|
||||
message = clean_ansi(data.data.slice(1));
|
||||
if (cond !== null && !cond.test(message)) {
|
||||
return;
|
||||
}
|
||||
@@ -81,9 +80,9 @@
|
||||
} else {
|
||||
alert(note + '\n' + message);
|
||||
}
|
||||
return butterfly.ws.removeEventListener('message', alarm);
|
||||
return butterfly.ws.shell.removeEventListener('message', alarm);
|
||||
};
|
||||
butterfly.ws.addEventListener('message', alarm);
|
||||
butterfly.ws.shell.addEventListener('message', alarm);
|
||||
return butterfly.body.classList.add('alarm');
|
||||
};
|
||||
|
||||
@@ -122,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,11 +143,20 @@
|
||||
});
|
||||
|
||||
addEventListener('paste', function(e) {
|
||||
var data;
|
||||
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');
|
||||
butterfly.send(data);
|
||||
size = 1024;
|
||||
send = function() {
|
||||
butterfly.send(data.substring(0, size));
|
||||
data = data.substring(size);
|
||||
if (data.length) {
|
||||
return setTimeout(send, 25);
|
||||
}
|
||||
};
|
||||
send();
|
||||
return e.preventDefault();
|
||||
});
|
||||
|
||||
@@ -157,14 +166,161 @@
|
||||
}
|
||||
});
|
||||
|
||||
Terminal.on('change', function(line) {
|
||||
if (indexOf.call(line.classList, 'extended') >= 0) {
|
||||
return line.addEventListener('click', (function(line) {
|
||||
return function() {
|
||||
var after, before;
|
||||
if (indexOf.call(line.classList, 'expanded') >= 0) {
|
||||
return line.classList.remove('expanded');
|
||||
} else {
|
||||
before = line.getBoundingClientRect().height;
|
||||
line.classList.add('expanded');
|
||||
after = line.getBoundingClientRect().height;
|
||||
return document.body.scrollTop += after - before;
|
||||
}
|
||||
};
|
||||
})(line));
|
||||
}
|
||||
});
|
||||
|
||||
walk = function(node, callback) {
|
||||
var child, j, len, ref, results;
|
||||
ref = node.childNodes;
|
||||
results = [];
|
||||
for (j = 0, len = ref.length; j < len; j++) {
|
||||
child = ref[j];
|
||||
callback.call(child);
|
||||
results.push(walk(child, callback));
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
linkify = function(text) {
|
||||
var emailAddressPattern, pseudoUrlPattern, urlPattern;
|
||||
urlPattern = /\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim;
|
||||
pseudoUrlPattern = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
|
||||
emailAddressPattern = /[\w.]+@[a-zA-Z_-]+?(?:\.[a-zA-Z]{2,6})+/gim;
|
||||
return text.replace(urlPattern, '<a href="$&">$&</a>').replace(pseudoUrlPattern, '$1<a href="http://$2">$2</a>').replace(emailAddressPattern, '<a href="mailto:$&">$&</a>');
|
||||
};
|
||||
|
||||
tags = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>'
|
||||
};
|
||||
|
||||
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, val;
|
||||
if (this.nodeType === 3) {
|
||||
val = this.nodeValue;
|
||||
linkified = linkify(escape(val));
|
||||
if (linkified !== val) {
|
||||
newNode = document.createElement('span');
|
||||
newNode.innerHTML = linkified;
|
||||
this.parentElement.replaceChild(newNode, this);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
tid = null;
|
||||
|
||||
packSize = 1000;
|
||||
|
||||
histSize = 100;
|
||||
|
||||
maybePack = function() {
|
||||
var hist, i, j, pack, packfrag, ref;
|
||||
if (!(butterfly.term.childElementCount > packSize + butterfly.rows)) {
|
||||
return;
|
||||
}
|
||||
hist = document.getElementById('packed');
|
||||
packfrag = document.createDocumentFragment('fragment');
|
||||
for (i = j = 0, ref = packSize; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) {
|
||||
packfrag.appendChild(butterfly.term.firstChild);
|
||||
}
|
||||
pack = document.createElement('div');
|
||||
pack.classList.add('pack');
|
||||
pack.appendChild(packfrag);
|
||||
hist.appendChild(pack);
|
||||
if (hist.childElementCount > histSize) {
|
||||
hist.firstChild.remove();
|
||||
}
|
||||
return tid = setTimeout(maybePack);
|
||||
};
|
||||
|
||||
Terminal.on('refresh', function() {
|
||||
if (tid) {
|
||||
clearTimeout(tid);
|
||||
}
|
||||
return maybePack();
|
||||
});
|
||||
|
||||
Terminal.on('clear', function() {
|
||||
var hist, newHist;
|
||||
newHist = document.createElement('div');
|
||||
newHist.id = 'packed';
|
||||
hist = document.getElementById('packed');
|
||||
return butterfly.body.replaceChild(newHist, hist);
|
||||
});
|
||||
|
||||
Popup = (function() {
|
||||
function Popup() {
|
||||
this.el = document.getElementById('popup');
|
||||
@@ -175,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);
|
||||
};
|
||||
@@ -185,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 = '';
|
||||
};
|
||||
@@ -320,13 +470,13 @@
|
||||
|
||||
Selection.prototype.go = function(n) {
|
||||
var index;
|
||||
index = butterfly.children.indexOf(this.startLine) + n;
|
||||
if (!((0 <= index && index < butterfly.children.length))) {
|
||||
index = Array.prototype.indexOf.call(butterfly.term.childNodes, this.startLine) + n;
|
||||
if (!((0 <= index && index < butterfly.term.childElementCount))) {
|
||||
return;
|
||||
}
|
||||
while (!butterfly.children[index].textContent.match(/\S/)) {
|
||||
while (!butterfly.term.childNodes[index].textContent.match(/\S/)) {
|
||||
index += n;
|
||||
if (!((0 <= index && index < butterfly.children.length))) {
|
||||
if (!((0 <= index && index < butterfly.term.childElementCount))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -344,7 +494,7 @@
|
||||
|
||||
Selection.prototype.selectLine = function(index) {
|
||||
var line, lineEnd, lineStart;
|
||||
line = butterfly.children[index];
|
||||
line = butterfly.term.childNodes[index];
|
||||
lineStart = {
|
||||
node: line.firstChild,
|
||||
offset: 0
|
||||
@@ -444,7 +594,7 @@
|
||||
})();
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
var ref, ref1;
|
||||
var r, ref, ref1;
|
||||
if (ref = e.keyCode, indexOf.call([16, 17, 18, 19], ref) >= 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -481,8 +631,9 @@
|
||||
return cancel(e);
|
||||
}
|
||||
if (!selection && e.ctrlKey && e.shiftKey && e.keyCode === 38) {
|
||||
r = Math.max(butterfly.term.childElementCount - butterfly.rows, 0);
|
||||
selection = new Selection();
|
||||
selection.selectLine(butterfly.y - 1);
|
||||
selection.selectLine(r + butterfly.y - 1);
|
||||
selection.apply();
|
||||
return cancel(e);
|
||||
}
|
||||
@@ -550,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>';
|
||||
@@ -559,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>";
|
||||
}
|
||||
@@ -614,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;
|
||||
@@ -631,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);
|
||||
@@ -639,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));
|
||||
@@ -657,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
|
||||
|
||||
4
butterfly/static/ext.min.js
vendored
4
butterfly/static/ext.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
@@ -17,7 +17,7 @@
|
||||
/* These a the default variables */
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
@@ -40,7 +40,7 @@
|
||||
/* These are all imported files */
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
@@ -57,7 +57,7 @@
|
||||
/* You can change this file to import any webfont: */
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
@@ -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 {
|
||||
@@ -111,7 +111,7 @@ body {
|
||||
/* You can comment / uncomment the following to enable/disable terminal effects. */
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
@@ -123,22 +123,17 @@ body {
|
||||
/* You should have received a copy of the GNU General Public License */
|
||||
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
body {
|
||||
transition: 200ms;
|
||||
transition: filter 200ms;
|
||||
transform-origin: bottom; }
|
||||
body.bell {
|
||||
-webkit-filter: blur(2px);
|
||||
filter: blur(2px); }
|
||||
body.skip {
|
||||
-webkit-filter: sepia(1);
|
||||
filter: sepia(1); }
|
||||
body.selection {
|
||||
-webkit-filter: saturate(2);
|
||||
filter: saturate(2); }
|
||||
body.alarm {
|
||||
-webkit-filter: hue-rotate(150deg);
|
||||
filter: hue-rotate(150deg); }
|
||||
body.dead {
|
||||
-webkit-filter: grayscale(1);
|
||||
filter: grayscale(1); }
|
||||
body.dead:after {
|
||||
content: "CLOSED";
|
||||
@@ -155,7 +150,6 @@ body {
|
||||
opacity: .2;
|
||||
font-weight: 900; }
|
||||
body.stopped {
|
||||
-webkit-filter: brightness(50%);
|
||||
filter: brightness(50%); }
|
||||
body.locked::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 0, 0, 0.7); }
|
||||
@@ -169,7 +163,7 @@ body {
|
||||
/* @import all_fx */
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
@@ -183,7 +177,7 @@ body {
|
||||
/* The color theme is defined in this one: */
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
@@ -357,7 +351,7 @@ body {
|
||||
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
@@ -2792,7 +2786,7 @@ body {
|
||||
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
@@ -2810,12 +2804,32 @@ html, body {
|
||||
color: #f4ead5; }
|
||||
|
||||
body {
|
||||
padding-bottom: .5em;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
/* Pop ups */ }
|
||||
body a {
|
||||
text-decoration: underline rgba(244, 234, 213, 0.2);
|
||||
transition: text-decoration-color 500ms; }
|
||||
body a:hover {
|
||||
text-decoration: underline; }
|
||||
body .line.active {
|
||||
background-color: transparent; }
|
||||
body .line.extended {
|
||||
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 {
|
||||
cursor: zoom-out;
|
||||
background-color: #09080a; }
|
||||
body .line.extended.expanded .extra {
|
||||
display: block;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all; }
|
||||
body::-webkit-scrollbar {
|
||||
background: #110f13;
|
||||
width: 0.75em; }
|
||||
@@ -2838,7 +2852,7 @@ body {
|
||||
color: #f4ead5;
|
||||
font-size: 1em; }
|
||||
body #popup form h2, body #popup > div h2 {
|
||||
margin: 0 0.5em 0.5em 0.5em; }
|
||||
margin: 0 .5em .5em .5em; }
|
||||
body #popup form select, body #popup > div select {
|
||||
min-width: 300px;
|
||||
padding: .5em;
|
||||
@@ -2847,13 +2861,26 @@ 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; }
|
||||
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
@@ -2872,7 +2899,7 @@ body {
|
||||
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* 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 */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
6
butterfly/static/main.min.js
vendored
6
butterfly/static/main.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
{% from tornado.options import options %}
|
||||
{% from uuid import uuid4 %}
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
@@ -14,7 +15,13 @@
|
||||
</head>
|
||||
|
||||
<body spellcheck="false"
|
||||
data-force-unicode-width="{{ 'yes' if options.force_unicode_width else 'no' }}">
|
||||
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>
|
||||
@@ -22,5 +29,8 @@
|
||||
'' if options.unminified else 'min.')) }}"></script>
|
||||
<script src="{{ static_url('ext.%sjs' % (
|
||||
'' if options.unminified else 'min.')) }}"></script>
|
||||
<script src="{{ reverse_url('LocalJsStatic') }}"></script>
|
||||
<div id="packed"></div>
|
||||
<div id="term"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -18,5 +18,9 @@
|
||||
{{ colors.white }} Y Y {{ colors.light_white }}From:{{ colors.white }}
|
||||
! ! {{ 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
|
||||
|
||||
For more information type: {{ colors.white }}$ {{ colors.green }}butterfly help{{ colors.reset }}
|
||||
{% 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 }}
|
||||
{% end %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2015 Florian Mounier
|
||||
# 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
|
||||
@@ -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()
|
||||
@@ -47,10 +49,16 @@ except NameError:
|
||||
|
||||
|
||||
class Terminal(object):
|
||||
def __init__(self, user, path, session, socket, host, render_string, send):
|
||||
self.host = host
|
||||
sessions = {}
|
||||
|
||||
def __init__(self, user, path, session, socket, uri, render_string,
|
||||
broadcast):
|
||||
self.sessions[session] = self
|
||||
self.history_size = 50000
|
||||
self.history = ''
|
||||
self.uri = uri
|
||||
self.session = session
|
||||
self.send = send
|
||||
self.broadcast = broadcast
|
||||
self.fd = None
|
||||
self.closed = False
|
||||
self.socket = socket
|
||||
@@ -67,7 +75,7 @@ class Terminal(object):
|
||||
if tornado.options.options.unsecure:
|
||||
if self.user:
|
||||
try:
|
||||
self.callee = utils.User(name=self.user)
|
||||
self.callee = self.user
|
||||
except LookupError:
|
||||
log.debug(
|
||||
"Can't switch to user %s" % self.user, exc_info=True)
|
||||
@@ -88,14 +96,22 @@ class Terminal(object):
|
||||
butterfly=self,
|
||||
version=__version__,
|
||||
opts=tornado.options.options,
|
||||
colors=utils.ansi_colors)
|
||||
.decode('utf-8')
|
||||
.replace('\r', '')
|
||||
.replace('\n', '\r\n'))
|
||||
self.send('S' + motd)
|
||||
uri=self.uri,
|
||||
colors=utils.ansi_colors
|
||||
).decode('utf-8')
|
||||
.replace('\r', '')
|
||||
.replace('\n', '\r\n'))
|
||||
self.send(motd)
|
||||
|
||||
log.info('Forking pty for user %r' % self.user)
|
||||
|
||||
def send(self, message):
|
||||
if message is not None:
|
||||
self.history += message
|
||||
if len(self.history) > self.history_size:
|
||||
self.history = self.history[-self.history_size:]
|
||||
self.broadcast(self.session, message)
|
||||
|
||||
def pty(self):
|
||||
# Make a "unique" id in 4 bytes
|
||||
self.uid = ''.join(
|
||||
@@ -114,10 +130,13 @@ class Terminal(object):
|
||||
self.communicate()
|
||||
|
||||
def determine_user(self):
|
||||
if self.callee is None and (
|
||||
tornado.options.options.unsecure and
|
||||
tornado.options.options.login):
|
||||
# If callee is now known and we have unsecure connection
|
||||
if not tornado.options.options.unsecure:
|
||||
# Secure mode we must have already a callee
|
||||
assert self.callee is not None
|
||||
return
|
||||
|
||||
# If we should login, login
|
||||
if tornado.options.options.login:
|
||||
user = ''
|
||||
while user == '':
|
||||
try:
|
||||
@@ -131,13 +150,11 @@ class Terminal(object):
|
||||
except Exception:
|
||||
log.debug("Can't switch to user %s" % user, exc_info=True)
|
||||
self.callee = utils.User(name='nobody')
|
||||
elif (tornado.options.options.unsecure and not
|
||||
tornado.options.options.login):
|
||||
# if login is not required, we will use the same user as
|
||||
# butterfly is executed
|
||||
self.callee = utils.User()
|
||||
return
|
||||
|
||||
assert self.callee is not None
|
||||
# if login is not required, we will use the same user as
|
||||
# butterfly is executed
|
||||
self.callee = self.callee or utils.User()
|
||||
|
||||
def shell(self):
|
||||
try:
|
||||
@@ -147,18 +164,19 @@ class Terminal(object):
|
||||
"Can't chdir to %s" % (self.path or self.callee.dir),
|
||||
exc_info=True)
|
||||
|
||||
env = os.environ
|
||||
# If local and local user is the same as login user
|
||||
# We set the env of the user from the browser
|
||||
# Usefull when running as root
|
||||
if self.caller == self.callee:
|
||||
env = os.environ
|
||||
env.update(self.socket.env)
|
||||
else:
|
||||
# May need more?
|
||||
env = {}
|
||||
env["TERM"] = "xterm-256color"
|
||||
env["COLORTERM"] = "butterfly"
|
||||
env["HOME"] = self.callee.dir
|
||||
env["LOCATION"] = "http%s://%s:%d/" % (
|
||||
"s" if not tornado.options.options.unsecure else "",
|
||||
tornado.options.options.host, tornado.options.options.port)
|
||||
env["LOCATION"] = self.uri
|
||||
env['BUTTERFLY_PATH'] = os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), 'bin'))
|
||||
|
||||
@@ -167,7 +185,6 @@ class Terminal(object):
|
||||
except Exception:
|
||||
log.debug("Can't get ttyname", exc_info=True)
|
||||
tty = ''
|
||||
|
||||
if self.caller != self.callee:
|
||||
try:
|
||||
os.chown(os.ttyname(0), self.callee.uid, -1)
|
||||
@@ -177,22 +194,21 @@ class Terminal(object):
|
||||
utils.add_user_info(
|
||||
self.uid,
|
||||
tty, os.getpid(),
|
||||
self.callee.name, self.host)
|
||||
self.callee.name, self.uri)
|
||||
|
||||
if not tornado.options.options.unsecure or (
|
||||
self.socket.local and
|
||||
self.caller == self.callee and
|
||||
server == self.callee
|
||||
) or 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
|
||||
tornado.options.options.login 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)
|
||||
@@ -211,14 +227,23 @@ class Terminal(object):
|
||||
args = tornado.options.options.cmd.split(' ')
|
||||
else:
|
||||
args = [tornado.options.options.shell or self.callee.shell]
|
||||
args.append('-i')
|
||||
args.append('-il')
|
||||
|
||||
# In some cases some shells don't export SHELL var
|
||||
env['SHELL'] = args[0]
|
||||
|
||||
os.execvpe(args[0], args, env)
|
||||
# This process has been replaced
|
||||
|
||||
if tornado.options.options.pam_profile:
|
||||
if not server.root:
|
||||
print('You must be root to use pam_profile option.')
|
||||
sys.exit(3)
|
||||
pam_path = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)), 'pam.py')
|
||||
os.execvpe(sys.executable, [
|
||||
sys.executable, pam_path, self.callee.name,
|
||||
tornado.options.options.pam_profile], env)
|
||||
|
||||
# Unsecure connection with su
|
||||
if server.root:
|
||||
if self.socket.local:
|
||||
@@ -238,11 +263,10 @@ class Terminal(object):
|
||||
else:
|
||||
args = ['/bin/su']
|
||||
|
||||
if sys.platform == 'linux':
|
||||
args.append('-p')
|
||||
if tornado.options.options.shell:
|
||||
args.append('-s')
|
||||
args.append(tornado.options.options.shell)
|
||||
args.append('-l')
|
||||
if sys.platform.startswith('linux') and tornado.options.options.shell:
|
||||
args.append('-s')
|
||||
args.append(tornado.options.options.shell)
|
||||
args.append(self.callee.name)
|
||||
os.execvpe(args[0], args, env)
|
||||
|
||||
@@ -272,17 +296,18 @@ class Terminal(object):
|
||||
self.on_close()
|
||||
self.close()
|
||||
|
||||
if message[0] == 'R':
|
||||
cols, rows = map(int, message[1:].split(','))
|
||||
log.debug('WRIT<%r' % message)
|
||||
self.writer.write(message)
|
||||
self.writer.flush()
|
||||
|
||||
def ctl(self, message):
|
||||
if message['cmd'] == 'size':
|
||||
cols = message['cols']
|
||||
rows = message['rows']
|
||||
s = struct.pack("HHHH", rows, cols, 0, 0)
|
||||
fcntl.ioctl(self.fd, termios.TIOCSWINSZ, s)
|
||||
log.info('SIZE (%d, %d)' % (cols, rows))
|
||||
|
||||
elif message[0] == 'S':
|
||||
log.debug('WRIT<%r' % message)
|
||||
self.writer.write(message[1:])
|
||||
self.writer.flush()
|
||||
|
||||
def shell_handler(self, fd, events):
|
||||
if events & ioloop.READ:
|
||||
try:
|
||||
@@ -292,7 +317,7 @@ class Terminal(object):
|
||||
|
||||
log.debug('READ>%r' % read)
|
||||
if read and len(read) != 0:
|
||||
self.send('S' + read.decode('utf-8', 'replace'))
|
||||
self.send(read.decode('utf-8', 'replace'))
|
||||
else:
|
||||
events = ioloop.ERROR
|
||||
|
||||
@@ -326,7 +351,10 @@ class Terminal(object):
|
||||
log.debug('closing fd fail', exc_info=True)
|
||||
|
||||
try:
|
||||
os.kill(self.pid, signal.SIGKILL)
|
||||
os.kill(self.pid, signal.SIGHUP)
|
||||
os.kill(self.pid, signal.SIGCONT)
|
||||
os.waitpid(self.pid, 0)
|
||||
except Exception:
|
||||
log.debug('waitpid fail', exc_info=True)
|
||||
|
||||
del self.sessions[self.session]
|
||||
|
||||
Submodule butterfly/themes updated: 82ead044c2...d640d1ec1c
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2015 Florian Mounier
|
||||
# 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
|
||||
@@ -18,14 +18,13 @@
|
||||
|
||||
import os
|
||||
import pwd
|
||||
import time
|
||||
import sys
|
||||
import struct
|
||||
from logging import getLogger
|
||||
from collections import namedtuple
|
||||
import subprocess
|
||||
import tornado.options
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from collections import namedtuple
|
||||
from logging import getLogger
|
||||
|
||||
log = getLogger('butterfly')
|
||||
|
||||
@@ -140,7 +139,7 @@ class Socket(object):
|
||||
try:
|
||||
line = get_procfs_socket_line(get_hex_ip_port(pn[:2]))
|
||||
self.user = User(uid=int(line[7]))
|
||||
self.env = get_socket_env(line[9])
|
||||
self.env = get_socket_env(line[9], self.user)
|
||||
except Exception:
|
||||
log.debug('procfs was no good, aight', exc_info=True)
|
||||
|
||||
@@ -169,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
|
||||
@@ -203,23 +202,42 @@ def get_procfs_socket_line(hex_ip_port):
|
||||
|
||||
|
||||
# Linux only browser environment far fetch
|
||||
def get_socket_env(inode):
|
||||
def get_socket_env(inode, user):
|
||||
for pid in os.listdir("/proc/"):
|
||||
if not pid.isdigit():
|
||||
continue
|
||||
with open('/proc/%s/cmdline' % pid) as c:
|
||||
if c.read().split('\x00')[0] in [
|
||||
'gnome-session',
|
||||
'startkde',
|
||||
'xfce4-session']:
|
||||
with open('/proc/%s/environ' % pid) 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
|
||||
try:
|
||||
with open('/proc/%s/cmdline' % pid) as c:
|
||||
command = c.read().split('\x00')
|
||||
executable = command[0].split('/')[-1]
|
||||
if executable in ('sh', 'bash', 'zsh'):
|
||||
executable = command[1].split('/')[-1]
|
||||
if executable in [
|
||||
'gnome-session',
|
||||
'gnome-session-binary',
|
||||
'startkde',
|
||||
'startdde',
|
||||
'xfce4-session']:
|
||||
with open('/proc/%s/status' % pid) as e:
|
||||
uid = None
|
||||
for line in e.read().splitlines():
|
||||
parts = line.split('\t')
|
||||
if parts[0] == 'Uid:':
|
||||
uid = int(parts[1])
|
||||
break
|
||||
if not uid or uid != user.uid:
|
||||
continue
|
||||
|
||||
with open('/proc/%s/environ' % pid) 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
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
for pid in os.listdir("/proc/"):
|
||||
if not pid.isdigit():
|
||||
@@ -276,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):
|
||||
@@ -390,4 +409,5 @@ class AnsiColors(object):
|
||||
return '\x1b[0m'
|
||||
return ''
|
||||
|
||||
|
||||
ansi_colors = AnsiColors()
|
||||
|
||||
@@ -45,8 +45,7 @@ clean_ansi = (data) ->
|
||||
|
||||
setAlarm = (notification, cond) ->
|
||||
alarm = (data) ->
|
||||
message = clean_ansi data.data
|
||||
console.log message
|
||||
message = clean_ansi data.data.slice(1)
|
||||
return if cond isnt null and not cond.test(message)
|
||||
|
||||
butterfly.body.classList.remove 'alarm'
|
||||
@@ -63,9 +62,9 @@ setAlarm = (notification, cond) ->
|
||||
else
|
||||
alert(note + '\n' + message)
|
||||
|
||||
butterfly.ws.removeEventListener 'message', alarm
|
||||
butterfly.ws.shell.removeEventListener 'message', alarm
|
||||
|
||||
butterfly.ws.addEventListener 'message', alarm
|
||||
butterfly.ws.shell.addEventListener 'message', alarm
|
||||
butterfly.body.classList.add 'alarm'
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2015 Florian Mounier
|
||||
# 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
|
||||
@@ -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(
|
||||
@@ -33,9 +34,19 @@ addEventListener 'copy', copy = (e) ->
|
||||
e.clipboardData.setData 'text/plain', data.slice(0, -1)
|
||||
e.preventDefault()
|
||||
|
||||
|
||||
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')
|
||||
butterfly.send data
|
||||
# Send big data in chunks to prevent data loss
|
||||
size = 1024
|
||||
send = ->
|
||||
butterfly.send data.substring(0, size)
|
||||
data = data.substring(size)
|
||||
if data.length
|
||||
setTimeout send, 25
|
||||
send()
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
10
coffees/ext/expand_extended.coffee
Normal file
10
coffees/ext/expand_extended.coffee
Normal file
@@ -0,0 +1,10 @@
|
||||
Terminal.on 'change', (line) ->
|
||||
if 'extended' in line.classList
|
||||
line.addEventListener 'click', do (line) -> ->
|
||||
if 'expanded' in line.classList
|
||||
line.classList.remove 'expanded'
|
||||
else
|
||||
before = line.getBoundingClientRect().height
|
||||
line.classList.add 'expanded'
|
||||
after = line.getBoundingClientRect().height
|
||||
document.body.scrollTop += after - before
|
||||
33
coffees/ext/linkify.coffee
Normal file
33
coffees/ext/linkify.coffee
Normal file
@@ -0,0 +1,33 @@
|
||||
walk = (node, callback) ->
|
||||
for child in node.childNodes
|
||||
callback.call(child)
|
||||
walk child, callback
|
||||
|
||||
linkify = (text) ->
|
||||
# http://stackoverflow.com/questions/37684/how-to-replace-plain-urls-with-links
|
||||
urlPattern = (
|
||||
/\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim)
|
||||
pseudoUrlPattern = /(^|[^\/])(www\.[\S]+(\b|$))/gim
|
||||
emailAddressPattern = /[\w.]+@[a-zA-Z_-]+?(?:\.[a-zA-Z]{2,6})+/gim
|
||||
text
|
||||
.replace(urlPattern, '<a href="$&">$&</a>')
|
||||
.replace(pseudoUrlPattern, '$1<a href="http://$2">$2</a>')
|
||||
.replace(emailAddressPattern, '<a href="mailto:$&">$&</a>')
|
||||
|
||||
tags =
|
||||
'&': '&'
|
||||
'<': '<'
|
||||
'>': '>'
|
||||
|
||||
escape = (s) -> s.replace(/[&<>]/g, (tag) -> tags[tag] or tag)
|
||||
|
||||
Terminal.on 'change', (line) ->
|
||||
walk line, ->
|
||||
if @nodeType is 3
|
||||
val = @nodeValue
|
||||
linkified = linkify escape(val)
|
||||
if linkified isnt val
|
||||
newNode = document.createElement('span')
|
||||
newNode.innerHTML = linkified
|
||||
@parentElement.replaceChild newNode, @
|
||||
true
|
||||
52
coffees/ext/mobile.coffee
Normal file
52
coffees/ext/mobile.coffee
Normal 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
|
||||
@@ -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
|
||||
|
||||
29
coffees/ext/pack.coffee
Normal file
29
coffees/ext/pack.coffee
Normal file
@@ -0,0 +1,29 @@
|
||||
tid = null
|
||||
packSize = 1000
|
||||
histSize = 100
|
||||
|
||||
maybePack = ->
|
||||
return unless butterfly.term.childElementCount > packSize + butterfly.rows
|
||||
hist = document.getElementById 'packed'
|
||||
packfrag = document.createDocumentFragment 'fragment'
|
||||
for i in [0..packSize]
|
||||
packfrag.appendChild butterfly.term.firstChild
|
||||
pack = document.createElement 'div'
|
||||
pack.classList.add 'pack'
|
||||
pack.appendChild packfrag
|
||||
hist.appendChild pack
|
||||
|
||||
hist.firstChild.remove() if hist.childElementCount > histSize
|
||||
|
||||
tid = setTimeout maybePack
|
||||
|
||||
|
||||
Terminal.on 'refresh', ->
|
||||
clearTimeout tid if tid
|
||||
maybePack()
|
||||
|
||||
Terminal.on 'clear', ->
|
||||
newHist = document.createElement 'div'
|
||||
newHist.id = 'packed'
|
||||
hist = document.getElementById 'packed'
|
||||
butterfly.body.replaceChild newHist, hist
|
||||
@@ -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 = ''
|
||||
|
||||
|
||||
@@ -87,12 +87,13 @@ class Selection
|
||||
@go +1
|
||||
|
||||
go: (n) ->
|
||||
index = butterfly.children.indexOf(@startLine) + n
|
||||
return unless 0 <= index < butterfly.children.length
|
||||
index = Array.prototype.indexOf.call(
|
||||
butterfly.term.childNodes, @startLine) + n
|
||||
return unless 0 <= index < butterfly.term.childElementCount
|
||||
|
||||
until butterfly.children[index].textContent.match /\S/
|
||||
until butterfly.term.childNodes[index].textContent.match /\S/
|
||||
index += n
|
||||
return unless 0 <= index < butterfly.children.length
|
||||
return unless 0 <= index < butterfly.term.childElementCount
|
||||
|
||||
@selectLine index
|
||||
|
||||
@@ -104,7 +105,7 @@ class Selection
|
||||
@selection.addRange range
|
||||
|
||||
selectLine: (index) ->
|
||||
line = butterfly.children[index]
|
||||
line = butterfly.term.childNodes[index]
|
||||
lineStart =
|
||||
node: line.firstChild
|
||||
offset: 0
|
||||
@@ -204,8 +205,9 @@ document.addEventListener 'keydown', (e) ->
|
||||
|
||||
# Start selection mode with shift up
|
||||
if not selection and e.ctrlKey and e.shiftKey and e.keyCode == 38
|
||||
r = Math.max butterfly.term.childElementCount - butterfly.rows, 0
|
||||
selection = new Selection()
|
||||
selection.selectLine butterfly.y - 1
|
||||
selection.selectLine r + butterfly.y - 1
|
||||
selection.apply()
|
||||
return cancel e
|
||||
true
|
||||
|
||||
@@ -3,7 +3,7 @@ _set_theme_href = (href) ->
|
||||
img = document.createElement('img')
|
||||
img.onerror = ->
|
||||
setTimeout (-> butterfly?.resize()), 250
|
||||
img.src = href;
|
||||
img.src = href
|
||||
|
||||
_theme = localStorage?.getItem('theme')
|
||||
_set_theme_href(_theme) if _theme
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2015 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
|
||||
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2015 Florian Mounier
|
||||
# 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
|
||||
@@ -19,106 +19,107 @@ cols = rows = null
|
||||
quit = false
|
||||
openTs = (new Date()).getTime()
|
||||
|
||||
ws =
|
||||
shell: null
|
||||
ctl: null
|
||||
|
||||
$ = document.querySelectorAll.bind(document)
|
||||
|
||||
document.addEventListener 'DOMContentLoaded', ->
|
||||
term = null
|
||||
send = (data) ->
|
||||
ws.send 'S' + data
|
||||
|
||||
ctl = (type, args...) ->
|
||||
params = args.join(',')
|
||||
if type == 'Resize'
|
||||
ws.send 'R' + params
|
||||
|
||||
if location.protocol == 'https:'
|
||||
wsUrl = 'wss://'
|
||||
else
|
||||
wsUrl = 'ws://'
|
||||
|
||||
wsUrl += document.location.host + '/ws' + location.pathname
|
||||
ws = new WebSocket wsUrl
|
||||
rootPath = document.body.getAttribute('data-root-path')
|
||||
rootPath = rootPath.replace(/^\/+|\/+$/g, '')
|
||||
if rootPath.length
|
||||
rootPath = "/#{rootPath}"
|
||||
|
||||
ws.addEventListener 'open', ->
|
||||
wsUrl += document.location.host + rootPath
|
||||
path = '/'
|
||||
if path.indexOf('/session') < 0
|
||||
path += "session/#{document.body.getAttribute('data-session-token')}"
|
||||
|
||||
path += location.search
|
||||
|
||||
ws.shell = new WebSocket wsUrl + '/ws' + path
|
||||
ws.ctl = new WebSocket wsUrl + '/ctl' + path
|
||||
|
||||
open = ->
|
||||
console.log "WebSocket open", arguments
|
||||
term = new Terminal document.body, send, ctl
|
||||
term.ws = ws
|
||||
window.butterfly = term
|
||||
ws.send 'R' + term.cols + ',' + term.rows
|
||||
openTs = (new Date()).getTime()
|
||||
|
||||
ws.addEventListener 'error', ->
|
||||
console.log "WebSocket error", arguments
|
||||
|
||||
lastData = ''
|
||||
t_queue = null
|
||||
|
||||
queue = ''
|
||||
ws.addEventListener 'message', (e) ->
|
||||
if e.data[0] is 'R'
|
||||
[cols, rows] = e.data.slice(1).split(',')
|
||||
term.resize cols, rows, true
|
||||
return
|
||||
|
||||
if e.data[0] isnt 'S'
|
||||
console.error 'Garbage message'
|
||||
return
|
||||
|
||||
clearTimeout t_queue if t_queue
|
||||
queue += e.data.slice(1)
|
||||
if term.stop
|
||||
queue = queue.slice -10 * 1024
|
||||
|
||||
if queue.length > term.buffSize
|
||||
treat()
|
||||
else
|
||||
t_queue = setTimeout treat, 1
|
||||
|
||||
treat = ->
|
||||
term.write queue
|
||||
if term.stop
|
||||
term.stop = false
|
||||
if term
|
||||
term.body.classList.remove 'stopped'
|
||||
queue = ''
|
||||
term.out = ws.shell.send.bind(ws.shell)
|
||||
term.out '\x03\n'
|
||||
return
|
||||
|
||||
ws.addEventListener 'close', ->
|
||||
if (ws.shell.readyState is WebSocket.OPEN and
|
||||
ws.ctl.readyState is WebSocket.OPEN)
|
||||
|
||||
term = new Terminal(
|
||||
document.body, ws.shell.send.bind(ws.shell), ws.ctl.send.bind(ws.ctl))
|
||||
term.ws = ws
|
||||
window.butterfly = term
|
||||
ws.ctl.send JSON.stringify(cmd: 'open')
|
||||
ws.ctl.send JSON.stringify(
|
||||
cmd: 'size', cols: term.cols, rows: term.rows)
|
||||
openTs = (new Date()).getTime()
|
||||
console.log "WebSocket open end", arguments
|
||||
|
||||
error = ->
|
||||
console.error "WebSocket error", arguments
|
||||
|
||||
close = ->
|
||||
console.log "WebSocket closed", arguments
|
||||
setTimeout ->
|
||||
term.write 'Closed'
|
||||
# Allow quick reload
|
||||
term.skipNextKey = true
|
||||
term.body.classList.add('dead')
|
||||
# Don't autoclose if websocket didn't last 1 minute
|
||||
if (new Date()).getTime() - openTs > 60 * 1000
|
||||
open('','_self').close()
|
||||
, 1
|
||||
return if quit
|
||||
quit = true
|
||||
|
||||
term.write 'Closed'
|
||||
# Allow quick reload
|
||||
term.skipNextKey = true
|
||||
term.body.classList.add('dead')
|
||||
# Don't autoclose if websocket didn't last 1 minute
|
||||
if (new Date()).getTime() - openTs > 60 * 1000
|
||||
window.open('','_self').close()
|
||||
|
||||
reopenOnClose = ->
|
||||
setTimeout ->
|
||||
return if quit
|
||||
ws.shell = new WebSocket wsUrl + '/ws' + path
|
||||
init_shell_ws()
|
||||
, 100
|
||||
|
||||
write = (data) ->
|
||||
if term
|
||||
term.write data
|
||||
|
||||
write_request = (e) ->
|
||||
setTimeout write, 1, e.data
|
||||
|
||||
ctl = (e) ->
|
||||
cmd = JSON.parse(e.data)
|
||||
if cmd.cmd is 'size'
|
||||
term.resize cmd.cols, cmd.rows, true
|
||||
|
||||
init_shell_ws = ->
|
||||
ws.shell.addEventListener 'open', open
|
||||
ws.shell.addEventListener 'message', write_request
|
||||
ws.shell.addEventListener 'error', error
|
||||
ws.shell.addEventListener 'close', reopenOnClose
|
||||
|
||||
init_ctl_ws = ->
|
||||
ws.ctl.addEventListener 'open', open
|
||||
ws.ctl.addEventListener 'message', ctl
|
||||
ws.ctl.addEventListener 'error', error
|
||||
ws.ctl.addEventListener 'close', close
|
||||
|
||||
init_shell_ws()
|
||||
init_ctl_ws()
|
||||
|
||||
addEventListener 'beforeunload', ->
|
||||
if not quit
|
||||
'This will exit the terminal session'
|
||||
|
||||
|
||||
window.bench = (n=100000000) ->
|
||||
rnd = ''
|
||||
while rnd.length < n
|
||||
rnd += Math.random().toString(36).substring(2)
|
||||
|
||||
console.time('bench')
|
||||
console.profile('bench')
|
||||
term.write rnd
|
||||
console.profileEnd()
|
||||
console.timeEnd('bench')
|
||||
|
||||
|
||||
window.cbench = (n=100000000) ->
|
||||
rnd = ''
|
||||
while rnd.length < n
|
||||
rnd += "\x1b[#{30 + parseInt(Math.random() * 20)}m"
|
||||
rnd += Math.random().toString(36).substring(2)
|
||||
|
||||
console.time('cbench')
|
||||
console.profile('cbench')
|
||||
term.write rnd
|
||||
console.profileEnd()
|
||||
console.timeEnd('cbench')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
21
docker/run.sh
Normal file → Executable file
21
docker/run.sh
Normal file → Executable file
@@ -1,13 +1,14 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash -e
|
||||
|
||||
# if command starts with an option, prepend the default command and options
|
||||
if [ "${1:0:1}" = '-' ]; then
|
||||
set -- butterfly.server.py --unsecure --host=0.0.0.0 --port=${PORT:-57575} "$@"
|
||||
elif [ "$1" = 'butterfly.server.py' ]; then
|
||||
shift
|
||||
set -- butterfly.server.py --unsecure --host=0.0.0.0 --port=${PORT:-57575} "$@"
|
||||
fi
|
||||
|
||||
# Set password
|
||||
echo "root:${PASSWORD}" | chpasswd
|
||||
echo "root:${PASSWORD:-password}" | chpasswd
|
||||
|
||||
if [ -z ${PORT} ]
|
||||
then
|
||||
echo "Starting on default port: 57575"
|
||||
/opt/app/butterfly.server.py --unsecure --host=0.0.0.0
|
||||
else
|
||||
echo "Starting on port: ${PORT}"
|
||||
/opt/app/butterfly.server.py --unsecure --host=0.0.0.0 --port=${PORT}
|
||||
fi
|
||||
exec "$@"
|
||||
|
||||
18
package.json
18
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "butterfly",
|
||||
"version": "2.0.0",
|
||||
"version": "3.0.0",
|
||||
"description": "A sleek web based terminal emulator",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -13,13 +13,13 @@
|
||||
},
|
||||
"homepage": "https://github.com/paradoxxxzero/butterfly",
|
||||
"devDependencies": {
|
||||
"coffeelint": "^1.12.1",
|
||||
"grunt": "^0.4.5",
|
||||
"grunt-coffeelint": "0.0.13",
|
||||
"grunt-contrib-coffee": "^0.13.0",
|
||||
"grunt-contrib-cssmin": "^0.14.0",
|
||||
"grunt-contrib-uglify": "^0.9.2",
|
||||
"grunt-contrib-watch": "^0.6.1",
|
||||
"grunt-sass": "^1.1.0-beta"
|
||||
"coffeelint": "^1.15.7",
|
||||
"grunt": "^1.0.1",
|
||||
"grunt-coffeelint": "0.0.15",
|
||||
"grunt-contrib-coffee": "^1.0.0",
|
||||
"grunt-contrib-cssmin": "^1.0.1",
|
||||
"grunt-contrib-uglify": "^1.0.1",
|
||||
"grunt-contrib-watch": "^1.0.0",
|
||||
"grunt-sass": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
7
setup.cfg
Normal file
7
setup.cfg
Normal file
@@ -0,0 +1,7 @@
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
[tool:pytest]
|
||||
flake8-ignore =
|
||||
*.py E731 E402
|
||||
butterfly/bin/help.py E501
|
||||
38
setup.py
38
setup.py
@@ -5,27 +5,31 @@
|
||||
Butterfly - A sleek web based terminal emulator
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
ROOT = os.path.dirname(__file__)
|
||||
with open(os.path.join(ROOT, 'butterfly', '__init__.py')) as fd:
|
||||
__version__ = re.search("__version__ = '([^']+)'", fd.read()).group(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", 'tornado_systemd'],
|
||||
extras_requires=["libsass"],
|
||||
install_requires=["tornado>=3.2", "pyOpenSSL"],
|
||||
extras_require={
|
||||
'themes': ["libsass"],
|
||||
'systemd': ['tornado_systemd'],
|
||||
'lint': ['pytest', 'pytest-flake8', 'pytest-isort']
|
||||
},
|
||||
package_data={
|
||||
'butterfly': [
|
||||
'sass/*.sass',
|
||||
@@ -44,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)
|
||||
|
||||
Reference in New Issue
Block a user