143 Commits

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

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

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

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

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

Tested on Firefox Nightly 62 on Linux and Chromium 67 on Linux, with `fcitx` as input method.
2018-06-03 10:27:49 +08:00
Peter Cai
41ee5fb843 update grunt-sass
the old grunt-sass no longer works with newer node.js
2018-06-03 10:12:19 +08:00
Christoph Christen
ae6b36fa89 Updated docker baseimage 2018-01-02 21:05:49 +01:00
Mounier Florian
cfda54a724 Merge pull request #158 from brentley/master
updating setuptools
2017-12-19 18:02:48 +01:00
Brent Langston
033169ab08 updating setuptools 2017-12-19 10:56:53 -06:00
Florian Mounier
920c435b00 Bump 3.2.2 2017-11-23 14:57:08 +01:00
Florian Mounier
27e6aa8a5d Update changelog 2017-11-23 14:56:56 +01:00
Florian Mounier
92633f52ce Fix unescaping entities when linkifying 2017-11-23 14:56:18 +01:00
Florian Mounier
f5f854964b Bump 3.2.1 2017-09-27 11:55:15 +02:00
Florian Mounier
55528fdf91 Issue correct X.509 v3 certificates (you will need to re-generate your certs) 2017-09-27 11:54:58 +02:00
Florian Mounier
9eae13486e Use X509 v4. 2017-09-27 11:34:41 +02:00
Florian Mounier
79bd074dae Bump 3.2.0 2017-09-27 10:59:19 +02:00
Mounier Florian
7b0ba2bfe7 Merge pull request #147 from 3ch01c/master
updated cert generation to v3 to comply with new browser standards
2017-09-21 17:58:55 +02:00
Mounier Florian
db17b9d8ac Merge pull request #152 from f0ma/master
Fix problem with ignoring --shell option in python2
2017-09-21 17:58:11 +02:00
Stanislav Ivanov
b5de82bfcf Fix problem with ignoring --shell option in python2 2017-09-21 18:04:03 +03:00
Jack Miner
13dbe0434c updated cert generation to v3 to comply with new browser standards 2017-07-24 17:52:14 -06:00
Florian Mounier
ef0057c23f Bump 3.1.5 2017-05-29 10:32:28 +02:00
Mounier Florian
6bc8e1438f Merge pull request #146 from warpkwd/fix_i_dont_options
fix i-hereby-...-whatsoever option
2017-05-29 10:21:39 +02:00
Yukihiro KAWADA
8856ea9dc4 fix i-hereby-...-whatsoever option
i-hereby-...-whatsoever revise to i_hereby_whatsoever
2017-05-24 12:59:18 +09:00
Florian Mounier
4edb2d269f Bump 3.1.4 2017-05-15 15:32:50 +02:00
Florian Mounier
272891470c Add --i-hereby-declare-i-dont-want-any-security-whatsoever option. Fix #143 2017-05-15 15:32:39 +02:00
Florian Mounier
574b3dc74b Bump 3.1.3 2017-05-15 11:31:07 +02:00
Florian Mounier
269dd2b618 Fix crash on lsof on python3 2017-05-15 11:30:59 +02:00
Florian Mounier
0625e05cbb Actually fix white-space on folded lines. 2017-05-10 16:01:40 +02:00
Florian Mounier
6b1101bc45 Fix white-space on folded lines. 2017-05-10 15:59:42 +02:00
Florian Mounier
3e6d0b203f Bump 3.1.2 2017-05-03 10:27:40 +02:00
Florian Mounier
8189598dd6 Add __about__ __all__ 2017-05-03 10:27:30 +02:00
Florian Mounier
4a8b5f2147 Add yarn.lock 2017-05-02 18:17:46 +02:00
Florian Mounier
f9a1ff4dea Bump 3.1.1 2017-05-02 18:12:36 +02:00
Florian Mounier
96d88a5e91 Bump 3.1.0 2017-05-02 18:00:24 +02:00
Florian Mounier
bdc1c7a80d Add a Makefile. Lint code. Fix butterfly open. Add a CHANGELOG.md 2017-05-02 17:59:52 +02:00
Florian Mounier
eacfdcd52f Fix huge performance loss on extended lines 2017-04-04 18:14:27 +02:00
Florian Mounier
ed347e2bd0 Use a __about__ file 2017-03-30 10:20:25 +02:00
Florian Mounier
3228e8c204 Integrate new themes 2017-03-30 10:14:03 +02:00
Florian Mounier
b9c991e3b6 Use pkg_resources and bump 3.0.1 2017-03-20 10:43:23 +01:00
Florian Mounier
8ad12c2379 Don't import pam if not necessary + pep8 2017-03-20 10:32:55 +01:00
Florian Mounier
2aa237ef12 Fix certificate generation 2017-03-13 11:59:10 +01:00
Florian Mounier
40496eb9d1 Protect session closing. References #124 2017-02-21 11:14:04 +01:00
Florian Mounier
ffd19b8162 setup.py typo 2017-02-13 15:25:22 +01:00
Florian Mounier
6663568500 Version to beta 2017-02-13 15:23:31 +01:00
Florian Mounier
3a09c47ef0 Fix #132 2017-02-13 15:10:54 +01:00
Florian Mounier
41ab0f36ff Fix user argument 2017-02-13 11:54:40 +01:00
Florian Mounier
70e00ac696 Fix pam condition 2017-02-13 11:36:29 +01:00
Florian Mounier
70369a0b32 Remove systemd direct calls 2017-02-13 11:26:39 +01:00
Florian Mounier
8c20ffb943 Merge remote-tracking branch 'PeterCxy/patch-pam' 2017-02-13 11:18:01 +01:00
Florian Mounier
729c768dc2 Happy new year (a bit late) 2017-02-13 11:16:59 +01:00
Florian Mounier
17f8c1d1c9 Merge branch 'master' of github.com:paradoxxxzero/butterfly 2017-02-13 11:03:20 +01:00
Florian Mounier
964fd07143 uuid4 from Math.random is a security flaw 2017-02-13 10:45:31 +01:00
Florian Mounier
8553bbd0cb Typo 2017-02-13 10:37:04 +01:00
Peter Cai
f494541652 pam: environment should be reinitialized after authentication 2017-02-11 09:00:24 +08:00
Peter Cai
dd6c917462 pam: authenticate in a separate process 2017-02-11 08:56:17 +08:00
Peter Cai
9e03e24764 terminal: support PAM authentication
Fix #129

Actually, we are reinventing the wheel... But after all, it is not
possible to change the profile name of `su`, so we just pull in the PAM
bindings for Python and use it for PAM authentication.

A new option `--pam_profile` has been added for users to specify their
preferred PAM profile. Note that Butterfly should be started as ROOT or
it will not be possible to authenticate via PAM.
2017-02-10 20:13:09 +08:00
Mounier Florian
6b5f3ac76f Merge pull request #131 from PeterCxy/patch-keepalive
Send ping packets to keep connection alive
2017-02-10 10:07:03 +01:00
Peter Cai
a36579bb12 Send ping packets to keep connection alive
Fix #126

Idle WebSocket connections tend to be closed after some period of time.
This commit enables the Butterfly server to send ping packets
periodically in order to keep the connection alive.

A new option `keepalive_interval` is also introduced for users to
specify the interval to send `ping` packets. By default it is 30
seconds.
2017-02-10 15:11:51 +08:00
Florian Mounier
e4ce69a967 Fix keyboard selection 2016-11-28 14:38:49 +01:00
Florian Mounier
b0e1f37cac Interesting Fixes 2016-10-13 17:45:12 +02:00
Florian Mounier
da659b7526 Fix selection 2016-10-13 16:25:18 +02:00
Florian Mounier
08ecb4d0d2 Add a bottom margin 2016-10-13 16:19:33 +02:00
Florian Mounier
3624962d3c Historize by ext 2016-10-13 16:11:44 +02:00
Florian Mounier
b9f1727f1e Finally a near perfect resize 2016-10-13 15:24:57 +02:00
Florian Mounier
5a7c4da0b1 Unoptimized but working dom creation 2016-10-13 12:41:31 +02:00
Florian Mounier
fa2b9d2bee Rework refresh 2016-10-13 10:03:15 +02:00
Florian Mounier
3bb6da1eae Fix ctl 2016-10-11 11:22:09 +02:00
Florian Mounier
6c827206f7 Merge branch 'master' into 2ws 2016-10-07 11:32:15 +02:00
Florian Mounier
fdeba5a5d4 Fix big paste data loss 2016-10-07 11:31:51 +02:00
Florian Mounier
d0eb37765a Limit rate on paste 2016-10-07 11:30:59 +02:00
Florian Mounier
8dffb02980 Merge branch 'master' into 2ws 2016-10-07 09:59:14 +02:00
Florian Mounier
15ebdf6907 Add local script loading 2016-10-05 16:51:16 +02:00
Mounier Florian
6e29c702e3 Merge pull request #117 from cristen/master
Fix missing env variables for KDE5
2016-09-30 11:26:20 +02:00
Jean-Marc Martins
c3ad2f342a Fix missing env variables for KDE5 2016-09-30 11:23:26 +02:00
Florian Mounier
7d7f05e164 Fix scroll 2016-09-29 17:15:38 +02:00
Florian Mounier
64a8480938 Alpha bump 2016-09-29 11:43:25 +02:00
Florian Mounier
0142ec0a16 Add horizontal wrap (expandable no wrapping lines) on decset 77 2016-09-29 11:40:19 +02:00
Florian Mounier
97d435ce18 Add horizontal wrap (expandable no wrapping lines) on decset 77 2016-09-29 11:39:49 +02:00
Florian Mounier
4b3a5e1ae6 Add horizontal wrap (expandable no wrapping lines) on decset 77 2016-09-29 11:31:45 +02:00
Florian Mounier
9fcc156257 Merge branch 'master' of github.com:paradoxxxzero/butterfly 2016-09-28 17:42:37 +02:00
Florian Mounier
2887f6e25a Linkify as an extension. Finally fix #97 2016-09-28 17:42:23 +02:00
Mounier Florian
ffe8945c09 Merge pull request #116 from Silex/master
Docker updates
2016-09-06 10:12:12 +02:00
Philippe Vaucher
a3e78112a6 Improve README examples 2016-09-06 09:16:34 +02:00
Philippe Vaucher
e5eb7050e8 Add .dockerignore 2016-09-06 09:16:34 +02:00
Philippe Vaucher
b72da2e4ef Prettify README code blocks 2016-09-06 09:16:34 +02:00
Philippe Vaucher
2d3bed2fef Use ubuntu:14.04 base image 2016-09-06 09:16:34 +02:00
gar
cc510500a5 Do @thaJeztah container efficiency suggestions 2016-09-06 09:16:18 +02:00
gar
1ec50810f9 Make script executable 2016-09-06 09:14:46 +02:00
gar
524e578fca Updating readme with correct no password usage 2016-09-06 09:14:45 +02:00
gar
bce9f99b0b Updating docker container with the new usage flag 2016-09-06 09:07:52 +02:00
Philippe Vaucher
9bcc989149 Fix bug where PATH contains '.'
Starting bash without PATH set triggers something where PATH then
contains '.', preventing many tools from functionning correctly.

Starting a login shell prevents this.
2016-09-05 17:53:57 +02:00
Florian Mounier
1d324ed243 Wait a bit on close 2016-08-19 17:46:15 +02:00
Florian Mounier
3c2bf35b09 Fix most of things 2016-08-19 17:42:36 +02:00
Florian Mounier
fe258f44f8 Add a ctl ws, base all session on a session key. 2016-08-19 14:48:51 +02:00
Florian Mounier
1f9d263ad7 Fix unwanted change 2016-08-08 11:24:07 +02:00
Florian Mounier
fe01ffb2b4 Fix keyup 2016-08-08 11:19:54 +02:00
Florian Mounier
ac7e9bef8e Remove throuput control and use the pause/break key to prevent flood. 2016-08-08 11:12:22 +02:00
Florian Mounier
503de38429 Add uri_root_path option. References #104. 2016-06-13 16:15:57 +02:00
Florian Mounier
7ebb122221 Fix login=False when secure dropping in root 2016-05-11 12:02:25 +02:00
Florian Mounier
ec25edb657 Try to behave more like a unix term by sending sighup/sigcont on close and not sigkill. Might be a fix for #100 2016-02-26 14:59:10 +01:00
Florian Mounier
52714d81ab Login should be False by default 2016-02-26 14:41:05 +01:00
Florian Mounier
c048f1a4e6 Try to fix login again. #96 2016-02-02 10:20:46 +01:00
Florian Mounier
c0e2d8959b Merge #94 2016-01-18 10:19:01 +01:00
Florian Mounier
5c054ca290 Bump 2.0.1 2016-01-18 10:09:44 +01:00
Florian Mounier
9168878d92 Merge branch 'master' of github.com:paradoxxxzero/butterfly 2016-01-18 10:05:54 +01:00
Florian Mounier
056fbc02b1 Fix home/end 2016-01-18 10:05:50 +01:00
Florian Mounier
571f07946d Fix style path in help 2016-01-16 13:37:10 +01:00
Florian Mounier
e09bab810c Fix auto conf creation 2016-01-16 13:08:53 +01:00
Florian Mounier
efb019ed00 Fix #5. Use login su when unsecure 2016-01-06 14:53:53 +01:00
Florian Mounier
34d2711aa1 Merge branch 'master' of github.com:paradoxxxzero/butterfly 2016-01-06 13:18:09 +01:00
Florian Mounier
115190446b --login should work correctly. Fix #93 2016-01-06 13:17:09 +01:00
Mounier Florian
c8931c6135 Merge pull request #92 from jinmel/master
fixed shutil.get_terminal_size error
2016-01-06 12:39:42 +01:00
Jin Suk Park
ab7880779d fixed shutil.get_terminal_size error 2015-12-26 00:52:37 +00:00
Mounier Florian
f5724cc39d Update README.md 2015-10-30 12:07:31 +01:00
Florian Mounier
33d4051fca Build 2015-10-30 11:19:16 +01:00
Florian Mounier
28ebf9d8a2 Clear scrollback on hard reset 2015-10-27 16:23:37 +01:00
Florian Mounier
5714b97c77 Get the session for current user only for env far fetch. Work on readme for 2.0 2015-10-20 14:53:25 +02:00
Florian Mounier
a9c35d91f1 Add geolocation 2015-10-19 15:01:11 +02:00
Florian Mounier
573b4f1c1b Fix env for new gnome-session 2015-10-19 12:08:51 +02:00
Florian Mounier
856aac2bcb Clean up bin and create https://github.com/paradoxxxzero/butterfly-demos 2015-10-19 11:51:23 +02:00
Florian Mounier
e789622b7e With js. One day I will bundle everything in one shot. 2015-10-19 10:51:14 +02:00
Florian Mounier
84c4ff9414 Avoid broken image at launch on abstract image 2015-10-19 10:25:36 +02:00
Florian Mounier
4a3bd7f906 Fix setup.py 2015-10-16 17:56:44 +02:00
64 changed files with 4353 additions and 1473 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
.git
.gitignore
.dockerignore
Dockerfile
README.md
butterfly.png

4
.gitignore vendored
View File

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

2
.isort.cfg Normal file
View File

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

46
CHANGELOG.md Normal file
View File

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

View File

@@ -1,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"]

View File

@@ -36,7 +36,7 @@ module.exports = (grunt) ->
coffeelint:
butterfly:
'coffees/*.coffee'
'coffees/**/*.coffee'
watch:
options:

View File

@@ -1,4 +1,4 @@
butterfly Copyright (C) 2015 Florian Mounier, Kozea
butterfly Copyright(C) 2015-2017 Florian Mounier, Kozea
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or

37
Makefile Normal file
View File

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

10
Makefile.config Normal file
View File

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

129
README.md
View File

@@ -1,36 +1,83 @@
# ƸӜƷ butterfly
# ƸӜƷ butterfly 3.0
![](http://paradoxxxzero.github.io/assets/butterfly_1.gif)
![](http://paradoxxxzero.github.io/assets/butterfly_2.0_1.gif)
## Description
Butterfly is a tornado web server written in python which powers a full featured web terminal.
Butterfly is a xterm compatible terminal that runs in your browser.
The js part is heavily based on [term.js](https://github.com/chjj/term.js/) which is heavily based on [jslinux](http://bellard.org/jslinux/).
## Features
* xterm compatible (support a lot of unused features!)
* Native browser scroll and search
* Theming in css / sass [(20 preset themes)](https://github.com/paradoxxxzero/butterfly-themes) endless possibilities!
* HTML in your terminal! cat images and use <table>
* Multiple sessions support (à la screen -x) to simultaneously access a terminal from several places on the planet!
* Secure authentication with X509 certificates!
* 16,777,216 colors support!
* Keyboard text selection!
* Desktop notifications on terminal output!
* Geolocation from browser!
* May work on firefox too!
## Try it
```bash
$ pip install butterfly
$ butterfly.server.py
``` bash
$ pip install butterfly
$ pip install butterfly[themes] # If you want to use themes
$ pip install butterfly[systemd] # If you want to use systemd
$ butterfly
```
Then open [localhost:57575](http://localhost:57575) in your favorite browser and done.
A new tab should appear in your browser. Then type
``` bash
$ butterfly help
```
To get an overview of butterfly features.
## Run it as a server
``` bash
$ butterfly.server.py --host=myhost --port=57575
```
Or with login prompt
```bash
$ butterfly.server.py --host=myhost --port=57575 --login
```
Or with PAM authentication (ROOT required)
```bash
# butterfly.server.py --host=myhost --port=57575 --login --pam_profile=sshd
```
You can change `sshd` to your preferred PAM profile.
The first time it will ask you to generate the certificates (see: [here](http://paradoxxxzero.github.io/2014/03/21/butterfly-with-ssl-auth.html))
## Run it with systemd (linux)
Systemd provides a way to automatically activate daemons when needed (socket activation):
```bash
$ cd /etc/systemd/system
# curl -O https://raw.githubusercontent.com/paradoxxxzero/butterfly/master/butterfly.service
# curl -O https://raw.githubusercontent.com/paradoxxxzero/butterfly/master/butterfly.socket
# systemctl enable butterfly.socket
# systemctl start butterfly.socket
``` bash
$ cd /etc/systemd/system
$ curl -O https://raw.githubusercontent.com/paradoxxxzero/butterfly/master/butterfly.service
$ curl -O https://raw.githubusercontent.com/paradoxxxzero/butterfly/master/butterfly.socket
$ systemctl enable butterfly.socket
$ systemctl start butterfly.socket
```
Don't forget to update the /etc/butterfly/butterfly.conf file with your server options (host, port, shell, ...) and to install butterfly with the [systemd] flag.
## Contribute
and make the world better (or just butterfly).
@@ -41,42 +88,54 @@ If you don't know what to do go to the github issues and pick one you like.
If you want to motivate me to continue working on this project you can tip me, see: http://paradoxxxzero.github.io/about/
The dev requirements are coffee script and compass for the client side.
Run `python dev.py --debug --port=12345` and you are set (yes you can launch it from your regular butterfly instance)
Client side development use [grunt](http://gruntjs.com/) and [bower](http://bower.io/).
## Credits
The js part is based on [term.js](https://github.com/chjj/term.js/) which is based on [jslinux](http://bellard.org/jslinux/).
## Author
[Florian Mounier](http://paradoxxxzero.github.io/)
## License
```
butterfly Copyright (C) 2015 Florian Mounier
butterfly Copyright (C) 2015-2017 Florian Mounier
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
```
## Docker Usage
## Docker
There is a docker repository created for this project that is set to automatically rebuild when there is a push
into this repository: https://registry.hub.docker.com/u/garland/butterfly/
### Starting
### Example usage
docker run \
--env PASSWORD=password \
--env PORT=57575 \
-p 57575:57575 \
-d garland/butterfly
Starting with login and password
``` bash
docker run --env PASSWORD=password -d garland/butterfly --login
```
Starting with no password
``` bash
docker run -d -p 57575:57575 garland/butterfly
```
Starting with a different port
``` bash
docker run -d -p 12345:12345 garland/butterfly --port=12345
```

View File

@@ -3,7 +3,7 @@
# This file is part of butterfly
#
# butterfly Copyright (C) 2015 Florian Mounier
# butterfly Copyright(C) 2015-2017 Florian Mounier
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
@@ -20,7 +20,11 @@
import tornado.options
import tornado.ioloop
import tornado.httpserver
import tornado_systemd
try:
from tornado_systemd import SystemdHTTPServer as HTTPServer
except ImportError:
from tornado.httpserver import HTTPServer
import logging
import webbrowser
import uuid
@@ -40,6 +44,9 @@ tornado.options.define("unminified", default=False,
tornado.options.define("host", default='localhost', help="Server host")
tornado.options.define("port", default=57575, type=int, help="Server port")
tornado.options.define("keepalive_interval", default=30, type=int,
help="Interval between ping packets sent from server "
"to client (in seconds)")
tornado.options.define("one_shot", default=False,
help="Run a one-shot instance. Quit at term close")
tornado.options.define("shell", help="Shell to execute at login")
@@ -48,8 +55,18 @@ tornado.options.define("cmd",
help="Command to run instead of shell, f.i.: 'ls -l'")
tornado.options.define("unsecure", default=False,
help="Don't use ssl not recommended")
tornado.options.define("login", default=True,
tornado.options.define("i_hereby_declare_i_dont_want_any_security_whatsoever",
default=False,
help="Remove all security and warnings. There are some "
"use cases for that. Use this if you really know what "
"you are doing.")
tornado.options.define("login", default=False,
help="Use login screen at start")
tornado.options.define("pam_profile", default="", type=str,
help="When --login=True provided and running as ROOT, "
"use PAM with the specified PAM profile for "
"authentication and then execute the user's default "
"shell. Will override --shell.")
tornado.options.define("force_unicode_width",
default=False,
help="Force all unicode characters to the same width."
@@ -64,6 +81,10 @@ tornado.options.define("generate_current_user_pkcs", default=False,
tornado.options.define("generate_user_pkcs", default='',
help="Generate user pfx for client authentication "
"(Must be root to create for another user)")
tornado.options.define("uri_root_path", default='',
help="Sets the servier root path: "
"example.com/<uri_root_path>/static/")
if os.getuid() == 0:
ev = os.getenv('XDG_CONFIG_DIRS', '/etc')
@@ -77,17 +98,6 @@ butterfly_dir = os.path.join(ev, 'butterfly')
conf_file = os.path.join(butterfly_dir, 'butterfly.conf')
ssl_dir = os.path.join(butterfly_dir, 'ssl')
if not os.path.exists(conf_file):
try:
shutil.copy(
os.path.join(
os.path.abspath(os.path.dirname(__file__)),
'butterfly',
'butterfly.conf.default'), conf_file)
print('butterfly.conf installed in %s' % conf_file)
except:
pass
tornado.options.define("conf", default=conf_file,
help="Butterfly configuration file. "
"Contains the same options as command line.")
@@ -104,6 +114,21 @@ if os.path.exists(tornado.options.options.conf):
# Do it again to overwrite conf with args
tornado.options.parse_command_line()
# For next time, create them a conf file from template.
# Need to do this after parsing options so we do not trigger
# code import for butterfly module, in case that code is
# dependent on the set of parsed options.
if not os.path.exists(conf_file):
try:
import butterfly
shutil.copy(
os.path.join(
os.path.abspath(os.path.dirname(butterfly.__file__)),
'butterfly.conf.default'), conf_file)
print('butterfly.conf installed in %s' % conf_file)
except:
pass
options = tornado.options.options
for logger in ('tornado.access', 'tornado.application',
@@ -120,6 +145,9 @@ log = logging.getLogger('butterfly')
host = options.host
port = options.port
if options.i_hereby_declare_i_dont_want_any_security_whatsoever:
options.unsecure = True
if not os.path.exists(options.ssl_dir):
os.makedirs(options.ssl_dir)
@@ -128,6 +156,7 @@ if not os.path.exists(options.ssl_dir):
def to_abs(file):
return os.path.join(options.ssl_dir, file)
ca, ca_key, cert, cert_key, pkcs12 = map(to_abs, [
'butterfly_ca.crt', 'butterfly_ca.key',
'butterfly_%s.crt', 'butterfly_%s.key',
@@ -153,6 +182,10 @@ def read(file):
with open(file, 'rb') as fd:
return fd.read()
def b(s):
return s.encode('utf-8')
if options.generate_certs:
from OpenSSL import crypto
print('Generating certificates for %s (change it with --host)\n' % host)
@@ -162,6 +195,7 @@ if options.generate_certs:
ca_pk = crypto.PKey()
ca_pk.generate_key(crypto.TYPE_RSA, 2048)
ca_cert = crypto.X509()
ca_cert.set_version(2)
ca_cert.get_subject().CN = 'Butterfly CA on %s' % socket.gethostname()
fill_fields(ca_cert.get_subject())
ca_cert.set_serial_number(uuid.uuid4().int)
@@ -169,6 +203,21 @@ if options.generate_certs:
ca_cert.gmtime_adj_notAfter(315360000) # to 10y
ca_cert.set_issuer(ca_cert.get_subject()) # Self signed
ca_cert.set_pubkey(ca_pk)
ca_cert.add_extensions([
crypto.X509Extension(
b('basicConstraints'), True, b('CA:TRUE, pathlen:0')),
crypto.X509Extension(
b('keyUsage'), True, b('keyCertSign, cRLSign')),
crypto.X509Extension(
b('subjectKeyIdentifier'), False, b('hash'), subject=ca_cert),
])
ca_cert.add_extensions([
crypto.X509Extension(
b('authorityKeyIdentifier'), False,
b('issuer:always, keyid:always'),
issuer=ca_cert, subject=ca_cert
)
])
ca_cert.sign(ca_pk, 'sha512')
write(ca, crypto.dump_certificate(crypto.FILETYPE_PEM, ca_cert))
@@ -182,7 +231,23 @@ if options.generate_certs:
server_pk = crypto.PKey()
server_pk.generate_key(crypto.TYPE_RSA, 2048)
server_cert = crypto.X509()
server_cert.set_version(2)
server_cert.get_subject().CN = host
server_cert.add_extensions([
crypto.X509Extension(
b('basicConstraints'), False, b('CA:FALSE')),
crypto.X509Extension(
b('subjectKeyIdentifier'), False, b('hash'), subject=server_cert),
crypto.X509Extension(
b('subjectAltName'), False, b('DNS:%s' % host)),
])
server_cert.add_extensions([
crypto.X509Extension(
b('authorityKeyIdentifier'), False,
b('issuer:always, keyid:always'),
issuer=ca_cert, subject=ca_cert
)
])
fill_fields(server_cert.get_subject())
server_cert.set_serial_number(uuid.uuid4().int)
server_cert.gmtime_adj_notBefore(0) # From now
@@ -232,6 +297,7 @@ if (options.generate_current_user_pkcs or
client_pk.generate_key(crypto.TYPE_RSA, 2048)
client_cert = crypto.X509()
client_cert.set_version(2)
client_cert.get_subject().CN = user
fill_fields(client_cert.get_subject())
client_cert.set_serial_number(uuid.uuid4().int)
@@ -295,11 +361,10 @@ else:
from butterfly import application
application.butterfly_dir = butterfly_dir
log.info('Starting server')
http_server = tornado_systemd.SystemdHTTPServer(
application, ssl_options=ssl_opts)
http_server = HTTPServer(application, ssl_options=ssl_opts)
http_server.listen(port, address=host)
if http_server.systemd:
if getattr(http_server, 'systemd', False):
os.environ.pop('LISTEN_PID')
os.environ.pop('LISTEN_FDS')
@@ -310,8 +375,10 @@ ioloop = tornado.ioloop.IOLoop.instance()
if port == 0:
port = list(http_server._sockets.values())[0].getsockname()[1]
url = "http%s://%s:%d/" % (
"s" if not options.unsecure else "", host, port)
url = "http%s://%s:%d/%s" % (
"s" if not options.unsecure else "", host, port,
(options.uri_root_path.strip('/') + '/') if options.uri_root_path else ''
)
if not options.one_shot or not webbrowser.open(url):
log.warn('Butterfly is ready, open your browser to: %s' % url)

15
butterfly/__about__.py Normal file
View File

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

View File

@@ -1,7 +1,7 @@
# *-* coding: utf-8 *-*
# This file is part of butterfly
#
# butterfly Copyright (C) 2015 Florian Mounier
# butterfly Copyright(C) 2015-2017 Florian Mounier
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
@@ -14,8 +14,7 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
__version__ = '2.0.0-beta'
from .__about__ import * # noqa: F401,F403
import os
import tornado.web
@@ -23,6 +22,7 @@ import tornado.options
import tornado.web
from logging import getLogger
log = getLogger('butterfly')
@@ -31,10 +31,15 @@ class url(object):
self.url = url
def __call__(self, cls):
if tornado.options.options.uri_root_path:
url = '/' + tornado.options.options.uri_root_path.strip('/') + self.url
else:
url = self.url
application.add_handlers(
r'.*$',
(tornado.web.url(self.url, cls, name=cls.__name__),)
(tornado.web.url(url, cls, name=cls.__name__),)
)
return cls
@@ -46,13 +51,18 @@ class Route(tornado.web.RequestHandler):
@property
def builtin_themes_dir(self):
return os.path.join(
os.path.dirname(__file__), 'themes')
os.path.dirname(__file__), 'themes')
@property
def themes_dir(self):
return os.path.join(
self.application.butterfly_dir, 'themes')
@property
def local_js_dir(self):
return os.path.join(
self.application.butterfly_dir, 'js')
def get_theme_dir(self, theme):
if theme.startswith('built-in-'):
return os.path.join(
@@ -66,7 +76,10 @@ if hasattr(tornado.options.options, 'debug'):
application = tornado.web.Application(
static_path=os.path.join(os.path.dirname(__file__), "static"),
template_path=os.path.join(os.path.dirname(__file__), "templates"),
debug=tornado.options.options.debug
debug=tornado.options.options.debug,
static_url_prefix='%s/static/' % (
'/%s' % tornado.options.options.uri_root_path.strip('/')
if tornado.options.options.uri_root_path else '')
)
import butterfly.routes
import butterfly.routes # noqa: F401

View File

@@ -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')

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
#!/usr/bin/env python
import base64
import os
import subprocess
import butterfly
from butterfly.escapes import image
from butterfly.utils import ansi_colors
import os
import base64
import shutil
print(ansi_colors.white + "Welcome to the butterfly help." + ansi_colors.reset)
path = os.getenv('BUTTERFLY_PATH')
@@ -30,13 +32,14 @@ Butterfly is a xterm compliant terminal built with python and javascript.
{title}Butterfly programs:{reset}
{strong}b : {reset}Alias for {strong}butterfly{reset} executable. Takes a comand in parameter or launch a butterfly server for one shot use (if outside butterfly).
{strong}b cat : {reset}A wrapper around cat allowing to display images as <img> instead of binary.
{strong}b open : {reset}Open a new terminal at specified location.
{strong}b session : {reset}Open or rattach a butterfly session. Multiplexing is supported.
{strong}b colors : {reset}Test the terminal colors (16, 256 and 16777216 colors)
{strong}b hr : {reset}Put a html hr. This is a test for html output.
{strong}b calendar : {reset}Display current month using html. This is also a test for html output.
{strong}b : {reset}Alias for {strong}butterfly{reset} executable. Takes a comand in parameter or launch a butterfly server for one shot use (if outside butterfly).
{strong}b cat : {reset}A wrapper around cat allowing to display images as <img> instead of binary.
{strong}b open : {reset}Open a new terminal at specified location.
{strong}b session : {reset}Open or rattach a butterfly session. Multiplexing is supported.
{strong}b colors : {reset}Test the terminal colors (16, 256 and 16777216 colors)
{strong}b html : {reset}Output in html standard input.
For more butterfly programs check out: https://github.com/paradoxxxzero/butterfly-demos
{title}Styling butterfly:{reset}
@@ -55,7 +58,6 @@ Butterfly is a xterm compliant terminal built with python and javascript.
code=ansi_colors.light_yellow,
comment=ansi_colors.light_magenta,
reset=ansi_colors.reset,
rcol=shutil.get_terminal_size()[0] - 31,
rcol=int(subprocess.check_output(['stty', 'size']).split()[1]) - 31,
main=os.path.normpath(os.path.join(
os.path.abspath(os.path.dirname(__file__)),
'../sass/'))))
os.path.abspath(os.path.dirname(butterfly.__file__)), 'sass'))))

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env python
import sys
sys.stdout.write('\x1bP;HTML|<hr />\x1bP')
sys.stdout.flush()

View File

@@ -1,8 +1,19 @@
#!/usr/bin/env python
from butterfly.escapes import html
import argparse
import fileinput
import sys
from butterfly.escapes import html
parser = argparse.ArgumentParser(
description="Butterfly html converter.\n\n"
"Output in html standard input.\n"
"Example: $ echo \"<b>Bold</b>\" | b html",
formatter_class=argparse.RawTextHelpFormatter)
parser.parse_known_args()
with html():
for line in fileinput.input():
sys.stdout.write(line)

View File

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

View File

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

View File

@@ -1,12 +0,0 @@
#!/usr/bin/env python
import sys
w = sys.stdout.write
print('Image injection test')
injection = 'R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" onload="alert(\'pwnd\')" /><img src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
w('\x1bP;IMAGE|image/gif;%s' % injection)
w('\x1bP')
print('HTML script execution test')
w('\x1bP;HTML|<img src="https://imgs.xkcd.com/comics/hack.png" onload="alert(\'pwnd\')" />')
w('\x1bP')

View File

@@ -1,5 +1,9 @@
from contextlib import contextmanager
import sys
import termios
import tty
from contextlib import contextmanager
from butterfly.utils import ansi_colors as colors # noqa: F401
@contextmanager
@@ -34,9 +38,33 @@ def text():
sys.stdout.flush()
@contextmanager
def sass():
sys.stdout.write('\x1bP;SASS|')
yield
sys.stdout.write('\x1bP')
def geolocation():
sys.stdout.write('\x1b[?99n')
sys.stdout.flush()
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
rv = sys.stdin.read(1)
if rv != '\x1b':
raise
rv = sys.stdin.read(1)
if rv != '[':
raise
rv = sys.stdin.read(1)
if rv != '?':
raise
loc = ''
while rv != 'R':
rv = sys.stdin.read(1)
if rv != 'R':
loc += rv
except Exception:
return
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
if not loc or ';' not in loc:
return
return tuple(map(float, loc.split(';')))

192
butterfly/pam.py Normal file
View 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)

View File

@@ -1,7 +1,7 @@
# *-* coding: utf-8 *-*
# This file is part of butterfly
#
# butterfly Copyright (C) 2015 Florian Mounier
# butterfly Copyright(C) 2015-2017 Florian Mounier
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
@@ -16,16 +16,22 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
import os
import struct
import sys
import time
from collections import defaultdict
from mimetypes import guess_type
from uuid import uuid4
import tornado.escape
import tornado.options
import tornado.process
import tornado.escape
import tornado.web
import tornado.websocket
from mimetypes import guess_type
from collections import defaultdict
from butterfly import url, Route, utils, __version__
from butterfly import Route, url, utils
from butterfly.terminal import Terminal
@@ -35,12 +41,15 @@ def u(s):
return s
@url(r'/(?:user/(.+))?/?(?:wd/(.+))?/?(?:session/(.+))?')
@url(r'/(?:session/(?P<session>[^/]+)/?)?')
class Index(Route):
def get(self, user, path, session):
def get(self, session):
user = self.request.query_arguments.get(
'user', [b''])[0].decode('utf-8')
if not tornado.options.options.unsecure and user:
raise tornado.web.HTTPError(400)
return self.render('index.html')
return self.render(
'index.html', session=session or str(uuid4()))
@url(r'/theme/([^/]+)/style.css')
@@ -89,7 +98,6 @@ class Theme(Route):
@url(r'/theme/([^/]+)/(.+)')
class ThemeStatic(Route):
def get(self, theme, name):
if '..' in name:
raise tornado.web.HTTPError(403)
@@ -101,7 +109,19 @@ class ThemeStatic(Route):
raise tornado.web.HTTPError(403)
if os.path.exists(fn):
self.set_header("Content-Type", guess_type(fn)[0])
type = guess_type(fn)[0]
if type is None:
# Fallback if there's no mimetypes on the system
type = {
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'gif': 'image/gif',
'woff': 'application/font-woff',
'ttf': 'application/x-font-ttf'
}.get(fn.split('.')[-1], 'text/plain')
self.set_header("Content-Type", type)
with open(fn, 'rb') as s:
while True:
data = s.read(16384)
@@ -113,51 +133,48 @@ class ThemeStatic(Route):
raise tornado.web.HTTPError(404)
@url(r'/ws'
'(?:/user/(?P<user>[^/]+))?/?'
'(?:session/(?P<session>[^/]+))?/?'
'(?:/wd/(?P<path>.+))?')
class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
session_history_size = 50000
# List of websockets per session per user
# dict: user -> dict: session -> [TermWebSocket]
sessions = defaultdict(dict)
class KeptAliveWebSocketHandler(tornado.websocket.WebSocketHandler):
keepalive_timer = None
# Terminal for session per user
# dict: user -> dict: session -> Terminal
terminals = defaultdict(dict)
def open(self, *args, **kwargs):
self.keepalive_timer = tornado.ioloop.PeriodicCallback(
self.send_ping, tornado.options.options.keepalive_interval * 1000)
self.keepalive_timer.start()
# All terminals sockets for systemd socket deactivation
sockets = []
def send_ping(self):
t = int(time.time())
frame = struct.pack('<I', t) # A ping frame based on time
self.log.info("Sending ping frame %s" % t)
try:
self.ping(frame)
except tornado.websocket.WebSocketClosedError:
self.keepalive_timer.stop()
# Session history
history = {}
def on_close(self):
if self.keepalive_timer is not None:
self.keepalive_timer.stop()
def open(self, user, path, session):
@url(r'/ctl/session/(?P<session>[^/]+)')
class TermCtlWebSocket(Route, KeptAliveWebSocketHandler):
sessions = defaultdict(list)
sessions_secure_users = {}
def open(self, session):
super(TermCtlWebSocket, self).open(session)
self.session = session
self.closed = False
self.secure_user = None
# Prevent cross domain
if self.request.headers['Origin'] not in (
'http://%s' % self.request.headers['Host'],
'https://%s' % self.request.headers['Host']):
self.log.warning(
'Unauthorized connection attempt: from : %s to: %s' % (
self.request.headers['Origin'],
self.request.headers['Host']))
self.close()
return
TermWebSocket.sockets.append(self)
self.log.info('Websocket opened %r' % self)
self.set_nodelay(True)
self.log.info('Websocket /ctl opened %r' % self)
def create_terminal(self):
socket = utils.Socket(self.ws_connection.stream.socket)
opts = tornado.options.options
user = self.request.query_arguments.get(
'user', [b''])[0].decode('utf-8')
path = self.request.query_arguments.get(
'path', [b''])[0].decode('utf-8')
secure_user = None
if not opts.unsecure:
if not tornado.options.options.unsecure:
user = utils.parse_cert(
self.ws_connection.stream.socket.getpeercert())
assert user, 'No user in certificate'
@@ -167,137 +184,148 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
raise Exception('Invalid user in certificate')
# Certificate authed user
self.secure_user = user
secure_user = user
elif socket.local and socket.user == utils.User():
elif socket.local and socket.user == utils.User() and not user:
# Local to local returning browser user
self.secure_user = socket.user
secure_user = socket.user
elif user:
try:
user = utils.User(name=user)
except LookupError:
raise Exception('Invalid user')
# Handling terminal session
if session:
if session in self.user_sessions:
# Session already here, registering websocket
self.user_sessions[session].append(self)
self.write_message('S' + TermWebSocket.history[session])
# And returning, we don't want another terminal
return
if secure_user:
user = secure_user
if self.session in self.sessions and self.session in (
self.sessions_secure_users):
if user.name != self.sessions_secure_users[self.session]:
# Restrict to authorized users
raise tornado.web.HTTPError(403)
else:
# New session, opening terminal
self.user_sessions[session] = [self]
TermWebSocket.history[session] = ''
self.sessions_secure_users[self.session] = user.name
self.sessions[self.session].append(self)
terminal = Terminal.sessions.get(self.session)
# Handling terminal session
if terminal:
TermWebSocket.last.write_message(terminal.history)
# And returning, we don't want another terminal
return
# New session, opening terminal
terminal = Terminal(
user, path, session, socket,
self.request.headers['Host'], self.render_string, self.write)
user, path, self.session, socket,
self.request.full_url().replace('/ctl/', '/'), self.render_string,
TermWebSocket.broadcast)
terminal.pty()
if session:
if not self.secure_user:
self.log.error(
'No terminal session without secure authenticated user'
'or local user.')
self._terminal = terminal
self.session = None
else:
self.log.info('Openning session %s for secure user %r' % (
session, self.secure_user))
self.user_terminals[session] = terminal
else:
self._terminal = terminal
@property
def user_sessions(self):
"""Return the dict session of socket lists"""
if not self.secure_user:
return {}
return TermWebSocket.sessions[self.secure_user.name]
@property
def user_terminals(self):
"""Return the dict session of terminal"""
if not self.secure_user:
return {}
return TermWebSocket.terminals[self.secure_user.name]
self.log.info('Openning session %s for secure user %r' % (
self.session, user))
@classmethod
def close_all(cls, session, user):
terminals = TermWebSocket.terminals.get(user.name)
del terminals[session]
sessions = TermWebSocket.sessions.get(user.name)
if sessions:
sockets = sessions[session]
for socket in sockets[:]:
socket.on_close()
socket.close()
del sessions[session]
@classmethod
def broadcast(cls, session, message, user, emitter=None):
if message[0] == 'S':
cls.history[session] += message[1:]
if len(cls.history[session]) > cls.session_history_size:
cls.history[session] = cls.history[session][
-cls.session_history_size:]
sessions = cls.sessions.get(user.name, [])
for session in sessions[session]:
def broadcast(cls, session, message, emitter=None):
for wsocket in cls.sessions[session]:
try:
if session != emitter:
session.write_message(message)
if wsocket != emitter:
wsocket.write_message(message)
except Exception:
session.log.exception('Error on broadcast')
session.close()
def write(self, message):
if self.session and self.secure_user:
if message is None:
TermWebSocket.close_all(self.session, self.secure_user)
else:
TermWebSocket.broadcast(
self.session, message, self.secure_user)
else:
if message is None:
self.on_close()
self.close()
else:
self.write_message(message)
wsocket.log.exception('Error on broadcast')
wsocket.close()
def on_message(self, message):
if self.session and self.secure_user:
term = self.user_terminals.get(self.session)
term and term.write(message)
if message[0] == 'R':
# Broadcast resize
TermWebSocket.broadcast(
self.session, message, self.secure_user, self)
cmd = json.loads(message)
if cmd['cmd'] == 'open':
self.create_terminal()
else:
self._terminal.write(message)
try:
Terminal.sessions[self.session].ctl(cmd)
except Exception:
# FF strange bug
pass
self.broadcast(self.session, message, self)
def on_close(self):
super(TermCtlWebSocket, self).on_close()
if self.closed:
return
self.closed = True
self.log.info('Websocket closed %r' % self)
TermWebSocket.sockets.remove(self)
if self.session:
self.user_sessions[self.session].remove(self)
elif hasattr(self, '_terminal'):
self._terminal.close()
else:
self.log.error(
'Socket with neither session nor terminal %r' % self)
opts = tornado.options.options
if opts.one_shot or (
self.application.systemd and
not len(TermWebSocket.sockets) and
self.log.info('Websocket /ctl closed %r' % self)
if self in self.sessions[self.session]:
self.sessions[self.session].remove(self)
if tornado.options.options.one_shot or (
getattr(self.application, 'systemd', False) and
not sum([
len(sessions)
for user, sessions in TermWebSocket.terminals.items()])):
len(wsockets)
for session, wsockets in self.sessions.items()])):
sys.exit(0)
@url(r'/ws/session/(?P<session>[^/]+)')
class TermWebSocket(Route, KeptAliveWebSocketHandler):
# List of websockets per session
sessions = defaultdict(list)
# Last is kept for session shared history send
last = None
# Session history
history = {}
def open(self, session):
super(TermWebSocket, self).open(session)
self.set_nodelay(True)
self.session = session
self.closed = False
self.sessions[session].append(self)
self.__class__.last = self
self.log.info('Websocket /ws opened %r' % self)
@classmethod
def close_session(cls, session):
wsockets = (cls.sessions.get(session, []) +
TermCtlWebSocket.sessions.get(session, []))
for wsocket in wsockets:
wsocket.on_close()
wsocket.close()
if session in cls.sessions:
del cls.sessions[session]
if session in TermCtlWebSocket.sessions_secure_users:
del TermCtlWebSocket.sessions_secure_users[session]
if session in TermCtlWebSocket.sessions:
del TermCtlWebSocket.sessions[session]
@classmethod
def broadcast(cls, session, message, emitter=None):
if message is None:
cls.close_session(session)
return
wsockets = cls.sessions.get(session)
for wsocket in wsockets:
try:
if wsocket != emitter:
wsocket.write_message(message)
except Exception:
wsocket.log.exception('Error on broadcast')
wsocket.close()
def on_message(self, message):
Terminal.sessions[self.session].write(message)
def on_close(self):
super(TermWebSocket, self).on_close()
if self.closed:
return
self.closed = True
self.log.info('Websocket /ws closed %r' % self)
self.sessions[self.session].remove(self)
@url(r'/sessions/list.json')
class SessionsList(Route):
"""Get the theme list"""
@@ -315,7 +343,7 @@ class SessionsList(Route):
self.set_header('Content-Type', 'application/json')
self.write(tornado.escape.json_encode({
'sessions': sorted(
TermWebSocket.sessions.get(user, [])),
TermWebSocket.sessions),
'user': user
}))
@@ -340,7 +368,7 @@ class ThemesList(Route):
'built-in-%s' % theme
for theme in os.listdir(self.builtin_themes_dir)
if os.path.isdir(os.path.join(
self.builtin_themes_dir, theme)) and
self.builtin_themes_dir, theme)) and
not theme.startswith('.')]
else:
builtin_themes = []
@@ -351,3 +379,22 @@ class ThemesList(Route):
'builtin_themes': sorted(builtin_themes),
'dir': self.themes_dir
}))
@url('/local.js')
class LocalJsStatic(Route):
def get(self):
self.set_header("Content-Type", 'application/javascript')
if os.path.exists(self.local_js_dir):
for fn in os.listdir(self.local_js_dir):
if not fn.endswith('.js'):
continue
with open(os.path.join(self.local_js_dir, fn), 'rb') as s:
while True:
data = s.read(16384)
if data:
self.write(data)
else:
self.write(';')
break
self.finish()

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -23,7 +23,7 @@ $weights: (ExtraLight 100) (Light 300) (Regular 400) (Medium 500) (Semibold 600)
@font-face
font-family: "SourceCodePro"
src: url("/static/fonts/SourceCodePro-#{$weight_name}.otf") format("woff")
src: url("fonts/SourceCodePro-#{$weight_name}.otf") format("woff")
font-weight: nth($weight, 2)
body

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -22,13 +22,38 @@ html, body
color: $fg
body
padding-bottom: .5em
white-space: nowrap
overflow-x: hidden
overflow-y: scroll
a
text-decoration: underline rgba($fg, .2)
transition: text-decoration-color 500ms
&:hover
text-decoration: underline
.line.active
background-color: $active-bg
.line.extended
cursor: zoom-in
background-image: linear-gradient(90deg, rgba(darken($bg, 3%), 0), 95%, darken($bg, 3%))
.extra
display: none
&:not(.expanded):hover
background-color: lighten($bg, 2%)
&.expanded
cursor: zoom-out
background-color: darken($bg, 3%)
.extra
display: block
white-space: pre-wrap
word-break: break-all
&::-webkit-scrollbar
background: $scroll-bg
width: $scroll-width
@@ -68,5 +93,20 @@ body
padding: .5em
font-size: .75em
#input-view
position: fixed
z-index: 100
padding: 0
margin: 0
text-decoration: underline
#input-helper
position: fixed
z-index: -100
opacity: 0
white-space: nowrap
overflow: hidden
resize: none
.terminal
outline: none

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -16,27 +16,22 @@
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
body
transition: 200ms
transition: filter 200ms
transform-origin: bottom
&.bell
-webkit-filter: blur(2px)
filter: blur(2px)
&.skip
-webkit-filter: sepia(1)
filter: sepia(1)
&.selection
-webkit-filter: saturate(2)
filter: saturate(2)
&.alarm
-webkit-filter: hue-rotate(150deg)
filter: hue-rotate(150deg)
&.dead
-webkit-filter: grayscale(1)
filter: grayscale(1)
&:after
@@ -55,7 +50,6 @@ body
font-weight: 900
&.stopped
-webkit-filter: brightness(50%)
filter: brightness(50%)
&.locked

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */

View File

@@ -1,5 +1,5 @@
(function() {
var Popup, Selection, _set_theme_href, _theme, alt, cancel, clean_ansi, copy, ctrl, first, nextLeaf, popup, previousLeaf, selection, setAlarm, virtualInput,
var Popup, Selection, _set_theme_href, _theme, alt, cancel, clean_ansi, copy, ctrl, escape, histSize, linkify, maybePack, nextLeaf, packSize, popup, previousLeaf, selection, setAlarm, tags, tid, walk,
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
clean_ansi = function(data) {
@@ -62,8 +62,7 @@
var alarm;
alarm = function(data) {
var message, note, notif;
message = clean_ansi(data.data);
console.log(message);
message = clean_ansi(data.data.slice(1));
if (cond !== null && !cond.test(message)) {
return;
}
@@ -81,9 +80,9 @@
} else {
alert(note + '\n' + message);
}
return butterfly.ws.removeEventListener('message', alarm);
return butterfly.ws.shell.removeEventListener('message', alarm);
};
butterfly.ws.addEventListener('message', alarm);
butterfly.ws.shell.addEventListener('message', alarm);
return butterfly.body.classList.add('alarm');
};
@@ -122,13 +121,14 @@
});
addEventListener('copy', copy = function(e) {
var data, end, j, len1, line, ref, sel;
var data, end, j, len, line, ref, sel;
document.getElementsByTagName('body')[0].contentEditable = false;
butterfly.bell("copied");
e.clipboardData.clearData();
sel = getSelection().toString().replace(/\u00A0/g, ' ').replace(/\u2007/g, ' ');
data = '';
ref = sel.split('\n');
for (j = 0, len1 = ref.length; j < len1; j++) {
for (j = 0, len = ref.length; j < len; j++) {
line = ref[j];
if (line.slice(-1) === '\u23CE') {
end = '';
@@ -143,11 +143,20 @@
});
addEventListener('paste', function(e) {
var data;
var data, send, size;
document.getElementsByTagName('body')[0].contentEditable = false;
butterfly.bell("pasted");
data = e.clipboardData.getData('text/plain');
data = data.replace(/\r\n/g, '\n').replace(/\n/g, '\r');
butterfly.send(data);
size = 1024;
send = function() {
butterfly.send(data.substring(0, size));
data = data.substring(size);
if (data.length) {
return setTimeout(send, 25);
}
};
send();
return e.preventDefault();
});
@@ -157,14 +166,161 @@
}
});
Terminal.on('change', function(line) {
if (indexOf.call(line.classList, 'extended') >= 0) {
return line.addEventListener('click', (function(line) {
return function() {
var after, before;
if (indexOf.call(line.classList, 'expanded') >= 0) {
return line.classList.remove('expanded');
} else {
before = line.getBoundingClientRect().height;
line.classList.add('expanded');
after = line.getBoundingClientRect().height;
return document.body.scrollTop += after - before;
}
};
})(line));
}
});
walk = function(node, callback) {
var child, j, len, ref, results;
ref = node.childNodes;
results = [];
for (j = 0, len = ref.length; j < len; j++) {
child = ref[j];
callback.call(child);
results.push(walk(child, callback));
}
return results;
};
linkify = function(text) {
var emailAddressPattern, pseudoUrlPattern, urlPattern;
urlPattern = /\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim;
pseudoUrlPattern = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
emailAddressPattern = /[\w.]+@[a-zA-Z_-]+?(?:\.[a-zA-Z]{2,6})+/gim;
return text.replace(urlPattern, '<a href="$&">$&</a>').replace(pseudoUrlPattern, '$1<a href="http://$2">$2</a>').replace(emailAddressPattern, '<a href="mailto:$&">$&</a>');
};
tags = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
};
escape = function(s) {
return s.replace(/[&<>]/g, function(tag) {
return tags[tag] || tag;
});
};
Terminal.on('change', function(line) {
return walk(line, function() {
var linkified, newNode, val;
if (this.nodeType === 3) {
val = this.nodeValue;
linkified = linkify(escape(val));
if (linkified !== val) {
newNode = document.createElement('span');
newNode.innerHTML = linkified;
this.parentElement.replaceChild(newNode, this);
return true;
}
}
});
});
ctrl = false;
alt = false;
addEventListener('touchstart', function(e) {
if (e.touches.length === 2) {
return ctrl = true;
} else if (e.touches.length === 3) {
ctrl = false;
return alt = true;
} else if (e.touches.length === 4) {
ctrl = true;
return alt = true;
}
});
window.mobileKeydown = function(e) {
var _altKey, _ctrlKey, _keyCode;
if (ctrl || alt) {
_ctrlKey = ctrl;
_altKey = alt;
_keyCode = e.keyCode;
if (e.keyCode >= 97 && e.keyCode <= 122) {
_keyCode -= 32;
}
e = new KeyboardEvent('keydown', {
ctrlKey: _ctrlKey,
altKey: _altKey,
keyCode: _keyCode
});
ctrl = alt = false;
setTimeout(function() {
return window.dispatchEvent(e);
}, 0);
return true;
} else {
return false;
}
};
document.addEventListener('keydown', function(e) {
if (!(e.altKey && e.keyCode === 79)) {
return true;
}
open(location.href);
open(location.origin);
return cancel(e);
});
tid = null;
packSize = 1000;
histSize = 100;
maybePack = function() {
var hist, i, j, pack, packfrag, ref;
if (!(butterfly.term.childElementCount > packSize + butterfly.rows)) {
return;
}
hist = document.getElementById('packed');
packfrag = document.createDocumentFragment('fragment');
for (i = j = 0, ref = packSize; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) {
packfrag.appendChild(butterfly.term.firstChild);
}
pack = document.createElement('div');
pack.classList.add('pack');
pack.appendChild(packfrag);
hist.appendChild(pack);
if (hist.childElementCount > histSize) {
hist.firstChild.remove();
}
return tid = setTimeout(maybePack);
};
Terminal.on('refresh', function() {
if (tid) {
clearTimeout(tid);
}
return maybePack();
});
Terminal.on('clear', function() {
var hist, newHist;
newHist = document.createElement('div');
newHist.id = 'packed';
hist = document.getElementById('packed');
return butterfly.body.replaceChild(newHist, hist);
});
Popup = (function() {
function Popup() {
this.el = document.getElementById('popup');
@@ -175,9 +331,6 @@
Popup.prototype.open = function(html) {
this.el.innerHTML = html;
this.el.classList.remove('hidden');
if (typeof InstallTrigger !== "undefined") {
document.body.contentEditable = 'false';
}
addEventListener('click', this.bound_click_maybe_close);
return addEventListener('keydown', this.bound_key_maybe_close);
};
@@ -185,9 +338,6 @@
Popup.prototype.close = function() {
removeEventListener('click', this.bound_click_maybe_close);
removeEventListener('keydown', this.bound_key_maybe_close);
if (typeof InstallTrigger !== "undefined") {
document.body.contentEditable = 'true';
}
this.el.classList.add('hidden');
return this.el.innerHTML = '';
};
@@ -320,13 +470,13 @@
Selection.prototype.go = function(n) {
var index;
index = butterfly.children.indexOf(this.startLine) + n;
if (!((0 <= index && index < butterfly.children.length))) {
index = Array.prototype.indexOf.call(butterfly.term.childNodes, this.startLine) + n;
if (!((0 <= index && index < butterfly.term.childElementCount))) {
return;
}
while (!butterfly.children[index].textContent.match(/\S/)) {
while (!butterfly.term.childNodes[index].textContent.match(/\S/)) {
index += n;
if (!((0 <= index && index < butterfly.children.length))) {
if (!((0 <= index && index < butterfly.term.childElementCount))) {
return;
}
}
@@ -344,7 +494,7 @@
Selection.prototype.selectLine = function(index) {
var line, lineEnd, lineStart;
line = butterfly.children[index];
line = butterfly.term.childNodes[index];
lineStart = {
node: line.firstChild,
offset: 0
@@ -444,7 +594,7 @@
})();
document.addEventListener('keydown', function(e) {
var ref, ref1;
var r, ref, ref1;
if (ref = e.keyCode, indexOf.call([16, 17, 18, 19], ref) >= 0) {
return true;
}
@@ -481,8 +631,9 @@
return cancel(e);
}
if (!selection && e.ctrlKey && e.shiftKey && e.keyCode === 38) {
r = Math.max(butterfly.term.childElementCount - butterfly.rows, 0);
selection = new Selection();
selection.selectLine(butterfly.y - 1);
selection.selectLine(r + butterfly.y - 1);
selection.apply();
return cancel(e);
}
@@ -550,7 +701,7 @@
}
oReq = new XMLHttpRequest();
oReq.addEventListener('load', function() {
var j, len1, out, ref, response, session;
var j, len, out, ref, response, session;
response = JSON.parse(this.responseText);
out = '<div>';
out += '<h2>Session list</h2>';
@@ -559,7 +710,7 @@
} else {
out += '<ul>';
ref = response.sessions;
for (j = 0, len1 = ref.length; j < len1; j++) {
for (j = 0, len = ref.length; j < len; j++) {
session = ref[j];
out += "<li><a href=\"/session/" + session + "\">" + session + "</a></li>";
}
@@ -614,7 +765,7 @@
}
oReq = new XMLHttpRequest();
oReq.addEventListener('load', function() {
var builtin_themes, inner, j, k, len1, len2, option, response, theme, theme_list, themes, url;
var builtin_themes, inner, j, k, len, len1, option, response, theme, theme_list, themes, url;
response = JSON.parse(this.responseText);
builtin_themes = response.builtin_themes;
themes = response.themes;
@@ -631,7 +782,7 @@
option("/static/main.css", 'default');
if (themes.length) {
inner += '<optgroup label="Local themes">';
for (j = 0, len1 = themes.length; j < len1; j++) {
for (j = 0, len = themes.length; j < len; j++) {
theme = themes[j];
url = "/theme/" + theme + "/style.css";
option(url, theme);
@@ -639,7 +790,7 @@
inner += '</optgroup>';
}
inner += '<optgroup label="Built-in themes">';
for (k = 0, len2 = builtin_themes.length; k < len2; k++) {
for (k = 0, len1 = builtin_themes.length; k < len1; k++) {
theme = builtin_themes[k];
url = "/theme/" + theme + "/style.css";
option(url, theme.slice('built-in-'.length));
@@ -657,74 +808,6 @@
return cancel(e);
});
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
ctrl = false;
alt = false;
first = true;
virtualInput = document.createElement('input');
virtualInput.type = 'password';
virtualInput.style.position = 'fixed';
virtualInput.style.top = 0;
virtualInput.style.left = 0;
virtualInput.style.border = 'none';
virtualInput.style.outline = 'none';
virtualInput.style.opacity = 0;
virtualInput.value = '0';
document.body.appendChild(virtualInput);
virtualInput.addEventListener('blur', function() {
return setTimeout(((function(_this) {
return function() {
return _this.focus();
};
})(this)), 10);
});
addEventListener('click', function() {
return virtualInput.focus();
});
addEventListener('touchstart', function(e) {
if (e.touches.length === 2) {
return ctrl = true;
} else if (e.touches.length === 3) {
ctrl = false;
return alt = true;
} else if (e.touches.length === 4) {
ctrl = true;
return alt = true;
}
});
virtualInput.addEventListener('keydown', function(e) {
butterfly.keyDown(e);
return true;
});
virtualInput.addEventListener('input', function(e) {
var len;
len = this.value.length;
if (len === 0) {
e.keyCode = 8;
butterfly.keyDown(e);
this.value = '0';
return true;
}
e.keyCode = this.value.charAt(1).charCodeAt(0);
if ((ctrl || alt) && !first) {
e.keyCode = this.value.charAt(1).charCodeAt(0);
e.ctrlKey = ctrl;
e.altKey = alt;
if (e.keyCode >= 97 && e.keyCode <= 122) {
e.keyCode -= 32;
}
butterfly.keyDown(e);
this.value = '0';
ctrl = alt = false;
return true;
}
butterfly.keyPress(e);
first = false;
this.value = '0';
return true;
});
}
}).call(this);
//# sourceMappingURL=ext.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -17,7 +17,7 @@
/* These a the default variables */
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -40,7 +40,7 @@
/* These are all imported files */
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -57,7 +57,7 @@
/* You can change this file to import any webfont: */
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -70,37 +70,37 @@
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
@font-face {
font-family: "SourceCodePro";
src: url("/static/fonts/SourceCodePro-ExtraLight.otf") format("woff");
src: url("fonts/SourceCodePro-ExtraLight.otf") format("woff");
font-weight: 100; }
@font-face {
font-family: "SourceCodePro";
src: url("/static/fonts/SourceCodePro-Light.otf") format("woff");
src: url("fonts/SourceCodePro-Light.otf") format("woff");
font-weight: 300; }
@font-face {
font-family: "SourceCodePro";
src: url("/static/fonts/SourceCodePro-Regular.otf") format("woff");
src: url("fonts/SourceCodePro-Regular.otf") format("woff");
font-weight: 400; }
@font-face {
font-family: "SourceCodePro";
src: url("/static/fonts/SourceCodePro-Medium.otf") format("woff");
src: url("fonts/SourceCodePro-Medium.otf") format("woff");
font-weight: 500; }
@font-face {
font-family: "SourceCodePro";
src: url("/static/fonts/SourceCodePro-Semibold.otf") format("woff");
src: url("fonts/SourceCodePro-Semibold.otf") format("woff");
font-weight: 600; }
@font-face {
font-family: "SourceCodePro";
src: url("/static/fonts/SourceCodePro-Bold.otf") format("woff");
src: url("fonts/SourceCodePro-Bold.otf") format("woff");
font-weight: 700; }
@font-face {
font-family: "SourceCodePro";
src: url("/static/fonts/SourceCodePro-Black.otf") format("woff");
src: url("fonts/SourceCodePro-Black.otf") format("woff");
font-weight: 900; }
body {
@@ -111,7 +111,7 @@ body {
/* You can comment / uncomment the following to enable/disable terminal effects. */
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -123,22 +123,17 @@ body {
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
body {
transition: 200ms;
transition: filter 200ms;
transform-origin: bottom; }
body.bell {
-webkit-filter: blur(2px);
filter: blur(2px); }
body.skip {
-webkit-filter: sepia(1);
filter: sepia(1); }
body.selection {
-webkit-filter: saturate(2);
filter: saturate(2); }
body.alarm {
-webkit-filter: hue-rotate(150deg);
filter: hue-rotate(150deg); }
body.dead {
-webkit-filter: grayscale(1);
filter: grayscale(1); }
body.dead:after {
content: "CLOSED";
@@ -155,7 +150,6 @@ body {
opacity: .2;
font-weight: 900; }
body.stopped {
-webkit-filter: brightness(50%);
filter: brightness(50%); }
body.locked::-webkit-scrollbar-thumb {
background: rgba(255, 0, 0, 0.7); }
@@ -169,7 +163,7 @@ body {
/* @import all_fx */
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -183,7 +177,7 @@ body {
/* The color theme is defined in this one: */
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -357,7 +351,7 @@ body {
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -2792,7 +2786,7 @@ body {
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -2810,12 +2804,32 @@ html, body {
color: #f4ead5; }
body {
padding-bottom: .5em;
white-space: nowrap;
overflow-x: hidden;
overflow-y: scroll;
/* Pop ups */ }
body a {
text-decoration: underline rgba(244, 234, 213, 0.2);
transition: text-decoration-color 500ms; }
body a:hover {
text-decoration: underline; }
body .line.active {
background-color: transparent; }
body .line.extended {
cursor: zoom-in;
background-image: linear-gradient(90deg, rgba(9, 8, 10, 0), 95%, #09080a); }
body .line.extended .extra {
display: none; }
body .line.extended:not(.expanded):hover {
background-color: #161419; }
body .line.extended.expanded {
cursor: zoom-out;
background-color: #09080a; }
body .line.extended.expanded .extra {
display: block;
white-space: pre-wrap;
word-break: break-all; }
body::-webkit-scrollbar {
background: #110f13;
width: 0.75em; }
@@ -2838,7 +2852,7 @@ body {
color: #f4ead5;
font-size: 1em; }
body #popup form h2, body #popup > div h2 {
margin: 0 0.5em 0.5em 0.5em; }
margin: 0 .5em .5em .5em; }
body #popup form select, body #popup > div select {
min-width: 300px;
padding: .5em;
@@ -2847,13 +2861,26 @@ body {
display: block;
padding: .5em;
font-size: .75em; }
body #input-view {
position: fixed;
z-index: 100;
padding: 0;
margin: 0;
text-decoration: underline; }
body #input-helper {
position: fixed;
z-index: -100;
opacity: 0;
white-space: nowrap;
overflow: hidden;
resize: none; }
.terminal {
outline: none; }
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -2872,7 +2899,7 @@ body {
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2015 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html>
{% from tornado.options import options %}
{% from uuid import uuid4 %}
<html>
<head>
<meta charset="utf-8">
@@ -14,7 +15,13 @@
</head>
<body spellcheck="false"
data-force-unicode-width="{{ 'yes' if options.force_unicode_width else 'no' }}">
data-force-unicode-width="{{ 'yes' if options.force_unicode_width else 'no' }}"
data-root-path="{{ options.uri_root_path }}"
data-session-token={{ session }}>
<textarea id="input-helper">
</textarea>
<div id="input-view" class="hidden">
</div>
<div id="popup" class="hidden">
</div>
<script src="{{ static_url('html-sanitizer.js') }}"></script>
@@ -22,5 +29,8 @@
'' if options.unminified else 'min.')) }}"></script>
<script src="{{ static_url('ext.%sjs' % (
'' if options.unminified else 'min.')) }}"></script>
<script src="{{ reverse_url('LocalJsStatic') }}"></script>
<div id="packed"></div>
<div id="term"></div>
</body>
</html>

View File

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

View File

@@ -1,7 +1,7 @@
# *-* coding: utf-8 *-*
# This file is part of butterfly
#
# butterfly Copyright (C) 2015 Florian Mounier
# butterfly Copyright(C) 2015-2017 Florian Mounier
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
@@ -25,13 +25,15 @@ import string
import struct
import sys
import termios
from logging import getLogger
import tornado.ioloop
import tornado.options
import tornado.process
import tornado.web
import tornado.websocket
from logging import getLogger
from butterfly import utils, __version__
from butterfly import __version__, utils
log = getLogger('butterfly')
ioloop = tornado.ioloop.IOLoop.instance()
@@ -47,10 +49,16 @@ except NameError:
class Terminal(object):
def __init__(self, user, path, session, socket, host, render_string, send):
self.host = host
sessions = {}
def __init__(self, user, path, session, socket, uri, render_string,
broadcast):
self.sessions[session] = self
self.history_size = 50000
self.history = ''
self.uri = uri
self.session = session
self.send = send
self.broadcast = broadcast
self.fd = None
self.closed = False
self.socket = socket
@@ -67,7 +75,7 @@ class Terminal(object):
if tornado.options.options.unsecure:
if self.user:
try:
self.callee = utils.User(name=self.user)
self.callee = self.user
except LookupError:
log.debug(
"Can't switch to user %s" % self.user, exc_info=True)
@@ -88,14 +96,22 @@ class Terminal(object):
butterfly=self,
version=__version__,
opts=tornado.options.options,
colors=utils.ansi_colors)
.decode('utf-8')
.replace('\r', '')
.replace('\n', '\r\n'))
self.send('S' + motd)
uri=self.uri,
colors=utils.ansi_colors
).decode('utf-8')
.replace('\r', '')
.replace('\n', '\r\n'))
self.send(motd)
log.info('Forking pty for user %r' % self.user)
def send(self, message):
if message is not None:
self.history += message
if len(self.history) > self.history_size:
self.history = self.history[-self.history_size:]
self.broadcast(self.session, message)
def pty(self):
# Make a "unique" id in 4 bytes
self.uid = ''.join(
@@ -114,10 +130,13 @@ class Terminal(object):
self.communicate()
def determine_user(self):
if self.callee is None and (
tornado.options.options.unsecure and
tornado.options.options.login):
# If callee is now known and we have unsecure connection
if not tornado.options.options.unsecure:
# Secure mode we must have already a callee
assert self.callee is not None
return
# If we should login, login
if tornado.options.options.login:
user = ''
while user == '':
try:
@@ -131,13 +150,11 @@ class Terminal(object):
except Exception:
log.debug("Can't switch to user %s" % user, exc_info=True)
self.callee = utils.User(name='nobody')
elif (tornado.options.options.unsecure and not
tornado.options.options.login):
# if login is not required, we will use the same user as
# butterfly is executed
self.callee = utils.User()
return
assert self.callee is not None
# if login is not required, we will use the same user as
# butterfly is executed
self.callee = self.callee or utils.User()
def shell(self):
try:
@@ -147,18 +164,19 @@ class Terminal(object):
"Can't chdir to %s" % (self.path or self.callee.dir),
exc_info=True)
env = os.environ
# If local and local user is the same as login user
# We set the env of the user from the browser
# Usefull when running as root
if self.caller == self.callee:
env = os.environ
env.update(self.socket.env)
else:
# May need more?
env = {}
env["TERM"] = "xterm-256color"
env["COLORTERM"] = "butterfly"
env["HOME"] = self.callee.dir
env["LOCATION"] = "http%s://%s:%d/" % (
"s" if not tornado.options.options.unsecure else "",
tornado.options.options.host, tornado.options.options.port)
env["LOCATION"] = self.uri
env['BUTTERFLY_PATH'] = os.path.abspath(os.path.join(
os.path.dirname(__file__), 'bin'))
@@ -167,7 +185,6 @@ class Terminal(object):
except Exception:
log.debug("Can't get ttyname", exc_info=True)
tty = ''
if self.caller != self.callee:
try:
os.chown(os.ttyname(0), self.callee.uid, -1)
@@ -177,22 +194,21 @@ class Terminal(object):
utils.add_user_info(
self.uid,
tty, os.getpid(),
self.callee.name, self.host)
self.callee.name, self.uri)
if not tornado.options.options.unsecure or (
self.socket.local and
self.caller == self.callee and
server == self.callee
) or not tornado.options.options.login:
local_login = (
self.socket.local and self.caller == self.callee and
server == self.callee)
secure = not tornado.options.options.unsecure
force_login = tornado.options.options.login
ignore_security = (
tornado.options.options.
i_hereby_declare_i_dont_want_any_security_whatsoever)
if not force_login and (ignore_security or secure or local_login):
# User has been auth with ssl or is the same user as server
# or login is explicitly turned off
if (
not tornado.options.options.unsecure and
tornado.options.options.login and not (
self.socket.local and
self.caller == self.callee and
server == self.callee
)):
if secure and not local_login:
# User is authed by ssl, setting groups
try:
os.initgroups(self.callee.name, self.callee.gid)
@@ -211,14 +227,23 @@ class Terminal(object):
args = tornado.options.options.cmd.split(' ')
else:
args = [tornado.options.options.shell or self.callee.shell]
args.append('-i')
args.append('-il')
# In some cases some shells don't export SHELL var
env['SHELL'] = args[0]
os.execvpe(args[0], args, env)
# This process has been replaced
if tornado.options.options.pam_profile:
if not server.root:
print('You must be root to use pam_profile option.')
sys.exit(3)
pam_path = os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'pam.py')
os.execvpe(sys.executable, [
sys.executable, pam_path, self.callee.name,
tornado.options.options.pam_profile], env)
# Unsecure connection with su
if server.root:
if self.socket.local:
@@ -238,11 +263,10 @@ class Terminal(object):
else:
args = ['/bin/su']
if sys.platform == 'linux':
args.append('-p')
if tornado.options.options.shell:
args.append('-s')
args.append(tornado.options.options.shell)
args.append('-l')
if sys.platform.startswith('linux') and tornado.options.options.shell:
args.append('-s')
args.append(tornado.options.options.shell)
args.append(self.callee.name)
os.execvpe(args[0], args, env)
@@ -272,17 +296,18 @@ class Terminal(object):
self.on_close()
self.close()
if message[0] == 'R':
cols, rows = map(int, message[1:].split(','))
log.debug('WRIT<%r' % message)
self.writer.write(message)
self.writer.flush()
def ctl(self, message):
if message['cmd'] == 'size':
cols = message['cols']
rows = message['rows']
s = struct.pack("HHHH", rows, cols, 0, 0)
fcntl.ioctl(self.fd, termios.TIOCSWINSZ, s)
log.info('SIZE (%d, %d)' % (cols, rows))
elif message[0] == 'S':
log.debug('WRIT<%r' % message)
self.writer.write(message[1:])
self.writer.flush()
def shell_handler(self, fd, events):
if events & ioloop.READ:
try:
@@ -292,7 +317,7 @@ class Terminal(object):
log.debug('READ>%r' % read)
if read and len(read) != 0:
self.send('S' + read.decode('utf-8', 'replace'))
self.send(read.decode('utf-8', 'replace'))
else:
events = ioloop.ERROR
@@ -326,7 +351,10 @@ class Terminal(object):
log.debug('closing fd fail', exc_info=True)
try:
os.kill(self.pid, signal.SIGKILL)
os.kill(self.pid, signal.SIGHUP)
os.kill(self.pid, signal.SIGCONT)
os.waitpid(self.pid, 0)
except Exception:
log.debug('waitpid fail', exc_info=True)
del self.sessions[self.session]

View File

@@ -1,7 +1,7 @@
# *-* coding: utf-8 *-*
# This file is part of butterfly
#
# butterfly Copyright (C) 2015 Florian Mounier
# butterfly Copyright(C) 2015-2017 Florian Mounier
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
@@ -18,14 +18,13 @@
import os
import pwd
import time
import sys
import struct
from logging import getLogger
from collections import namedtuple
import subprocess
import tornado.options
import re
import struct
import subprocess
import sys
import time
from collections import namedtuple
from logging import getLogger
log = getLogger('butterfly')
@@ -140,7 +139,7 @@ class Socket(object):
try:
line = get_procfs_socket_line(get_hex_ip_port(pn[:2]))
self.user = User(uid=int(line[7]))
self.env = get_socket_env(line[9])
self.env = get_socket_env(line[9], self.user)
except Exception:
log.debug('procfs was no good, aight', exc_info=True)
@@ -169,7 +168,7 @@ def get_lsof_socket_line(addr, port):
# May want to make this into a dictionary in the future...
regex = "\w+\s+(?P<pid>\d+)\s+(?P<user>\w+).*\s" \
"(?P<laddr>.*?):(?P<lport>\d+)->(?P<raddr>.*?):(?P<rport>\d+)"
output = subprocess.check_output(['lsof', '-Pni'])
output = subprocess.check_output(['lsof', '-Pni']).decode('utf-8')
lines = output.split('\n')
for line in lines:
# Look for local address with peer port
@@ -203,23 +202,42 @@ def get_procfs_socket_line(hex_ip_port):
# Linux only browser environment far fetch
def get_socket_env(inode):
def get_socket_env(inode, user):
for pid in os.listdir("/proc/"):
if not pid.isdigit():
continue
with open('/proc/%s/cmdline' % pid) as c:
if c.read().split('\x00')[0] in [
'gnome-session',
'startkde',
'xfce4-session']:
with open('/proc/%s/environ' % pid) as e:
keyvals = e.read().split('\x00')
env = {}
for keyval in keyvals:
if '=' in keyval:
key, val = keyval.split('=', 1)
env[key] = val
return env
try:
with open('/proc/%s/cmdline' % pid) as c:
command = c.read().split('\x00')
executable = command[0].split('/')[-1]
if executable in ('sh', 'bash', 'zsh'):
executable = command[1].split('/')[-1]
if executable in [
'gnome-session',
'gnome-session-binary',
'startkde',
'startdde',
'xfce4-session']:
with open('/proc/%s/status' % pid) as e:
uid = None
for line in e.read().splitlines():
parts = line.split('\t')
if parts[0] == 'Uid:':
uid = int(parts[1])
break
if not uid or uid != user.uid:
continue
with open('/proc/%s/environ' % pid) as e:
keyvals = e.read().split('\x00')
env = {}
for keyval in keyvals:
if '=' in keyval:
key, val = keyval.split('=', 1)
env[key] = val
return env
except Exception:
continue
for pid in os.listdir("/proc/"):
if not pid.isdigit():
@@ -276,11 +294,12 @@ def get_wtmp_file():
if os.path.exists(file):
return file
UTmp = namedtuple(
'UTmp',
['type', 'pid', 'line', 'id', 'user', 'host',
'exit0', 'exit1', 'session',
'sec', 'usec', 'addr0', 'addr1', 'addr2', 'addr3', 'unused'])
'UTmp',
['type', 'pid', 'line', 'id', 'user', 'host',
'exit0', 'exit1', 'session',
'sec', 'usec', 'addr0', 'addr1', 'addr2', 'addr3', 'unused'])
def utmp_line(id, type, pid, fd, user, host, ts):
@@ -390,4 +409,5 @@ class AnsiColors(object):
return '\x1b[0m'
return ''
ansi_colors = AnsiColors()

View File

@@ -45,8 +45,7 @@ clean_ansi = (data) ->
setAlarm = (notification, cond) ->
alarm = (data) ->
message = clean_ansi data.data
console.log message
message = clean_ansi data.data.slice(1)
return if cond isnt null and not cond.test(message)
butterfly.body.classList.remove 'alarm'
@@ -63,9 +62,9 @@ setAlarm = (notification, cond) ->
else
alert(note + '\n' + message)
butterfly.ws.removeEventListener 'message', alarm
butterfly.ws.shell.removeEventListener 'message', alarm
butterfly.ws.addEventListener 'message', alarm
butterfly.ws.shell.addEventListener 'message', alarm
butterfly.body.classList.add 'alarm'

View File

@@ -1,7 +1,7 @@
# *-* coding: utf-8 *-*
# This file is part of butterfly
#
# butterfly Copyright (C) 2015 Florian Mounier
# butterfly Copyright(C) 2015-2017 Florian Mounier
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
@@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
addEventListener 'copy', copy = (e) ->
document.getElementsByTagName('body')[0].contentEditable = false
butterfly.bell "copied"
e.clipboardData.clearData()
sel = getSelection().toString().replace(
@@ -33,9 +34,19 @@ addEventListener 'copy', copy = (e) ->
e.clipboardData.setData 'text/plain', data.slice(0, -1)
e.preventDefault()
addEventListener 'paste', (e) ->
document.getElementsByTagName('body')[0].contentEditable = false
butterfly.bell "pasted"
data = e.clipboardData.getData 'text/plain'
data = data.replace(/\r\n/g, '\n').replace(/\n/g, '\r')
butterfly.send data
# Send big data in chunks to prevent data loss
size = 1024
send = ->
butterfly.send data.substring(0, size)
data = data.substring(size)
if data.length
setTimeout send, 25
send()
e.preventDefault()

View 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

View 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 =
'&': '&amp;'
'<': '&lt;'
'>': '&gt;'
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
View File

@@ -0,0 +1,52 @@
# *-* coding: utf-8 *-*
# This file is part of butterfly
#
# butterfly Copyright(C) 2015-2017 Florian Mounier
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
ctrl = false
alt = false
addEventListener 'touchstart', (e) ->
if e.touches.length == 2
ctrl = true
else if e.touches.length == 3
ctrl = false
alt = true
else if e.touches.length == 4
ctrl = true
alt = true
# Dispatch a new event if the current event need to
# be modified with ctrlKey and altKey from touch events
# If so, this function will return true and dispatch the new event.
# The caller should return immediately upon receiving true.
window.mobileKeydown = (e) ->
if ctrl or alt
_ctrlKey = ctrl
_altKey = alt
_keyCode = e.keyCode
if e.keyCode >= 97 && e.keyCode <= 122
_keyCode -= 32
e = new KeyboardEvent 'keydown',
ctrlKey: _ctrlKey,
altKey: _altKey,
keyCode: _keyCode
ctrl = alt = false
setTimeout ->
window.dispatchEvent e
, 0
return true
else
return false

View File

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

29
coffees/ext/pack.coffee Normal file
View 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

View File

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

View File

@@ -87,12 +87,13 @@ class Selection
@go +1
go: (n) ->
index = butterfly.children.indexOf(@startLine) + n
return unless 0 <= index < butterfly.children.length
index = Array.prototype.indexOf.call(
butterfly.term.childNodes, @startLine) + n
return unless 0 <= index < butterfly.term.childElementCount
until butterfly.children[index].textContent.match /\S/
until butterfly.term.childNodes[index].textContent.match /\S/
index += n
return unless 0 <= index < butterfly.children.length
return unless 0 <= index < butterfly.term.childElementCount
@selectLine index
@@ -104,7 +105,7 @@ class Selection
@selection.addRange range
selectLine: (index) ->
line = butterfly.children[index]
line = butterfly.term.childNodes[index]
lineStart =
node: line.firstChild
offset: 0
@@ -204,8 +205,9 @@ document.addEventListener 'keydown', (e) ->
# Start selection mode with shift up
if not selection and e.ctrlKey and e.shiftKey and e.keyCode == 38
r = Math.max butterfly.term.childElementCount - butterfly.rows, 0
selection = new Selection()
selection.selectLine butterfly.y - 1
selection.selectLine r + butterfly.y - 1
selection.apply()
return cancel e
true

View File

@@ -3,7 +3,7 @@ _set_theme_href = (href) ->
img = document.createElement('img')
img.onerror = ->
setTimeout (-> butterfly?.resize()), 250
img.src = href;
img.src = href
_theme = localStorage?.getItem('theme')
_set_theme_href(_theme) if _theme

View File

@@ -1,80 +0,0 @@
# *-* coding: utf-8 *-*
# This file is part of butterfly
#
# butterfly Copyright (C) 2015 Florian Mounier
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
if /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
.test navigator.userAgent
ctrl = false
alt = false
first = true
virtualInput = document.createElement 'input'
virtualInput.type = 'password'
virtualInput.style.position = 'fixed'
virtualInput.style.top = 0
virtualInput.style.left = 0
virtualInput.style.border = 'none'
virtualInput.style.outline = 'none'
virtualInput.style.opacity = 0
virtualInput.value = '0'
document.body.appendChild virtualInput
virtualInput.addEventListener 'blur', ->
setTimeout((=> @focus()), 10)
addEventListener 'click', ->
virtualInput.focus()
addEventListener 'touchstart', (e) ->
if e.touches.length == 2
ctrl = true
else if e.touches.length == 3
ctrl = false
alt = true
else if e.touches.length == 4
ctrl = true
alt = true
virtualInput.addEventListener 'keydown', (e) ->
butterfly.keyDown(e)
return true
virtualInput.addEventListener 'input', (e) ->
len = @value.length
if len == 0
e.keyCode = 8
butterfly.keyDown e
@value = '0'
return true
e.keyCode = @value.charAt(1).charCodeAt(0)
if (ctrl or alt) and not first
e.keyCode = @value.charAt(1).charCodeAt(0)
e.ctrlKey = ctrl
e.altKey = alt
if e.keyCode >= 97 && e.keyCode <= 122
e.keyCode -= 32
butterfly.keyDown e
@value = '0'
ctrl = alt = false
return true
butterfly.keyPress e
first = false
@value = '0'
true

View File

@@ -1,7 +1,7 @@
# *-* coding: utf-8 *-*
# This file is part of butterfly
#
# butterfly Copyright (C) 2015 Florian Mounier
# butterfly Copyright(C) 2015-2017 Florian Mounier
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
@@ -19,106 +19,107 @@ cols = rows = null
quit = false
openTs = (new Date()).getTime()
ws =
shell: null
ctl: null
$ = document.querySelectorAll.bind(document)
document.addEventListener 'DOMContentLoaded', ->
term = null
send = (data) ->
ws.send 'S' + data
ctl = (type, args...) ->
params = args.join(',')
if type == 'Resize'
ws.send 'R' + params
if location.protocol == 'https:'
wsUrl = 'wss://'
else
wsUrl = 'ws://'
wsUrl += document.location.host + '/ws' + location.pathname
ws = new WebSocket wsUrl
rootPath = document.body.getAttribute('data-root-path')
rootPath = rootPath.replace(/^\/+|\/+$/g, '')
if rootPath.length
rootPath = "/#{rootPath}"
ws.addEventListener 'open', ->
wsUrl += document.location.host + rootPath
path = '/'
if path.indexOf('/session') < 0
path += "session/#{document.body.getAttribute('data-session-token')}"
path += location.search
ws.shell = new WebSocket wsUrl + '/ws' + path
ws.ctl = new WebSocket wsUrl + '/ctl' + path
open = ->
console.log "WebSocket open", arguments
term = new Terminal document.body, send, ctl
term.ws = ws
window.butterfly = term
ws.send 'R' + term.cols + ',' + term.rows
openTs = (new Date()).getTime()
ws.addEventListener 'error', ->
console.log "WebSocket error", arguments
lastData = ''
t_queue = null
queue = ''
ws.addEventListener 'message', (e) ->
if e.data[0] is 'R'
[cols, rows] = e.data.slice(1).split(',')
term.resize cols, rows, true
return
if e.data[0] isnt 'S'
console.error 'Garbage message'
return
clearTimeout t_queue if t_queue
queue += e.data.slice(1)
if term.stop
queue = queue.slice -10 * 1024
if queue.length > term.buffSize
treat()
else
t_queue = setTimeout treat, 1
treat = ->
term.write queue
if term.stop
term.stop = false
if term
term.body.classList.remove 'stopped'
queue = ''
term.out = ws.shell.send.bind(ws.shell)
term.out '\x03\n'
return
ws.addEventListener 'close', ->
if (ws.shell.readyState is WebSocket.OPEN and
ws.ctl.readyState is WebSocket.OPEN)
term = new Terminal(
document.body, ws.shell.send.bind(ws.shell), ws.ctl.send.bind(ws.ctl))
term.ws = ws
window.butterfly = term
ws.ctl.send JSON.stringify(cmd: 'open')
ws.ctl.send JSON.stringify(
cmd: 'size', cols: term.cols, rows: term.rows)
openTs = (new Date()).getTime()
console.log "WebSocket open end", arguments
error = ->
console.error "WebSocket error", arguments
close = ->
console.log "WebSocket closed", arguments
setTimeout ->
term.write 'Closed'
# Allow quick reload
term.skipNextKey = true
term.body.classList.add('dead')
# Don't autoclose if websocket didn't last 1 minute
if (new Date()).getTime() - openTs > 60 * 1000
open('','_self').close()
, 1
return if quit
quit = true
term.write 'Closed'
# Allow quick reload
term.skipNextKey = true
term.body.classList.add('dead')
# Don't autoclose if websocket didn't last 1 minute
if (new Date()).getTime() - openTs > 60 * 1000
window.open('','_self').close()
reopenOnClose = ->
setTimeout ->
return if quit
ws.shell = new WebSocket wsUrl + '/ws' + path
init_shell_ws()
, 100
write = (data) ->
if term
term.write data
write_request = (e) ->
setTimeout write, 1, e.data
ctl = (e) ->
cmd = JSON.parse(e.data)
if cmd.cmd is 'size'
term.resize cmd.cols, cmd.rows, true
init_shell_ws = ->
ws.shell.addEventListener 'open', open
ws.shell.addEventListener 'message', write_request
ws.shell.addEventListener 'error', error
ws.shell.addEventListener 'close', reopenOnClose
init_ctl_ws = ->
ws.ctl.addEventListener 'open', open
ws.ctl.addEventListener 'message', ctl
ws.ctl.addEventListener 'error', error
ws.ctl.addEventListener 'close', close
init_shell_ws()
init_ctl_ws()
addEventListener 'beforeunload', ->
if not quit
'This will exit the terminal session'
window.bench = (n=100000000) ->
rnd = ''
while rnd.length < n
rnd += Math.random().toString(36).substring(2)
console.time('bench')
console.profile('bench')
term.write rnd
console.profileEnd()
console.timeEnd('bench')
window.cbench = (n=100000000) ->
rnd = ''
while rnd.length < n
rnd += "\x1b[#{30 + parseInt(Math.random() * 20)}m"
rnd += Math.random().toString(36).substring(2)
console.time('cbench')
console.profile('cbench')
term.write rnd
console.profileEnd()
console.timeEnd('cbench')

File diff suppressed because it is too large Load Diff

21
docker/run.sh Normal file → Executable file
View 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 "$@"

View File

@@ -1,6 +1,6 @@
{
"name": "butterfly",
"version": "2.0.0",
"version": "3.0.0",
"description": "A sleek web based terminal emulator",
"repository": {
"type": "git",
@@ -13,13 +13,13 @@
},
"homepage": "https://github.com/paradoxxxzero/butterfly",
"devDependencies": {
"coffeelint": "^1.12.1",
"grunt": "^0.4.5",
"grunt-coffeelint": "0.0.13",
"grunt-contrib-coffee": "^0.13.0",
"grunt-contrib-cssmin": "^0.14.0",
"grunt-contrib-uglify": "^0.9.2",
"grunt-contrib-watch": "^0.6.1",
"grunt-sass": "^1.1.0-beta"
"coffeelint": "^1.15.7",
"grunt": "^1.0.1",
"grunt-coffeelint": "0.0.15",
"grunt-contrib-coffee": "^1.0.0",
"grunt-contrib-cssmin": "^1.0.1",
"grunt-contrib-uglify": "^1.0.1",
"grunt-contrib-watch": "^1.0.0",
"grunt-sass": "^2.1.0"
}
}

7
setup.cfg Normal file
View File

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

View File

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

1782
yarn.lock Normal file

File diff suppressed because it is too large Load Diff