175 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
Florian Mounier
fb3ec14b43 Bump to 2.0.0-beta 2015-10-16 17:43:04 +02:00
Florian Mounier
4e4d54de1f Fix cursor blur. Fix scrollLock on focus/blur. Rework binaries. 2015-10-16 15:58:32 +02:00
Florian Mounier
e6f618ef52 Remove old theme mechanism 2015-10-14 18:00:48 +02:00
Florian Mounier
0337663059 Forgotten minified 2015-10-14 13:20:25 +02:00
Florian Mounier
7b37716177 Add themes as a submodule and handle both builtin themes and local themes. 2015-10-14 13:19:52 +02:00
Florian Mounier
2d554483e1 Fix send 2015-10-13 11:49:14 +02:00
Florian Mounier
fc5879f2d4 Try fixing session size 2015-10-13 11:40:32 +02:00
Florian Mounier
7371b8b4e1 Fix force close with warning 2015-10-13 10:01:53 +02:00
Florian Mounier
71820849eb Add a shortcut to open a new tab 2015-10-12 17:50:21 +02:00
Florian Mounier
7b5a4ee244 Improve /proc linux socket detection 2015-10-12 17:48:57 +02:00
Florian Mounier
e8512fc2b8 Be more permissive on local for same private ip. 2015-10-09 16:43:10 +02:00
Florian Mounier
9c36b0c8c1 Add active line class 2015-10-09 12:02:31 +02:00
Florian Mounier
ab8e65924d Fix popup when no theme present 2015-10-08 17:50:23 +02:00
Florian Mounier
ca26454aa0 Fix bad link for ssl reference 2015-10-08 16:49:48 +02:00
Florian Mounier
0f36db5264 Fix firefox popup with content editable. 2015-10-08 14:18:35 +02:00
Florian Mounier
834200256c Fix bopen/bsession. 2015-10-08 13:58:56 +02:00
Florian Mounier
96606d2b0b Add session list 2015-10-08 12:56:56 +02:00
Florian Mounier
7501aab797 Add font family variable 2015-10-08 12:37:05 +02:00
Florian Mounier
38a4c4083d Rework style paradigm 2015-10-08 11:37:48 +02:00
Florian Mounier
c937a8753d Add a regex on notif handler + an ansi cleaner 2015-10-07 18:25:39 +02:00
Florian Mounier
7916014854 Copyright bump 2015-10-07 16:40:50 +02:00
Florian Mounier
93ff8a3969 Add an exit confirm dialog 2015-10-07 16:38:29 +02:00
Florian Mounier
0f7a51d451 Add a default conf file. Handle themes in a far more intelligent way by adding a popup and saving conf in localStorage. 2015-10-07 16:03:32 +02:00
Florian Mounier
f67054f9ff Fix session saved history 2015-10-07 10:07:16 +02:00
Florian Mounier
ced1148275 Fix server exit while there are active sessions 2015-10-06 17:01:28 +02:00
Florian Mounier
140b0902fc Fix faint 2015-10-06 14:13:17 +02:00
Florian Mounier
909bcfa9f4 Add various SGR (add italic / crossed out / fainted support). Support resize in alternate buffer. 2015-10-06 14:07:31 +02:00
Florian Mounier
cf5051d414 Fix race condition 2015-10-06 10:17:14 +02:00
Florian Mounier
811620557a Some doc fix 2015-10-05 17:53:51 +02:00
Florian Mounier
0e97cc8362 Add theme and session support bin. 2015-10-05 17:03:26 +02:00
Florian Mounier
6d346af6f4 Fix alt buffer restore 2015-10-05 11:56:34 +02:00
Florian Mounier
893ec72270 Source environment from desktop environment 2015-10-05 11:33:07 +02:00
76 changed files with 6027 additions and 2137 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

3
.gitmodules vendored
View File

@@ -0,0 +1,3 @@
[submodule "butterfly/themes"]
path = butterfly/themes
url = https://github.com/paradoxxxzero/butterfly-themes

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) 2014 Florian Mounier, Kozea
butterfly Copyright(C) 2015-2017 Florian Mounier, Kozea
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or

37
Makefile Normal file
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) 2014 Florian Mounier
butterfly Copyright (C) 2015-2017 Florian Mounier
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
```
## Docker Usage
## Docker
There is a docker repository created for this project that is set to automatically rebuild when there is a push
into this repository: https://registry.hub.docker.com/u/garland/butterfly/
### Starting
### Example usage
docker run \
--env PASSWORD=password \
--env PORT=57575 \
-p 57575:57575 \
-d garland/butterfly
Starting with login and password
``` bash
docker run --env PASSWORD=password -d garland/butterfly --login
```
Starting with no password
``` bash
docker run -d -p 57575:57575 garland/butterfly
```
Starting with a different port
``` bash
docker run -d -p 12345:12345 garland/butterfly --port=12345
```

20
bower.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "butterfly",
"version": "1.0.0",
"authors": [
"Florian Mounier <florian.mounier@kozea.fr>"
],
"description": "A sleek web based terminal emulator",
"license": "None",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"google-caja": "*"
}
}

View File

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

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

View File

@@ -1,8 +0,0 @@
#!/usr/bin/env python
import sys
import os
rows, cols = map(int, os.popen('stty size', 'r').read().split())
for r in range(rows):
for c in range(cols):
sys.stdout.write('\x1b[48;2;%d;%d;%dm ' % (255 - r, 255 - c, 255))

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

View File

@@ -1,8 +0,0 @@
#!/usr/bin/env python
from butterfly.escapes import html
import fileinput
import sys
with html():
for line in fileinput.input():
sys.stdout.write(line)

View File

@@ -1,13 +0,0 @@
#!/usr/bin/env python
import os
import webbrowser
import argparse
parser = argparse.ArgumentParser(description='Butterfly tab opener.')
parser.add_argument(
'location',
default=os.getcwd(),
help='Directory to open the new tab in. (Defaults to current)')
args = parser.parse_args()
webbrowser.open('%swd%s' % (os.getenv('LOCATION'), args.location))

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,52 +0,0 @@
#!/usr/bin/env python
from butterfly.escapes import image
from butterfly.utils import ansi_colors
import os
import base64
print(ansi_colors.white + "Welcome to the butterfly help." + ansi_colors.reset)
with image('image/png'):
with open(
os.path.join(
os.path.abspath(os.path.dirname(__file__)),
'../static/images/favicon.png'), 'rb') as i:
print(base64.b64encode(i.read()).decode('ascii'))
print("""
Butterfly is a xterm compliant terminal built with python and javascript.
{title}Terminal functionalities:{reset}
{strong}[Alt] + [a] : {reset}Set an alarm which sends a notification when a modification is detected.
{strong}[Ctrl] + [Shift] + [Up] : {reset}Trigger visual selection mode. Hitting [Enter] inserts the selection in the prompt.
{strong}[ScrollLock] : {reset}Lock the scrolling to the current position. Press again to release.
{strong}[Alt] + [z] : {reset}Escape: don't catch the next pressed key. Useful for using native search for example. ([Alt] + [z] then [Ctrl] + [f]).
{strong}[Ctrl] + [c] <<hold>> : {reset}Cut the output when [Ctrl] + [c] is not enough.
{title}Butterfly programs:{reset}
{strong}bcat : {reset}A wrapper around cat allowing to display images as <img> instead of binary.
{strong}bopen : {reset}Open a new terminal at specified location.
{strong}b16M : {reset}Test the 16M colors support in terminal.
{strong}bhr : {reset}Put a html hr. This is a test and needs --allow-html-escapes flag.
{strong}bcal : {reset}Display current month using html. This is a test and needs --allow-html-escapes flag.
{title}Styling butterfly:{reset}
To style butterfly in sass, you need to have the libsass python library installed.
You will have to:
$ cp {main} ~/.butterfly/style.sass
or for system wide:
# cp {main} /etc/butterfly/style.sass
and then edit this file.
You can also copy the imported sass files in the same dir.
Sass files are compiled on the fly so just reload your tab to see the changes.
It is also possible to use a style.css file and re do all the styling in css exclusively.\
""".format(
title=ansi_colors.light_blue,
strong=ansi_colors.white,
reset=ansi_colors.reset,
main=os.path.normpath(os.path.join(
os.path.abspath(os.path.dirname(__file__)),
'../sass/main.sass'))))

7
butterfly/bin/bcat → butterfly/bin/cat.py Executable file → Normal file
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",

146
butterfly/bin/colors.py Normal file
View File

@@ -0,0 +1,146 @@
#!/usr/bin/env python
import argparse
import sys
parser = argparse.ArgumentParser(
description='Butterfly terminal color tester.')
parser.add_argument(
'--colors',
default='16',
choices=['8', '16', '256', '16M'],
help='Set the color mode to test')
args = parser.parse_args()
print()
if args.colors in ['8', '16']:
print('Background\n')
for l in range(3):
sys.stdout.write(' ')
for i in range(8):
sys.stdout.write('\x1b[%dm \x1b[m ' % (40 + i))
sys.stdout.write('\n')
sys.stdout.flush()
if args.colors == '16':
print()
for l in range(3):
sys.stdout.write(' ')
for i in range(8):
sys.stdout.write('\x1b[%dm \x1b[m ' % (100 + i))
sys.stdout.write('\n')
sys.stdout.flush()
print('\nForeground\n')
for l in range(3):
sys.stdout.write(' ')
for i in range(8):
sys.stdout.write('\x1b[%dm ░▒▓██\x1b[m ' % (30 + i))
sys.stdout.write('\n')
sys.stdout.flush()
if args.colors == '16':
print()
for l in range(3):
sys.stdout.write(' ')
for i in range(8):
sys.stdout.write('\x1b[1;%dm ░▒▓██\x1b[m ' % (30 + i))
sys.stdout.write('\n')
sys.stdout.flush()
if args.colors == '256':
for i in range(16):
sys.stdout.write('\x1b[48;5;%dm \x1b[m' % (i))
print()
for i in range(16):
sys.stdout.write('\x1b[48;5;%dm %03d\x1b[m' % (i, i))
print()
for j in range(6):
for i in range(36):
sys.stdout.write('\x1b[48;5;%dm \x1b[m' % (16 + j * 36 + i))
print()
for i in range(36):
sys.stdout.write('\x1b[48;5;%dm %03d\x1b[m' % (
16 + j * 36 + i, 16 + j * 36 + i))
print()
for i in range(24):
sys.stdout.write('\x1b[48;5;%dm \x1b[m' % (232 + i))
print()
for i in range(24):
sys.stdout.write('\x1b[48;5;%dm %03d\x1b[m' % (232 + i, 232 + i))
if args.colors == '16M':
b = 0
g = 0
for r in range(256):
if r == 128:
print()
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
print()
r = 255
b = 0
for g in range(256):
if g == 128:
print()
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
print()
r = 255
g = 255
for b in range(256):
if b == 128:
print()
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
print()
r = 255
b = 255
for g in reversed(range(256)):
if g == 127:
print()
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
print()
g = 0
b = 255
for r in reversed(range(256)):
if r == 127:
print()
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
print()
r = 0
g = 0
for b in reversed(range(256)):
if b == 127:
print()
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
print()
r = 0
b = 0
for g in range(256):
if g == 128:
print()
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
print()
r = 0
g = 255
for b in range(256):
if b == 128:
print()
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
print()
b = 255
g = 255
for r in range(256):
if r == 128:
print()
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
print()

63
butterfly/bin/help.py Normal file
View File

@@ -0,0 +1,63 @@
#!/usr/bin/env python
import base64
import os
import subprocess
import butterfly
from butterfly.escapes import image
from butterfly.utils import ansi_colors
print(ansi_colors.white + "Welcome to the butterfly help." + ansi_colors.reset)
path = os.getenv('BUTTERFLY_PATH')
if path:
path = os.path.join(path, '../static/images/favicon.png')
if path and os.path.exists(path):
with image('image/png'):
with open(path, 'rb') as i:
print(base64.b64encode(i.read()).decode('ascii'))
print("""
Butterfly is a xterm compliant terminal built with python and javascript.
{title}Terminal functionalities:{reset}
{strong}[ScrollLock] : {reset}Lock the scrolling to the current position. Press again to release.
{strong}[Ctrl] + [c] <<hold>> : {reset}Cut the output when [Ctrl] + [c] is not enough.
{strong}[Ctrl] + [Shift] + [Up] : {reset}Trigger visual selection mode. Hitting [Enter] inserts the selection in the prompt.
{strong}[Alt] + [a] : {reset}Set an alarm which sends a notification when a modification is detected. (Ring on regexp match with [Shift])
{strong}[Alt] + [s] : {reset}Open theme selection prompt. Use [Alt] + [Shift] + [s] to refresh current theme.
{strong}[Alt] + [e] : {reset}List open user sessions. (Only available in secure mode)
{strong}[Alt] + [o] : {reset}Open new terminal (As a popup)
{strong}[Alt] + [z] : {reset}Escape: don't catch the next pressed key.
Useful for using native search for example. ([Alt] + [z] then [Ctrl] + [f]).
{title}Butterfly programs:{reset}
{strong}b : {reset}Alias for {strong}butterfly{reset} executable. Takes a comand in parameter or launch a butterfly server for one shot use (if outside butterfly).
{strong}b cat : {reset}A wrapper around cat allowing to display images as <img> instead of binary.
{strong}b open : {reset}Open a new terminal at specified location.
{strong}b session : {reset}Open or rattach a butterfly session. Multiplexing is supported.
{strong}b colors : {reset}Test the terminal colors (16, 256 and 16777216 colors)
{strong}b html : {reset}Output in html standard input.
For more butterfly programs check out: https://github.com/paradoxxxzero/butterfly-demos
{title}Styling butterfly:{reset}
To style butterfly in sass, you need to have the libsass python library installed.
Theming is done by overriding the default sass files located in {code}{main}{reset} in your theme directory.
This directory can include images and custom fonts.
Please take a look at official themes here: https://github.com/paradoxxxzero/butterfly-themes
and submit your best themes as pull request!
\x1b[{rcol}G\x1b[3m{dark}butterfly @ 2015 Mounier Florian{reset}\
""".format(
title=ansi_colors.light_blue,
dark=ansi_colors.light_black,
strong=ansi_colors.white,
code=ansi_colors.light_yellow,
comment=ansi_colors.light_magenta,
reset=ansi_colors.reset,
rcol=int(subprocess.check_output(['stty', 'size']).split()[1]) - 31,
main=os.path.normpath(os.path.join(
os.path.abspath(os.path.dirname(butterfly.__file__)), 'sass'))))

19
butterfly/bin/html.py Normal file
View File

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

27
butterfly/bin/open.py Normal file
View File

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

15
butterfly/bin/session.py Normal file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env python
import argparse
import os
import webbrowser
parser = argparse.ArgumentParser(description='Butterfly session opener.')
parser.add_argument(
'session',
help='Open or rattach a butterfly session. '
'(Only in secure mode or in user unsecure mode (no su login))')
args = parser.parse_args()
url = '%ssession/%s' % (os.getenv('LOCATION', '/'), args.session)
if not webbrowser.open(url):
print('Unable to open browser, please go to %s' % url)

View File

@@ -0,0 +1,58 @@
# Butterfly autogenerated config file
# Activate debug mode
#
#debug=False
# In debug mode produce more verbose output
#
#more=False
# Use unminified version of js for development
#
#unminified=False
# Server host
# Use 'localhost' for local only
# Use your ip to share over your network
# Use '0.0.0.0' to listen to every network
#
#host='localhost'
# Server port
#
#port=57575
# Shell to launch at start (defaults to user shell)
#
#shell=None # shell='/bin/bash' for instance
# Motd, path to custom message of the day file
#
#motd='motd'
# Command to run instead of shell
#
#cmd=None # cmd='ls -l'
# Unsecure mode
# This mode use http without ssl and is therefore NOT RECOMMENDED
# Please generate yourself a certificate using the butterfly.server.py command
#
#unsecure=False
# Force user login in unsecure mode
#
#login=False
# Force unicode width
# This mode force every character to be the same width
# Which can be useful in some case
# But this breaks unicode display of varying width character
#
#force_unicode_width=False
# SSL version defaults to auto
#
#ssl_version=None

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
@@ -32,3 +36,35 @@ def text():
yield
sys.stdout.write('\x1bP')
sys.stdout.flush()
def geolocation():
sys.stdout.write('\x1b[?99n')
sys.stdout.flush()
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
rv = sys.stdin.read(1)
if rv != '\x1b':
raise
rv = sys.stdin.read(1)
if rv != '[':
raise
rv = sys.stdin.read(1)
if rv != '?':
raise
loc = ''
while rv != 'R':
rv = sys.stdin.read(1)
if rv != 'R':
loc += rv
except Exception:
return
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
if not loc or ';' not in loc:
return
return tuple(map(float, loc.split(';')))

192
butterfly/pam.py Normal file
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,14 +16,22 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
import os
import struct
import sys
import time
from collections import defaultdict
from mimetypes import guess_type
from uuid import uuid4
import tornado.escape
import tornado.options
import tornado.process
import tornado.web
import tornado.websocket
from collections import defaultdict
from butterfly import url, Route, utils, __version__
from butterfly import Route, url, utils
from butterfly.terminal import Terminal
@@ -33,111 +41,140 @@ def u(s):
return s
@url(r'/(?:user/(.+))?/?(?:wd/(.+))?/?(?:session/(.+))?')
@url(r'/(?:session/(?P<session>[^/]+)/?)?')
class Index(Route):
def get(self, user, path, session):
def get(self, session):
user = self.request.query_arguments.get(
'user', [b''])[0].decode('utf-8')
if not tornado.options.options.unsecure and user:
raise tornado.web.HTTPError(400)
return self.render('index.html')
return self.render(
'index.html', session=session or str(uuid4()))
@url(r'/style.css')
class Style(Route):
def get(self):
default_style = os.path.join(
os.path.dirname(__file__), 'static', 'main.css')
@url(r'/theme/([^/]+)/style.css')
class Theme(Route):
def get(self, theme):
self.log.info('Getting style')
css = utils.get_style()
try:
import sass
sass.CompileError
except Exception:
self.log.error(
'You must install libsass to use sass '
'(pip install libsass)')
return
base_dir = self.get_theme_dir(theme)
style = None
for ext in ['css', 'scss', 'sass']:
probable_style = os.path.join(base_dir, 'style.%s' % ext)
if os.path.exists(probable_style):
style = probable_style
if not style:
raise tornado.web.HTTPError(404)
sass_path = os.path.join(
os.path.dirname(__file__), 'sass')
css = None
try:
css = sass.compile(filename=style, include_paths=[
base_dir, sass_path])
except sass.CompileError:
self.log.error(
'Unable to compile style (filename: %s, paths: %r) ' % (
style, [base_dir, sass_path]), exc_info=True)
if not style:
raise tornado.web.HTTPError(500)
self.log.debug('Style ok')
self.set_header("Content-Type", "text/css")
self.write(css)
self.finish()
if css:
self.write(css)
else:
with open(default_style) as s:
@url(r'/theme/([^/]+)/(.+)')
class ThemeStatic(Route):
def get(self, theme, name):
if '..' in name:
raise tornado.web.HTTPError(403)
base_dir = self.get_theme_dir(theme)
fn = os.path.normpath(os.path.join(base_dir, name))
if not fn.startswith(base_dir):
raise tornado.web.HTTPError(403)
if os.path.exists(fn):
type = guess_type(fn)[0]
if type is None:
# Fallback if there's no mimetypes on the system
type = {
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'gif': 'image/gif',
'woff': 'application/font-woff',
'ttf': 'application/x-font-ttf'
}.get(fn.split('.')[-1], 'text/plain')
self.set_header("Content-Type", type)
with open(fn, 'rb') as s:
while True:
data = s.read(16384)
if data:
self.write(data)
else:
break
self.finish()
@url(r'/theme/font/([^/]+)')
class Font(Route):
def get(self, name):
if not tornado.options.options.theme or not name:
raise tornado.web.HTTPError(404)
font = 'themes/%s/font/%s' % (
tornado.options.options.theme,
name)
for fn in [
'/etc/butterfly/%s' % font,
os.path.expanduser('~/.butterfly/%s' % font)]:
if os.path.exists(fn):
ext = fn.split('.')[-1]
self.set_header("Content-Type", "application/x-font-%s" % ext)
with open(fn, 'rb') as s:
while True:
data = s.read(16384)
if data:
self.write(data)
else:
break
self.finish()
self.finish()
raise tornado.web.HTTPError(404)
@url(r'/ws'
'(?:/user/(?P<user>[^/]+))?/?'
'(?:session/(?P<session>[^/]+))?/?'
'(?:/wd/(?P<path>.+))?')
class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
session_history_size = 10000
# List of websockets per session per user
# dict: user -> dict: session -> [TermWebSocket]
sessions = defaultdict(dict)
class KeptAliveWebSocketHandler(tornado.websocket.WebSocketHandler):
keepalive_timer = None
# Terminal for session per user
# dict: user -> dict: session -> Terminal
terminals = defaultdict(dict)
def open(self, *args, **kwargs):
self.keepalive_timer = tornado.ioloop.PeriodicCallback(
self.send_ping, tornado.options.options.keepalive_interval * 1000)
self.keepalive_timer.start()
# All terminals sockets for systemd socket deactivation
sockets = []
def send_ping(self):
t = int(time.time())
frame = struct.pack('<I', t) # A ping frame based on time
self.log.info("Sending ping frame %s" % t)
try:
self.ping(frame)
except tornado.websocket.WebSocketClosedError:
self.keepalive_timer.stop()
# Session history
history = {}
def on_close(self):
if self.keepalive_timer is not None:
self.keepalive_timer.stop()
def open(self, user, path, session):
@url(r'/ctl/session/(?P<session>[^/]+)')
class TermCtlWebSocket(Route, KeptAliveWebSocketHandler):
sessions = defaultdict(list)
sessions_secure_users = {}
def open(self, session):
super(TermCtlWebSocket, self).open(session)
self.session = session
self.closed = False
self.secure_user = None
# Prevent cross domain
if self.request.headers['Origin'] not in (
'http://%s' % self.request.headers['Host'],
'https://%s' % self.request.headers['Host']):
self.log.warning(
'Unauthorized connection attempt: from : %s to: %s' % (
self.request.headers['Origin'],
self.request.headers['Host']))
self.close()
return
TermWebSocket.sockets.append(self)
self.log.info('Websocket opened %r' % self)
self.set_nodelay(True)
self.log.info('Websocket /ctl opened %r' % self)
def create_terminal(self):
socket = utils.Socket(self.ws_connection.stream.socket)
opts = tornado.options.options
user = self.request.query_arguments.get(
'user', [b''])[0].decode('utf-8')
path = self.request.query_arguments.get(
'path', [b''])[0].decode('utf-8')
secure_user = None
if not opts.unsecure:
if not tornado.options.options.unsecure:
user = utils.parse_cert(
self.ws_connection.stream.socket.getpeercert())
assert user, 'No user in certificate'
@@ -147,126 +184,152 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
raise Exception('Invalid user in certificate')
# Certificate authed user
self.secure_user = user
secure_user = user
elif socket.local and socket.user == utils.User():
elif socket.local and socket.user == utils.User() and not user:
# Local to local returning browser user
self.secure_user = socket.user
secure_user = socket.user
elif user:
try:
user = utils.User(name=user)
except LookupError:
raise Exception('Invalid user')
# Handling terminal session
if session:
if session in self.user_sessions:
# Session already here, registering websocket
self.user_sessions[session].append(self)
self.write_message(TermWebSocket.history[session])
# And returning, we don't want another terminal
return
if secure_user:
user = secure_user
if self.session in self.sessions and self.session in (
self.sessions_secure_users):
if user.name != self.sessions_secure_users[self.session]:
# Restrict to authorized users
raise tornado.web.HTTPError(403)
else:
# New session, opening terminal
self.user_sessions[session] = [self]
TermWebSocket.history[session] = ''
self.sessions_secure_users[self.session] = user.name
self.sessions[self.session].append(self)
terminal = Terminal.sessions.get(self.session)
# Handling terminal session
if terminal:
TermWebSocket.last.write_message(terminal.history)
# And returning, we don't want another terminal
return
# New session, opening terminal
terminal = Terminal(
user, path, session, socket,
self.request.headers['Host'], self.render_string, self.write)
user, path, self.session, socket,
self.request.full_url().replace('/ctl/', '/'), self.render_string,
TermWebSocket.broadcast)
terminal.pty()
if session:
if not self.secure_user:
self.log.error(
'No terminal session without secure authenticated user'
'or local user.')
self._terminal = terminal
self.session = None
else:
self.log.info('Openning session %s for secure user %r' % (
session, self.secure_user))
self.user_terminals[session] = terminal
else:
self._terminal = terminal
@property
def user_sessions(self):
"""Return the dict session of socket lists"""
if not self.secure_user:
return {}
return TermWebSocket.sessions[self.secure_user.name]
@property
def user_terminals(self):
"""Return the dict session of terminal"""
if not self.secure_user:
return {}
return TermWebSocket.terminals[self.secure_user.name]
self.log.info('Openning session %s for secure user %r' % (
self.session, user))
@classmethod
def close_all(cls, session, user):
sessions = TermWebSocket.sessions.get(user.name)
if sessions:
sockets = sessions[session]
for socket in sockets[:]:
socket.on_close()
socket.close()
del sessions[session]
terminals = TermWebSocket.terminals.get(user.name)
del terminals[session]
@classmethod
def broadcast(cls, session, message, user):
cls.history[session] += message
if len(cls.history) > cls.session_history_size:
cls.history[session] = cls.history[session][
-cls.session_history_size:]
sessions = cls.sessions.get(user.name, [])
for session in sessions[session]:
def broadcast(cls, session, message, emitter=None):
for wsocket in cls.sessions[session]:
try:
session.write_message(message)
if wsocket != emitter:
wsocket.write_message(message)
except Exception:
session.close()
def write(self, message):
if self.session and self.secure_user:
if message is None:
TermWebSocket.close_all(self.session, self.secure_user)
else:
TermWebSocket.broadcast(
self.session, message, self.secure_user)
else:
if message is None:
self.on_close()
self.close()
else:
self.write_message(message)
wsocket.log.exception('Error on broadcast')
wsocket.close()
def on_message(self, message):
if self.session and self.secure_user:
term = self.user_terminals.get(self.session)
term and term.write(message)
cmd = json.loads(message)
if cmd['cmd'] == 'open':
self.create_terminal()
else:
self._terminal.write(message)
try:
Terminal.sessions[self.session].ctl(cmd)
except Exception:
# FF strange bug
pass
self.broadcast(self.session, message, self)
def on_close(self):
super(TermCtlWebSocket, self).on_close()
if self.closed:
return
self.closed = True
self.log.info('Websocket closed %r' % self)
TermWebSocket.sockets.remove(self)
if self.session:
self.user_sessions[self.session].remove(self)
elif hasattr(self, '_terminal'):
self._terminal.close()
else:
self.log.error(
'Socket with neither session nor terminal %r' % self)
if self.application.systemd and not len(TermWebSocket.sockets):
self.log.info('Websocket /ctl closed %r' % self)
if self in self.sessions[self.session]:
self.sessions[self.session].remove(self)
if tornado.options.options.one_shot or (
getattr(self.application, 'systemd', False) and
not sum([
len(wsockets)
for session, wsockets in self.sessions.items()])):
sys.exit(0)
@url(r'/sessions')
class Sessions(Route):
"""List available sessions"""
@url(r'/ws/session/(?P<session>[^/]+)')
class TermWebSocket(Route, KeptAliveWebSocketHandler):
# List of websockets per session
sessions = defaultdict(list)
# Last is kept for session shared history send
last = None
# Session history
history = {}
def open(self, session):
super(TermWebSocket, self).open(session)
self.set_nodelay(True)
self.session = session
self.closed = False
self.sessions[session].append(self)
self.__class__.last = self
self.log.info('Websocket /ws opened %r' % self)
@classmethod
def close_session(cls, session):
wsockets = (cls.sessions.get(session, []) +
TermCtlWebSocket.sessions.get(session, []))
for wsocket in wsockets:
wsocket.on_close()
wsocket.close()
if session in cls.sessions:
del cls.sessions[session]
if session in TermCtlWebSocket.sessions_secure_users:
del TermCtlWebSocket.sessions_secure_users[session]
if session in TermCtlWebSocket.sessions:
del TermCtlWebSocket.sessions[session]
@classmethod
def broadcast(cls, session, message, emitter=None):
if message is None:
cls.close_session(session)
return
wsockets = cls.sessions.get(session)
for wsocket in wsockets:
try:
if wsocket != emitter:
wsocket.write_message(message)
except Exception:
wsocket.log.exception('Error on broadcast')
wsocket.close()
def on_message(self, message):
Terminal.sessions[self.session].write(message)
def on_close(self):
super(TermWebSocket, self).on_close()
if self.closed:
return
self.closed = True
self.log.info('Websocket /ws closed %r' % self)
self.sessions[self.session].remove(self)
@url(r'/sessions/list.json')
class SessionsList(Route):
"""Get the theme list"""
def get(self):
if tornado.options.options.unsecure:
raise tornado.web.HTTPError(403)
@@ -277,5 +340,61 @@ class Sessions(Route):
if not user:
raise tornado.web.HTTPError(403)
return self.render(
'list.html', sessions=TermWebSocket.sessions.get(user, []))
self.set_header('Content-Type', 'application/json')
self.write(tornado.escape.json_encode({
'sessions': sorted(
TermWebSocket.sessions),
'user': user
}))
@url(r'/themes/list.json')
class ThemesList(Route):
"""Get the theme list"""
def get(self):
if os.path.exists(self.themes_dir):
themes = [
theme
for theme in os.listdir(self.themes_dir)
if os.path.isdir(os.path.join(self.themes_dir, theme)) and
not theme.startswith('.')]
else:
themes = []
if os.path.exists(self.builtin_themes_dir):
builtin_themes = [
'built-in-%s' % theme
for theme in os.listdir(self.builtin_themes_dir)
if os.path.isdir(os.path.join(
self.builtin_themes_dir, theme)) and
not theme.startswith('.')]
else:
builtin_themes = []
self.set_header('Content-Type', 'application/json')
self.write(tornado.escape.json_encode({
'themes': sorted(themes),
'builtin_themes': sorted(builtin_themes),
'dir': self.themes_dir
}))
@url('/local.js')
class LocalJsStatic(Route):
def get(self):
self.set_header("Content-Type", 'application/javascript')
if os.path.exists(self.local_js_dir):
for fn in os.listdir(self.local_js_dir):
if not fn.endswith('.js'):
continue
with open(os.path.join(self.local_js_dir, fn), 'rb') as s:
while True:
data = s.read(16384)
if data:
self.write(data)
else:
self.write(';')
break
self.finish()

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2014 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -18,22 +18,19 @@
/* Here are the 16 "normal" colors for theming */
+termcolor(0, #2e3436) /* Black */
+termcolor(1, #cc0000) /* Red */
+termcolor(2, #4e9a06) /* Green */
+termcolor(3, #c4a000) /* Yellow */
+termcolor(4, #3465a4) /* Blue */
+termcolor(5, #75507b) /* Magenta */
+termcolor(6, #06989a) /* Cyan */
+termcolor(7, #d3d7cf) /* White */
+termcolor(8, #555753) /* Bright Black */
+termcolor(9, #ef2929) /* Bright Red */
+termcolor(10, #8ae234) /* Bright Green */
+termcolor(11, #fce94f) /* Bright Yellow */
+termcolor(12, #729fcf) /* Bright Blue */
+termcolor(13, #ad7fa8) /* Bright Magenta */
+termcolor(14, #34e2e2) /* Bright Cyan */
+termcolor(15, #eeeeec) /* Bright White */
$bg: #110f13
$fg: #f4ead5
+termcolor(0, nth($colors, 1))
+termcolor(1, nth($colors, 2))
+termcolor(2, nth($colors, 3))
+termcolor(3, nth($colors, 4))
+termcolor(4, nth($colors, 5))
+termcolor(5, nth($colors, 6))
+termcolor(6, nth($colors, 7))
+termcolor(7, nth($colors, 8))
+termcolor(8, nth($colors, 9))
+termcolor(9, nth($colors, 10))
+termcolor(10, nth($colors, 11))
+termcolor(11, nth($colors, 12))
+termcolor(12, nth($colors, 13))
+termcolor(13, nth($colors, 14))
+termcolor(14, nth($colors, 15))
+termcolor(15, nth($colors, 16))

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2014 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -15,11 +15,9 @@
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
$fg: #fff !default
$bg: #000 !default
/* Here are the 240 xterm colors */
/* See http://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg */
$st: 00, 95, 135, 175, 215, 255
@for $i from 0 through 215
@@ -32,5 +30,5 @@ $st: 00, 95, 135, 175, 215, 255
$l: 8 + $i * 10
+termcolor($i + 232, rgb($l, $l, $l))
+termcolor(256, $bg)
+termcolor(257, $fg)
+termcolor(256, $default-bg)
+termcolor(257, $default-fg)

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2014 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -15,15 +15,14 @@
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
$shadow: 0 !default
$shadow-alpha: 0 !default
=termcolor($i, $color)
.bg-color-#{$i}
background-color: $color
&.reverse-video
color: $color !important
@if $color == transparent
color: $reverse-transparent !important
@else
color: $color !important
.fg-color-#{$i}
color: $color

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2014 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -15,9 +15,6 @@
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
$fg: #fff !default
$shadow-alpha: 0 !default
.focus .cursor
transition: 300ms

View File

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

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2014 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -18,25 +18,95 @@
html, body
margin: 0
padding: 0
line-height: 1.2
background-color: $bg
color: $fg
body
padding-bottom: .5em
white-space: nowrap
overflow-x: hidden
overflow-y: scroll
a
text-decoration: underline rgba($fg, .2)
transition: text-decoration-color 500ms
&:hover
text-decoration: underline
.line.active
background-color: $active-bg
.line.extended
cursor: zoom-in
background-image: linear-gradient(90deg, rgba(darken($bg, 3%), 0), 95%, darken($bg, 3%))
.extra
display: none
&:not(.expanded):hover
background-color: lighten($bg, 2%)
&.expanded
cursor: zoom-out
background-color: darken($bg, 3%)
.extra
display: block
white-space: pre-wrap
word-break: break-all
&::-webkit-scrollbar
background: $bg
width: .75em
background: $scroll-bg
width: $scroll-width
&::-webkit-scrollbar-thumb
background: rgba($fg, .1)
background: $scroll-fg
&::-webkit-scrollbar-thumb:hover
background: rgba($fg, .15)
background: $scroll-fg-hover
/* Pop ups */
.hidden
display: none !important
#popup
position: fixed
display: flex
align-items: center
justify-content: center
width: 100%
height: 100%
form, > div
padding: 1.5em
background: $popup-bg
color: $popup-fg
font-size: $popup-fs
h2
margin: 0 .5em .5em .5em
select
min-width: 300px
padding: .5em
width: 100%
label
display: block
padding: .5em
font-size: .75em
#input-view
position: fixed
z-index: 100
padding: 0
margin: 0
text-decoration: underline
#input-helper
position: fixed
z-index: -100
opacity: 0
white-space: nowrap
overflow: hidden
resize: none
.terminal
outline: none

View File

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

View File

@@ -0,0 +1,38 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* Theses are the various imported style files
/* THIS NEEDS the python `libsass` library to be installed.
/* You can copy the imported files in the theme dir, they will be imported prioritarily.
/* You can change this file to import any webfont:
@import font
/* You can comment / uncomment the following to enable/disable terminal effects.
@import light_fx
/* Comment this one to remove the blurry text:
@import text_fx
/* @import all_fx
@import colors
/* The color theme is defined in this one:
@import 16_colors
@import 256_colors
@import layout
@import cursor
@import term_styles

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2014 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -15,22 +15,30 @@
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
$fg: #fff !default
$bg: #000 !default
.bold
font-weight: bold
.underline
text-decoration: underline
.italic
font-style: italic
.faint
opacity: .6
.crossed
text-decoration: line-through
/* Not supported, emulated
/* .blink
/* text-decoration: blink
.blink
animation: blink 1s ease-in-out infinite
.blink-fast
animation: blink 250ms ease-in-out infinite
@keyframes blink
0%
opacity: 1
@@ -46,7 +54,8 @@ $bg: #000 !default
color: $bg
background-color: $fg
.blur .cursor.reverse-video
.blur .cursor
border: 1px solid $fg
background: none
.nbsp

View File

@@ -0,0 +1,54 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* Variables */
/** Font
$font-family: "SourceCodePro" !default
$font-size: 1em !default
$font-line-height: 1.2 !default
/** Colors */
/* Foreground */
$fg: #f4ead5 !default
/* Background */
$bg: #110f13 !default
$default-bg: transparent !default
$active-bg: transparent !default
$default-fg: $fg !default
$reverse-transparent: $bg !default
/* 16 Colors in this orders: Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, Bright Black, Bright Red, Bright Green, Bright Yellow, Bright Blue, Bright Magenta, Bright Cyan, Bright White */
$colors: #2e3436, #cc0000, #4e9a06, #c4a000, #3465a4, #75507b, #06989a, #d3d7cf, #555753, #ef2929, #8ae234, #fce94f, #729fcf, #ad7fa8, #34e2e2, #eeeeec !default
/** Text effects */
/* The shadow is the size of the blur (in px for instance)
$shadow: 0 !default
/* The shadow alpha is the opacity of the shadow
$shadow-alpha: 0 !default
/** Scroll */
$scroll-bg: $bg !default
$scroll-fg: rgba($fg, .1) !default
$scroll-fg-hover: rgba($fg, .1) !default
$scroll-width: .75em !default
/** Popup */
$popup-bg: rgba(127, 127, 127, .5) !default
$popup-fg: $fg !default
$popup-fs: 1em !default

View File

@@ -1,7 +1,7 @@
/* *-* coding: utf-8 *-* */
/* This file is part of butterfly */
/* butterfly Copyright (C) 2014 Florian Mounier */
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
@@ -15,28 +15,12 @@
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* Theses are the various imported style files
/* You can put this file in /etc/butterfly/style.sass or ~/.butterfly/style.sass
/* To customize the style of your terminal.
/* THIS NEEDS the python `libsass` library to be installed.
/* You can also copy the imported files in those dirs, they will be imported prioritarily.
/* You can copy the imported files in the theme dir, they will be imported prioritarily.
/* You can change this file to import any webfont:
@import font
/* These a the default variables */
@import variables
/* You can comment / uncomment the following to enable/disable terminal effects.
@import light_fx
/* Comment this one to remove the blurry text:
@import text_fx
/* @import all_fx
@import colors
/* The color theme is defined in this one:
@import 16_colors
@import 256_colors
@import layout
@import cursor
@import term_styles
/* These are all imported files */
@import styles

View File

@@ -1,24 +1,88 @@
(function() {
var Selection, alt, cancel, copy, ctrl, first, nextLeaf, previousLeaf, selection, setAlarm, virtualInput,
var Popup, Selection, _set_theme_href, _theme, alt, cancel, clean_ansi, copy, ctrl, escape, histSize, linkify, maybePack, nextLeaf, packSize, popup, previousLeaf, selection, setAlarm, tags, tid, walk,
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
setAlarm = function(notification) {
clean_ansi = function(data) {
var c, i, out, state;
if (data.indexOf('\x1b') < 0) {
return data;
}
i = -1;
out = '';
state = 'normal';
while (i < data.length - 1) {
c = data.charAt(++i);
switch (state) {
case 'normal':
if (c === '\x1b') {
state = 'escaped';
break;
}
out += c;
break;
case 'escaped':
if (c === '[') {
state = 'csi';
break;
}
if (c === ']') {
state = 'osc';
break;
}
if ('#()%*+-./'.indexOf(c) >= 0) {
i++;
}
state = 'normal';
break;
case 'csi':
if ("?>!$\" '".indexOf(c) >= 0) {
break;
}
if (('0' <= c && c <= '9')) {
break;
}
if (c === ';') {
break;
}
state = 'normal';
break;
case 'osc':
if (c === "\x1b" || c === "\x07") {
if (c === "\x1b") {
i++;
}
state = 'normal';
}
}
}
return out;
};
setAlarm = function(notification, cond) {
var alarm;
alarm = function(data) {
var note;
var message, note, notif;
message = clean_ansi(data.data.slice(1));
if (cond !== null && !cond.test(message)) {
return;
}
butterfly.body.classList.remove('alarm');
note = "New activity on butterfly terminal [" + butterfly.title + "]";
note = "Butterfly [" + butterfly.title + "]";
if (notification) {
new Notification(note, {
body: data.data,
notif = new Notification(note, {
body: message,
icon: '/static/images/favicon.png'
});
notif.onclick = function() {
window.focus();
return notif.close();
};
} else {
alert(note + '\n' + data.data);
alert(note + '\n' + message);
}
return butterfly.ws.removeEventListener('message', alarm);
return butterfly.ws.shell.removeEventListener('message', alarm);
};
butterfly.ws.addEventListener('message', alarm);
butterfly.ws.shell.addEventListener('message', alarm);
return butterfly.body.classList.add('alarm');
};
@@ -34,27 +98,37 @@
};
document.addEventListener('keydown', function(e) {
var cond;
if (!(e.altKey && e.keyCode === 65)) {
return true;
}
cond = null;
if (e.shiftKey) {
cond = prompt('Ring alarm when encountering the following text: (can be a regexp)');
if (!cond) {
return;
}
cond = new RegExp(cond);
}
if (Notification && Notification.permission === 'default') {
Notification.requestPermission(function() {
return setAlarm(Notification.permission === 'granted');
return setAlarm(Notification.permission === 'granted', cond);
});
} else {
setAlarm(Notification.permission === 'granted');
setAlarm(Notification.permission === 'granted', cond);
}
return cancel(e);
});
addEventListener('copy', copy = function(e) {
var data, end, j, len1, line, ref, sel;
var data, end, j, len, line, ref, sel;
document.getElementsByTagName('body')[0].contentEditable = false;
butterfly.bell("copied");
e.clipboardData.clearData();
sel = getSelection().toString().replace(/\u00A0/g, ' ').replace(/\u2007/g, ' ');
data = '';
ref = sel.split('\n');
for (j = 0, len1 = ref.length; j < len1; j++) {
for (j = 0, len = ref.length; j < len; j++) {
line = ref[j];
if (line.slice(-1) === '\u23CE') {
end = '';
@@ -69,14 +143,232 @@
});
addEventListener('paste', function(e) {
var data;
var data, send, size;
document.getElementsByTagName('body')[0].contentEditable = false;
butterfly.bell("pasted");
data = e.clipboardData.getData('text/plain');
data = data.replace(/\r\n/g, '\n').replace(/\n/g, '\r');
butterfly.send(data);
size = 1024;
send = function() {
butterfly.send(data.substring(0, size));
data = data.substring(size);
if (data.length) {
return setTimeout(send, 25);
}
};
send();
return e.preventDefault();
});
addEventListener('beforeunload', function(e) {
if (!(butterfly.body.classList.contains('dead') || location.href.indexOf('session') > -1)) {
return e.returnValue = 'This terminal is active and not in session. Are you sure you want to kill it?';
}
});
Terminal.on('change', function(line) {
if (indexOf.call(line.classList, 'extended') >= 0) {
return line.addEventListener('click', (function(line) {
return function() {
var after, before;
if (indexOf.call(line.classList, 'expanded') >= 0) {
return line.classList.remove('expanded');
} else {
before = line.getBoundingClientRect().height;
line.classList.add('expanded');
after = line.getBoundingClientRect().height;
return document.body.scrollTop += after - before;
}
};
})(line));
}
});
walk = function(node, callback) {
var child, j, len, ref, results;
ref = node.childNodes;
results = [];
for (j = 0, len = ref.length; j < len; j++) {
child = ref[j];
callback.call(child);
results.push(walk(child, callback));
}
return results;
};
linkify = function(text) {
var emailAddressPattern, pseudoUrlPattern, urlPattern;
urlPattern = /\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim;
pseudoUrlPattern = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
emailAddressPattern = /[\w.]+@[a-zA-Z_-]+?(?:\.[a-zA-Z]{2,6})+/gim;
return text.replace(urlPattern, '<a href="$&">$&</a>').replace(pseudoUrlPattern, '$1<a href="http://$2">$2</a>').replace(emailAddressPattern, '<a href="mailto:$&">$&</a>');
};
tags = {
'&': '&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.origin);
return cancel(e);
});
tid = null;
packSize = 1000;
histSize = 100;
maybePack = function() {
var hist, i, j, pack, packfrag, ref;
if (!(butterfly.term.childElementCount > packSize + butterfly.rows)) {
return;
}
hist = document.getElementById('packed');
packfrag = document.createDocumentFragment('fragment');
for (i = j = 0, ref = packSize; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) {
packfrag.appendChild(butterfly.term.firstChild);
}
pack = document.createElement('div');
pack.classList.add('pack');
pack.appendChild(packfrag);
hist.appendChild(pack);
if (hist.childElementCount > histSize) {
hist.firstChild.remove();
}
return tid = setTimeout(maybePack);
};
Terminal.on('refresh', function() {
if (tid) {
clearTimeout(tid);
}
return maybePack();
});
Terminal.on('clear', function() {
var hist, newHist;
newHist = document.createElement('div');
newHist.id = 'packed';
hist = document.getElementById('packed');
return butterfly.body.replaceChild(newHist, hist);
});
Popup = (function() {
function Popup() {
this.el = document.getElementById('popup');
this.bound_click_maybe_close = this.click_maybe_close.bind(this);
this.bound_key_maybe_close = this.key_maybe_close.bind(this);
}
Popup.prototype.open = function(html) {
this.el.innerHTML = html;
this.el.classList.remove('hidden');
addEventListener('click', this.bound_click_maybe_close);
return addEventListener('keydown', this.bound_key_maybe_close);
};
Popup.prototype.close = function() {
removeEventListener('click', this.bound_click_maybe_close);
removeEventListener('keydown', this.bound_key_maybe_close);
this.el.classList.add('hidden');
return this.el.innerHTML = '';
};
Popup.prototype.click_maybe_close = function(e) {
var t;
t = e.target;
while (t.parentElement) {
if (Array.prototype.slice.call(this.el.children).indexOf(t) > -1) {
return true;
}
t = t.parentElement;
}
this.close();
return cancel(e);
};
Popup.prototype.key_maybe_close = function(e) {
if (e.keyCode !== 27) {
return true;
}
this.close();
return cancel(e);
};
return Popup;
})();
popup = new Popup();
selection = null;
cancel = function(ev) {
@@ -178,13 +470,13 @@
Selection.prototype.go = function(n) {
var index;
index = butterfly.children.indexOf(this.startLine) + n;
if (!((0 <= index && index < butterfly.children.length))) {
index = Array.prototype.indexOf.call(butterfly.term.childNodes, this.startLine) + n;
if (!((0 <= index && index < butterfly.term.childElementCount))) {
return;
}
while (!butterfly.children[index].textContent.match(/\S/)) {
while (!butterfly.term.childNodes[index].textContent.match(/\S/)) {
index += n;
if (!((0 <= index && index < butterfly.children.length))) {
if (!((0 <= index && index < butterfly.term.childElementCount))) {
return;
}
}
@@ -202,7 +494,7 @@
Selection.prototype.selectLine = function(index) {
var line, lineEnd, lineStart;
line = butterfly.children[index];
line = butterfly.term.childNodes[index];
lineStart = {
node: line.firstChild,
offset: 0
@@ -263,7 +555,7 @@
} else {
node = needle.node;
}
text = node.textContent;
text = node != null ? node.textContent : void 0;
i = needle.offset;
if (backward) {
while (node) {
@@ -276,7 +568,7 @@
}
}
node = previousLeaf(node);
text = node.textContent;
text = node != null ? node.textContent : void 0;
i = text.length;
}
} else {
@@ -290,7 +582,7 @@
}
}
node = nextLeaf(node);
text = node.textContent;
text = node != null ? node.textContent : void 0;
i = 0;
}
}
@@ -302,7 +594,7 @@
})();
document.addEventListener('keydown', function(e) {
var ref, ref1;
var r, ref, ref1;
if (ref = e.keyCode, indexOf.call([16, 17, 18, 19], ref) >= 0) {
return true;
}
@@ -339,8 +631,9 @@
return cancel(e);
}
if (!selection && e.ctrlKey && e.shiftKey && e.keyCode === 38) {
r = Math.max(butterfly.term.childElementCount - butterfly.rows, 0);
selection = new Selection();
selection.selectLine(butterfly.y - 1);
selection.selectLine(r + butterfly.y - 1);
selection.apply();
return cancel(e);
}
@@ -401,74 +694,120 @@
return sel.modify('extend', 'forward', 'character');
});
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
ctrl = false;
alt = false;
first = true;
virtualInput = document.createElement('input');
virtualInput.type = 'password';
virtualInput.style.position = 'fixed';
virtualInput.style.top = 0;
virtualInput.style.left = 0;
virtualInput.style.border = 'none';
virtualInput.style.outline = 'none';
virtualInput.style.opacity = 0;
virtualInput.value = '0';
document.body.appendChild(virtualInput);
virtualInput.addEventListener('blur', function() {
return setTimeout(((function(_this) {
return function() {
return _this.focus();
};
})(this)), 10);
});
addEventListener('click', function() {
return virtualInput.focus();
});
addEventListener('touchstart', function(e) {
if (e.touches.length === 2) {
return ctrl = true;
} else if (e.touches.length === 3) {
ctrl = false;
return alt = true;
} else if (e.touches.length === 4) {
ctrl = true;
return alt = true;
}
});
virtualInput.addEventListener('keydown', function(e) {
butterfly.keyDown(e);
document.addEventListener('keydown', function(e) {
var oReq;
if (!(e.altKey && e.keyCode === 69)) {
return true;
});
virtualInput.addEventListener('input', function(e) {
var len;
len = this.value.length;
if (len === 0) {
e.keyCode = 8;
butterfly.keyDown(e);
this.value = '0';
return true;
}
e.keyCode = this.value.charAt(1).charCodeAt(0);
if ((ctrl || alt) && !first) {
e.keyCode = this.value.charAt(1).charCodeAt(0);
e.ctrlKey = ctrl;
e.altKey = alt;
if (e.keyCode >= 97 && e.keyCode <= 122) {
e.keyCode -= 32;
}
oReq = new XMLHttpRequest();
oReq.addEventListener('load', function() {
var j, len, out, ref, response, session;
response = JSON.parse(this.responseText);
out = '<div>';
out += '<h2>Session list</h2>';
if (response.sessions.length === 0) {
out += "No current session for user " + response.user;
} else {
out += '<ul>';
ref = response.sessions;
for (j = 0, len = ref.length; j < len; j++) {
session = ref[j];
out += "<li><a href=\"/session/" + session + "\">" + session + "</a></li>";
}
butterfly.keyDown(e);
this.value = '0';
ctrl = alt = false;
return true;
out += '</ul>';
}
butterfly.keyPress(e);
first = false;
this.value = '0';
return true;
out += '</div>';
return popup.open(out);
});
oReq.open("GET", "/sessions/list.json");
oReq.send();
return cancel(e);
});
_set_theme_href = function(href) {
var img;
document.getElementById('style').setAttribute('href', href);
img = document.createElement('img');
img.onerror = function() {
return setTimeout((function() {
return typeof butterfly !== "undefined" && butterfly !== null ? butterfly.resize() : void 0;
}), 250);
};
return img.src = href;
};
_theme = typeof localStorage !== "undefined" && localStorage !== null ? localStorage.getItem('theme') : void 0;
if (_theme) {
_set_theme_href(_theme);
}
this.set_theme = function(theme) {
_theme = theme;
if (typeof localStorage !== "undefined" && localStorage !== null) {
localStorage.setItem('theme', theme);
}
if (theme) {
return _set_theme_href(theme);
}
};
document.addEventListener('keydown', function(e) {
var oReq, style;
if (!(e.altKey && e.keyCode === 83)) {
return true;
}
if (e.shiftKey) {
style = document.getElementById('style').getAttribute('href');
style = style.split('?')[0];
_set_theme_href(style + '?' + (new Date().getTime()));
return cancel(e);
}
oReq = new XMLHttpRequest();
oReq.addEventListener('load', function() {
var builtin_themes, inner, j, k, len, len1, option, response, theme, theme_list, themes, url;
response = JSON.parse(this.responseText);
builtin_themes = response.builtin_themes;
themes = response.themes;
inner = "<form>\n <h2>Pick a theme:</h2>\n <select id=\"theme_list\">";
option = function(url, theme) {
inner += '<option ';
if (_theme === url) {
inner += 'selected ';
}
inner += "value=\"" + url + "\">";
inner += theme;
return inner += '</option>';
};
option("/static/main.css", 'default');
if (themes.length) {
inner += '<optgroup label="Local themes">';
for (j = 0, len = themes.length; j < len; j++) {
theme = themes[j];
url = "/theme/" + theme + "/style.css";
option(url, theme);
}
inner += '</optgroup>';
}
inner += '<optgroup label="Built-in themes">';
for (k = 0, len1 = builtin_themes.length; k < len1; k++) {
theme = builtin_themes[k];
url = "/theme/" + theme + "/style.css";
option(url, theme.slice('built-in-'.length));
}
inner += '</optgroup>';
inner += " </select>\n <label>You can create yours in " + response.dir + ".</label>\n</form>";
popup.open(inner);
theme_list = document.getElementById('theme_list');
return theme_list.addEventListener('change', function() {
return set_theme(theme_list.value);
});
});
oReq.open("GET", "/themes/list.json");
oReq.send();
return cancel(e);
});
}).call(this);
//# sourceMappingURL=ext.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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">
@@ -10,15 +11,26 @@
<link rel="shortcut icon" href="{{ static_url('images/favicon.png') }}">
<title>Butterfly</title>
<link href="/style.css" rel="stylesheet">
<link href="{{ static_url('main.css') }}" rel="stylesheet" id="style">
</head>
<body spellcheck="false"
data-force-unicode-width="{{ 'yes' if options.force_unicode_width else 'no' }}">
data-force-unicode-width="{{ 'yes' if options.force_unicode_width else 'no' }}"
data-root-path="{{ options.uri_root_path }}"
data-session-token={{ session }}>
<textarea id="input-helper">
</textarea>
<div id="input-view" class="hidden">
</div>
<div id="popup" class="hidden">
</div>
<script src="{{ static_url('html-sanitizer.js') }}"></script>
<script src="{{ static_url('main.%sjs' % (
'' if options.unminified else 'min.')) }}"></script>
<script src="{{ static_url('ext.%sjs' % (
'' if options.unminified else 'min.')) }}"></script>
<script src="{{ reverse_url('LocalJsStatic') }}"></script>
<div id="packed"></div>
<div id="term"></div>
</body>
</html>

View File

@@ -1,24 +0,0 @@
<!DOCTYPE html>
{% from tornado.options import options %}
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Butterfly - A web terminal based on websocket and tornado">
<meta name="author" content="Mounier Florian">
<link rel="shortcut icon" href="{{ static_url('images/favicon.png') }}">
<title>Butterfly</title>
<link href="/style.css" rel="stylesheet">
</head>
<body>
<h1>Currently open butterfly sessions :</h1>
<ul>
{% for session in sessions %}
<li><h2><a target="_blank" href="/session/{{ session }}">{{ session }}</a></h2></li>
{% end %}
</ul>
</body>
</html>

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: $ 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'))
uri=self.uri,
colors=utils.ansi_colors
).decode('utf-8')
.replace('\r', '')
.replace('\n', '\r\n'))
self.send(motd)
log.info('Forking pty for user %r' % self.user)
def send(self, message):
if message is not None:
self.history += message
if len(self.history) > self.history_size:
self.history = self.history[-self.history_size:]
self.broadcast(self.session, message)
def pty(self):
# Make a "unique" id in 4 bytes
self.uid = ''.join(
@@ -114,10 +130,13 @@ class Terminal(object):
self.communicate()
def determine_user(self):
if self.callee is None and (
tornado.options.options.unsecure and
tornado.options.options.login):
# If callee is now known and we have unsecure connection
if not tornado.options.options.unsecure:
# Secure mode we must have already a callee
assert self.callee is not None
return
# If we should login, login
if tornado.options.options.login:
user = ''
while user == '':
try:
@@ -131,13 +150,11 @@ class Terminal(object):
except Exception:
log.debug("Can't switch to user %s" % user, exc_info=True)
self.callee = utils.User(name='nobody')
elif (tornado.options.options.unsecure and not
tornado.options.options.login):
# if login is not required, we will use the same user as
# butterfly is executed
self.callee = utils.User()
return
assert self.callee is not None
# if login is not required, we will use the same user as
# butterfly is executed
self.callee = self.callee or utils.User()
def shell(self):
try:
@@ -147,27 +164,27 @@ class Terminal(object):
"Can't chdir to %s" % (self.path or self.callee.dir),
exc_info=True)
env = os.environ
# If local and local user is the same as login user
# We set the env of the user from the browser
# Usefull when running as root
if self.caller == self.callee:
env = os.environ
env.update(self.socket.env)
else:
# May need more?
env = {}
env["TERM"] = "xterm-256color"
env["COLORTERM"] = "butterfly"
env["HOME"] = self.callee.dir
env["LOCATION"] = "http%s://%s:%d/" % (
"s" if not tornado.options.options.unsecure else "",
tornado.options.options.host, tornado.options.options.port)
env["PATH"] = '%s:%s' % (os.path.abspath(os.path.join(
os.path.dirname(__file__), 'bin')), env.get("PATH"))
env["LOCATION"] = self.uri
env['BUTTERFLY_PATH'] = os.path.abspath(os.path.join(
os.path.dirname(__file__), 'bin'))
try:
tty = os.ttyname(0).replace('/dev/', '')
except Exception:
log.debug("Can't get ttyname", exc_info=True)
tty = ''
if self.caller != self.callee:
try:
os.chown(os.ttyname(0), self.callee.uid, -1)
@@ -177,22 +194,21 @@ class Terminal(object):
utils.add_user_info(
self.uid,
tty, os.getpid(),
self.callee.name, self.host)
self.callee.name, self.uri)
if not tornado.options.options.unsecure or (
self.socket.local and
self.caller == self.callee and
server == self.callee
) or not tornado.options.options.login:
local_login = (
self.socket.local and self.caller == self.callee and
server == self.callee)
secure = not tornado.options.options.unsecure
force_login = tornado.options.options.login
ignore_security = (
tornado.options.options.
i_hereby_declare_i_dont_want_any_security_whatsoever)
if not force_login and (ignore_security or secure or local_login):
# User has been auth with ssl or is the same user as server
# or login is explicitly turned off
if (
not tornado.options.options.unsecure and
tornado.options.options.login and not (
self.socket.local and
self.caller == self.callee and
server == self.callee
)):
if secure and not local_login:
# User is authed by ssl, setting groups
try:
os.initgroups(self.callee.name, self.callee.gid)
@@ -211,11 +227,23 @@ class Terminal(object):
args = tornado.options.options.cmd.split(' ')
else:
args = [tornado.options.options.shell or self.callee.shell]
args.append('-i')
args.append('-il')
# In some cases some shells don't export SHELL var
env['SHELL'] = args[0]
os.execvpe(args[0], args, env)
# This process has been replaced
if tornado.options.options.pam_profile:
if not server.root:
print('You must be root to use pam_profile option.')
sys.exit(3)
pam_path = os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'pam.py')
os.execvpe(sys.executable, [
sys.executable, pam_path, self.callee.name,
tornado.options.options.pam_profile], env)
# Unsecure connection with su
if server.root:
if self.socket.local:
@@ -235,11 +263,10 @@ class Terminal(object):
else:
args = ['/bin/su']
if sys.platform == 'linux':
args.append('-p')
if tornado.options.options.shell:
args.append('-s')
args.append(tornado.options.options.shell)
args.append('-l')
if sys.platform.startswith('linux') and tornado.options.options.shell:
args.append('-s')
args.append(tornado.options.options.shell)
args.append(self.callee.name)
os.execvpe(args[0], args, env)
@@ -269,15 +296,17 @@ class Terminal(object):
self.on_close()
self.close()
if message[0] == 'R':
cols, rows = map(int, message[1:].split(','))
log.debug('WRIT<%r' % message)
self.writer.write(message)
self.writer.flush()
def ctl(self, message):
if message['cmd'] == 'size':
cols = message['cols']
rows = message['rows']
s = struct.pack("HHHH", rows, cols, 0, 0)
fcntl.ioctl(self.fd, termios.TIOCSWINSZ, s)
log.info('SIZE (%d, %d)' % (cols, rows))
elif message[0] == 'S':
log.debug('WRIT<%r' % message)
self.writer.write(message[1:])
self.writer.flush()
def shell_handler(self, fd, events):
if events & ioloop.READ:
@@ -322,7 +351,10 @@ class Terminal(object):
log.debug('closing fd fail', exc_info=True)
try:
os.kill(self.pid, signal.SIGKILL)
os.kill(self.pid, signal.SIGHUP)
os.kill(self.pid, signal.SIGCONT)
os.waitpid(self.pid, 0)
except Exception:
log.debug('waitpid fail', exc_info=True)
del self.sessions[self.session]

1
butterfly/themes Submodule

Submodule butterfly/themes added at d640d1ec1c

View File

@@ -1,7 +1,7 @@
# *-* coding: utf-8 *-*
# This file is part of butterfly
#
# butterfly Copyright (C) 2014 Florian Mounier
# butterfly Copyright(C) 2015-2017 Florian Mounier
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
@@ -18,57 +18,45 @@
import os
import pwd
import time
import sys
import struct
from logging import getLogger
from collections import namedtuple
import subprocess
import tornado.options
import re
import struct
import subprocess
import sys
import time
from collections import namedtuple
from logging import getLogger
log = getLogger('butterfly')
def get_style():
style = None
def get_hex_ip_port(remote):
ip, port = remote
if ip.startswith('::ffff:'):
ip = ip[len('::ffff:'):]
splits = ip.split('.')
if ':' not in ip and len(splits) == 4:
# Must be an ipv4
return '%02X%02X%02X%02X:%04X' % (
int(splits[3]),
int(splits[2]),
int(splits[1]),
int(splits[0]),
int(port)
)
try:
import ipaddress
except ImportError:
print('Please install ipaddress backport for ipv6 user detection')
return ''
if tornado.options.options.theme:
theme = 'themes/%s/' % tornado.options.options.theme
else:
theme = '/'
# Endian reverse:
ipv6_parts = ipaddress.IPv6Address(ip).exploded.split(':')
for i in range(0, 8, 2):
ipv6_parts[i], ipv6_parts[i + 1] = (
ipv6_parts[i + 1][2:] + ipv6_parts[i + 1][:2],
ipv6_parts[i][2:] + ipv6_parts[i][:2])
for ext in ['css', 'scss', 'sass']:
for fn in [
'/etc/butterfly/%sstyle' % theme,
os.path.expanduser('~/.butterfly/%sstyle' % theme)]:
if os.path.exists('%s.%s' % (fn, ext)):
style = '%s.%s' % (fn, ext)
if style is None:
return
if style.endswith('.scss') or style.endswith('.sass'):
sass_path = os.path.join(
os.path.dirname(__file__), 'sass')
try:
import sass
except Exception:
log.error('You must install libsass to use sass '
'(pip install libsass)')
return
try:
return sass.compile(filename=style, include_paths=[
theme, sass_path])
except sass.CompileError:
log.error(
'Unable to compile style.scss (filename: %s, paths: %r) ' % (
style, [theme, sass_path]), exc_info=True)
return
with open(style) as s:
return s.read()
return ''.join(ipv6_parts) + ':%04X' % port
def parse_cert(cert):
@@ -149,9 +137,9 @@ class Socket(object):
# If there is procfs, get as much info as we can
if os.path.exists('/proc/net'):
try:
line = get_procfs_socket_line(self.remote_port)
line = get_procfs_socket_line(get_hex_ip_port(pn[:2]))
self.user = User(uid=int(line[7]))
self.env = get_socket_env(line[9])
self.env = get_socket_env(line[9], self.user)
except Exception:
log.debug('procfs was no good, aight', exc_info=True)
@@ -165,7 +153,8 @@ class Socket(object):
@property
def local(self):
return self.remote_addr in ['127.0.0.1', '::1']
return (self.remote_addr in ['127.0.0.1', '::1', '::ffff:127.0.0.1'] or
self.local_addr == self.remote_addr)
def __repr__(self):
return '<Socket L: %s:%d R: %s:%d User: %r>' % (
@@ -179,7 +168,7 @@ def get_lsof_socket_line(addr, port):
# May want to make this into a dictionary in the future...
regex = "\w+\s+(?P<pid>\d+)\s+(?P<user>\w+).*\s" \
"(?P<laddr>.*?):(?P<lport>\d+)->(?P<raddr>.*?):(?P<rport>\d+)"
output = subprocess.check_output(['lsof', '-Pni'])
output = subprocess.check_output(['lsof', '-Pni']).decode('utf-8')
lines = output.split('\n')
for line in lines:
# Look for local address with peer port
@@ -192,33 +181,64 @@ def get_lsof_socket_line(addr, port):
# Linux only socket line get
def get_procfs_socket_line(port):
def get_procfs_socket_line(hex_ip_port):
fn = None
if len(hex_ip_port) == 13: # ipv4
fn = '/proc/net/tcp'
elif len(hex_ip_port) == 37: # ipv6
fn = '/proc/net/tcp6'
if not fn:
return
try:
with open('/proc/net/tcp') as k:
with open(fn) as k:
lines = k.readlines()
for line in lines:
# Look for local address with peer port
if line.split()[1] == '0100007F:%X' % port:
if line.split()[1] == hex_ip_port:
# We got the socket
return line.split()
except Exception:
log.debug('getting socket inet4 line fail', exc_info=True)
try:
with open('/proc/net/tcp6') as k:
lines = k.readlines()
for line in lines:
# Look for local address with peer port
if line.split()[1] == (
'00000000000000000000000001000000:%X' % port):
# We got the socket
return line.split()
except Exception:
log.debug('getting socket inet6 line fail', exc_info=True)
log.debug('getting socket %s line fail' % fn, exc_info=True)
# Linux only browser environment far fetch
def get_socket_env(inode):
def get_socket_env(inode, user):
for pid in os.listdir("/proc/"):
if not pid.isdigit():
continue
try:
with open('/proc/%s/cmdline' % pid) as c:
command = c.read().split('\x00')
executable = command[0].split('/')[-1]
if executable in ('sh', 'bash', 'zsh'):
executable = command[1].split('/')[-1]
if executable in [
'gnome-session',
'gnome-session-binary',
'startkde',
'startdde',
'xfce4-session']:
with open('/proc/%s/status' % pid) as e:
uid = None
for line in e.read().splitlines():
parts = line.split('\t')
if parts[0] == 'Uid:':
uid = int(parts[1])
break
if not uid or uid != user.uid:
continue
with open('/proc/%s/environ' % pid) as e:
keyvals = e.read().split('\x00')
env = {}
for keyval in keyvals:
if '=' in keyval:
key, val = keyval.split('=', 1)
env[key] = val
return env
except Exception:
continue
for pid in os.listdir("/proc/"):
if not pid.isdigit():
continue
@@ -274,11 +294,12 @@ def get_wtmp_file():
if os.path.exists(file):
return file
UTmp = namedtuple(
'UTmp',
['type', 'pid', 'line', 'id', 'user', 'host',
'exit0', 'exit1', 'session',
'sec', 'usec', 'addr0', 'addr1', 'addr2', 'addr3', 'unused'])
'UTmp',
['type', 'pid', 'line', 'id', 'user', 'host',
'exit0', 'exit1', 'session',
'sec', 'usec', 'addr0', 'addr1', 'addr2', 'addr3', 'unused'])
def utmp_line(id, type, pid, fd, user, host, ts):
@@ -388,4 +409,5 @@ class AnsiColors(object):
return '\x1b[0m'
return ''
ansi_colors = AnsiColors()

View File

@@ -1,19 +1,70 @@
setAlarm = (notification) ->
clean_ansi = (data) ->
# Fast ansi clean (not complete)
if data.indexOf('\x1b') < 0
return data
i = -1
out = ''
state = 'normal'
while i < data.length - 1
c = data.charAt ++i
switch state
when 'normal'
if c is '\x1b'
state = 'escaped'
break
out += c
when 'escaped'
if c is '['
state = 'csi'
break
if c is ']'
state = 'osc'
break
if '#()%*+-./'.indexOf(c) >= 0
i++
state = 'normal'
when 'csi'
if "?>!$\" '".indexOf(c) >= 0
break
if '0' <= c <= '9'
break
break if c is ';'
state = 'normal'
when 'osc'
if c is "\x1b" or c is "\x07"
i++ if c is "\x1b"
state = 'normal'
return out
setAlarm = (notification, cond) ->
alarm = (data) ->
message = clean_ansi data.data.slice(1)
return if cond isnt null and not cond.test(message)
butterfly.body.classList.remove 'alarm'
note = "New activity on butterfly terminal [#{ butterfly.title }]"
note = "Butterfly [#{ butterfly.title }]"
if notification
new Notification(
notif = new Notification(
note,
body: data.data,
body: message,
icon: '/static/images/favicon.png')
notif.onclick = ->
window.focus()
notif.close()
else
alert(note + '\n' + data.data)
alert(note + '\n' + message)
butterfly.ws.removeEventListener 'message', alarm
butterfly.ws.shell.removeEventListener 'message', alarm
butterfly.ws.addEventListener 'message', alarm
butterfly.ws.shell.addEventListener 'message', alarm
butterfly.body.classList.add 'alarm'
@@ -27,10 +78,17 @@ cancel = (ev) ->
document.addEventListener 'keydown', (e) ->
return true unless e.altKey and e.keyCode is 65
cond = null
if e.shiftKey
cond = prompt('Ring alarm when encountering the following text:
(can be a regexp)')
return unless cond
cond = new RegExp(cond)
if Notification and Notification.permission is 'default'
Notification.requestPermission ->
setAlarm(Notification.permission is 'granted')
setAlarm(Notification.permission is 'granted', cond)
else
setAlarm(Notification.permission is 'granted')
setAlarm(Notification.permission is 'granted', cond)
cancel(e)

View File

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

View File

@@ -0,0 +1,5 @@
addEventListener 'beforeunload', (e) ->
unless (butterfly.body.classList.contains('dead') or
location.href.indexOf('session') > -1)
e.returnValue = 'This terminal is active and not in session.
Are you sure you want to kill it?'

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

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

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

36
coffees/ext/popup.coffee Normal file
View File

@@ -0,0 +1,36 @@
class Popup
constructor: ->
@el = document.getElementById('popup')
@bound_click_maybe_close = @click_maybe_close.bind(@)
@bound_key_maybe_close = @key_maybe_close.bind(@)
open: (html) ->
@el.innerHTML = html
@el.classList.remove 'hidden'
addEventListener 'click', @bound_click_maybe_close
addEventListener 'keydown', @bound_key_maybe_close
close: ->
removeEventListener 'click', @bound_click_maybe_close
removeEventListener 'keydown', @bound_key_maybe_close
@el.classList.add 'hidden'
@el.innerHTML = ''
click_maybe_close: (e) ->
t = e.target
while t.parentElement
return true if Array.prototype.slice.call(@el.children).indexOf(t) > -1
t = t.parentElement
@close()
cancel e
key_maybe_close: (e) ->
return true unless e.keyCode is 27
@close()
cancel e
popup = new Popup()

View File

@@ -1,7 +1,7 @@
# *-* coding: utf-8 *-*
# This file is part of butterfly
#
# butterfly Copyright (C) 2014 Florian Mounier
# butterfly Copyright (C) 2015 Florian Mounier
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
@@ -87,12 +87,13 @@ class Selection
@go +1
go: (n) ->
index = butterfly.children.indexOf(@startLine) + n
return unless 0 <= index < butterfly.children.length
index = Array.prototype.indexOf.call(
butterfly.term.childNodes, @startLine) + n
return unless 0 <= index < butterfly.term.childElementCount
until butterfly.children[index].textContent.match /\S/
until butterfly.term.childNodes[index].textContent.match /\S/
index += n
return unless 0 <= index < butterfly.children.length
return unless 0 <= index < butterfly.term.childElementCount
@selectLine index
@@ -104,7 +105,7 @@ class Selection
@selection.addRange range
selectLine: (index) ->
line = butterfly.children[index]
line = butterfly.term.childNodes[index]
lineStart =
node: line.firstChild
offset: 0
@@ -148,7 +149,7 @@ class Selection
else
node = needle.node
text = node.textContent
text = node?.textContent
i = needle.offset
if backward
while node
@@ -156,7 +157,7 @@ class Selection
if text[--i].match til
return node: node, offset: i + 1
node = previousLeaf node
text = node.textContent
text = node?.textContent
i = text.length
else
while node
@@ -164,7 +165,7 @@ class Selection
if text[i++].match til
return node: node, offset: i - 1
node = nextLeaf node
text = node.textContent
text = node?.textContent
i = 0
return needle
@@ -204,8 +205,9 @@ document.addEventListener 'keydown', (e) ->
# Start selection mode with shift up
if not selection and e.ctrlKey and e.shiftKey and e.keyCode == 38
r = Math.max butterfly.term.childElementCount - butterfly.rows, 0
selection = new Selection()
selection.selectLine butterfly.y - 1
selection.selectLine r + butterfly.y - 1
selection.apply()
return cancel e
true

View File

@@ -0,0 +1,22 @@
document.addEventListener 'keydown', (e) ->
return true unless e.altKey and e.keyCode is 69
oReq = new XMLHttpRequest()
oReq.addEventListener 'load', ->
response = JSON.parse(@responseText)
out = '<div>'
out += '<h2>Session list</h2>'
if response.sessions.length is 0
out += "No current session for user #{response.user}"
else
out += '<ul>'
for session in response.sessions
out += "<li><a href=\"/session/#{session}\">#{session}</a></li>"
out += '</ul>'
out += '</div>'
popup.open out
oReq.open("GET", "/sessions/list.json")
oReq.send()
cancel e

80
coffees/ext/theme.coffee Normal file
View File

@@ -0,0 +1,80 @@
_set_theme_href = (href) ->
document.getElementById('style').setAttribute('href', href)
img = document.createElement('img')
img.onerror = ->
setTimeout (-> butterfly?.resize()), 250
img.src = href
_theme = localStorage?.getItem('theme')
_set_theme_href(_theme) if _theme
@set_theme = (theme) ->
_theme = theme
localStorage?.setItem('theme', theme)
_set_theme_href(theme) if theme
document.addEventListener 'keydown', (e) ->
return true unless e.altKey and e.keyCode is 83
if e.shiftKey
style = document.getElementById('style').getAttribute('href')
style = style.split('?')[0]
_set_theme_href style + '?' + (new Date().getTime())
return cancel(e)
oReq = new XMLHttpRequest()
oReq.addEventListener 'load', ->
response = JSON.parse(@responseText)
builtin_themes = response.builtin_themes
themes = response.themes
# if themes.length is 0
# alert("No themes found in #{response.dir}.\n
# Please install themes with butterfly.server.py --install-themes")
# return
inner = """
<form>
<h2>Pick a theme:</h2>
<select id="theme_list">
"""
option = (url, theme) ->
inner += '<option '
if _theme is url
inner += 'selected '
inner += "value=\"#{url}\">"
inner += theme
inner += '</option>'
option "/static/main.css", 'default'
if themes.length
inner += '<optgroup label="Local themes">'
for theme in themes
url = "/theme/#{theme}/style.css"
option url, theme
inner += '</optgroup>'
inner += '<optgroup label="Built-in themes">'
for theme in builtin_themes
url = "/theme/#{theme}/style.css"
option url, theme.slice('built-in-'.length)
inner += '</optgroup>'
inner += """
</select>
<label>You can create yours in #{response.dir}.</label>
</form>
"""
popup.open inner
theme_list = document.getElementById('theme_list')
theme_list.addEventListener 'change', -> set_theme theme_list.value
oReq.open("GET", "/themes/list.json")
oReq.send()
cancel e

View File

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

View File

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

File diff suppressed because it is too large Load Diff

21
docker/run.sh Normal file → Executable file
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.9.3",
"grunt": "^0.4.5",
"grunt-coffeelint": "0.0.13",
"grunt-contrib-coffee": "^0.13.0",
"grunt-contrib-cssmin": "^0.12.2",
"grunt-contrib-uglify": "^0.9.1",
"grunt-contrib-watch": "^0.6.1",
"grunt-sass": "^0.18.1"
"coffeelint": "^1.15.7",
"grunt": "^1.0.1",
"grunt-coffeelint": "0.0.15",
"grunt-contrib-coffee": "^1.0.0",
"grunt-contrib-cssmin": "^1.0.1",
"grunt-contrib-uglify": "^1.0.1",
"grunt-contrib-watch": "^1.0.0",
"grunt-sass": "^2.1.0"
}
}

1
scripts/b Symbolic link
View File

@@ -0,0 +1 @@
butterfly

43
scripts/butterfly Executable file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python
import os
import sys
import argparse
if (os.getenv('COLORTERM', '') != 'butterfly' and
len(sys.argv) == 1) or (
os.getenv('COLORTERM', '') == 'butterfly' and
len(sys.argv) > 1 and sys.argv[1] == 'run'):
os.execvp('butterfly.server.py', [
'butterfly', '--unsecure', '--port=0', '--one-shot'])
path = os.getenv('BUTTERFLY_PATH')
if not path:
try:
import butterfly
path = os.path.join(
os.path.dirname(butterfly.__file__), 'bin')
except Exception:
pass
os.putenv('BUTTERFLY_PATH', path)
if path is None:
print("Can't get butterfly path. Aborting.")
sys.exit(1)
parser = argparse.ArgumentParser(
add_help=False,
description='Butterfly launcher. Please specify a command')
parser.add_argument('-h', '--help', action="store_true",
help="show this help message and exit")
parser.add_argument(
'command',
nargs='?',
choices=[x[:-3] for x in os.listdir(path) if x.endswith('.py')])
args, _ = parser.parse_known_args()
if not args.command:
parser.print_help()
else:
file_ = os.path.join(path, '%s.py' % args.command)
sys.argv = sys.argv[1:]
exec(compile(open(file_).read(), file_, 'exec'))

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

1782
yarn.lock Normal file

File diff suppressed because it is too large Load Diff