mirror of
https://github.com/paradoxxxzero/butterfly.git
synced 2026-06-10 06:14:39 +00:00
Compare commits
223 Commits
domscroll_
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da79ffe04b | ||
|
|
c348e1f285 | ||
|
|
91d52ed6ae | ||
|
|
06751c68f9 | ||
|
|
a9854e9136 | ||
|
|
039c730409 | ||
|
|
82676862ca | ||
|
|
5b6b61286d | ||
|
|
f32cb4d358 | ||
|
|
ad155f1f17 | ||
|
|
9e1045de9b | ||
|
|
db3d37f6fe | ||
|
|
611f2e30d6 | ||
|
|
1984e4b869 | ||
|
|
f58ea904b3 | ||
|
|
af0f4d20fe | ||
|
|
10b5ce3bcc | ||
|
|
a0287946d9 | ||
|
|
fbd71d55ef | ||
|
|
0ac8437387 | ||
|
|
866b56b682 | ||
|
|
4d87059872 | ||
|
|
5bbe456496 | ||
|
|
5b9cc257a8 | ||
|
|
34b6287e0c | ||
|
|
41ee5fb843 | ||
|
|
ae6b36fa89 | ||
|
|
cfda54a724 | ||
|
|
033169ab08 | ||
|
|
920c435b00 | ||
|
|
27e6aa8a5d | ||
|
|
92633f52ce | ||
|
|
f5f854964b | ||
|
|
55528fdf91 | ||
|
|
9eae13486e | ||
|
|
79bd074dae | ||
|
|
7b0ba2bfe7 | ||
|
|
db17b9d8ac | ||
|
|
b5de82bfcf | ||
|
|
13dbe0434c | ||
|
|
ef0057c23f | ||
|
|
6bc8e1438f | ||
|
|
8856ea9dc4 | ||
|
|
4edb2d269f | ||
|
|
272891470c | ||
|
|
574b3dc74b | ||
|
|
269dd2b618 | ||
|
|
0625e05cbb | ||
|
|
6b1101bc45 | ||
|
|
3e6d0b203f | ||
|
|
8189598dd6 | ||
|
|
4a8b5f2147 | ||
|
|
f9a1ff4dea | ||
|
|
96d88a5e91 | ||
|
|
bdc1c7a80d | ||
|
|
eacfdcd52f | ||
|
|
ed347e2bd0 | ||
|
|
3228e8c204 | ||
|
|
b9c991e3b6 | ||
|
|
8ad12c2379 | ||
|
|
2aa237ef12 | ||
|
|
40496eb9d1 | ||
|
|
ffd19b8162 | ||
|
|
6663568500 | ||
|
|
3a09c47ef0 | ||
|
|
41ab0f36ff | ||
|
|
70e00ac696 | ||
|
|
70369a0b32 | ||
|
|
8c20ffb943 | ||
|
|
729c768dc2 | ||
|
|
17f8c1d1c9 | ||
|
|
964fd07143 | ||
|
|
8553bbd0cb | ||
|
|
f494541652 | ||
|
|
dd6c917462 | ||
|
|
9e03e24764 | ||
|
|
6b5f3ac76f | ||
|
|
a36579bb12 | ||
|
|
e4ce69a967 | ||
|
|
b0e1f37cac | ||
|
|
da659b7526 | ||
|
|
08ecb4d0d2 | ||
|
|
3624962d3c | ||
|
|
b9f1727f1e | ||
|
|
5a7c4da0b1 | ||
|
|
fa2b9d2bee | ||
|
|
3bb6da1eae | ||
|
|
6c827206f7 | ||
|
|
fdeba5a5d4 | ||
|
|
d0eb37765a | ||
|
|
8dffb02980 | ||
|
|
15ebdf6907 | ||
|
|
6e29c702e3 | ||
|
|
c3ad2f342a | ||
|
|
7d7f05e164 | ||
|
|
64a8480938 | ||
|
|
0142ec0a16 | ||
|
|
97d435ce18 | ||
|
|
4b3a5e1ae6 | ||
|
|
9fcc156257 | ||
|
|
2887f6e25a | ||
|
|
ffe8945c09 | ||
|
|
a3e78112a6 | ||
|
|
e5eb7050e8 | ||
|
|
b72da2e4ef | ||
|
|
2d3bed2fef | ||
|
|
cc510500a5 | ||
|
|
1ec50810f9 | ||
|
|
524e578fca | ||
|
|
bce9f99b0b | ||
|
|
9bcc989149 | ||
|
|
1d324ed243 | ||
|
|
3c2bf35b09 | ||
|
|
fe258f44f8 | ||
|
|
1f9d263ad7 | ||
|
|
fe01ffb2b4 | ||
|
|
ac7e9bef8e | ||
|
|
503de38429 | ||
|
|
7ebb122221 | ||
|
|
ec25edb657 | ||
|
|
52714d81ab | ||
|
|
c048f1a4e6 | ||
|
|
c0e2d8959b | ||
|
|
5c054ca290 | ||
|
|
9168878d92 | ||
|
|
056fbc02b1 | ||
|
|
571f07946d | ||
|
|
e09bab810c | ||
|
|
efb019ed00 | ||
|
|
34d2711aa1 | ||
|
|
115190446b | ||
|
|
c8931c6135 | ||
|
|
ab7880779d | ||
|
|
f5724cc39d | ||
|
|
33d4051fca | ||
|
|
28ebf9d8a2 | ||
|
|
5714b97c77 | ||
|
|
a9c35d91f1 | ||
|
|
573b4f1c1b | ||
|
|
856aac2bcb | ||
|
|
e789622b7e | ||
|
|
84c4ff9414 | ||
|
|
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 | ||
|
|
6d98a5c5ac | ||
|
|
6c5cbeaca5 | ||
|
|
5aa697381a | ||
|
|
ed03f94c84 | ||
|
|
97de0cc46c | ||
|
|
6e5edde6dc | ||
|
|
81c61ec466 | ||
|
|
95ded4370d | ||
|
|
4e66196d65 | ||
|
|
8c3c780b12 | ||
|
|
d99cffabc4 | ||
|
|
a08cff653d | ||
|
|
354280bbda | ||
|
|
89a7e05f55 | ||
|
|
7d8f3b2845 | ||
|
|
060a2666b5 | ||
|
|
81d9fea01a | ||
|
|
3fa14e9718 | ||
|
|
78e3050387 | ||
|
|
324b6aa020 | ||
|
|
326a42b7f1 | ||
|
|
5fea2d9294 | ||
|
|
807d40bf9f | ||
|
|
5aac9886c5 | ||
|
|
1fafe99fd1 | ||
|
|
cee1983ca7 | ||
|
|
38980afe50 | ||
|
|
04ff7aea62 | ||
|
|
da3e9237be | ||
|
|
c345367d6b | ||
|
|
923e49565b | ||
|
|
f9b4a7a8eb | ||
|
|
3d14bce231 | ||
|
|
2b5ea8dc9a | ||
|
|
5bdcc8dd71 | ||
|
|
e48f029c68 | ||
|
|
10f364f693 | ||
|
|
4492b59e99 | ||
|
|
790a4b8072 | ||
|
|
13cc6b52e7 | ||
|
|
9a58059c1d | ||
|
|
23c905e05e | ||
|
|
809136c7d7 | ||
|
|
a2ab676451 | ||
|
|
5822ba2114 | ||
|
|
e86ff5a93a | ||
|
|
23ffeb764e | ||
|
|
683d325522 |
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
.gitignore
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
README.md
|
||||
butterfly.png
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,7 +1,13 @@
|
||||
*.crt
|
||||
*.key
|
||||
*.p12
|
||||
*.pyc
|
||||
node_modules/
|
||||
*.src.coffee
|
||||
*.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
|
||||
```
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/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))
|
||||
8
bin/bcat
8
bin/bcat
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
import base64
|
||||
from butterfly.escapes import image
|
||||
|
||||
with image():
|
||||
with open(sys.argv[1], 'rb') as f:
|
||||
print(base64.b64encode(f.read()).decode('ascii'))
|
||||
32
bin/ils
32
bin/ils
@@ -1,32 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#Depends: pillow
|
||||
#Broken: Too slow !
|
||||
from PIL import Image
|
||||
import os
|
||||
import mimetypes
|
||||
import base64
|
||||
import io
|
||||
print('\x1bP;HTML|')
|
||||
|
||||
out = ''
|
||||
|
||||
for f in os.listdir(os.getcwd()):
|
||||
mime = mimetypes.guess_type(f)[0]
|
||||
if 'image' in (mime or ''):
|
||||
# try:
|
||||
with open(f, 'rb') as buf:
|
||||
# im = Image.open(f)
|
||||
# im.thumbnail((100, 100), Image.ANTIALIAS)
|
||||
# buf = io.BytesIO()
|
||||
# im.save(buf, im.format)
|
||||
# buf.seek(0)
|
||||
out += '<img width="200" height="100" src="data:%s;base64,%s" alt="%s" />' % (
|
||||
mime,
|
||||
base64.b64encode(buf.read()).decode('ascii'),
|
||||
f)
|
||||
# except Exception:
|
||||
# pass
|
||||
|
||||
print(out)
|
||||
|
||||
print('\x1bP')
|
||||
13
bin/month
13
bin/month
@@ -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')
|
||||
4
bin/nt
4
bin/nt
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import webbrowser
|
||||
webbrowser.open('%swd%s' % (os.getenv('LOCATION'), os.getcwd()))
|
||||
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,70 +20,142 @@
|
||||
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
|
||||
|
||||
|
||||
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("allow_html_escapes", default=False,
|
||||
help="Allow use of HTML escapes. "
|
||||
"Really unsafe as it is now.")
|
||||
tornado.options.define("login", default=True,
|
||||
tornado.options.define("i_hereby_declare_i_dont_want_any_security_whatsoever",
|
||||
default=False,
|
||||
help="Remove all security and warnings. There are some "
|
||||
"use cases for that. Use this if you really know what "
|
||||
"you are doing.")
|
||||
tornado.options.define("login", default=False,
|
||||
help="Use login screen at start")
|
||||
tornado.options.define("pam_profile", default="", type=str,
|
||||
help="When --login=True provided and running as ROOT, "
|
||||
"use PAM with the specified PAM profile for "
|
||||
"authentication and then execute the user's default "
|
||||
"shell. Will override --shell.")
|
||||
tornado.options.define("force_unicode_width",
|
||||
default=False,
|
||||
help="Force all unicode characters to the same width."
|
||||
"Useful for avoiding layout mess.")
|
||||
tornado.options.define("ssl_version", default=None,
|
||||
help="SSL protocol version")
|
||||
tornado.options.define("generate_certs", default=False,
|
||||
help="Generate butterfly certificates")
|
||||
tornado.options.define("generate_current_user_pkcs", default=False,
|
||||
help="Generate current user pfx for client "
|
||||
"authentication")
|
||||
tornado.options.define("generate_user_pkcs", default='',
|
||||
help="Generate user pfx for client authentication")
|
||||
tornado.options.define("unminified", default=False,
|
||||
help="Use the unminified js (for development only)")
|
||||
help="Generate user pfx for client authentication "
|
||||
"(Must be root to create for another user)")
|
||||
tornado.options.define("uri_root_path", default='',
|
||||
help="Sets the servier root path: "
|
||||
"example.com/<uri_root_path>/static/")
|
||||
|
||||
|
||||
if os.getuid() == 0:
|
||||
ev = os.getenv('XDG_CONFIG_DIRS', '/etc')
|
||||
else:
|
||||
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. "
|
||||
"Contains the same options as command line.")
|
||||
|
||||
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
|
||||
|
||||
import logging
|
||||
for logger in ('tornado.access', 'tornado.application',
|
||||
'tornado.general', 'butterfly'):
|
||||
level = logging.WARNING
|
||||
if tornado.options.options.debug:
|
||||
if options.debug:
|
||||
level = logging.INFO
|
||||
if tornado.options.options.more:
|
||||
if options.more:
|
||||
level = logging.DEBUG
|
||||
logging.getLogger(logger).setLevel(level)
|
||||
|
||||
log = logging.getLogger('butterfly')
|
||||
log.info('Starting server')
|
||||
|
||||
host = tornado.options.options.host
|
||||
port = tornado.options.options.port
|
||||
host = options.host
|
||||
port = options.port
|
||||
|
||||
if os.getuid() == 0:
|
||||
ssl_dir = os.path.join(os.path.abspath(os.sep), 'etc', 'butterfly', 'ssl')
|
||||
else:
|
||||
ssl_dir = os.path.join(os.path.expanduser('~'), '.butterfly', 'ssl')
|
||||
if options.i_hereby_declare_i_dont_want_any_security_whatsoever:
|
||||
options.unsecure = True
|
||||
|
||||
if not os.path.exists(ssl_dir):
|
||||
os.makedirs(ssl_dir)
|
||||
|
||||
if not os.path.exists(options.ssl_dir):
|
||||
os.makedirs(options.ssl_dir)
|
||||
|
||||
|
||||
def to_abs(file):
|
||||
return os.path.join(ssl_dir, 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',
|
||||
@@ -110,7 +182,11 @@ def read(file):
|
||||
with open(file, 'rb') as fd:
|
||||
return fd.read()
|
||||
|
||||
if tornado.options.options.generate_certs:
|
||||
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)
|
||||
|
||||
@@ -119,6 +195,7 @@ if tornado.options.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)
|
||||
@@ -126,7 +203,22 @@ if tornado.options.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.sign(ca_pk, 'sha1')
|
||||
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))
|
||||
write(ca_key, crypto.dump_privatekey(crypto.FILETYPE_PEM, ca_pk))
|
||||
@@ -139,14 +231,30 @@ if tornado.options.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
|
||||
server_cert.gmtime_adj_notAfter(315360000) # to 10y
|
||||
server_cert.set_issuer(ca_cert.get_subject()) # Signed by ca
|
||||
server_cert.set_pubkey(server_pk)
|
||||
server_cert.sign(ca_pk, 'sha1')
|
||||
server_cert.sign(ca_pk, 'sha512')
|
||||
|
||||
write(cert % host, crypto.dump_certificate(
|
||||
crypto.FILETYPE_PEM, server_cert))
|
||||
@@ -159,13 +267,29 @@ if tornado.options.options.generate_certs:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if tornado.options.options.generate_user_pkcs:
|
||||
if (options.generate_current_user_pkcs or
|
||||
options.generate_user_pkcs):
|
||||
from butterfly import utils
|
||||
try:
|
||||
current_user = utils.User()
|
||||
except Exception:
|
||||
current_user = None
|
||||
|
||||
from OpenSSL import crypto
|
||||
if not all(map(os.path.exists, [ca, ca_key])):
|
||||
print('Please generate certificates using --generate-certs before')
|
||||
sys.exit(1)
|
||||
|
||||
user = tornado.options.options.generate_user_pkcs
|
||||
if options.generate_current_user_pkcs:
|
||||
user = current_user.name
|
||||
else:
|
||||
user = options.generate_user_pkcs
|
||||
|
||||
if user != current_user.name and current_user.uid != 0:
|
||||
print('Cannot create certificate for another user with '
|
||||
'current privileges.')
|
||||
sys.exit(1)
|
||||
|
||||
ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, read(ca))
|
||||
ca_pk = crypto.load_privatekey(crypto.FILETYPE_PEM, read(ca_key))
|
||||
|
||||
@@ -173,6 +297,7 @@ if tornado.options.options.generate_user_pkcs:
|
||||
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)
|
||||
@@ -180,8 +305,8 @@ if tornado.options.options.generate_user_pkcs:
|
||||
client_cert.gmtime_adj_notAfter(315360000) # to 10y
|
||||
client_cert.set_issuer(ca_cert.get_subject()) # Signed by ca
|
||||
client_cert.set_pubkey(client_pk)
|
||||
client_cert.sign(client_pk, 'sha1')
|
||||
client_cert.sign(ca_pk, 'sha1')
|
||||
client_cert.sign(client_pk, 'sha512')
|
||||
client_cert.sign(ca_pk, 'sha512')
|
||||
|
||||
pfx = crypto.PKCS12()
|
||||
pfx.set_certificate(client_cert)
|
||||
@@ -202,7 +327,7 @@ if tornado.options.options.generate_user_pkcs:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if tornado.options.options.unsecure:
|
||||
if options.unsecure:
|
||||
ssl_opts = None
|
||||
else:
|
||||
if not all(map(os.path.exists, [cert % host, cert_key % host, ca])):
|
||||
@@ -223,42 +348,39 @@ else:
|
||||
'ca_certs': ca,
|
||||
'cert_reqs': ssl.CERT_REQUIRED
|
||||
}
|
||||
if tornado.options.options.ssl_version is not None:
|
||||
if options.ssl_version is not None:
|
||||
if not hasattr(
|
||||
ssl, 'PROTOCOL_%s' % tornado.options.options.ssl_version):
|
||||
ssl, 'PROTOCOL_%s' % options.ssl_version):
|
||||
print(
|
||||
"Unknown SSL protocol %s" %
|
||||
tornado.options.options.ssl_version)
|
||||
options.ssl_version)
|
||||
sys.exit(1)
|
||||
ssl_opts['ssl_version'] = getattr(
|
||||
ssl, 'PROTOCOL_%s' % tornado.options.options.ssl_version)
|
||||
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)
|
||||
url = "http%s://%s:%d/*" % (
|
||||
"s" if not tornado.options.options.unsecure else "", host, port)
|
||||
|
||||
if http_server.systemd:
|
||||
if getattr(http_server, 'systemd', False):
|
||||
os.environ.pop('LISTEN_PID')
|
||||
os.environ.pop('LISTEN_FDS')
|
||||
|
||||
# This is for debugging purpose
|
||||
try:
|
||||
from wsreload.client import sporadic_reload, watch
|
||||
except ImportError:
|
||||
log.debug('wsreload not found')
|
||||
else:
|
||||
sporadic_reload({'url': url})
|
||||
|
||||
files = ['butterfly/static/javascripts/',
|
||||
'butterfly/static/stylesheets/',
|
||||
'butterfly/templates/']
|
||||
watch({'url': url}, files, unwatch_at_exit=True)
|
||||
|
||||
log.info('Starting loop')
|
||||
|
||||
ioloop = tornado.ioloop.IOLoop.instance()
|
||||
|
||||
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__ = '1.5.10'
|
||||
|
||||
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
|
||||
|
||||
33
butterfly/bin/cat.py
Normal file
33
butterfly/bin/cat.py
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python
|
||||
import argparse
|
||||
import base64
|
||||
import mimetypes
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from butterfly.escapes import image
|
||||
|
||||
parser = argparse.ArgumentParser(description='Butterfly cat wrapper.')
|
||||
parser.add_argument('-o', action="store_true",
|
||||
dest='original', help='Force original cat')
|
||||
parser.add_argument(
|
||||
'files', metavar='FILES', nargs='+',
|
||||
help='Force original cat')
|
||||
|
||||
args, remaining = parser.parse_known_args()
|
||||
if args.original:
|
||||
os.execvp('/usr/bin/cat', remaining + args.files)
|
||||
|
||||
|
||||
for file in args.files:
|
||||
if (not os.path.exists(sys.argv[1])):
|
||||
print('%s: No such file' % file)
|
||||
else:
|
||||
mime = mimetypes.guess_type(file)[0]
|
||||
if mime and 'image' in mime:
|
||||
with image(mime):
|
||||
with open(file, 'rb') as f:
|
||||
print(base64.b64encode(f.read()).decode('ascii'))
|
||||
else:
|
||||
subprocess.call(['cat'] + remaining + [file])
|
||||
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,29 +1,70 @@
|
||||
import sys
|
||||
import termios
|
||||
import tty
|
||||
from contextlib import contextmanager
|
||||
|
||||
from butterfly.utils import ansi_colors as colors # noqa: F401
|
||||
|
||||
|
||||
@contextmanager
|
||||
def html():
|
||||
print('\x1bP;HTML|')
|
||||
sys.stdout.write('\x1bP;HTML|')
|
||||
yield
|
||||
print('\x1bP')
|
||||
sys.stdout.write('\x1bP')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def image():
|
||||
print('\x1bP;IMAGE|')
|
||||
def image(mime='image'):
|
||||
sys.stdout.write('\x1bP;IMAGE|%s;' % mime)
|
||||
yield
|
||||
print('\x1bP')
|
||||
sys.stdout.write('\x1bP\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def prompt():
|
||||
print('\x1bP;PROMPT|')
|
||||
sys.stdout.write('\x1bP;PROMPT|')
|
||||
yield
|
||||
print('\x1bP')
|
||||
sys.stdout.write('\x1bP')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def text():
|
||||
print('\x1bP;TEXT|')
|
||||
sys.stdout.write('\x1bP;TEXT|')
|
||||
yield
|
||||
print('\x1bP')
|
||||
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) 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,31 +16,23 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import pty
|
||||
import json
|
||||
import os
|
||||
import io
|
||||
import struct
|
||||
import fcntl
|
||||
import termios
|
||||
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
|
||||
import tornado.process
|
||||
import tornado.ioloop
|
||||
import tornado.options
|
||||
import sys
|
||||
import signal
|
||||
from butterfly import url, Route, utils, __version__
|
||||
|
||||
ioloop = tornado.ioloop.IOLoop.instance()
|
||||
|
||||
server = utils.User()
|
||||
daemon = utils.User(name='daemon')
|
||||
|
||||
# Python 2 backward compatibility
|
||||
try:
|
||||
input = raw_input
|
||||
except NameError:
|
||||
pass
|
||||
from butterfly import Route, url, utils
|
||||
from butterfly.terminal import Terminal
|
||||
|
||||
|
||||
def u(s):
|
||||
@@ -49,317 +41,360 @@ def u(s):
|
||||
return s
|
||||
|
||||
|
||||
def motd(socket):
|
||||
return (
|
||||
'''
|
||||
B ` '
|
||||
;,,, ` ' ,,,;
|
||||
`Y888888bo. : : .od888888Y'
|
||||
8888888888b. : : .d8888888888
|
||||
88888Y' `Y8b. ` ' .d8Y' `Y88888
|
||||
j88888 R.db.B Yb. ' ' .dY R.db.B 88888k
|
||||
`888 RY88YB `b ( ) d' RY88YB 888'
|
||||
888b R'"B ,', R"'B d888
|
||||
j888888bd8gf"' ':' `"?g8bd888888k
|
||||
R'Y'B .8' d' 'b '8. R'Y'X
|
||||
R!B .8' RdbB d'; ;`b RdbB '8. R!B
|
||||
d88 R`'B 8 ; ; 8 R`'B 88b Rbutterfly Zv %sB
|
||||
d888b .g8 ',' 8g. d888b
|
||||
:888888888Y' 'Y888888888: AConnecting to:B
|
||||
'! 8888888' `8888888 !' G%sB
|
||||
'8Y R`Y Y'B Y8'
|
||||
R Y Y AFrom:R
|
||||
! ! G%sX
|
||||
|
||||
'''
|
||||
.replace('G', '\x1b[3%d;1m' % (
|
||||
1 if tornado.options.options.unsecure else 2))
|
||||
.replace('B', '\x1b[34;1m')
|
||||
.replace('R', '\x1b[37;1m')
|
||||
.replace('Z', '\x1b[33;1m')
|
||||
.replace('A', '\x1b[37;0m')
|
||||
.replace('X', '\x1b[0m')
|
||||
.replace('\n', '\r\n')
|
||||
% (__version__,
|
||||
'%s:%d' % (socket.local_addr, socket.local_port),
|
||||
'%s:%d' % (socket.remote_addr, socket.remote_port)))
|
||||
|
||||
|
||||
@url(r'/(?:user/(.+))?/?(?:wd/(.+))?')
|
||||
@url(r'/(?:session/(?P<session>[^/]+)/?)?')
|
||||
class Index(Route):
|
||||
def get(self, user, path):
|
||||
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()
|
||||
self.finish()
|
||||
raise tornado.web.HTTPError(404)
|
||||
|
||||
|
||||
@url(r'/ws(?:/user/([^/]+))?/?(?:/wd/(.+))?')
|
||||
class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
|
||||
class KeptAliveWebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||
keepalive_timer = None
|
||||
|
||||
terminals = set()
|
||||
|
||||
def pty(self):
|
||||
self.pid, self.fd = pty.fork()
|
||||
if self.pid == 0:
|
||||
self.shell()
|
||||
else:
|
||||
self.communicate()
|
||||
|
||||
def shell(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
|
||||
user = input('login: ')
|
||||
try:
|
||||
self.callee = utils.User(name=user)
|
||||
except:
|
||||
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()
|
||||
|
||||
assert self.callee is not None
|
||||
def open(self, *args, **kwargs):
|
||||
self.keepalive_timer = tornado.ioloop.PeriodicCallback(
|
||||
self.send_ping, tornado.options.options.keepalive_interval * 1000)
|
||||
self.keepalive_timer.start()
|
||||
|
||||
def send_ping(self):
|
||||
t = int(time.time())
|
||||
frame = struct.pack('<I', t) # A ping frame based on time
|
||||
self.log.info("Sending ping frame %s" % t)
|
||||
try:
|
||||
os.chdir(self.path or self.callee.dir)
|
||||
except:
|
||||
pass
|
||||
self.ping(frame)
|
||||
except tornado.websocket.WebSocketClosedError:
|
||||
self.keepalive_timer.stop()
|
||||
|
||||
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.update(self.socket.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"))
|
||||
def on_close(self):
|
||||
if self.keepalive_timer is not None:
|
||||
self.keepalive_timer.stop()
|
||||
|
||||
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:
|
||||
# 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
|
||||
)):
|
||||
# User is authed by ssl, setting groups
|
||||
try:
|
||||
os.initgroups(self.callee.name, self.callee.gid)
|
||||
os.setgid(self.callee.gid)
|
||||
os.setuid(self.callee.uid)
|
||||
except:
|
||||
print('The server must be run as root '
|
||||
'if you want to log as different user\n')
|
||||
sys.exit(1)
|
||||
|
||||
if tornado.options.options.cmd:
|
||||
args = tornado.options.options.cmd.split(' ')
|
||||
else:
|
||||
args = [tornado.options.options.shell or self.callee.shell]
|
||||
args.append('-i')
|
||||
@url(r'/ctl/session/(?P<session>[^/]+)')
|
||||
class TermCtlWebSocket(Route, KeptAliveWebSocketHandler):
|
||||
sessions = defaultdict(list)
|
||||
sessions_secure_users = {}
|
||||
|
||||
os.execvpe(args[0], args, env)
|
||||
# This process has been replaced
|
||||
def open(self, session):
|
||||
super(TermCtlWebSocket, self).open(session)
|
||||
self.session = session
|
||||
self.closed = False
|
||||
self.log.info('Websocket /ctl opened %r' % self)
|
||||
|
||||
# Unsecure connection with su
|
||||
if server.root:
|
||||
if self.socket.local:
|
||||
if self.callee != self.caller:
|
||||
# Force password prompt by dropping rights
|
||||
# to the daemon user
|
||||
os.setuid(daemon.uid)
|
||||
else:
|
||||
# We are not local so we should always get a password prompt
|
||||
if self.callee == daemon:
|
||||
# No logging from daemon
|
||||
sys.exit(1)
|
||||
os.setuid(daemon.uid)
|
||||
def create_terminal(self):
|
||||
socket = utils.Socket(self.ws_connection.stream.socket)
|
||||
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 os.path.exists('/usr/bin/su'):
|
||||
args = ['/usr/bin/su']
|
||||
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(self.callee.name)
|
||||
os.execvpe(args[0], args, env)
|
||||
|
||||
def communicate(self):
|
||||
fcntl.fcntl(self.fd, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||
|
||||
def utf8_error(e):
|
||||
self.log.error(e)
|
||||
|
||||
self.reader = io.open(
|
||||
self.fd,
|
||||
'rb',
|
||||
buffering=0,
|
||||
closefd=False
|
||||
)
|
||||
self.writer = io.open(
|
||||
self.fd,
|
||||
'wt',
|
||||
encoding='utf-8',
|
||||
closefd=False
|
||||
)
|
||||
ioloop.add_handler(
|
||||
self.fd, self.shell_handler, ioloop.READ | ioloop.ERROR)
|
||||
|
||||
def open(self, user, path):
|
||||
self.fd = None
|
||||
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
|
||||
|
||||
self.socket = utils.Socket(self.ws_connection.stream.socket)
|
||||
self.set_nodelay(True)
|
||||
self.log.info('Websocket opened %r' % self.socket)
|
||||
self.path = path
|
||||
self.user = user.decode('utf-8') if user else None
|
||||
self.caller = self.callee = None
|
||||
|
||||
# If local we have the user connecting
|
||||
if self.socket.local and self.socket.user is not None:
|
||||
self.caller = self.socket.user
|
||||
|
||||
if tornado.options.options.unsecure:
|
||||
if self.user:
|
||||
try:
|
||||
self.callee = utils.User(name=self.user)
|
||||
except LookupError:
|
||||
self.callee = None
|
||||
|
||||
# If no user where given and we are local, keep the same user
|
||||
# as the one who opened the socket
|
||||
# ie: the one openning a terminal in borwser
|
||||
if not self.callee and not self.user and self.socket.local:
|
||||
self.callee = self.caller
|
||||
else:
|
||||
user = utils.parse_cert(self.stream.socket.getpeercert())
|
||||
if not tornado.options.options.unsecure:
|
||||
user = utils.parse_cert(
|
||||
self.ws_connection.stream.socket.getpeercert())
|
||||
assert user, 'No user in certificate'
|
||||
self.user = user
|
||||
try:
|
||||
self.callee = utils.User(name=self.user)
|
||||
user = utils.User(name=user)
|
||||
except LookupError:
|
||||
raise Exception('Invalid user in certificate')
|
||||
|
||||
TermWebSocket.terminals.add(self)
|
||||
# Certificate authed user
|
||||
secure_user = user
|
||||
|
||||
self.write_message(motd(self.socket))
|
||||
self.pty()
|
||||
elif socket.local and socket.user == utils.User() and not user:
|
||||
# Local to local returning browser user
|
||||
secure_user = socket.user
|
||||
elif user:
|
||||
try:
|
||||
user = utils.User(name=user)
|
||||
except LookupError:
|
||||
raise Exception('Invalid user')
|
||||
|
||||
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:
|
||||
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, self.session, socket,
|
||||
self.request.full_url().replace('/ctl/', '/'), self.render_string,
|
||||
TermWebSocket.broadcast)
|
||||
|
||||
terminal.pty()
|
||||
self.log.info('Openning session %s for secure user %r' % (
|
||||
self.session, user))
|
||||
|
||||
@classmethod
|
||||
def broadcast(cls, session, message, emitter=None):
|
||||
for wsocket in cls.sessions[session]:
|
||||
try:
|
||||
if wsocket != emitter:
|
||||
wsocket.write_message(message)
|
||||
except Exception:
|
||||
wsocket.log.exception('Error on broadcast')
|
||||
wsocket.close()
|
||||
|
||||
def on_message(self, message):
|
||||
if not hasattr(self, 'writer'):
|
||||
self.on_close()
|
||||
self.close()
|
||||
return
|
||||
if message[0] == 'R':
|
||||
cols, rows = map(int, message[1:].split(','))
|
||||
s = struct.pack("HHHH", rows, cols, 0, 0)
|
||||
fcntl.ioctl(self.fd, termios.TIOCSWINSZ, s)
|
||||
self.log.info('SIZE (%d, %d)' % (cols, rows))
|
||||
elif message[0] == 'S':
|
||||
self.log.info('WRIT<%r' % message)
|
||||
self.writer.write(message[1:])
|
||||
self.writer.flush()
|
||||
|
||||
def shell_handler(self, fd, events):
|
||||
if events & ioloop.READ:
|
||||
cmd = json.loads(message)
|
||||
if cmd['cmd'] == 'open':
|
||||
self.create_terminal()
|
||||
else:
|
||||
try:
|
||||
read = self.reader.read()
|
||||
except IOError:
|
||||
read = ''
|
||||
|
||||
self.log.info('READ>%r' % read)
|
||||
if read and len(read) != 0 and self.ws_connection:
|
||||
self.write_message(read.decode('utf-8', 'replace'))
|
||||
else:
|
||||
events = ioloop.ERROR
|
||||
|
||||
if events & ioloop.ERROR:
|
||||
self.log.info('Error on fd %d, closing' % fd)
|
||||
# Terminated
|
||||
self.on_close()
|
||||
self.close()
|
||||
Terminal.sessions[self.session].ctl(cmd)
|
||||
except Exception:
|
||||
# FF strange bug
|
||||
pass
|
||||
self.broadcast(self.session, message, self)
|
||||
|
||||
def on_close(self):
|
||||
if self.fd is not None:
|
||||
self.log.info('Closing fd %d' % self.fd)
|
||||
super(TermCtlWebSocket, self).on_close()
|
||||
if self.closed:
|
||||
return
|
||||
self.closed = True
|
||||
self.log.info('Websocket /ctl closed %r' % self)
|
||||
if self in self.sessions[self.session]:
|
||||
self.sessions[self.session].remove(self)
|
||||
|
||||
if getattr(self, 'pid', 0) == 0:
|
||||
self.log.info('pid is 0')
|
||||
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'/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
|
||||
|
||||
try:
|
||||
ioloop.remove_handler(self.fd)
|
||||
except Exception:
|
||||
self.log.error('handler removal fail', exc_info=True)
|
||||
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()
|
||||
|
||||
try:
|
||||
os.close(self.fd)
|
||||
except Exception:
|
||||
self.log.debug('closing fd fail', exc_info=True)
|
||||
def on_message(self, message):
|
||||
Terminal.sessions[self.session].write(message)
|
||||
|
||||
try:
|
||||
os.kill(self.pid, signal.SIGKILL)
|
||||
os.waitpid(self.pid, 0)
|
||||
except Exception:
|
||||
self.log.debug('waitpid fail', exc_info=True)
|
||||
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)
|
||||
|
||||
TermWebSocket.terminals.remove(self)
|
||||
self.log.info('Websocket closed')
|
||||
|
||||
if self.application.systemd and not len(TermWebSocket.terminals):
|
||||
self.log.info('No more terminals, exiting...')
|
||||
sys.exit(0)
|
||||
@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)
|
||||
|
||||
cert = self.request.get_ssl_certificate()
|
||||
user = utils.parse_cert(cert)
|
||||
|
||||
if not user:
|
||||
raise tornado.web.HTTPError(403)
|
||||
|
||||
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,19 +18,19 @@
|
||||
|
||||
/* Here are the 16 "normal" colors for theming */
|
||||
|
||||
+termcolor(0, #2e3436)
|
||||
+termcolor(1, #cc0000)
|
||||
+termcolor(2, #4e9a06)
|
||||
+termcolor(3, #c4a000)
|
||||
+termcolor(4, #3465a4)
|
||||
+termcolor(5, #75507b)
|
||||
+termcolor(6, #06989a)
|
||||
+termcolor(7, #d3d7cf)
|
||||
+termcolor(8, #555753)
|
||||
+termcolor(9, #ef2929)
|
||||
+termcolor(10, #8ae234)
|
||||
+termcolor(11, #fce94f)
|
||||
+termcolor(12, #729fcf)
|
||||
+termcolor(13, #ad7fa8)
|
||||
+termcolor(14, #34e2e2)
|
||||
+termcolor(15, #eeeeec)
|
||||
+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)
|
||||
|
||||
6
butterfly/sass/_all_fx.sass
Normal file
6
butterfly/sass/_all_fx.sass
Normal file
@@ -0,0 +1,6 @@
|
||||
body
|
||||
&.copied
|
||||
transform: scale(1.05)
|
||||
|
||||
&.pasted
|
||||
transform: scale(.95)
|
||||
@@ -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,25 +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
|
||||
|
||||
$bg: #110f13
|
||||
$fg: #f4ead5
|
||||
|
||||
html, body
|
||||
background-color: $bg
|
||||
|
||||
.terminal
|
||||
background-color: $bg
|
||||
color: $fg
|
||||
|
||||
|
||||
=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,22 +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
|
||||
|
||||
::-webkit-scrollbar
|
||||
background: $bg
|
||||
width: .75em
|
||||
.line.active
|
||||
background-color: $active-bg
|
||||
|
||||
::-webkit-scrollbar-thumb
|
||||
background: rgba($fg, .1)
|
||||
.line.extended
|
||||
cursor: zoom-in
|
||||
background-image: linear-gradient(90deg, rgba(darken($bg, 3%), 0), 95%, darken($bg, 3%))
|
||||
|
||||
::-webkit-scrollbar-thumb:hover
|
||||
background: rgba($fg, .15)
|
||||
.extra
|
||||
display: none
|
||||
|
||||
&:not(.expanded):hover
|
||||
background-color: lighten($bg, 2%)
|
||||
|
||||
&.expanded
|
||||
cursor: zoom-out
|
||||
background-color: darken($bg, 3%)
|
||||
|
||||
.extra
|
||||
display: block
|
||||
white-space: pre-wrap
|
||||
word-break: break-all
|
||||
|
||||
&::-webkit-scrollbar
|
||||
background: $scroll-bg
|
||||
width: $scroll-width
|
||||
|
||||
&::-webkit-scrollbar-thumb
|
||||
background: $scroll-fg
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover
|
||||
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 */
|
||||
@@ -15,33 +15,23 @@
|
||||
/* 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: 6px !default
|
||||
$shadow-alpha: .5 !default
|
||||
|
||||
.terminal
|
||||
text-shadow: 0 0 $shadow rgba($fg, $shadow-alpha)
|
||||
transition: 200ms
|
||||
body
|
||||
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
|
||||
@@ -59,12 +49,12 @@ $shadow-alpha: .5 !default
|
||||
opacity: .2
|
||||
font-weight: 900
|
||||
|
||||
&.copied
|
||||
transform: scale(1.05)
|
||||
|
||||
&.pasted
|
||||
transform: scale(.95)
|
||||
|
||||
&.stopped
|
||||
-webkit-filter: brightness(50%)
|
||||
filter: brightness(50%)
|
||||
|
||||
&.locked
|
||||
&::-webkit-scrollbar-thumb
|
||||
background: rgba(red, .7)
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover
|
||||
background: rgba(red, .8)
|
||||
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
|
||||
@@ -58,3 +67,7 @@ $bg: #000 !default
|
||||
|
||||
.inline-image
|
||||
max-width: 100%
|
||||
max-height: 50vh
|
||||
|
||||
a
|
||||
color: inherit
|
||||
|
||||
6
butterfly/sass/_text_fx.sass
Normal file
6
butterfly/sass/_text_fx.sass
Normal file
@@ -0,0 +1,6 @@
|
||||
$fg: #fff !default
|
||||
$shadow: 6px !default
|
||||
$shadow-alpha: .5 !default
|
||||
|
||||
body
|
||||
text-shadow: 0 0 $shadow rgba($fg, $shadow-alpha)
|
||||
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,11 +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/>. */
|
||||
|
||||
@import font
|
||||
@import fx
|
||||
@import colors
|
||||
@import 16_colors
|
||||
@import 256_colors
|
||||
@import layout
|
||||
@import cursor
|
||||
@import term_styles
|
||||
/* 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.
|
||||
|
||||
/* These a the default variables */
|
||||
@import variables
|
||||
|
||||
/* These are all imported files */
|
||||
@import styles
|
||||
|
||||
@@ -1,25 +1,89 @@
|
||||
(function() {
|
||||
var Selection, alt, cancel, copy, ctrl, first, next_leaf, previous_leaf, selection, set_alarm, virtual_input,
|
||||
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; };
|
||||
|
||||
set_alarm = 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;
|
||||
butterfly.element.classList.remove('alarm');
|
||||
note = "New activity on butterfly terminal [" + butterfly.title + "]";
|
||||
var message, note, notif;
|
||||
message = clean_ansi(data.data.slice(1));
|
||||
if (cond !== null && !cond.test(message)) {
|
||||
return;
|
||||
}
|
||||
butterfly.body.classList.remove('alarm');
|
||||
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);
|
||||
return butterfly.element.classList.add('alarm');
|
||||
butterfly.ws.shell.addEventListener('message', alarm);
|
||||
return butterfly.body.classList.add('alarm');
|
||||
};
|
||||
|
||||
cancel = function(ev) {
|
||||
@@ -33,28 +97,38 @@
|
||||
return false;
|
||||
};
|
||||
|
||||
addEventListener('keydown', function(e) {
|
||||
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 set_alarm(Notification.permission === 'granted');
|
||||
return setAlarm(Notification.permission === 'granted', cond);
|
||||
});
|
||||
} else {
|
||||
set_alarm(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) {
|
||||
@@ -90,7 +382,7 @@
|
||||
return false;
|
||||
};
|
||||
|
||||
previous_leaf = function(node) {
|
||||
previousLeaf = function(node) {
|
||||
var previous;
|
||||
previous = node.previousSibling;
|
||||
if (!previous) {
|
||||
@@ -105,7 +397,7 @@
|
||||
return previous;
|
||||
};
|
||||
|
||||
next_leaf = function(node) {
|
||||
nextLeaf = function(node) {
|
||||
var next;
|
||||
next = node.nextSibling;
|
||||
if (!next) {
|
||||
@@ -122,16 +414,16 @@
|
||||
|
||||
Selection = (function() {
|
||||
function Selection() {
|
||||
butterfly.element.classList.add('selection');
|
||||
butterfly.body.classList.add('selection');
|
||||
this.selection = getSelection();
|
||||
}
|
||||
|
||||
Selection.prototype.reset = function() {
|
||||
var fake_range, ref, results;
|
||||
var fakeRange, ref, results;
|
||||
this.selection = getSelection();
|
||||
fake_range = document.createRange();
|
||||
fake_range.setStart(this.selection.anchorNode, this.selection.anchorOffset);
|
||||
fake_range.setEnd(this.selection.focusNode, this.selection.focusOffset);
|
||||
fakeRange = document.createRange();
|
||||
fakeRange.setStart(this.selection.anchorNode, this.selection.anchorOffset);
|
||||
fakeRange.setEnd(this.selection.focusNode, this.selection.focusOffset);
|
||||
this.start = {
|
||||
node: this.selection.anchorNode,
|
||||
offset: this.selection.anchorOffset
|
||||
@@ -140,17 +432,17 @@
|
||||
node: this.selection.focusNode,
|
||||
offset: this.selection.focusOffset
|
||||
};
|
||||
if (fake_range.collapsed) {
|
||||
if (fakeRange.collapsed) {
|
||||
ref = [this.end, this.start], this.start = ref[0], this.end = ref[1];
|
||||
}
|
||||
this.start_line = this.start.node;
|
||||
while (!this.start_line.classList || indexOf.call(this.start_line.classList, 'line') < 0) {
|
||||
this.start_line = this.start_line.parentNode;
|
||||
this.startLine = this.start.node;
|
||||
while (!this.startLine.classList || indexOf.call(this.startLine.classList, 'line') < 0) {
|
||||
this.startLine = this.startLine.parentNode;
|
||||
}
|
||||
this.end_line = this.end.node;
|
||||
this.endLine = this.end.node;
|
||||
results = [];
|
||||
while (!this.end_line.classList || indexOf.call(this.end_line.classList, 'line') < 0) {
|
||||
results.push(this.end_line = this.end_line.parentNode);
|
||||
while (!this.endLine.classList || indexOf.call(this.endLine.classList, 'line') < 0) {
|
||||
results.push(this.endLine = this.endLine.parentNode);
|
||||
}
|
||||
return results;
|
||||
};
|
||||
@@ -160,7 +452,7 @@
|
||||
};
|
||||
|
||||
Selection.prototype.destroy = function() {
|
||||
butterfly.element.classList.remove('selection');
|
||||
butterfly.body.classList.remove('selection');
|
||||
return this.clear();
|
||||
};
|
||||
|
||||
@@ -178,17 +470,17 @@
|
||||
|
||||
Selection.prototype.go = function(n) {
|
||||
var index;
|
||||
index = butterfly.children.indexOf(this.start_line) + 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;
|
||||
}
|
||||
}
|
||||
return this.select_line(index);
|
||||
return this.selectLine(index);
|
||||
};
|
||||
|
||||
Selection.prototype.apply = function() {
|
||||
@@ -200,30 +492,30 @@
|
||||
return this.selection.addRange(range);
|
||||
};
|
||||
|
||||
Selection.prototype.select_line = function(index) {
|
||||
var line, line_end, line_start;
|
||||
line = butterfly.children[index];
|
||||
line_start = {
|
||||
Selection.prototype.selectLine = function(index) {
|
||||
var line, lineEnd, lineStart;
|
||||
line = butterfly.term.childNodes[index];
|
||||
lineStart = {
|
||||
node: line.firstChild,
|
||||
offset: 0
|
||||
};
|
||||
line_end = {
|
||||
lineEnd = {
|
||||
node: line.lastChild,
|
||||
offset: line.lastChild.textContent.length
|
||||
};
|
||||
this.start = this.walk(line_start, /\S/);
|
||||
return this.end = this.walk(line_end, /\S/, true);
|
||||
this.start = this.walk(lineStart, /\S/);
|
||||
return this.end = this.walk(lineEnd, /\S/, true);
|
||||
};
|
||||
|
||||
Selection.prototype.collapsed = function(start, end) {
|
||||
var fake_range;
|
||||
fake_range = document.createRange();
|
||||
fake_range.setStart(start.node, start.offset);
|
||||
fake_range.setEnd(end.node, end.offset);
|
||||
return fake_range.collapsed;
|
||||
var fakeRange;
|
||||
fakeRange = document.createRange();
|
||||
fakeRange.setStart(start.node, start.offset);
|
||||
fakeRange.setEnd(end.node, end.offset);
|
||||
return fakeRange.collapsed;
|
||||
};
|
||||
|
||||
Selection.prototype.shrink_right = function() {
|
||||
Selection.prototype.shrinkRight = function() {
|
||||
var end, node;
|
||||
node = this.walk(this.end, /\s/, true);
|
||||
end = this.walk(node, /\S/, true);
|
||||
@@ -232,7 +524,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
Selection.prototype.shrink_left = function() {
|
||||
Selection.prototype.shrinkLeft = function() {
|
||||
var node, start;
|
||||
node = this.walk(this.start, /\s/);
|
||||
start = this.walk(node, /\S/);
|
||||
@@ -241,13 +533,13 @@
|
||||
}
|
||||
};
|
||||
|
||||
Selection.prototype.expand_right = function() {
|
||||
Selection.prototype.expandRight = function() {
|
||||
var node;
|
||||
node = this.walk(this.end, /\S/);
|
||||
return this.end = this.walk(node, /\s/);
|
||||
};
|
||||
|
||||
Selection.prototype.expand_left = function() {
|
||||
Selection.prototype.expandLeft = function() {
|
||||
var node;
|
||||
node = this.walk(this.start, /\S/, true);
|
||||
return this.start = this.walk(node, /\s/, true);
|
||||
@@ -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) {
|
||||
@@ -275,8 +567,8 @@
|
||||
};
|
||||
}
|
||||
}
|
||||
node = previous_leaf(node);
|
||||
text = node.textContent;
|
||||
node = previousLeaf(node);
|
||||
text = node != null ? node.textContent : void 0;
|
||||
i = text.length;
|
||||
}
|
||||
} else {
|
||||
@@ -289,8 +581,8 @@
|
||||
};
|
||||
}
|
||||
}
|
||||
node = next_leaf(node);
|
||||
text = node.textContent;
|
||||
node = nextLeaf(node);
|
||||
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;
|
||||
}
|
||||
@@ -323,13 +615,13 @@
|
||||
selection.down();
|
||||
}
|
||||
} else if (e.keyCode === 39) {
|
||||
selection.shrink_left();
|
||||
selection.shrinkLeft();
|
||||
} else if (e.keyCode === 38) {
|
||||
selection.expand_left();
|
||||
selection.expandLeft();
|
||||
} else if (e.keyCode === 37) {
|
||||
selection.shrink_right();
|
||||
selection.shrinkRight();
|
||||
} else if (e.keyCode === 40) {
|
||||
selection.expand_right();
|
||||
selection.expandRight();
|
||||
} else {
|
||||
return cancel(e);
|
||||
}
|
||||
@@ -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.select_line(butterfly.y - 1);
|
||||
selection.selectLine(r + butterfly.y - 1);
|
||||
selection.apply();
|
||||
return cancel(e);
|
||||
}
|
||||
@@ -369,7 +662,7 @@
|
||||
});
|
||||
|
||||
document.addEventListener('dblclick', function(e) {
|
||||
var anchorNode, anchorOffset, new_range, range, sel;
|
||||
var anchorNode, anchorOffset, newRange, range, sel;
|
||||
if (e.ctrlKey || e.altkey) {
|
||||
return;
|
||||
}
|
||||
@@ -382,10 +675,10 @@
|
||||
range.setEnd(sel.focusNode, sel.focusOffset);
|
||||
if (range.collapsed) {
|
||||
sel.removeAllRanges();
|
||||
new_range = document.createRange();
|
||||
new_range.setStart(sel.focusNode, sel.focusOffset);
|
||||
new_range.setEnd(sel.anchorNode, sel.anchorOffset);
|
||||
sel.addRange(new_range);
|
||||
newRange = document.createRange();
|
||||
newRange.setStart(sel.focusNode, sel.focusOffset);
|
||||
newRange.setEnd(sel.anchorNode, sel.anchorOffset);
|
||||
sel.addRange(newRange);
|
||||
}
|
||||
while (!(sel.toString().match(/\s/) || !sel.toString())) {
|
||||
sel.modify('extend', 'forward', 'character');
|
||||
@@ -401,107 +694,120 @@
|
||||
return sel.modify('extend', 'forward', 'character');
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var req;
|
||||
return;
|
||||
req = null;
|
||||
return butterfly.native_scroll_to = function(scroll) {
|
||||
var diff, e, scroll_step, step;
|
||||
if (scroll == null) {
|
||||
scroll = -1;
|
||||
}
|
||||
e = butterfly.parent;
|
||||
if (req) {
|
||||
cancelAnimationFrame(req);
|
||||
}
|
||||
if (scroll === -1 || (scroll > e.scrollHeight - e.getBoundingClientRect().height)) {
|
||||
scroll = e.scrollHeight - e.getBoundingClientRect().height;
|
||||
}
|
||||
diff = scroll - e.scrollTop;
|
||||
if (diff === 0) {
|
||||
return;
|
||||
}
|
||||
step = diff / 25;
|
||||
scroll_step = function() {
|
||||
if (Math.abs(e.scrollTop - scroll) < Math.abs(step)) {
|
||||
return e.scrollTop = scroll;
|
||||
} else {
|
||||
e.scrollTop += step;
|
||||
return req = requestAnimationFrame(scroll_step);
|
||||
document.addEventListener('keydown', function(e) {
|
||||
var oReq;
|
||||
if (!(e.altKey && e.keyCode === 69)) {
|
||||
return true;
|
||||
}
|
||||
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>";
|
||||
}
|
||||
};
|
||||
return req = requestAnimationFrame(scroll_step);
|
||||
};
|
||||
out += '</ul>';
|
||||
}
|
||||
out += '</div>';
|
||||
return popup.open(out);
|
||||
});
|
||||
oReq.open("GET", "/sessions/list.json");
|
||||
oReq.send();
|
||||
return cancel(e);
|
||||
});
|
||||
|
||||
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
|
||||
ctrl = false;
|
||||
alt = false;
|
||||
first = true;
|
||||
virtual_input = document.createElement('input');
|
||||
virtual_input.type = 'password';
|
||||
virtual_input.style.position = 'fixed';
|
||||
virtual_input.style.top = 0;
|
||||
virtual_input.style.left = 0;
|
||||
virtual_input.style.border = 'none';
|
||||
virtual_input.style.outline = 'none';
|
||||
virtual_input.style.opacity = 0;
|
||||
virtual_input.value = '0';
|
||||
document.body.appendChild(virtual_input);
|
||||
virtual_input.addEventListener('blur', function() {
|
||||
return setTimeout(((function(_this) {
|
||||
return function() {
|
||||
return _this.focus();
|
||||
};
|
||||
})(this)), 10);
|
||||
});
|
||||
addEventListener('click', function() {
|
||||
return virtual_input.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;
|
||||
}
|
||||
});
|
||||
virtual_input.addEventListener('keydown', function(e) {
|
||||
butterfly.keyDown(e);
|
||||
return true;
|
||||
});
|
||||
virtual_input.addEventListener('input', function(e) {
|
||||
var len;
|
||||
len = this.value.length;
|
||||
if (len === 0) {
|
||||
e.keyCode = 8;
|
||||
butterfly.keyDown(e);
|
||||
this.value = '0';
|
||||
return true;
|
||||
}
|
||||
e.keyCode = this.value.charAt(1).charCodeAt(0);
|
||||
if ((ctrl || alt) && !first) {
|
||||
e.keyCode = this.value.charAt(1).charCodeAt(0);
|
||||
e.ctrlKey = ctrl;
|
||||
e.altKey = alt;
|
||||
if (e.keyCode >= 97 && e.keyCode <= 122) {
|
||||
e.keyCode -= 32;
|
||||
}
|
||||
butterfly.keyDown(e);
|
||||
this.value = '0';
|
||||
ctrl = alt = false;
|
||||
return true;
|
||||
}
|
||||
butterfly.keyPress(e);
|
||||
first = false;
|
||||
this.value = '0';
|
||||
return true;
|
||||
});
|
||||
_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
1
butterfly/static/html-sanitizer.js
Normal file
1
butterfly/static/html-sanitizer.js
Normal file
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
6
butterfly/static/main.min.js
vendored
6
butterfly/static/main.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
{% from tornado.options import options %}
|
||||
{% from uuid import uuid4 %}
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
@@ -10,14 +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-allow-html="{{ 'yes' if options.allow_html_escapes 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>
|
||||
|
||||
26
butterfly/templates/motd
Normal file
26
butterfly/templates/motd
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
{{ colors.blue }} ` '
|
||||
;,,, ` ' ,,,;
|
||||
`Y888888bo. : : .od888888Y'
|
||||
8888888888b. : : .d8888888888
|
||||
88888Y' `Y8b. ` ' .d8Y' `Y88888
|
||||
j88888 {{ colors.white }}.db.{{ colors.blue }} Yb. ' ' .dY {{ colors.white }}.db.{{ colors.blue }} 88888k
|
||||
`888 {{ colors.white }}Y88Y{{ colors.blue }} `b ( ) d' {{ colors.white }}Y88Y{{ colors.blue }} 888'
|
||||
888b {{ colors.white }}'"{{ colors.blue }} ,', {{ colors.white }}"'{{ colors.blue }} d888
|
||||
j888888bd8gf"' ':' `"?g8bd888888k
|
||||
{{ colors.white }}'Y'{{ colors.blue }} .8' d' 'b '8. {{ colors.white }}'Y'{{ colors.reset }}
|
||||
{{ colors.white }}!{{ colors.blue }} .8' {{ colors.white }}db{{ colors.blue }} d'; ;`b {{ colors.white }}db{{ colors.blue }} '8. {{ colors.white }}!{{ colors.blue }}
|
||||
d88 {{ colors.white }}`'{{ colors.blue }} 8 ; ; 8 {{ colors.white }}`'{{ colors.blue }} 88b {{ colors.white }}butterfly {{ colors.yellow }}v {{ version }}{{ colors.blue }}
|
||||
d888b .g8 ',' 8g. d888b
|
||||
:888888888Y' 'Y888888888: {{ colors.light_white }}Connecting to:{{ colors.blue }}
|
||||
'! 8888888' `8888888 !' {{ colors.red if opts.unsecure else colors.green }}{{ butterfly.socket.local_addr }}:{{ butterfly.socket.local_port }}{{ colors.blue }}
|
||||
'8Y {{ colors.white }}`Y Y'{{ colors.blue }} Y8'
|
||||
{{ colors.white }} Y Y {{ colors.light_white }}From:{{ colors.white }}
|
||||
! ! {{ colors.red if opts.unsecure else colors.green }}{{ butterfly.socket.remote_addr }}:{{ butterfly.socket.remote_port }}{{ colors.reset }}
|
||||
|
||||
For more information type: {{ colors.white }}$ {{ colors.green }}butterfly help{{ 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 %}
|
||||
360
butterfly/terminal.py
Normal file
360
butterfly/terminal.py
Normal file
@@ -0,0 +1,360 @@
|
||||
# *-* 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/>.
|
||||
|
||||
import fcntl
|
||||
import io
|
||||
import os
|
||||
import pty
|
||||
import random
|
||||
import signal
|
||||
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 butterfly import __version__, utils
|
||||
|
||||
log = getLogger('butterfly')
|
||||
ioloop = tornado.ioloop.IOLoop.instance()
|
||||
server = utils.User()
|
||||
daemon = utils.User(name='daemon')
|
||||
|
||||
|
||||
# Python 2 backward compatibility
|
||||
try:
|
||||
input = raw_input
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
|
||||
class Terminal(object):
|
||||
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.broadcast = broadcast
|
||||
self.fd = None
|
||||
self.closed = False
|
||||
self.socket = socket
|
||||
log.info('Terminal opening with session: %s and socket %r' % (
|
||||
self.session, self.socket))
|
||||
self.path = path
|
||||
self.user = user if user else None
|
||||
self.caller = self.callee = None
|
||||
|
||||
# If local we have the user connecting
|
||||
if self.socket.local and self.socket.user is not None:
|
||||
self.caller = self.socket.user
|
||||
|
||||
if tornado.options.options.unsecure:
|
||||
if self.user:
|
||||
try:
|
||||
self.callee = self.user
|
||||
except LookupError:
|
||||
log.debug(
|
||||
"Can't switch to user %s" % self.user, exc_info=True)
|
||||
self.callee = None
|
||||
|
||||
# If no user where given and we are local, keep the same
|
||||
# user as the one who opened the socket ie: the one
|
||||
# openning a terminal in browser
|
||||
if not self.callee and not self.user and self.socket.local:
|
||||
self.user = self.callee = self.caller
|
||||
else:
|
||||
# Authed user
|
||||
self.callee = self.user
|
||||
|
||||
if tornado.options.options.motd != '':
|
||||
motd = (render_string(
|
||||
tornado.options.options.motd,
|
||||
butterfly=self,
|
||||
version=__version__,
|
||||
opts=tornado.options.options,
|
||||
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(
|
||||
random.choice(
|
||||
string.ascii_lowercase + string.ascii_uppercase +
|
||||
string.digits)
|
||||
for _ in range(4))
|
||||
|
||||
self.pid, self.fd = pty.fork()
|
||||
if self.pid == 0:
|
||||
self.determine_user()
|
||||
log.debug('Pty forked for user %r caller %r callee %r' % (
|
||||
self.user, self.caller, self.callee))
|
||||
self.shell()
|
||||
else:
|
||||
self.communicate()
|
||||
|
||||
def determine_user(self):
|
||||
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:
|
||||
user = input('login: ')
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
log.debug("Error in login input", exc_info=True)
|
||||
pass
|
||||
|
||||
try:
|
||||
self.callee = utils.User(name=user)
|
||||
except Exception:
|
||||
log.debug("Can't switch to user %s" % user, exc_info=True)
|
||||
self.callee = utils.User(name='nobody')
|
||||
return
|
||||
|
||||
# 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:
|
||||
os.chdir(self.path or self.callee.dir)
|
||||
except Exception:
|
||||
log.debug(
|
||||
"Can't chdir to %s" % (self.path or self.callee.dir),
|
||||
exc_info=True)
|
||||
|
||||
# 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"] = 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)
|
||||
except Exception:
|
||||
log.debug("Can't chown ttyname", exc_info=True)
|
||||
|
||||
utils.add_user_info(
|
||||
self.uid,
|
||||
tty, os.getpid(),
|
||||
self.callee.name, self.uri)
|
||||
|
||||
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 secure and not local_login:
|
||||
# User is authed by ssl, setting groups
|
||||
try:
|
||||
os.initgroups(self.callee.name, self.callee.gid)
|
||||
os.setgid(self.callee.gid)
|
||||
os.setuid(self.callee.uid)
|
||||
# Apparently necessary for some cmd
|
||||
env['LOGNAME'] = env['USER'] = self.callee.name
|
||||
except Exception:
|
||||
log.error(
|
||||
'The server must be run as root '
|
||||
'if you want to log as different user\n',
|
||||
exc_info=True)
|
||||
sys.exit(1)
|
||||
|
||||
if tornado.options.options.cmd:
|
||||
args = tornado.options.options.cmd.split(' ')
|
||||
else:
|
||||
args = [tornado.options.options.shell or self.callee.shell]
|
||||
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:
|
||||
if self.callee != self.caller:
|
||||
# Force password prompt by dropping rights
|
||||
# to the daemon user
|
||||
os.setuid(daemon.uid)
|
||||
else:
|
||||
# We are not local so we should always get a password prompt
|
||||
if self.callee == daemon:
|
||||
# No logging from daemon
|
||||
sys.exit(1)
|
||||
os.setuid(daemon.uid)
|
||||
|
||||
if os.path.exists('/usr/bin/su'):
|
||||
args = ['/usr/bin/su']
|
||||
else:
|
||||
args = ['/bin/su']
|
||||
|
||||
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)
|
||||
|
||||
def communicate(self):
|
||||
fcntl.fcntl(self.fd, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||
|
||||
def utf8_error(e):
|
||||
log.error(e)
|
||||
|
||||
self.reader = io.open(
|
||||
self.fd,
|
||||
'rb',
|
||||
buffering=0,
|
||||
closefd=False
|
||||
)
|
||||
self.writer = io.open(
|
||||
self.fd,
|
||||
'wt',
|
||||
encoding='utf-8',
|
||||
closefd=False
|
||||
)
|
||||
ioloop.add_handler(
|
||||
self.fd, self.shell_handler, ioloop.READ | ioloop.ERROR)
|
||||
|
||||
def write(self, message):
|
||||
if not hasattr(self, 'writer'):
|
||||
self.on_close()
|
||||
self.close()
|
||||
|
||||
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))
|
||||
|
||||
def shell_handler(self, fd, events):
|
||||
if events & ioloop.READ:
|
||||
try:
|
||||
read = self.reader.read()
|
||||
except IOError:
|
||||
read = ''
|
||||
|
||||
log.debug('READ>%r' % read)
|
||||
if read and len(read) != 0:
|
||||
self.send(read.decode('utf-8', 'replace'))
|
||||
else:
|
||||
events = ioloop.ERROR
|
||||
|
||||
if events & ioloop.ERROR:
|
||||
log.info('Error on fd %d, closing' % fd)
|
||||
# Terminated
|
||||
self.send(None) # Close all
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
if self.closed:
|
||||
return
|
||||
self.closed = True
|
||||
if self.fd is not None:
|
||||
log.info('Closing fd %d' % self.fd)
|
||||
|
||||
if getattr(self, 'pid', 0) == 0:
|
||||
log.info('pid is 0')
|
||||
return
|
||||
|
||||
utils.rm_user_info(self.uid, self.pid)
|
||||
|
||||
try:
|
||||
ioloop.remove_handler(self.fd)
|
||||
except Exception:
|
||||
log.error('handler removal fail', exc_info=True)
|
||||
|
||||
try:
|
||||
os.close(self.fd)
|
||||
except Exception:
|
||||
log.debug('closing fd fail', exc_info=True)
|
||||
|
||||
try:
|
||||
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,45 +18,45 @@
|
||||
|
||||
import os
|
||||
import pwd
|
||||
from logging import getLogger
|
||||
import subprocess
|
||||
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
|
||||
for ext in ['css', 'scss', 'sass']:
|
||||
for fn in [
|
||||
'/etc/butterfly/style',
|
||||
os.path.expanduser('~/.butterfly/style')]:
|
||||
if os.path.exists('%s.%s' % (fn, ext)):
|
||||
style = '%s.%s' % (fn, ext)
|
||||
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 style is None:
|
||||
return
|
||||
# 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])
|
||||
|
||||
if style.endswith('.scss') or style.endswith('.sass'):
|
||||
sass_path = os.path.join(
|
||||
os.path.dirname(__file__), 'sass')
|
||||
try:
|
||||
import sass
|
||||
except:
|
||||
log.error('You must install libsass to use sass '
|
||||
'(pip install libsass)')
|
||||
return
|
||||
|
||||
try:
|
||||
return sass.compile(filename=style, include_paths=[sass_path])
|
||||
except sass.CompileError:
|
||||
log.error(
|
||||
'Unable to compile style.scss (filename: %s, paths: %r) ' % (
|
||||
style, [sass_path]), exc_info=True)
|
||||
return
|
||||
|
||||
with open(style) as s:
|
||||
return s.read()
|
||||
return ''.join(ipv6_parts) + ':%04X' % port
|
||||
|
||||
|
||||
def parse_cert(cert):
|
||||
@@ -120,9 +120,14 @@ class Socket(object):
|
||||
sn = socket.getsockname()
|
||||
self.local_addr = sn[0]
|
||||
self.local_port = sn[1]
|
||||
pn = socket.getpeername()
|
||||
self.remote_addr = pn[0]
|
||||
self.remote_port = pn[1]
|
||||
try:
|
||||
pn = socket.getpeername()
|
||||
self.remote_addr = pn[0]
|
||||
self.remote_port = pn[1]
|
||||
except Exception:
|
||||
log.debug("Can't get peer name", exc_info=True)
|
||||
self.remote_addr = '???'
|
||||
self.remote_port = 0
|
||||
self.user = None
|
||||
self.env = {}
|
||||
|
||||
@@ -132,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)
|
||||
|
||||
@@ -148,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>' % (
|
||||
@@ -162,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
|
||||
@@ -175,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:
|
||||
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:
|
||||
log.debug('getting socket inet6 line fail', exc_info=True)
|
||||
except Exception:
|
||||
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
|
||||
@@ -222,3 +259,155 @@ def get_socket_env(inode):
|
||||
key, val = keyval.split('=', 1)
|
||||
env[key] = val
|
||||
return env
|
||||
|
||||
|
||||
utmp_struct = struct.Struct('hi32s4s32s256shhiii4i20s')
|
||||
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
b = lambda x: x
|
||||
else:
|
||||
def b(x):
|
||||
if isinstance(x, str):
|
||||
return x.encode('utf-8')
|
||||
return x
|
||||
|
||||
|
||||
def get_utmp_file():
|
||||
for file in (
|
||||
'/var/run/utmp',
|
||||
'/var/adm/utmp',
|
||||
'/var/adm/utmpx',
|
||||
'/etc/utmp',
|
||||
'/etc/utmpx',
|
||||
'/var/run/utx.active'):
|
||||
if os.path.exists(file):
|
||||
return file
|
||||
|
||||
|
||||
def get_wtmp_file():
|
||||
for file in (
|
||||
'/var/log/wtmp',
|
||||
'/var/adm/wtmp',
|
||||
'/var/adm/wtmpx',
|
||||
'/var/run/utx.log'):
|
||||
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'])
|
||||
|
||||
|
||||
def utmp_line(id, type, pid, fd, user, host, ts):
|
||||
return UTmp(
|
||||
type, # Type, 7 : user process
|
||||
pid, # pid
|
||||
b(fd), # line
|
||||
b(id), # id
|
||||
b(user), # user
|
||||
b(host), # host
|
||||
0, # exit 0
|
||||
0, # exit 1
|
||||
0, # session
|
||||
int(ts), # sec
|
||||
int(10 ** 6 * (ts - int(ts))), # usec
|
||||
0, # addr 0
|
||||
0, # addr 1
|
||||
0, # addr 2
|
||||
0, # addr 3
|
||||
b('') # unused
|
||||
)
|
||||
|
||||
|
||||
def add_user_info(id, fd, pid, user, host):
|
||||
# Freebsd format is not yet supported.
|
||||
# Please submit PR
|
||||
if sys.platform != 'linux':
|
||||
return
|
||||
utmp = utmp_line(id, 7, pid, fd, user, host, time.time())
|
||||
for kind, file in {
|
||||
'utmp': get_utmp_file(),
|
||||
'wtmp': get_wtmp_file()}.items():
|
||||
if not file:
|
||||
continue
|
||||
try:
|
||||
with open(file, 'rb+') as f:
|
||||
s = f.read(utmp_struct.size)
|
||||
while s:
|
||||
entry = UTmp(*utmp_struct.unpack(s))
|
||||
if kind == 'utmp' and entry.id == utmp.id:
|
||||
# Same id recycling
|
||||
f.seek(f.tell() - utmp_struct.size)
|
||||
f.write(utmp_struct.pack(*utmp))
|
||||
break
|
||||
s = f.read(utmp_struct.size)
|
||||
else:
|
||||
f.write(utmp_struct.pack(*utmp))
|
||||
except Exception:
|
||||
log.debug('Unable to write utmp info to ' + file, exc_info=True)
|
||||
|
||||
|
||||
def rm_user_info(id, pid):
|
||||
if sys.platform != 'linux':
|
||||
return
|
||||
utmp = utmp_line(id, 8, pid, '', '', '', time.time())
|
||||
for kind, file in {
|
||||
'utmp': get_utmp_file(),
|
||||
'wtmp': get_wtmp_file()}.items():
|
||||
if not file:
|
||||
continue
|
||||
try:
|
||||
with open(file, 'rb+') as f:
|
||||
s = f.read(utmp_struct.size)
|
||||
while s:
|
||||
entry = UTmp(*utmp_struct.unpack(s))
|
||||
if entry.id == utmp.id:
|
||||
if kind == 'utmp':
|
||||
# Same id closing
|
||||
f.seek(f.tell() - utmp_struct.size)
|
||||
f.write(utmp_struct.pack(*utmp))
|
||||
break
|
||||
else:
|
||||
utmp = utmp_line(
|
||||
id, 8, pid, entry.line, entry.user, '',
|
||||
time.time())
|
||||
|
||||
s = f.read(utmp_struct.size)
|
||||
else:
|
||||
f.write(utmp_struct.pack(*utmp))
|
||||
|
||||
except Exception:
|
||||
log.debug('Unable to update utmp info to ' + file, exc_info=True)
|
||||
|
||||
|
||||
class AnsiColors(object):
|
||||
colors = {
|
||||
'black': 30,
|
||||
'red': 31,
|
||||
'green': 32,
|
||||
'yellow': 33,
|
||||
'blue': 34,
|
||||
'magenta': 35,
|
||||
'cyan': 36,
|
||||
'white': 37
|
||||
}
|
||||
|
||||
def __getattr__(self, key):
|
||||
bold = True
|
||||
if key.startswith('light_'):
|
||||
bold = False
|
||||
key = key[len('light_'):]
|
||||
if key in self.colors:
|
||||
return '\x1b[%d%sm' % (
|
||||
self.colors[key],
|
||||
';1' if bold else '')
|
||||
if key == 'reset':
|
||||
return '\x1b[0m'
|
||||
return ''
|
||||
|
||||
|
||||
ansi_colors = AnsiColors()
|
||||
|
||||
@@ -1,20 +1,71 @@
|
||||
set_alarm = (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) ->
|
||||
butterfly.element.classList.remove 'alarm'
|
||||
note = "New activity on butterfly terminal [#{ butterfly.title }]"
|
||||
message = clean_ansi data.data.slice(1)
|
||||
return if cond isnt null and not cond.test(message)
|
||||
|
||||
butterfly.body.classList.remove 'alarm'
|
||||
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.element.classList.add 'alarm'
|
||||
butterfly.ws.shell.addEventListener 'message', alarm
|
||||
butterfly.body.classList.add 'alarm'
|
||||
|
||||
|
||||
cancel = (ev) ->
|
||||
@@ -24,13 +75,20 @@ cancel = (ev) ->
|
||||
false
|
||||
|
||||
|
||||
addEventListener 'keydown', (e) ->
|
||||
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 ->
|
||||
set_alarm(Notification.permission is 'granted')
|
||||
setAlarm(Notification.permission is 'granted', cond)
|
||||
else
|
||||
set_alarm(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
|
||||
@@ -22,7 +22,7 @@ cancel = (ev) ->
|
||||
ev.cancelBubble = true
|
||||
false
|
||||
|
||||
previous_leaf = (node) ->
|
||||
previousLeaf = (node) ->
|
||||
previous = node.previousSibling
|
||||
if not previous
|
||||
previous = node.parentNode.previousSibling
|
||||
@@ -32,7 +32,7 @@ previous_leaf = (node) ->
|
||||
previous = previous.lastChild
|
||||
previous
|
||||
|
||||
next_leaf = (node) ->
|
||||
nextLeaf = (node) ->
|
||||
next = node.nextSibling
|
||||
if not next
|
||||
next = node.parentNode.nextSibling
|
||||
@@ -44,14 +44,14 @@ next_leaf = (node) ->
|
||||
|
||||
class Selection
|
||||
constructor: ->
|
||||
butterfly.element.classList.add('selection')
|
||||
butterfly.body.classList.add('selection')
|
||||
@selection = getSelection()
|
||||
|
||||
reset: ->
|
||||
@selection = getSelection()
|
||||
fake_range = document.createRange()
|
||||
fake_range.setStart(@selection.anchorNode, @selection.anchorOffset)
|
||||
fake_range.setEnd(@selection.focusNode, @selection.focusOffset)
|
||||
fakeRange = document.createRange()
|
||||
fakeRange.setStart(@selection.anchorNode, @selection.anchorOffset)
|
||||
fakeRange.setEnd(@selection.focusNode, @selection.focusOffset)
|
||||
@start =
|
||||
node: @selection.anchorNode
|
||||
offset: @selection.anchorOffset
|
||||
@@ -59,22 +59,22 @@ class Selection
|
||||
node: @selection.focusNode
|
||||
offset: @selection.focusOffset
|
||||
|
||||
if fake_range.collapsed
|
||||
if fakeRange.collapsed
|
||||
[@start, @end] = [@end, @start]
|
||||
|
||||
@start_line = @start.node
|
||||
while not @start_line.classList or 'line' not in @start_line.classList
|
||||
@start_line = @start_line.parentNode
|
||||
@startLine = @start.node
|
||||
while not @startLine.classList or 'line' not in @startLine.classList
|
||||
@startLine = @startLine.parentNode
|
||||
|
||||
@end_line = @end.node
|
||||
while not @end_line.classList or 'line' not in @end_line.classList
|
||||
@end_line = @end_line.parentNode
|
||||
@endLine = @end.node
|
||||
while not @endLine.classList or 'line' not in @endLine.classList
|
||||
@endLine = @endLine.parentNode
|
||||
|
||||
clear: ->
|
||||
@selection.removeAllRanges()
|
||||
|
||||
destroy: ->
|
||||
butterfly.element.classList.remove('selection')
|
||||
butterfly.body.classList.remove('selection')
|
||||
@clear()
|
||||
|
||||
text: ->
|
||||
@@ -87,14 +87,15 @@ class Selection
|
||||
@go +1
|
||||
|
||||
go: (n) ->
|
||||
index = butterfly.children.indexOf(@start_line) + 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
|
||||
|
||||
@select_line index
|
||||
@selectLine index
|
||||
|
||||
apply: ->
|
||||
@clear()
|
||||
@@ -103,42 +104,42 @@ class Selection
|
||||
range.setEnd @end.node, @end.offset
|
||||
@selection.addRange range
|
||||
|
||||
select_line: (index) ->
|
||||
line = butterfly.children[index]
|
||||
line_start =
|
||||
selectLine: (index) ->
|
||||
line = butterfly.term.childNodes[index]
|
||||
lineStart =
|
||||
node: line.firstChild
|
||||
offset: 0
|
||||
|
||||
line_end =
|
||||
lineEnd =
|
||||
node: line.lastChild
|
||||
offset: line.lastChild.textContent.length
|
||||
|
||||
@start = @walk line_start, /\S/
|
||||
@end = @walk line_end, /\S/, true
|
||||
@start = @walk lineStart, /\S/
|
||||
@end = @walk lineEnd, /\S/, true
|
||||
|
||||
collapsed: (start, end) ->
|
||||
fake_range = document.createRange()
|
||||
fake_range.setStart(start.node, start.offset)
|
||||
fake_range.setEnd(end.node, end.offset)
|
||||
fake_range.collapsed
|
||||
fakeRange = document.createRange()
|
||||
fakeRange.setStart(start.node, start.offset)
|
||||
fakeRange.setEnd(end.node, end.offset)
|
||||
fakeRange.collapsed
|
||||
|
||||
shrink_right: ->
|
||||
shrinkRight: ->
|
||||
node = @walk @end, /\s/, true
|
||||
end = @walk node, /\S/, true
|
||||
if not @collapsed(@start, end)
|
||||
@end = end
|
||||
|
||||
shrink_left: ->
|
||||
shrinkLeft: ->
|
||||
node = @walk @start, /\s/
|
||||
start = @walk node, /\S/
|
||||
if not @collapsed(start, @end)
|
||||
@start = start
|
||||
|
||||
expand_right: ->
|
||||
expandRight: ->
|
||||
node = @walk @end, /\S/
|
||||
@end = @walk node, /\s/
|
||||
|
||||
expand_left: ->
|
||||
expandLeft: ->
|
||||
node = @walk @start, /\S/, true
|
||||
@start = @walk node, /\s/, true
|
||||
|
||||
@@ -148,23 +149,23 @@ class Selection
|
||||
else
|
||||
node = needle.node
|
||||
|
||||
text = node.textContent
|
||||
text = node?.textContent
|
||||
i = needle.offset
|
||||
if backward
|
||||
while node
|
||||
while i > 0
|
||||
if text[--i].match til
|
||||
return node: node, offset: i + 1
|
||||
node = previous_leaf node
|
||||
text = node.textContent
|
||||
node = previousLeaf node
|
||||
text = node?.textContent
|
||||
i = text.length
|
||||
else
|
||||
while node
|
||||
while i < text.length
|
||||
if text[i++].match til
|
||||
return node: node, offset: i - 1
|
||||
node = next_leaf node
|
||||
text = node.textContent
|
||||
node = nextLeaf node
|
||||
text = node?.textContent
|
||||
i = 0
|
||||
|
||||
return needle
|
||||
@@ -189,13 +190,13 @@ document.addEventListener 'keydown', (e) ->
|
||||
else if e.keyCode == 40
|
||||
selection.down()
|
||||
else if e.keyCode == 39
|
||||
selection.shrink_left()
|
||||
selection.shrinkLeft()
|
||||
else if e.keyCode == 38
|
||||
selection.expand_left()
|
||||
selection.expandLeft()
|
||||
else if e.keyCode == 37
|
||||
selection.shrink_right()
|
||||
selection.shrinkRight()
|
||||
else if e.keyCode == 40
|
||||
selection.expand_right()
|
||||
selection.expandRight()
|
||||
else
|
||||
return cancel e
|
||||
|
||||
@@ -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.select_line butterfly.y - 1
|
||||
selection.selectLine r + butterfly.y - 1
|
||||
selection.apply()
|
||||
return cancel e
|
||||
true
|
||||
@@ -235,10 +237,10 @@ document.addEventListener 'dblclick', (e) ->
|
||||
range.setEnd(sel.focusNode, sel.focusOffset)
|
||||
if range.collapsed
|
||||
sel.removeAllRanges()
|
||||
new_range = document.createRange()
|
||||
new_range.setStart(sel.focusNode, sel.focusOffset)
|
||||
new_range.setEnd(sel.anchorNode, sel.anchorOffset)
|
||||
sel.addRange(new_range)
|
||||
newRange = document.createRange()
|
||||
newRange.setStart(sel.focusNode, sel.focusOffset)
|
||||
newRange.setEnd(sel.anchorNode, sel.anchorOffset)
|
||||
sel.addRange(newRange)
|
||||
|
||||
until sel.toString().match(/\s/) or not sel.toString()
|
||||
sel.modify 'extend', 'forward', 'character'
|
||||
|
||||
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
|
||||
@@ -1,24 +0,0 @@
|
||||
|
||||
document.addEventListener 'DOMContentLoaded', ->
|
||||
return
|
||||
|
||||
req = null
|
||||
|
||||
butterfly.native_scroll_to = (scroll=-1) ->
|
||||
e = butterfly.parent
|
||||
cancelAnimationFrame req if req
|
||||
if scroll is -1 or (
|
||||
scroll > e.scrollHeight - e.getBoundingClientRect().height)
|
||||
scroll = e.scrollHeight - e.getBoundingClientRect().height
|
||||
|
||||
diff = scroll - e.scrollTop
|
||||
return if diff is 0
|
||||
step = diff / 25
|
||||
scroll_step = ->
|
||||
if Math.abs(e.scrollTop - scroll) < Math.abs(step)
|
||||
e.scrollTop = scroll
|
||||
else
|
||||
e.scrollTop += step
|
||||
req = requestAnimationFrame scroll_step
|
||||
|
||||
req = requestAnimationFrame scroll_step
|
||||
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
|
||||
virtual_input = document.createElement 'input'
|
||||
virtual_input.type = 'password'
|
||||
virtual_input.style.position = 'fixed'
|
||||
virtual_input.style.top = 0
|
||||
virtual_input.style.left = 0
|
||||
virtual_input.style.border = 'none'
|
||||
virtual_input.style.outline = 'none'
|
||||
virtual_input.style.opacity = 0
|
||||
virtual_input.value = '0'
|
||||
document.body.appendChild virtual_input
|
||||
|
||||
virtual_input.addEventListener 'blur', ->
|
||||
setTimeout((=> @focus()), 10)
|
||||
|
||||
addEventListener 'click', ->
|
||||
virtual_input.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
|
||||
|
||||
virtual_input.addEventListener 'keydown', (e) ->
|
||||
butterfly.keyDown(e)
|
||||
return true
|
||||
|
||||
virtual_input.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
|
||||
@@ -17,97 +17,109 @@
|
||||
|
||||
cols = rows = null
|
||||
quit = false
|
||||
open_ts = (new Date()).getTime()
|
||||
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:'
|
||||
ws_url = 'wss://'
|
||||
wsUrl = 'wss://'
|
||||
else
|
||||
ws_url = 'ws://'
|
||||
wsUrl = 'ws://'
|
||||
|
||||
ws_url += document.location.host + '/ws' + location.pathname
|
||||
ws = new WebSocket ws_url
|
||||
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
|
||||
open_ts = (new Date()).getTime()
|
||||
|
||||
ws.addEventListener 'error', ->
|
||||
console.log "WebSocket error", arguments
|
||||
|
||||
t_stop = null
|
||||
last_data = ''
|
||||
ws.addEventListener 'message', (e) ->
|
||||
if term.stop
|
||||
last_data += e.data
|
||||
last_data = last_data.slice(-10 * 1024) # Keep last 10kb
|
||||
if t_stop
|
||||
clearTimeout t_stop if t_stop
|
||||
t_stop = setTimeout ->
|
||||
term.stop = false
|
||||
term.element.classList.remove 'stopped'
|
||||
term.write last_data
|
||||
last_data = ''
|
||||
t_stop = null
|
||||
, 100
|
||||
if term
|
||||
term.body.classList.remove 'stopped'
|
||||
term.out = ws.shell.send.bind(ws.shell)
|
||||
term.out '\x03\n'
|
||||
return
|
||||
|
||||
term.write e.data
|
||||
if (ws.shell.readyState is WebSocket.OPEN and
|
||||
ws.ctl.readyState is WebSocket.OPEN)
|
||||
|
||||
ws.addEventListener 'close', ->
|
||||
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.element.classList.add('dead')
|
||||
, 1
|
||||
return if quit
|
||||
quit = true
|
||||
# Don't autoclose if websocket didn't last 1 minute
|
||||
if (new Date()).getTime() - open_ts > 60 * 1000
|
||||
open('','_self').close()
|
||||
|
||||
term = new Terminal document.body, send, ctl
|
||||
term.write 'Closed'
|
||||
# Allow quick reload
|
||||
term.skipNextKey = true
|
||||
term.body.classList.add('dead')
|
||||
# Don't autoclose if websocket didn't last 1 minute
|
||||
if (new Date()).getTime() - openTs > 60 * 1000
|
||||
window.open('','_self').close()
|
||||
|
||||
reopenOnClose = ->
|
||||
setTimeout ->
|
||||
return if quit
|
||||
ws.shell = new WebSocket wsUrl + '/ws' + path
|
||||
init_shell_ws()
|
||||
, 100
|
||||
|
||||
write = (data) ->
|
||||
if term
|
||||
term.write data
|
||||
|
||||
write_request = (e) ->
|
||||
setTimeout write, 1, e.data
|
||||
|
||||
ctl = (e) ->
|
||||
cmd = JSON.parse(e.data)
|
||||
if cmd.cmd is 'size'
|
||||
term.resize cmd.cols, cmd.rows, true
|
||||
|
||||
init_shell_ws = ->
|
||||
ws.shell.addEventListener 'open', open
|
||||
ws.shell.addEventListener 'message', write_request
|
||||
ws.shell.addEventListener 'error', error
|
||||
ws.shell.addEventListener 'close', reopenOnClose
|
||||
|
||||
init_ctl_ws = ->
|
||||
ws.ctl.addEventListener 'open', open
|
||||
ws.ctl.addEventListener 'message', ctl
|
||||
ws.ctl.addEventListener 'error', error
|
||||
ws.ctl.addEventListener 'close', close
|
||||
|
||||
init_shell_ws()
|
||||
init_ctl_ws()
|
||||
|
||||
addEventListener 'beforeunload', ->
|
||||
if not quit
|
||||
'This will exit the terminal session'
|
||||
|
||||
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')
|
||||
|
||||
1179
coffees/term.coffee
1179
coffees/term.coffee
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": "1.5.10",
|
||||
"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
|
||||
48
setup.py
48
setup.py
@@ -5,43 +5,53 @@
|
||||
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',
|
||||
'static/html-sanitizer.js',
|
||||
'static/*.min.js',
|
||||
'templates/index.html'
|
||||
'templates/index.html',
|
||||
'bin/*',
|
||||
'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