mirror of
https://github.com/paradoxxxzero/butterfly.git
synced 2026-06-10 06:14:39 +00:00
Compare commits
175 Commits
| 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 | ||
|
|
4a3bd7f906 | ||
|
|
fb3ec14b43 | ||
|
|
4e4d54de1f | ||
|
|
e6f618ef52 | ||
|
|
0337663059 | ||
|
|
7b37716177 | ||
|
|
2d554483e1 | ||
|
|
fc5879f2d4 | ||
|
|
7371b8b4e1 | ||
|
|
71820849eb | ||
|
|
7b5a4ee244 | ||
|
|
e8512fc2b8 | ||
|
|
9c36b0c8c1 | ||
|
|
ab8e65924d | ||
|
|
ca26454aa0 | ||
|
|
0f36db5264 | ||
|
|
834200256c | ||
|
|
96606d2b0b | ||
|
|
7501aab797 | ||
|
|
38a4c4083d | ||
|
|
c937a8753d | ||
|
|
7916014854 | ||
|
|
93ff8a3969 | ||
|
|
0f7a51d451 | ||
|
|
f67054f9ff | ||
|
|
ced1148275 | ||
|
|
140b0902fc | ||
|
|
909bcfa9f4 | ||
|
|
cf5051d414 | ||
|
|
811620557a | ||
|
|
0e97cc8362 | ||
|
|
6d346af6f4 | ||
|
|
893ec72270 |
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
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -0,0 +1,3 @@
|
||||
[submodule "butterfly/themes"]
|
||||
path = butterfly/themes
|
||||
url = https://github.com/paradoxxxzero/butterfly-themes
|
||||
|
||||
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) 2014 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) 2014 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
|
||||
```
|
||||
|
||||
20
bower.json
Normal file
20
bower.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "butterfly",
|
||||
"version": "1.0.0",
|
||||
"authors": [
|
||||
"Florian Mounier <florian.mounier@kozea.fr>"
|
||||
],
|
||||
"description": "A sleek web based terminal emulator",
|
||||
"license": "None",
|
||||
"private": true,
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests"
|
||||
],
|
||||
"dependencies": {
|
||||
"google-caja": "*"
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2014 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,12 +20,18 @@
|
||||
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
|
||||
import ssl
|
||||
import getpass
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import socket
|
||||
import sys
|
||||
@@ -33,20 +39,38 @@ import sys
|
||||
tornado.options.define("debug", default=False, help="Debug mode")
|
||||
tornado.options.define("more", default=False,
|
||||
help="Debug mode with more verbosity")
|
||||
tornado.options.define("unminified", default=False,
|
||||
help="Use the unminified js (for development only)")
|
||||
|
||||
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")
|
||||
tornado.options.define("motd", default='motd', help="Path to the motd file.")
|
||||
tornado.options.define("cmd",
|
||||
help="Command to run instead of shell, f.i.: 'ls -l'")
|
||||
tornado.options.define("unsecure", default=False,
|
||||
help="Don't use ssl not recommended")
|
||||
tornado.options.define("i_hereby_declare_i_dont_want_any_security_whatsoever",
|
||||
default=False,
|
||||
help="Remove all security and warnings. There are some "
|
||||
"use cases for that. Use this if you really know what "
|
||||
"you are doing.")
|
||||
tornado.options.define("login", default=False,
|
||||
help="Use login screen at start")
|
||||
tornado.options.define("pam_profile", default="", type=str,
|
||||
help="When --login=True provided and running as ROOT, "
|
||||
"use PAM with the specified PAM profile for "
|
||||
"authentication and then execute the user's default "
|
||||
"shell. Will override --shell.")
|
||||
tornado.options.define("force_unicode_width",
|
||||
default=False,
|
||||
help="Force all unicode characters to the same width."
|
||||
"Useful for avoiding layout mess.")
|
||||
tornado.options.define("login", default=True,
|
||||
help="Use login screen at start")
|
||||
tornado.options.define("ssl_version", default=None,
|
||||
help="SSL protocol version")
|
||||
tornado.options.define("generate_certs", default=False,
|
||||
@@ -57,19 +81,22 @@ 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("unminified", default=False,
|
||||
help="Use the unminified js (for development only)")
|
||||
tornado.options.define("theme", default=None,
|
||||
help="Specify a theme for butterfly.")
|
||||
tornado.options.define("uri_root_path", default='',
|
||||
help="Sets the servier root path: "
|
||||
"example.com/<uri_root_path>/static/")
|
||||
|
||||
|
||||
if os.getuid() == 0:
|
||||
conf_file = os.path.join(
|
||||
os.path.abspath(os.sep), 'etc', 'butterfly', 'butterfly.conf')
|
||||
ssl_dir = os.path.join(os.path.abspath(os.sep), 'etc', 'butterfly', 'ssl')
|
||||
ev = os.getenv('XDG_CONFIG_DIRS', '/etc')
|
||||
else:
|
||||
conf_file = os.path.join(
|
||||
os.path.expanduser('~'), '.butterfly', 'butterfly.conf')
|
||||
ssl_dir = os.path.join(os.path.expanduser('~'), '.butterfly', 'ssl')
|
||||
ev = os.getenv(
|
||||
'XDG_CONFIG_HOME', os.path.join(
|
||||
os.getenv('HOME', os.path.expanduser('~')),
|
||||
'.config'))
|
||||
|
||||
butterfly_dir = os.path.join(ev, 'butterfly')
|
||||
conf_file = os.path.join(butterfly_dir, 'butterfly.conf')
|
||||
ssl_dir = os.path.join(butterfly_dir, 'ssl')
|
||||
|
||||
tornado.options.define("conf", default=conf_file,
|
||||
help="Butterfly configuration file. "
|
||||
@@ -78,11 +105,30 @@ tornado.options.define("conf", default=conf_file,
|
||||
tornado.options.define("ssl_dir", default=ssl_dir,
|
||||
help="Force SSL directory location")
|
||||
|
||||
# Do it once to get the conf path
|
||||
tornado.options.parse_command_line()
|
||||
|
||||
if os.path.exists(tornado.options.options.conf):
|
||||
tornado.options.parse_config_file(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',
|
||||
@@ -95,11 +141,14 @@ for logger in ('tornado.access', 'tornado.application',
|
||||
logging.getLogger(logger).setLevel(level)
|
||||
|
||||
log = logging.getLogger('butterfly')
|
||||
log.info('Starting server')
|
||||
|
||||
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)
|
||||
|
||||
@@ -107,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',
|
||||
@@ -132,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)
|
||||
@@ -141,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)
|
||||
@@ -148,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))
|
||||
@@ -161,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
|
||||
@@ -211,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)
|
||||
@@ -272,12 +359,12 @@ else:
|
||||
ssl, 'PROTOCOL_%s' % options.ssl_version)
|
||||
|
||||
from butterfly import application
|
||||
|
||||
http_server = tornado_systemd.SystemdHTTPServer(
|
||||
application, ssl_options=ssl_opts)
|
||||
application.butterfly_dir = butterfly_dir
|
||||
log.info('Starting server')
|
||||
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')
|
||||
|
||||
@@ -285,7 +372,15 @@ log.info('Starting loop')
|
||||
|
||||
ioloop = tornado.ioloop.IOLoop.instance()
|
||||
|
||||
url = "http%s://%s:%d/" % (
|
||||
"s" if not options.unsecure else "", host, port)
|
||||
log.warn('Butterfly is ready, open your browser to: %s' % url)
|
||||
if port == 0:
|
||||
port = list(http_server._sockets.values())[0].getsockname()[1]
|
||||
|
||||
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)
|
||||
|
||||
ioloop.start()
|
||||
|
||||
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) 2014 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'
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -43,12 +48,38 @@ class Route(tornado.web.RequestHandler):
|
||||
def log(self):
|
||||
return log
|
||||
|
||||
@property
|
||||
def builtin_themes_dir(self):
|
||||
return os.path.join(
|
||||
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(
|
||||
self.builtin_themes_dir, theme[len('built-in-'):])
|
||||
return os.path.join(
|
||||
self.themes_dir, theme)
|
||||
|
||||
|
||||
# Imported from executable
|
||||
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,8 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
import os
|
||||
rows, cols = map(int, os.popen('stty size', 'r').read().split())
|
||||
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm ' % (255 - r, 255 - c, 255))
|
||||
@@ -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,4 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
sys.stdout.write('\x1bP;HTML|<hr />\x1bP')
|
||||
sys.stdout.flush()
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
from butterfly.escapes import html
|
||||
import fileinput
|
||||
import sys
|
||||
|
||||
with html():
|
||||
for line in fileinput.input():
|
||||
sys.stdout.write(line)
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import webbrowser
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Butterfly tab opener.')
|
||||
parser.add_argument(
|
||||
'location',
|
||||
default=os.getcwd(),
|
||||
help='Directory to open the new tab in. (Defaults to current)')
|
||||
args = parser.parse_args()
|
||||
|
||||
webbrowser.open('%swd%s' % (os.getenv('LOCATION'), args.location))
|
||||
@@ -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,52 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
from butterfly.escapes import image
|
||||
from butterfly.utils import ansi_colors
|
||||
import os
|
||||
import base64
|
||||
|
||||
print(ansi_colors.white + "Welcome to the butterfly help." + ansi_colors.reset)
|
||||
with image('image/png'):
|
||||
with open(
|
||||
os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'../static/images/favicon.png'), 'rb') as i:
|
||||
print(base64.b64encode(i.read()).decode('ascii'))
|
||||
print("""
|
||||
Butterfly is a xterm compliant terminal built with python and javascript.
|
||||
|
||||
{title}Terminal functionalities:{reset}
|
||||
{strong}[Alt] + [a] : {reset}Set an alarm which sends a notification when a modification is detected.
|
||||
{strong}[Ctrl] + [Shift] + [Up] : {reset}Trigger visual selection mode. Hitting [Enter] inserts the selection in the prompt.
|
||||
{strong}[ScrollLock] : {reset}Lock the scrolling to the current position. Press again to release.
|
||||
{strong}[Alt] + [z] : {reset}Escape: don't catch the next pressed key. Useful for using native search for example. ([Alt] + [z] then [Ctrl] + [f]).
|
||||
{strong}[Ctrl] + [c] <<hold>> : {reset}Cut the output when [Ctrl] + [c] is not enough.
|
||||
|
||||
|
||||
{title}Butterfly programs:{reset}
|
||||
{strong}bcat : {reset}A wrapper around cat allowing to display images as <img> instead of binary.
|
||||
{strong}bopen : {reset}Open a new terminal at specified location.
|
||||
{strong}b16M : {reset}Test the 16M colors support in terminal.
|
||||
{strong}bhr : {reset}Put a html hr. This is a test and needs --allow-html-escapes flag.
|
||||
{strong}bcal : {reset}Display current month using html. This is a test and needs --allow-html-escapes flag.
|
||||
|
||||
|
||||
{title}Styling butterfly:{reset}
|
||||
To style butterfly in sass, you need to have the libsass python library installed.
|
||||
|
||||
You will have to:
|
||||
$ cp {main} ~/.butterfly/style.sass
|
||||
or for system wide:
|
||||
# cp {main} /etc/butterfly/style.sass
|
||||
and then edit this file.
|
||||
|
||||
You can also copy the imported sass files in the same dir.
|
||||
Sass files are compiled on the fly so just reload your tab to see the changes.
|
||||
|
||||
It is also possible to use a style.css file and re do all the styling in css exclusively.\
|
||||
""".format(
|
||||
title=ansi_colors.light_blue,
|
||||
strong=ansi_colors.white,
|
||||
reset=ansi_colors.reset,
|
||||
main=os.path.normpath(os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'../sass/main.sass'))))
|
||||
7
butterfly/bin/bcat → butterfly/bin/cat.py
Executable file → Normal file
7
butterfly/bin/bcat → butterfly/bin/cat.py
Executable file → Normal file
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import base64
|
||||
import mimetypes
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from butterfly.escapes import image
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Butterfly cat wrapper.')
|
||||
parser.add_argument('-o', action="store_true",
|
||||
146
butterfly/bin/colors.py
Normal file
146
butterfly/bin/colors.py
Normal file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Butterfly terminal color tester.')
|
||||
parser.add_argument(
|
||||
'--colors',
|
||||
default='16',
|
||||
choices=['8', '16', '256', '16M'],
|
||||
help='Set the color mode to test')
|
||||
args = parser.parse_args()
|
||||
|
||||
print()
|
||||
|
||||
|
||||
if args.colors in ['8', '16']:
|
||||
print('Background\n')
|
||||
for l in range(3):
|
||||
sys.stdout.write(' ')
|
||||
for i in range(8):
|
||||
sys.stdout.write('\x1b[%dm \x1b[m ' % (40 + i))
|
||||
sys.stdout.write('\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
if args.colors == '16':
|
||||
print()
|
||||
for l in range(3):
|
||||
sys.stdout.write(' ')
|
||||
for i in range(8):
|
||||
sys.stdout.write('\x1b[%dm \x1b[m ' % (100 + i))
|
||||
sys.stdout.write('\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
print('\nForeground\n')
|
||||
|
||||
for l in range(3):
|
||||
sys.stdout.write(' ')
|
||||
for i in range(8):
|
||||
sys.stdout.write('\x1b[%dm ░▒▓██\x1b[m ' % (30 + i))
|
||||
sys.stdout.write('\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
if args.colors == '16':
|
||||
print()
|
||||
for l in range(3):
|
||||
sys.stdout.write(' ')
|
||||
for i in range(8):
|
||||
sys.stdout.write('\x1b[1;%dm ░▒▓██\x1b[m ' % (30 + i))
|
||||
sys.stdout.write('\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
if args.colors == '256':
|
||||
for i in range(16):
|
||||
sys.stdout.write('\x1b[48;5;%dm \x1b[m' % (i))
|
||||
print()
|
||||
for i in range(16):
|
||||
sys.stdout.write('\x1b[48;5;%dm %03d\x1b[m' % (i, i))
|
||||
print()
|
||||
|
||||
for j in range(6):
|
||||
for i in range(36):
|
||||
sys.stdout.write('\x1b[48;5;%dm \x1b[m' % (16 + j * 36 + i))
|
||||
print()
|
||||
for i in range(36):
|
||||
sys.stdout.write('\x1b[48;5;%dm %03d\x1b[m' % (
|
||||
16 + j * 36 + i, 16 + j * 36 + i))
|
||||
print()
|
||||
for i in range(24):
|
||||
sys.stdout.write('\x1b[48;5;%dm \x1b[m' % (232 + i))
|
||||
print()
|
||||
for i in range(24):
|
||||
sys.stdout.write('\x1b[48;5;%dm %03d\x1b[m' % (232 + i, 232 + i))
|
||||
|
||||
if args.colors == '16M':
|
||||
b = 0
|
||||
g = 0
|
||||
for r in range(256):
|
||||
if r == 128:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
|
||||
r = 255
|
||||
b = 0
|
||||
for g in range(256):
|
||||
if g == 128:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
|
||||
r = 255
|
||||
g = 255
|
||||
for b in range(256):
|
||||
if b == 128:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
|
||||
r = 255
|
||||
b = 255
|
||||
for g in reversed(range(256)):
|
||||
if g == 127:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
|
||||
g = 0
|
||||
b = 255
|
||||
for r in reversed(range(256)):
|
||||
if r == 127:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
|
||||
r = 0
|
||||
g = 0
|
||||
for b in reversed(range(256)):
|
||||
if b == 127:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
|
||||
r = 0
|
||||
b = 0
|
||||
for g in range(256):
|
||||
if g == 128:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
|
||||
r = 0
|
||||
g = 255
|
||||
for b in range(256):
|
||||
if b == 128:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
|
||||
b = 255
|
||||
g = 255
|
||||
for r in range(256):
|
||||
if r == 128:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
63
butterfly/bin/help.py
Normal file
63
butterfly/bin/help.py
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python
|
||||
import base64
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import butterfly
|
||||
from butterfly.escapes import image
|
||||
from butterfly.utils import ansi_colors
|
||||
|
||||
print(ansi_colors.white + "Welcome to the butterfly help." + ansi_colors.reset)
|
||||
path = os.getenv('BUTTERFLY_PATH')
|
||||
if path:
|
||||
path = os.path.join(path, '../static/images/favicon.png')
|
||||
|
||||
if path and os.path.exists(path):
|
||||
with image('image/png'):
|
||||
with open(path, 'rb') as i:
|
||||
print(base64.b64encode(i.read()).decode('ascii'))
|
||||
print("""
|
||||
Butterfly is a xterm compliant terminal built with python and javascript.
|
||||
|
||||
{title}Terminal functionalities:{reset}
|
||||
{strong}[ScrollLock] : {reset}Lock the scrolling to the current position. Press again to release.
|
||||
{strong}[Ctrl] + [c] <<hold>> : {reset}Cut the output when [Ctrl] + [c] is not enough.
|
||||
{strong}[Ctrl] + [Shift] + [Up] : {reset}Trigger visual selection mode. Hitting [Enter] inserts the selection in the prompt.
|
||||
{strong}[Alt] + [a] : {reset}Set an alarm which sends a notification when a modification is detected. (Ring on regexp match with [Shift])
|
||||
{strong}[Alt] + [s] : {reset}Open theme selection prompt. Use [Alt] + [Shift] + [s] to refresh current theme.
|
||||
{strong}[Alt] + [e] : {reset}List open user sessions. (Only available in secure mode)
|
||||
{strong}[Alt] + [o] : {reset}Open new terminal (As a popup)
|
||||
{strong}[Alt] + [z] : {reset}Escape: don't catch the next pressed key.
|
||||
Useful for using native search for example. ([Alt] + [z] then [Ctrl] + [f]).
|
||||
|
||||
|
||||
{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 html : {reset}Output in html standard input.
|
||||
|
||||
For more butterfly programs check out: https://github.com/paradoxxxzero/butterfly-demos
|
||||
|
||||
|
||||
{title}Styling butterfly:{reset}
|
||||
To style butterfly in sass, you need to have the libsass python library installed.
|
||||
|
||||
Theming is done by overriding the default sass files located in {code}{main}{reset} in your theme directory.
|
||||
This directory can include images and custom fonts.
|
||||
Please take a look at official themes here: https://github.com/paradoxxxzero/butterfly-themes
|
||||
and submit your best themes as pull request!
|
||||
|
||||
\x1b[{rcol}G\x1b[3m{dark}butterfly @ 2015 Mounier Florian{reset}\
|
||||
""".format(
|
||||
title=ansi_colors.light_blue,
|
||||
dark=ansi_colors.light_black,
|
||||
strong=ansi_colors.white,
|
||||
code=ansi_colors.light_yellow,
|
||||
comment=ansi_colors.light_magenta,
|
||||
reset=ansi_colors.reset,
|
||||
rcol=int(subprocess.check_output(['stty', 'size']).split()[1]) - 31,
|
||||
main=os.path.normpath(os.path.join(
|
||||
os.path.abspath(os.path.dirname(butterfly.__file__)), 'sass'))))
|
||||
19
butterfly/bin/html.py
Normal file
19
butterfly/bin/html.py
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python
|
||||
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)
|
||||
27
butterfly/bin/open.py
Normal file
27
butterfly/bin/open.py
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python
|
||||
import argparse
|
||||
import os
|
||||
import webbrowser
|
||||
|
||||
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(
|
||||
'location',
|
||||
nargs='?',
|
||||
default=os.getcwd(),
|
||||
help='Directory to open the new tab in. (Defaults to current)')
|
||||
args = parser.parse_args()
|
||||
|
||||
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)
|
||||
15
butterfly/bin/session.py
Normal file
15
butterfly/bin/session.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python
|
||||
import argparse
|
||||
import os
|
||||
import webbrowser
|
||||
|
||||
parser = argparse.ArgumentParser(description='Butterfly session opener.')
|
||||
parser.add_argument(
|
||||
'session',
|
||||
help='Open or rattach a butterfly session. '
|
||||
'(Only in secure mode or in user unsecure mode (no su login))')
|
||||
args = parser.parse_args()
|
||||
|
||||
url = '%ssession/%s' % (os.getenv('LOCATION', '/'), args.session)
|
||||
if not webbrowser.open(url):
|
||||
print('Unable to open browser, please go to %s' % url)
|
||||
58
butterfly/butterfly.conf.default
Normal file
58
butterfly/butterfly.conf.default
Normal file
@@ -0,0 +1,58 @@
|
||||
# Butterfly autogenerated config file
|
||||
|
||||
# Activate debug mode
|
||||
#
|
||||
#debug=False
|
||||
|
||||
# In debug mode produce more verbose output
|
||||
#
|
||||
#more=False
|
||||
|
||||
# Use unminified version of js for development
|
||||
#
|
||||
#unminified=False
|
||||
|
||||
# Server host
|
||||
# Use 'localhost' for local only
|
||||
# Use your ip to share over your network
|
||||
# Use '0.0.0.0' to listen to every network
|
||||
#
|
||||
#host='localhost'
|
||||
|
||||
# Server port
|
||||
#
|
||||
#port=57575
|
||||
|
||||
# Shell to launch at start (defaults to user shell)
|
||||
#
|
||||
#shell=None # shell='/bin/bash' for instance
|
||||
|
||||
# Motd, path to custom message of the day file
|
||||
#
|
||||
#motd='motd'
|
||||
|
||||
# Command to run instead of shell
|
||||
#
|
||||
#cmd=None # cmd='ls -l'
|
||||
|
||||
|
||||
# Unsecure mode
|
||||
# This mode use http without ssl and is therefore NOT RECOMMENDED
|
||||
# Please generate yourself a certificate using the butterfly.server.py command
|
||||
#
|
||||
#unsecure=False
|
||||
|
||||
# Force user login in unsecure mode
|
||||
#
|
||||
#login=False
|
||||
|
||||
# Force unicode width
|
||||
# This mode force every character to be the same width
|
||||
# Which can be useful in some case
|
||||
# But this breaks unicode display of varying width character
|
||||
#
|
||||
#force_unicode_width=False
|
||||
|
||||
# SSL version defaults to auto
|
||||
#
|
||||
#ssl_version=None
|
||||
@@ -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
|
||||
@@ -32,3 +36,35 @@ def text():
|
||||
yield
|
||||
sys.stdout.write('\x1bP')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
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,14 +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.web
|
||||
import tornado.websocket
|
||||
from collections import defaultdict
|
||||
from butterfly import url, Route, utils, __version__
|
||||
|
||||
from butterfly import Route, url, utils
|
||||
from butterfly.terminal import Terminal
|
||||
|
||||
|
||||
@@ -33,111 +41,140 @@ 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'/style.css')
|
||||
class Style(Route):
|
||||
|
||||
def get(self):
|
||||
default_style = os.path.join(
|
||||
os.path.dirname(__file__), 'static', 'main.css')
|
||||
@url(r'/theme/([^/]+)/style.css')
|
||||
class Theme(Route):
|
||||
|
||||
def get(self, theme):
|
||||
self.log.info('Getting style')
|
||||
css = utils.get_style()
|
||||
try:
|
||||
import sass
|
||||
sass.CompileError
|
||||
except Exception:
|
||||
self.log.error(
|
||||
'You must install libsass to use sass '
|
||||
'(pip install libsass)')
|
||||
return
|
||||
base_dir = self.get_theme_dir(theme)
|
||||
|
||||
style = None
|
||||
for ext in ['css', 'scss', 'sass']:
|
||||
probable_style = os.path.join(base_dir, 'style.%s' % ext)
|
||||
if os.path.exists(probable_style):
|
||||
style = probable_style
|
||||
|
||||
if not style:
|
||||
raise tornado.web.HTTPError(404)
|
||||
|
||||
sass_path = os.path.join(
|
||||
os.path.dirname(__file__), 'sass')
|
||||
|
||||
css = None
|
||||
try:
|
||||
css = sass.compile(filename=style, include_paths=[
|
||||
base_dir, sass_path])
|
||||
except sass.CompileError:
|
||||
self.log.error(
|
||||
'Unable to compile style (filename: %s, paths: %r) ' % (
|
||||
style, [base_dir, sass_path]), exc_info=True)
|
||||
if not style:
|
||||
raise tornado.web.HTTPError(500)
|
||||
|
||||
self.log.debug('Style ok')
|
||||
|
||||
self.set_header("Content-Type", "text/css")
|
||||
self.write(css)
|
||||
self.finish()
|
||||
|
||||
if css:
|
||||
self.write(css)
|
||||
else:
|
||||
with open(default_style) as s:
|
||||
|
||||
@url(r'/theme/([^/]+)/(.+)')
|
||||
class ThemeStatic(Route):
|
||||
def get(self, theme, name):
|
||||
if '..' in name:
|
||||
raise tornado.web.HTTPError(403)
|
||||
|
||||
base_dir = self.get_theme_dir(theme)
|
||||
|
||||
fn = os.path.normpath(os.path.join(base_dir, name))
|
||||
if not fn.startswith(base_dir):
|
||||
raise tornado.web.HTTPError(403)
|
||||
|
||||
if os.path.exists(fn):
|
||||
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)
|
||||
if data:
|
||||
self.write(data)
|
||||
else:
|
||||
break
|
||||
self.finish()
|
||||
|
||||
|
||||
@url(r'/theme/font/([^/]+)')
|
||||
class Font(Route):
|
||||
|
||||
def get(self, name):
|
||||
if not tornado.options.options.theme or not name:
|
||||
raise tornado.web.HTTPError(404)
|
||||
font = 'themes/%s/font/%s' % (
|
||||
tornado.options.options.theme,
|
||||
name)
|
||||
for fn in [
|
||||
'/etc/butterfly/%s' % font,
|
||||
os.path.expanduser('~/.butterfly/%s' % font)]:
|
||||
if os.path.exists(fn):
|
||||
ext = fn.split('.')[-1]
|
||||
self.set_header("Content-Type", "application/x-font-%s" % ext)
|
||||
with open(fn, 'rb') as s:
|
||||
while True:
|
||||
data = s.read(16384)
|
||||
if data:
|
||||
self.write(data)
|
||||
else:
|
||||
break
|
||||
self.finish()
|
||||
self.finish()
|
||||
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 = 10000
|
||||
# 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'
|
||||
@@ -147,126 +184,152 @@ 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(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):
|
||||
sessions = TermWebSocket.sessions.get(user.name)
|
||||
if sessions:
|
||||
sockets = sessions[session]
|
||||
for socket in sockets[:]:
|
||||
socket.on_close()
|
||||
socket.close()
|
||||
del sessions[session]
|
||||
|
||||
terminals = TermWebSocket.terminals.get(user.name)
|
||||
del terminals[session]
|
||||
|
||||
@classmethod
|
||||
def broadcast(cls, session, message, user):
|
||||
cls.history[session] += message
|
||||
if len(cls.history) > 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:
|
||||
session.write_message(message)
|
||||
if wsocket != emitter:
|
||||
wsocket.write_message(message)
|
||||
except Exception:
|
||||
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)
|
||||
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)
|
||||
if self.application.systemd and not len(TermWebSocket.sockets):
|
||||
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(wsockets)
|
||||
for session, wsockets in self.sessions.items()])):
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
@url(r'/sessions')
|
||||
class Sessions(Route):
|
||||
"""List available sessions"""
|
||||
@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"""
|
||||
|
||||
def get(self):
|
||||
if tornado.options.options.unsecure:
|
||||
raise tornado.web.HTTPError(403)
|
||||
@@ -277,5 +340,61 @@ class Sessions(Route):
|
||||
if not user:
|
||||
raise tornado.web.HTTPError(403)
|
||||
|
||||
return self.render(
|
||||
'list.html', sessions=TermWebSocket.sessions.get(user, []))
|
||||
self.set_header('Content-Type', 'application/json')
|
||||
self.write(tornado.escape.json_encode({
|
||||
'sessions': sorted(
|
||||
TermWebSocket.sessions),
|
||||
'user': user
|
||||
}))
|
||||
|
||||
|
||||
@url(r'/themes/list.json')
|
||||
class ThemesList(Route):
|
||||
"""Get the theme list"""
|
||||
|
||||
def get(self):
|
||||
|
||||
if os.path.exists(self.themes_dir):
|
||||
themes = [
|
||||
theme
|
||||
for theme in os.listdir(self.themes_dir)
|
||||
if os.path.isdir(os.path.join(self.themes_dir, theme)) and
|
||||
not theme.startswith('.')]
|
||||
else:
|
||||
themes = []
|
||||
|
||||
if os.path.exists(self.builtin_themes_dir):
|
||||
builtin_themes = [
|
||||
'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
|
||||
not theme.startswith('.')]
|
||||
else:
|
||||
builtin_themes = []
|
||||
|
||||
self.set_header('Content-Type', 'application/json')
|
||||
self.write(tornado.escape.json_encode({
|
||||
'themes': sorted(themes),
|
||||
'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) 2014 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,22 +18,19 @@
|
||||
|
||||
/* Here are the 16 "normal" colors for theming */
|
||||
|
||||
+termcolor(0, #2e3436) /* Black */
|
||||
+termcolor(1, #cc0000) /* Red */
|
||||
+termcolor(2, #4e9a06) /* Green */
|
||||
+termcolor(3, #c4a000) /* Yellow */
|
||||
+termcolor(4, #3465a4) /* Blue */
|
||||
+termcolor(5, #75507b) /* Magenta */
|
||||
+termcolor(6, #06989a) /* Cyan */
|
||||
+termcolor(7, #d3d7cf) /* White */
|
||||
+termcolor(8, #555753) /* Bright Black */
|
||||
+termcolor(9, #ef2929) /* Bright Red */
|
||||
+termcolor(10, #8ae234) /* Bright Green */
|
||||
+termcolor(11, #fce94f) /* Bright Yellow */
|
||||
+termcolor(12, #729fcf) /* Bright Blue */
|
||||
+termcolor(13, #ad7fa8) /* Bright Magenta */
|
||||
+termcolor(14, #34e2e2) /* Bright Cyan */
|
||||
+termcolor(15, #eeeeec) /* Bright White */
|
||||
|
||||
$bg: #110f13
|
||||
$fg: #f4ead5
|
||||
+termcolor(0, nth($colors, 1))
|
||||
+termcolor(1, nth($colors, 2))
|
||||
+termcolor(2, nth($colors, 3))
|
||||
+termcolor(3, nth($colors, 4))
|
||||
+termcolor(4, nth($colors, 5))
|
||||
+termcolor(5, nth($colors, 6))
|
||||
+termcolor(6, nth($colors, 7))
|
||||
+termcolor(7, nth($colors, 8))
|
||||
+termcolor(8, nth($colors, 9))
|
||||
+termcolor(9, nth($colors, 10))
|
||||
+termcolor(10, nth($colors, 11))
|
||||
+termcolor(11, nth($colors, 12))
|
||||
+termcolor(12, nth($colors, 13))
|
||||
+termcolor(13, nth($colors, 14))
|
||||
+termcolor(14, nth($colors, 15))
|
||||
+termcolor(15, nth($colors, 16))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2014 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 */
|
||||
@@ -15,11 +15,9 @@
|
||||
/* You should have received a copy of the GNU General Public License */
|
||||
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
|
||||
$fg: #fff !default
|
||||
$bg: #000 !default
|
||||
|
||||
/* Here are the 240 xterm colors */
|
||||
/* See http://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg */
|
||||
|
||||
$st: 00, 95, 135, 175, 215, 255
|
||||
|
||||
@for $i from 0 through 215
|
||||
@@ -32,5 +30,5 @@ $st: 00, 95, 135, 175, 215, 255
|
||||
$l: 8 + $i * 10
|
||||
+termcolor($i + 232, rgb($l, $l, $l))
|
||||
|
||||
+termcolor(256, $bg)
|
||||
+termcolor(257, $fg)
|
||||
+termcolor(256, $default-bg)
|
||||
+termcolor(257, $default-fg)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2014 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 */
|
||||
@@ -15,15 +15,14 @@
|
||||
/* You should have received a copy of the GNU General Public License */
|
||||
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
|
||||
$shadow: 0 !default
|
||||
$shadow-alpha: 0 !default
|
||||
|
||||
|
||||
=termcolor($i, $color)
|
||||
.bg-color-#{$i}
|
||||
background-color: $color
|
||||
&.reverse-video
|
||||
color: $color !important
|
||||
@if $color == transparent
|
||||
color: $reverse-transparent !important
|
||||
@else
|
||||
color: $color !important
|
||||
|
||||
.fg-color-#{$i}
|
||||
color: $color
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2014 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 */
|
||||
@@ -15,9 +15,6 @@
|
||||
/* You should have received a copy of the GNU General Public License */
|
||||
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
|
||||
$fg: #fff !default
|
||||
$shadow-alpha: 0 !default
|
||||
|
||||
.focus .cursor
|
||||
transition: 300ms
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2014 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,14 +17,16 @@
|
||||
|
||||
$weights: (ExtraLight 100) (Light 300) (Regular 400) (Medium 500) (Semibold 600) (Bold 700) (Black 900)
|
||||
|
||||
@each $weight in $weights
|
||||
$weight_name: nth($weight, 1)
|
||||
@if $font-family == "SourceCodePro"
|
||||
@each $weight in $weights
|
||||
$weight_name: nth($weight, 1)
|
||||
|
||||
@font-face
|
||||
font-family: "SourceCodePro"
|
||||
src: url("/static/fonts/SourceCodePro-#{$weight_name}.otf") format("woff")
|
||||
font-weight: nth($weight, 2)
|
||||
@font-face
|
||||
font-family: "SourceCodePro"
|
||||
src: url("fonts/SourceCodePro-#{$weight_name}.otf") format("woff")
|
||||
font-weight: nth($weight, 2)
|
||||
|
||||
body
|
||||
font-family: "SourceCodePro"
|
||||
line-height: 1.2
|
||||
font-family: $font-family
|
||||
font-size: $font-size
|
||||
line-height: $font-line-height
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2014 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,25 +18,95 @@
|
||||
html, body
|
||||
margin: 0
|
||||
padding: 0
|
||||
line-height: 1.2
|
||||
background-color: $bg
|
||||
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: $bg
|
||||
width: .75em
|
||||
background: $scroll-bg
|
||||
width: $scroll-width
|
||||
|
||||
&::-webkit-scrollbar-thumb
|
||||
background: rgba($fg, .1)
|
||||
background: $scroll-fg
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover
|
||||
background: rgba($fg, .15)
|
||||
background: $scroll-fg-hover
|
||||
|
||||
/* Pop ups */
|
||||
.hidden
|
||||
display: none !important
|
||||
|
||||
#popup
|
||||
position: fixed
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
form, > div
|
||||
padding: 1.5em
|
||||
background: $popup-bg
|
||||
color: $popup-fg
|
||||
font-size: $popup-fs
|
||||
|
||||
h2
|
||||
margin: 0 .5em .5em .5em
|
||||
select
|
||||
min-width: 300px
|
||||
padding: .5em
|
||||
width: 100%
|
||||
label
|
||||
display: block
|
||||
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) 2014 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
|
||||
|
||||
38
butterfly/sass/_styles.sass
Normal file
38
butterfly/sass/_styles.sass
Normal file
@@ -0,0 +1,38 @@
|
||||
/* *-* 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/>. */
|
||||
|
||||
/* Theses are the various imported style files
|
||||
/* THIS NEEDS the python `libsass` library to be installed.
|
||||
/* You can copy the imported files in the theme dir, they will be imported prioritarily.
|
||||
|
||||
/* You can change this file to import any webfont:
|
||||
@import font
|
||||
|
||||
/* You can comment / uncomment the following to enable/disable terminal effects.
|
||||
@import light_fx
|
||||
/* Comment this one to remove the blurry text:
|
||||
@import text_fx
|
||||
/* @import all_fx
|
||||
|
||||
@import colors
|
||||
/* The color theme is defined in this one:
|
||||
@import 16_colors
|
||||
@import 256_colors
|
||||
|
||||
@import layout
|
||||
@import cursor
|
||||
@import term_styles
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2014 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 */
|
||||
@@ -15,22 +15,30 @@
|
||||
/* You should have received a copy of the GNU General Public License */
|
||||
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
|
||||
|
||||
$fg: #fff !default
|
||||
$bg: #000 !default
|
||||
|
||||
.bold
|
||||
font-weight: bold
|
||||
|
||||
.underline
|
||||
text-decoration: underline
|
||||
|
||||
.italic
|
||||
font-style: italic
|
||||
|
||||
.faint
|
||||
opacity: .6
|
||||
|
||||
.crossed
|
||||
text-decoration: line-through
|
||||
|
||||
/* Not supported, emulated
|
||||
/* .blink
|
||||
/* text-decoration: blink
|
||||
.blink
|
||||
animation: blink 1s ease-in-out infinite
|
||||
|
||||
.blink-fast
|
||||
animation: blink 250ms ease-in-out infinite
|
||||
|
||||
@keyframes blink
|
||||
0%
|
||||
opacity: 1
|
||||
@@ -46,7 +54,8 @@ $bg: #000 !default
|
||||
color: $bg
|
||||
background-color: $fg
|
||||
|
||||
.blur .cursor.reverse-video
|
||||
.blur .cursor
|
||||
border: 1px solid $fg
|
||||
background: none
|
||||
|
||||
.nbsp
|
||||
|
||||
54
butterfly/sass/_variables.sass
Normal file
54
butterfly/sass/_variables.sass
Normal file
@@ -0,0 +1,54 @@
|
||||
/* *-* 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. */
|
||||
|
||||
/* Variables */
|
||||
|
||||
/** Font
|
||||
$font-family: "SourceCodePro" !default
|
||||
$font-size: 1em !default
|
||||
$font-line-height: 1.2 !default
|
||||
|
||||
/** Colors */
|
||||
/* Foreground */
|
||||
$fg: #f4ead5 !default
|
||||
/* Background */
|
||||
$bg: #110f13 !default
|
||||
|
||||
$default-bg: transparent !default
|
||||
$active-bg: transparent !default
|
||||
$default-fg: $fg !default
|
||||
|
||||
$reverse-transparent: $bg !default
|
||||
|
||||
/* 16 Colors in this orders: Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, Bright Black, Bright Red, Bright Green, Bright Yellow, Bright Blue, Bright Magenta, Bright Cyan, Bright White */
|
||||
$colors: #2e3436, #cc0000, #4e9a06, #c4a000, #3465a4, #75507b, #06989a, #d3d7cf, #555753, #ef2929, #8ae234, #fce94f, #729fcf, #ad7fa8, #34e2e2, #eeeeec !default
|
||||
|
||||
/** Text effects */
|
||||
|
||||
/* The shadow is the size of the blur (in px for instance)
|
||||
$shadow: 0 !default
|
||||
/* The shadow alpha is the opacity of the shadow
|
||||
$shadow-alpha: 0 !default
|
||||
|
||||
/** Scroll */
|
||||
$scroll-bg: $bg !default
|
||||
$scroll-fg: rgba($fg, .1) !default
|
||||
$scroll-fg-hover: rgba($fg, .1) !default
|
||||
$scroll-width: .75em !default
|
||||
|
||||
/** Popup */
|
||||
$popup-bg: rgba(127, 127, 127, .5) !default
|
||||
$popup-fg: $fg !default
|
||||
$popup-fs: 1em !default
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2014 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 */
|
||||
@@ -15,28 +15,12 @@
|
||||
/* You should have received a copy of the GNU General Public License */
|
||||
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
|
||||
|
||||
|
||||
/* Theses are the various imported style files
|
||||
/* You can put this file in /etc/butterfly/style.sass or ~/.butterfly/style.sass
|
||||
/* To customize the style of your terminal.
|
||||
/* THIS NEEDS the python `libsass` library to be installed.
|
||||
/* You can also copy the imported files in those dirs, they will be imported prioritarily.
|
||||
/* You can copy the imported files in the theme dir, they will be imported prioritarily.
|
||||
|
||||
/* You can change this file to import any webfont:
|
||||
@import font
|
||||
/* These a the default variables */
|
||||
@import variables
|
||||
|
||||
/* You can comment / uncomment the following to enable/disable terminal effects.
|
||||
@import light_fx
|
||||
/* Comment this one to remove the blurry text:
|
||||
@import text_fx
|
||||
/* @import all_fx
|
||||
|
||||
@import colors
|
||||
/* The color theme is defined in this one:
|
||||
@import 16_colors
|
||||
@import 256_colors
|
||||
|
||||
@import layout
|
||||
@import cursor
|
||||
@import term_styles
|
||||
/* These are all imported files */
|
||||
@import styles
|
||||
|
||||
@@ -1,24 +1,88 @@
|
||||
(function() {
|
||||
var Selection, alt, cancel, copy, ctrl, first, nextLeaf, 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; };
|
||||
|
||||
setAlarm = function(notification) {
|
||||
clean_ansi = function(data) {
|
||||
var c, i, out, state;
|
||||
if (data.indexOf('\x1b') < 0) {
|
||||
return data;
|
||||
}
|
||||
i = -1;
|
||||
out = '';
|
||||
state = 'normal';
|
||||
while (i < data.length - 1) {
|
||||
c = data.charAt(++i);
|
||||
switch (state) {
|
||||
case 'normal':
|
||||
if (c === '\x1b') {
|
||||
state = 'escaped';
|
||||
break;
|
||||
}
|
||||
out += c;
|
||||
break;
|
||||
case 'escaped':
|
||||
if (c === '[') {
|
||||
state = 'csi';
|
||||
break;
|
||||
}
|
||||
if (c === ']') {
|
||||
state = 'osc';
|
||||
break;
|
||||
}
|
||||
if ('#()%*+-./'.indexOf(c) >= 0) {
|
||||
i++;
|
||||
}
|
||||
state = 'normal';
|
||||
break;
|
||||
case 'csi':
|
||||
if ("?>!$\" '".indexOf(c) >= 0) {
|
||||
break;
|
||||
}
|
||||
if (('0' <= c && c <= '9')) {
|
||||
break;
|
||||
}
|
||||
if (c === ';') {
|
||||
break;
|
||||
}
|
||||
state = 'normal';
|
||||
break;
|
||||
case 'osc':
|
||||
if (c === "\x1b" || c === "\x07") {
|
||||
if (c === "\x1b") {
|
||||
i++;
|
||||
}
|
||||
state = 'normal';
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
setAlarm = function(notification, cond) {
|
||||
var alarm;
|
||||
alarm = function(data) {
|
||||
var note;
|
||||
var message, note, notif;
|
||||
message = clean_ansi(data.data.slice(1));
|
||||
if (cond !== null && !cond.test(message)) {
|
||||
return;
|
||||
}
|
||||
butterfly.body.classList.remove('alarm');
|
||||
note = "New activity on butterfly terminal [" + butterfly.title + "]";
|
||||
note = "Butterfly [" + butterfly.title + "]";
|
||||
if (notification) {
|
||||
new Notification(note, {
|
||||
body: data.data,
|
||||
notif = new Notification(note, {
|
||||
body: message,
|
||||
icon: '/static/images/favicon.png'
|
||||
});
|
||||
notif.onclick = function() {
|
||||
window.focus();
|
||||
return notif.close();
|
||||
};
|
||||
} else {
|
||||
alert(note + '\n' + data.data);
|
||||
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');
|
||||
};
|
||||
|
||||
@@ -34,27 +98,37 @@
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
var cond;
|
||||
if (!(e.altKey && e.keyCode === 65)) {
|
||||
return true;
|
||||
}
|
||||
cond = null;
|
||||
if (e.shiftKey) {
|
||||
cond = prompt('Ring alarm when encountering the following text: (can be a regexp)');
|
||||
if (!cond) {
|
||||
return;
|
||||
}
|
||||
cond = new RegExp(cond);
|
||||
}
|
||||
if (Notification && Notification.permission === 'default') {
|
||||
Notification.requestPermission(function() {
|
||||
return setAlarm(Notification.permission === 'granted');
|
||||
return setAlarm(Notification.permission === 'granted', cond);
|
||||
});
|
||||
} else {
|
||||
setAlarm(Notification.permission === 'granted');
|
||||
setAlarm(Notification.permission === 'granted', cond);
|
||||
}
|
||||
return cancel(e);
|
||||
});
|
||||
|
||||
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 = '';
|
||||
@@ -69,14 +143,232 @@
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
addEventListener('beforeunload', function(e) {
|
||||
if (!(butterfly.body.classList.contains('dead') || location.href.indexOf('session') > -1)) {
|
||||
return e.returnValue = 'This terminal is active and not in session. Are you sure you want to kill it?';
|
||||
}
|
||||
});
|
||||
|
||||
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.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');
|
||||
this.bound_click_maybe_close = this.click_maybe_close.bind(this);
|
||||
this.bound_key_maybe_close = this.key_maybe_close.bind(this);
|
||||
}
|
||||
|
||||
Popup.prototype.open = function(html) {
|
||||
this.el.innerHTML = html;
|
||||
this.el.classList.remove('hidden');
|
||||
addEventListener('click', this.bound_click_maybe_close);
|
||||
return addEventListener('keydown', this.bound_key_maybe_close);
|
||||
};
|
||||
|
||||
Popup.prototype.close = function() {
|
||||
removeEventListener('click', this.bound_click_maybe_close);
|
||||
removeEventListener('keydown', this.bound_key_maybe_close);
|
||||
this.el.classList.add('hidden');
|
||||
return this.el.innerHTML = '';
|
||||
};
|
||||
|
||||
Popup.prototype.click_maybe_close = function(e) {
|
||||
var t;
|
||||
t = e.target;
|
||||
while (t.parentElement) {
|
||||
if (Array.prototype.slice.call(this.el.children).indexOf(t) > -1) {
|
||||
return true;
|
||||
}
|
||||
t = t.parentElement;
|
||||
}
|
||||
this.close();
|
||||
return cancel(e);
|
||||
};
|
||||
|
||||
Popup.prototype.key_maybe_close = function(e) {
|
||||
if (e.keyCode !== 27) {
|
||||
return true;
|
||||
}
|
||||
this.close();
|
||||
return cancel(e);
|
||||
};
|
||||
|
||||
return Popup;
|
||||
|
||||
})();
|
||||
|
||||
popup = new Popup();
|
||||
|
||||
selection = null;
|
||||
|
||||
cancel = function(ev) {
|
||||
@@ -178,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;
|
||||
}
|
||||
}
|
||||
@@ -202,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
|
||||
@@ -263,7 +555,7 @@
|
||||
} else {
|
||||
node = needle.node;
|
||||
}
|
||||
text = node.textContent;
|
||||
text = node != null ? node.textContent : void 0;
|
||||
i = needle.offset;
|
||||
if (backward) {
|
||||
while (node) {
|
||||
@@ -276,7 +568,7 @@
|
||||
}
|
||||
}
|
||||
node = previousLeaf(node);
|
||||
text = node.textContent;
|
||||
text = node != null ? node.textContent : void 0;
|
||||
i = text.length;
|
||||
}
|
||||
} else {
|
||||
@@ -290,7 +582,7 @@
|
||||
}
|
||||
}
|
||||
node = nextLeaf(node);
|
||||
text = node.textContent;
|
||||
text = node != null ? node.textContent : void 0;
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
@@ -302,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;
|
||||
}
|
||||
@@ -339,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);
|
||||
}
|
||||
@@ -401,74 +694,120 @@
|
||||
return sel.modify('extend', 'forward', 'character');
|
||||
});
|
||||
|
||||
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);
|
||||
document.addEventListener('keydown', function(e) {
|
||||
var oReq;
|
||||
if (!(e.altKey && e.keyCode === 69)) {
|
||||
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;
|
||||
}
|
||||
oReq = new XMLHttpRequest();
|
||||
oReq.addEventListener('load', function() {
|
||||
var j, len, out, ref, response, session;
|
||||
response = JSON.parse(this.responseText);
|
||||
out = '<div>';
|
||||
out += '<h2>Session list</h2>';
|
||||
if (response.sessions.length === 0) {
|
||||
out += "No current session for user " + response.user;
|
||||
} else {
|
||||
out += '<ul>';
|
||||
ref = response.sessions;
|
||||
for (j = 0, len = ref.length; j < len; j++) {
|
||||
session = ref[j];
|
||||
out += "<li><a href=\"/session/" + session + "\">" + session + "</a></li>";
|
||||
}
|
||||
butterfly.keyDown(e);
|
||||
this.value = '0';
|
||||
ctrl = alt = false;
|
||||
return true;
|
||||
out += '</ul>';
|
||||
}
|
||||
butterfly.keyPress(e);
|
||||
first = false;
|
||||
this.value = '0';
|
||||
return true;
|
||||
out += '</div>';
|
||||
return popup.open(out);
|
||||
});
|
||||
oReq.open("GET", "/sessions/list.json");
|
||||
oReq.send();
|
||||
return cancel(e);
|
||||
});
|
||||
|
||||
_set_theme_href = function(href) {
|
||||
var img;
|
||||
document.getElementById('style').setAttribute('href', href);
|
||||
img = document.createElement('img');
|
||||
img.onerror = function() {
|
||||
return setTimeout((function() {
|
||||
return typeof butterfly !== "undefined" && butterfly !== null ? butterfly.resize() : void 0;
|
||||
}), 250);
|
||||
};
|
||||
return img.src = href;
|
||||
};
|
||||
|
||||
_theme = typeof localStorage !== "undefined" && localStorage !== null ? localStorage.getItem('theme') : void 0;
|
||||
|
||||
if (_theme) {
|
||||
_set_theme_href(_theme);
|
||||
}
|
||||
|
||||
this.set_theme = function(theme) {
|
||||
_theme = theme;
|
||||
if (typeof localStorage !== "undefined" && localStorage !== null) {
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
if (theme) {
|
||||
return _set_theme_href(theme);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
var oReq, style;
|
||||
if (!(e.altKey && e.keyCode === 83)) {
|
||||
return true;
|
||||
}
|
||||
if (e.shiftKey) {
|
||||
style = document.getElementById('style').getAttribute('href');
|
||||
style = style.split('?')[0];
|
||||
_set_theme_href(style + '?' + (new Date().getTime()));
|
||||
return cancel(e);
|
||||
}
|
||||
oReq = new XMLHttpRequest();
|
||||
oReq.addEventListener('load', function() {
|
||||
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;
|
||||
inner = "<form>\n <h2>Pick a theme:</h2>\n <select id=\"theme_list\">";
|
||||
option = function(url, theme) {
|
||||
inner += '<option ';
|
||||
if (_theme === url) {
|
||||
inner += 'selected ';
|
||||
}
|
||||
inner += "value=\"" + url + "\">";
|
||||
inner += theme;
|
||||
return inner += '</option>';
|
||||
};
|
||||
option("/static/main.css", 'default');
|
||||
if (themes.length) {
|
||||
inner += '<optgroup label="Local themes">';
|
||||
for (j = 0, len = themes.length; j < len; j++) {
|
||||
theme = themes[j];
|
||||
url = "/theme/" + theme + "/style.css";
|
||||
option(url, theme);
|
||||
}
|
||||
inner += '</optgroup>';
|
||||
}
|
||||
inner += '<optgroup label="Built-in themes">';
|
||||
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));
|
||||
}
|
||||
inner += '</optgroup>';
|
||||
inner += " </select>\n <label>You can create yours in " + response.dir + ".</label>\n</form>";
|
||||
popup.open(inner);
|
||||
theme_list = document.getElementById('theme_list');
|
||||
return theme_list.addEventListener('change', function() {
|
||||
return set_theme(theme_list.value);
|
||||
});
|
||||
});
|
||||
oReq.open("GET", "/themes/list.json");
|
||||
oReq.send();
|
||||
return cancel(e);
|
||||
});
|
||||
|
||||
}).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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
7
butterfly/static/main.min.js
vendored
7
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">
|
||||
@@ -10,15 +11,26 @@
|
||||
<link rel="shortcut icon" href="{{ static_url('images/favicon.png') }}">
|
||||
|
||||
<title>Butterfly</title>
|
||||
<link href="/style.css" rel="stylesheet">
|
||||
<link href="{{ static_url('main.css') }}" rel="stylesheet" id="style">
|
||||
</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>
|
||||
<script src="{{ static_url('main.%sjs' % (
|
||||
'' 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>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
{% from tornado.options import options %}
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Butterfly - A web terminal based on websocket and tornado">
|
||||
<meta name="author" content="Mounier Florian">
|
||||
<link rel="shortcut icon" href="{{ static_url('images/favicon.png') }}">
|
||||
|
||||
<title>Butterfly</title>
|
||||
<link href="/style.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Currently open butterfly sessions :</h1>
|
||||
<ul>
|
||||
{% for session in sessions %}
|
||||
<li><h2><a target="_blank" href="/session/{{ session }}">{{ session }}</a></h2></li>
|
||||
{% end %}
|
||||
</ul>
|
||||
</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: $ 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'))
|
||||
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,27 +164,27 @@ 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["PATH"] = '%s:%s' % (os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), 'bin')), env.get("PATH"))
|
||||
env["LOCATION"] = self.uri
|
||||
env['BUTTERFLY_PATH'] = os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), 'bin'))
|
||||
|
||||
try:
|
||||
tty = os.ttyname(0).replace('/dev/', '')
|
||||
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,11 +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:
|
||||
@@ -235,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)
|
||||
|
||||
@@ -269,15 +296,17 @@ 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:
|
||||
@@ -322,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]
|
||||
|
||||
1
butterfly/themes
Submodule
1
butterfly/themes
Submodule
Submodule butterfly/themes added at d640d1ec1c
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2014 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,57 +18,45 @@
|
||||
|
||||
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')
|
||||
|
||||
|
||||
def get_style():
|
||||
style = None
|
||||
def get_hex_ip_port(remote):
|
||||
ip, port = remote
|
||||
if ip.startswith('::ffff:'):
|
||||
ip = ip[len('::ffff:'):]
|
||||
splits = ip.split('.')
|
||||
if ':' not in ip and len(splits) == 4:
|
||||
# Must be an ipv4
|
||||
return '%02X%02X%02X%02X:%04X' % (
|
||||
int(splits[3]),
|
||||
int(splits[2]),
|
||||
int(splits[1]),
|
||||
int(splits[0]),
|
||||
int(port)
|
||||
)
|
||||
try:
|
||||
import ipaddress
|
||||
except ImportError:
|
||||
print('Please install ipaddress backport for ipv6 user detection')
|
||||
return ''
|
||||
|
||||
if tornado.options.options.theme:
|
||||
theme = 'themes/%s/' % tornado.options.options.theme
|
||||
else:
|
||||
theme = '/'
|
||||
# Endian reverse:
|
||||
ipv6_parts = ipaddress.IPv6Address(ip).exploded.split(':')
|
||||
for i in range(0, 8, 2):
|
||||
ipv6_parts[i], ipv6_parts[i + 1] = (
|
||||
ipv6_parts[i + 1][2:] + ipv6_parts[i + 1][:2],
|
||||
ipv6_parts[i][2:] + ipv6_parts[i][:2])
|
||||
|
||||
for ext in ['css', 'scss', 'sass']:
|
||||
for fn in [
|
||||
'/etc/butterfly/%sstyle' % theme,
|
||||
os.path.expanduser('~/.butterfly/%sstyle' % theme)]:
|
||||
if os.path.exists('%s.%s' % (fn, ext)):
|
||||
style = '%s.%s' % (fn, ext)
|
||||
|
||||
if style is None:
|
||||
return
|
||||
|
||||
if style.endswith('.scss') or style.endswith('.sass'):
|
||||
sass_path = os.path.join(
|
||||
os.path.dirname(__file__), 'sass')
|
||||
try:
|
||||
import sass
|
||||
except Exception:
|
||||
log.error('You must install libsass to use sass '
|
||||
'(pip install libsass)')
|
||||
return
|
||||
|
||||
try:
|
||||
return sass.compile(filename=style, include_paths=[
|
||||
theme, sass_path])
|
||||
except sass.CompileError:
|
||||
log.error(
|
||||
'Unable to compile style.scss (filename: %s, paths: %r) ' % (
|
||||
style, [theme, sass_path]), exc_info=True)
|
||||
return
|
||||
|
||||
with open(style) as s:
|
||||
return s.read()
|
||||
return ''.join(ipv6_parts) + ':%04X' % port
|
||||
|
||||
|
||||
def parse_cert(cert):
|
||||
@@ -149,9 +137,9 @@ class Socket(object):
|
||||
# If there is procfs, get as much info as we can
|
||||
if os.path.exists('/proc/net'):
|
||||
try:
|
||||
line = get_procfs_socket_line(self.remote_port)
|
||||
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)
|
||||
|
||||
@@ -165,7 +153,8 @@ class Socket(object):
|
||||
|
||||
@property
|
||||
def local(self):
|
||||
return self.remote_addr in ['127.0.0.1', '::1']
|
||||
return (self.remote_addr in ['127.0.0.1', '::1', '::ffff:127.0.0.1'] or
|
||||
self.local_addr == self.remote_addr)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Socket L: %s:%d R: %s:%d User: %r>' % (
|
||||
@@ -179,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
|
||||
@@ -192,33 +181,64 @@ def get_lsof_socket_line(addr, port):
|
||||
|
||||
|
||||
# Linux only socket line get
|
||||
def get_procfs_socket_line(port):
|
||||
def get_procfs_socket_line(hex_ip_port):
|
||||
fn = None
|
||||
if len(hex_ip_port) == 13: # ipv4
|
||||
fn = '/proc/net/tcp'
|
||||
elif len(hex_ip_port) == 37: # ipv6
|
||||
fn = '/proc/net/tcp6'
|
||||
if not fn:
|
||||
return
|
||||
try:
|
||||
with open('/proc/net/tcp') as k:
|
||||
with open(fn) as k:
|
||||
lines = k.readlines()
|
||||
for line in lines:
|
||||
# Look for local address with peer port
|
||||
if line.split()[1] == '0100007F:%X' % port:
|
||||
if line.split()[1] == hex_ip_port:
|
||||
# We got the socket
|
||||
return line.split()
|
||||
except Exception:
|
||||
log.debug('getting socket inet4 line fail', exc_info=True)
|
||||
|
||||
try:
|
||||
with open('/proc/net/tcp6') as k:
|
||||
lines = k.readlines()
|
||||
for line in lines:
|
||||
# Look for local address with peer port
|
||||
if line.split()[1] == (
|
||||
'00000000000000000000000001000000:%X' % port):
|
||||
# We got the socket
|
||||
return line.split()
|
||||
except Exception:
|
||||
log.debug('getting socket inet6 line fail', exc_info=True)
|
||||
log.debug('getting socket %s line fail' % fn, exc_info=True)
|
||||
|
||||
|
||||
# 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
|
||||
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():
|
||||
continue
|
||||
@@ -274,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):
|
||||
@@ -388,4 +409,5 @@ class AnsiColors(object):
|
||||
return '\x1b[0m'
|
||||
return ''
|
||||
|
||||
|
||||
ansi_colors = AnsiColors()
|
||||
|
||||
@@ -1,19 +1,70 @@
|
||||
setAlarm = (notification) ->
|
||||
clean_ansi = (data) ->
|
||||
# Fast ansi clean (not complete)
|
||||
|
||||
if data.indexOf('\x1b') < 0
|
||||
return data
|
||||
i = -1
|
||||
out = ''
|
||||
state = 'normal'
|
||||
while i < data.length - 1
|
||||
c = data.charAt ++i
|
||||
switch state
|
||||
when 'normal'
|
||||
if c is '\x1b'
|
||||
state = 'escaped'
|
||||
break
|
||||
out += c
|
||||
|
||||
when 'escaped'
|
||||
if c is '['
|
||||
state = 'csi'
|
||||
break
|
||||
|
||||
if c is ']'
|
||||
state = 'osc'
|
||||
break
|
||||
|
||||
if '#()%*+-./'.indexOf(c) >= 0
|
||||
i++
|
||||
state = 'normal'
|
||||
|
||||
when 'csi'
|
||||
if "?>!$\" '".indexOf(c) >= 0
|
||||
break
|
||||
if '0' <= c <= '9'
|
||||
break
|
||||
break if c is ';'
|
||||
state = 'normal'
|
||||
when 'osc'
|
||||
if c is "\x1b" or c is "\x07"
|
||||
i++ if c is "\x1b"
|
||||
state = 'normal'
|
||||
|
||||
return out
|
||||
|
||||
|
||||
setAlarm = (notification, cond) ->
|
||||
alarm = (data) ->
|
||||
message = clean_ansi data.data.slice(1)
|
||||
return if cond isnt null and not cond.test(message)
|
||||
|
||||
butterfly.body.classList.remove 'alarm'
|
||||
note = "New activity on butterfly terminal [#{ butterfly.title }]"
|
||||
note = "Butterfly [#{ butterfly.title }]"
|
||||
|
||||
if notification
|
||||
new Notification(
|
||||
notif = new Notification(
|
||||
note,
|
||||
body: data.data,
|
||||
body: message,
|
||||
icon: '/static/images/favicon.png')
|
||||
notif.onclick = ->
|
||||
window.focus()
|
||||
notif.close()
|
||||
else
|
||||
alert(note + '\n' + data.data)
|
||||
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'
|
||||
|
||||
|
||||
@@ -27,10 +78,17 @@ cancel = (ev) ->
|
||||
document.addEventListener 'keydown', (e) ->
|
||||
return true unless e.altKey and e.keyCode is 65
|
||||
|
||||
cond = null
|
||||
if e.shiftKey
|
||||
cond = prompt('Ring alarm when encountering the following text:
|
||||
(can be a regexp)')
|
||||
return unless cond
|
||||
cond = new RegExp(cond)
|
||||
|
||||
if Notification and Notification.permission is 'default'
|
||||
Notification.requestPermission ->
|
||||
setAlarm(Notification.permission is 'granted')
|
||||
setAlarm(Notification.permission is 'granted', cond)
|
||||
else
|
||||
setAlarm(Notification.permission is 'granted')
|
||||
setAlarm(Notification.permission is 'granted', cond)
|
||||
|
||||
cancel(e)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2014 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()
|
||||
|
||||
5
coffees/ext/close_confirm.coffee
Normal file
5
coffees/ext/close_confirm.coffee
Normal file
@@ -0,0 +1,5 @@
|
||||
addEventListener 'beforeunload', (e) ->
|
||||
unless (butterfly.body.classList.contains('dead') or
|
||||
location.href.indexOf('session') > -1)
|
||||
e.returnValue = 'This terminal is active and not in session.
|
||||
Are you sure you want to kill it?'
|
||||
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
|
||||
4
coffees/ext/new_term.coffee
Normal file
4
coffees/ext/new_term.coffee
Normal file
@@ -0,0 +1,4 @@
|
||||
document.addEventListener 'keydown', (e) ->
|
||||
return true unless e.altKey and e.keyCode is 79
|
||||
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
|
||||
36
coffees/ext/popup.coffee
Normal file
36
coffees/ext/popup.coffee
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
class Popup
|
||||
constructor: ->
|
||||
@el = document.getElementById('popup')
|
||||
@bound_click_maybe_close = @click_maybe_close.bind(@)
|
||||
@bound_key_maybe_close = @key_maybe_close.bind(@)
|
||||
|
||||
open: (html) ->
|
||||
@el.innerHTML = html
|
||||
@el.classList.remove 'hidden'
|
||||
|
||||
addEventListener 'click', @bound_click_maybe_close
|
||||
addEventListener 'keydown', @bound_key_maybe_close
|
||||
|
||||
close: ->
|
||||
removeEventListener 'click', @bound_click_maybe_close
|
||||
removeEventListener 'keydown', @bound_key_maybe_close
|
||||
|
||||
@el.classList.add 'hidden'
|
||||
@el.innerHTML = ''
|
||||
|
||||
click_maybe_close: (e) ->
|
||||
t = e.target
|
||||
while t.parentElement
|
||||
return true if Array.prototype.slice.call(@el.children).indexOf(t) > -1
|
||||
t = t.parentElement
|
||||
@close()
|
||||
cancel e
|
||||
|
||||
key_maybe_close: (e) ->
|
||||
return true unless e.keyCode is 27
|
||||
@close()
|
||||
cancel e
|
||||
|
||||
popup = new Popup()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2014 Florian Mounier
|
||||
# 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
|
||||
@@ -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
|
||||
@@ -148,7 +149,7 @@ class Selection
|
||||
else
|
||||
node = needle.node
|
||||
|
||||
text = node.textContent
|
||||
text = node?.textContent
|
||||
i = needle.offset
|
||||
if backward
|
||||
while node
|
||||
@@ -156,7 +157,7 @@ class Selection
|
||||
if text[--i].match til
|
||||
return node: node, offset: i + 1
|
||||
node = previousLeaf node
|
||||
text = node.textContent
|
||||
text = node?.textContent
|
||||
i = text.length
|
||||
else
|
||||
while node
|
||||
@@ -164,7 +165,7 @@ class Selection
|
||||
if text[i++].match til
|
||||
return node: node, offset: i - 1
|
||||
node = nextLeaf node
|
||||
text = node.textContent
|
||||
text = node?.textContent
|
||||
i = 0
|
||||
|
||||
return needle
|
||||
@@ -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
|
||||
|
||||
22
coffees/ext/sessions.coffee
Normal file
22
coffees/ext/sessions.coffee
Normal file
@@ -0,0 +1,22 @@
|
||||
document.addEventListener 'keydown', (e) ->
|
||||
return true unless e.altKey and e.keyCode is 69
|
||||
oReq = new XMLHttpRequest()
|
||||
oReq.addEventListener 'load', ->
|
||||
response = JSON.parse(@responseText)
|
||||
out = '<div>'
|
||||
out += '<h2>Session list</h2>'
|
||||
if response.sessions.length is 0
|
||||
out += "No current session for user #{response.user}"
|
||||
else
|
||||
out += '<ul>'
|
||||
for session in response.sessions
|
||||
out += "<li><a href=\"/session/#{session}\">#{session}</a></li>"
|
||||
out += '</ul>'
|
||||
|
||||
out += '</div>'
|
||||
|
||||
popup.open out
|
||||
|
||||
oReq.open("GET", "/sessions/list.json")
|
||||
oReq.send()
|
||||
cancel e
|
||||
80
coffees/ext/theme.coffee
Normal file
80
coffees/ext/theme.coffee
Normal file
@@ -0,0 +1,80 @@
|
||||
_set_theme_href = (href) ->
|
||||
document.getElementById('style').setAttribute('href', href)
|
||||
img = document.createElement('img')
|
||||
img.onerror = ->
|
||||
setTimeout (-> butterfly?.resize()), 250
|
||||
img.src = href
|
||||
|
||||
_theme = localStorage?.getItem('theme')
|
||||
_set_theme_href(_theme) if _theme
|
||||
|
||||
@set_theme = (theme) ->
|
||||
_theme = theme
|
||||
localStorage?.setItem('theme', theme)
|
||||
_set_theme_href(theme) if theme
|
||||
|
||||
document.addEventListener 'keydown', (e) ->
|
||||
return true unless e.altKey and e.keyCode is 83
|
||||
if e.shiftKey
|
||||
style = document.getElementById('style').getAttribute('href')
|
||||
style = style.split('?')[0]
|
||||
_set_theme_href style + '?' + (new Date().getTime())
|
||||
return cancel(e)
|
||||
|
||||
|
||||
oReq = new XMLHttpRequest()
|
||||
oReq.addEventListener 'load', ->
|
||||
response = JSON.parse(@responseText)
|
||||
builtin_themes = response.builtin_themes
|
||||
themes = response.themes
|
||||
|
||||
# if themes.length is 0
|
||||
# alert("No themes found in #{response.dir}.\n
|
||||
# Please install themes with butterfly.server.py --install-themes")
|
||||
# return
|
||||
|
||||
inner = """
|
||||
<form>
|
||||
<h2>Pick a theme:</h2>
|
||||
<select id="theme_list">
|
||||
"""
|
||||
option = (url, theme) ->
|
||||
inner += '<option '
|
||||
|
||||
if _theme is url
|
||||
inner += 'selected '
|
||||
|
||||
inner += "value=\"#{url}\">"
|
||||
inner += theme
|
||||
inner += '</option>'
|
||||
|
||||
option "/static/main.css", 'default'
|
||||
|
||||
if themes.length
|
||||
inner += '<optgroup label="Local themes">'
|
||||
for theme in themes
|
||||
url = "/theme/#{theme}/style.css"
|
||||
option url, theme
|
||||
inner += '</optgroup>'
|
||||
|
||||
inner += '<optgroup label="Built-in themes">'
|
||||
for theme in builtin_themes
|
||||
url = "/theme/#{theme}/style.css"
|
||||
option url, theme.slice('built-in-'.length)
|
||||
inner += '</optgroup>'
|
||||
|
||||
inner += """
|
||||
</select>
|
||||
<label>You can create yours in #{response.dir}.</label>
|
||||
</form>
|
||||
"""
|
||||
popup.open inner
|
||||
|
||||
theme_list = document.getElementById('theme_list')
|
||||
|
||||
theme_list.addEventListener 'change', -> set_theme theme_list.value
|
||||
|
||||
oReq.open("GET", "/themes/list.json")
|
||||
oReq.send()
|
||||
|
||||
cancel e
|
||||
@@ -1,80 +0,0 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2014 Florian Mounier
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
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) 2014 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,98 +19,107 @@ cols = rows = null
|
||||
quit = false
|
||||
openTs = (new Date()).getTime()
|
||||
|
||||
ws =
|
||||
shell: null
|
||||
ctl: null
|
||||
|
||||
$ = document.querySelectorAll.bind(document)
|
||||
|
||||
document.addEventListener 'DOMContentLoaded', ->
|
||||
|
||||
send = (data) ->
|
||||
ws.send 'S' + data
|
||||
|
||||
ctl = (type, args...) ->
|
||||
params = args.join(',')
|
||||
if type == 'Resize'
|
||||
ws.send 'R' + params
|
||||
term = null
|
||||
|
||||
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
|
||||
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) ->
|
||||
clearTimeout t_queue if t_queue
|
||||
queue += e.data
|
||||
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')
|
||||
, 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
|
||||
open('','_self').close()
|
||||
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()
|
||||
|
||||
term = new Terminal document.body, send, ctl
|
||||
addEventListener 'beforeunload', ->
|
||||
if not quit
|
||||
'This will exit the terminal session'
|
||||
|
||||
term.ws = ws
|
||||
window.butterfly = term
|
||||
|
||||
|
||||
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.9.3",
|
||||
"grunt": "^0.4.5",
|
||||
"grunt-coffeelint": "0.0.13",
|
||||
"grunt-contrib-coffee": "^0.13.0",
|
||||
"grunt-contrib-cssmin": "^0.12.2",
|
||||
"grunt-contrib-uglify": "^0.9.1",
|
||||
"grunt-contrib-watch": "^0.6.1",
|
||||
"grunt-sass": "^0.18.1"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
43
scripts/butterfly
Executable file
43
scripts/butterfly
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
if (os.getenv('COLORTERM', '') != 'butterfly' and
|
||||
len(sys.argv) == 1) or (
|
||||
os.getenv('COLORTERM', '') == 'butterfly' and
|
||||
len(sys.argv) > 1 and sys.argv[1] == 'run'):
|
||||
os.execvp('butterfly.server.py', [
|
||||
'butterfly', '--unsecure', '--port=0', '--one-shot'])
|
||||
|
||||
path = os.getenv('BUTTERFLY_PATH')
|
||||
if not path:
|
||||
try:
|
||||
import butterfly
|
||||
path = os.path.join(
|
||||
os.path.dirname(butterfly.__file__), 'bin')
|
||||
except Exception:
|
||||
pass
|
||||
os.putenv('BUTTERFLY_PATH', path)
|
||||
if path is None:
|
||||
print("Can't get butterfly path. Aborting.")
|
||||
sys.exit(1)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
add_help=False,
|
||||
description='Butterfly launcher. Please specify a command')
|
||||
parser.add_argument('-h', '--help', action="store_true",
|
||||
help="show this help message and exit")
|
||||
parser.add_argument(
|
||||
'command',
|
||||
nargs='?',
|
||||
choices=[x[:-3] for x in os.listdir(path) if x.endswith('.py')])
|
||||
|
||||
args, _ = parser.parse_known_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
else:
|
||||
file_ = os.path.join(path, '%s.py' % args.command)
|
||||
sys.argv = sys.argv[1:]
|
||||
exec(compile(open(file_).read(), file_, 'exec'))
|
||||
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
|
||||
45
setup.py
45
setup.py
@@ -5,29 +5,37 @@
|
||||
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.server.py', 'scripts/butterfly', 'scripts/b'],
|
||||
packages=['butterfly'],
|
||||
install_requires=["tornado>=3.2", "pyOpenSSL", 'tornado_systemd'],
|
||||
install_requires=["tornado>=3.2", "pyOpenSSL"],
|
||||
extras_require={
|
||||
'themes': ["libsass"],
|
||||
'systemd': ['tornado_systemd'],
|
||||
'lint': ['pytest', 'pytest-flake8', 'pytest-isort']
|
||||
},
|
||||
package_data={
|
||||
'butterfly': [
|
||||
'sass/*.sass',
|
||||
'themes/*.*',
|
||||
'themes/*/*.*',
|
||||
'themes/*/*/*.*',
|
||||
'static/fonts/*',
|
||||
'static/images/favicon.png',
|
||||
'static/main.css',
|
||||
@@ -35,16 +43,15 @@ options = dict(
|
||||
'static/*.min.js',
|
||||
'templates/index.html',
|
||||
'bin/*',
|
||||
'templates/motd'
|
||||
'templates/motd',
|
||||
'butterfly.conf.default'
|
||||
]
|
||||
},
|
||||
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