Mercurial > hg > rlgwebd
changeset 0:bd412f63ce0d
Put this project under version control, finally.
author | John "Elwin" Edwards <elwin@sdf.org> |
---|---|
date | Sun, 06 May 2012 08:45:40 -0700 |
parents | |
children | 9bef0941c6dd |
files | bell.svg index-rlg.html index-sh.html ptyhelper.c quickrypt.c rlgterm.js shterm.js termemu.js tty.css webtty.js webttyd.js |
diffstat | 11 files changed, 3461 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bell.svg Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="16" + height="16" + id="svg2" + version="1.1" + inkscape:version="0.48.1 r9760" + sodipodi:docname="New document 1"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="11.2" + inkscape:cx="17.844259" + inkscape:cy="5.9285714" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + inkscape:window-width="873" + inkscape:window-height="546" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="0" /> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(0,-1036.3622)"> + <path + style="fill:none;stroke:#000000;stroke-linejoin:round;stroke-opacity:1" + d="m 4.2931485,1040.1868 c 0.8310158,-1.1611 2.3601954,-2 3.7880723,-2 1.4278768,0 2.9570562,0.8389 3.7880722,2 1.238121,1.73 0.404997,4.2608 1,6.3033 0.234101,0.8036 1,2.3033 1,2.3033 l -11.5761445,0 c 0,0 0.7658995,-1.4997 1,-2.3033 0.5950026,-2.0425 -0.2381208,-4.5733 1,-6.3033 z" + id="rect2985" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ssssccss" /> + <path + sodipodi:type="arc" + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + id="path3762" + sodipodi:cx="11.696428" + sodipodi:cy="14.660714" + sodipodi:rx="1.25" + sodipodi:ry="1.25" + d="m 12.946428,14.660714 a 1.25,1.25 0 1 1 -2.5,0 1.25,1.25 0 1 1 2.5,0 z" + transform="translate(-1.25,1035.4693)" /> + </g> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/index-rlg.html Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,103 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> +<html> +<head> +<title>WebTTY</title> +<script type="text/javascript" src="termemu.js"></script> +<script type="text/javascript" src="rlgterm.js"></script> +<link rel="stylesheet" type="text/css" href="tty.css"> +</head> +<body onload="setup()" onkeydown="sendkey(event)"> +<h1>WebTTY</h1> +<div id ="top"> + <span id="ttitle"></span> + <img src="/bell.png" alt="bell" id="bell"> +</div> +<div id="termwrap">TERM</div> +<div class="keyrow"> + <div class="key" onclick="vkey('`')">`</div> + <div class="key" onclick="vkey('1')">1</div> + <div class="key" onclick="vkey('2')">2</div> + <div class="key" onclick="vkey('3')">3</div> + <div class="key" onclick="vkey('4')">4</div> + <div class="key" onclick="vkey('5')">5</div> + <div class="key" onclick="vkey('6')">6</div> + <div class="key" onclick="vkey('7')">7</div> + <div class="key" onclick="vkey('8')">8</div> + <div class="key" onclick="vkey('9')">9</div> + <div class="key" onclick="vkey('0')">0</div> + <div class="key" onclick="vkey('-')">-</div> + <div class="key" onclick="vkey('=')">=</div> + <div class="key" onclick="vkey('\b')" style="width: 2.5em">Bksp</div> +</div> +<div class="keyrow"> + <div class="key" onclick="vkey('\t')" style="width: 2.5em">Tab</div> + <div class="key" onclick="vkey('q')">Q</div> + <div class="key" onclick="vkey('w')">W</div> + <div class="key" onclick="vkey('e')">E</div> + <div class="key" onclick="vkey('r')">R</div> + <div class="key" onclick="vkey('t')">T</div> + <div class="key" onclick="vkey('y')">Y</div> + <div class="key" onclick="vkey('u')">U</div> + <div class="key" onclick="vkey('i')">I</div> + <div class="key" onclick="vkey('o')">O</div> + <div class="key" onclick="vkey('p')">P</div> + <div class="key" onclick="vkey('[')">[</div> + <div class="key" onclick="vkey(']')">]</div> + <div class="key" onclick="vkey('\\')">\</div> +</div> +<div class="keyrow"> + <div class="key" onclick="togglectrl()" id="ctrlkey">Ctrl</div> + <div class="key" onclick="vkey('a')">A</div> + <div class="key" onclick="vkey('s')">S</div> + <div class="key" onclick="vkey('d')">D</div> + <div class="key" onclick="vkey('f')">F</div> + <div class="key" onclick="vkey('g')">G</div> + <div class="key" onclick="vkey('h')">H</div> + <div class="key" onclick="vkey('j')">J</div> + <div class="key" onclick="vkey('k')">K</div> + <div class="key" onclick="vkey('l')">L</div> + <div class="key" onclick="vkey(';')">;</div> + <div class="key" onclick="vkey('\'')">'</div> + <div class="key" onclick="vkey('\n')" style="width: 4em">Ret</div> +</div> +<div class="keyrow"> + <div class="key" onclick="toggleshift()" id="shiftkey">Shift</div> + <div class="key" onclick="vkey('z')">Z</div> + <div class="key" onclick="vkey('x')">X</div> + <div class="key" onclick="vkey('c')">C</div> + <div class="key" onclick="vkey('v')">V</div> + <div class="key" onclick="vkey('b')">B</div> + <div class="key" onclick="vkey('n')">N</div> + <div class="key" onclick="vkey('m')">M</div> + <div class="key" onclick="vkey(',')">,</div> + <div class="key" onclick="vkey('.')">.</div> + <div class="key" onclick="vkey('/')">/</div> +</div> +<div class="keyrow"> + <div class="key" onclick="vkey(' ')" id="spacebar"></div> +</div> +<div class="rbutton" onclick="stop()">Stop</div> +<div class="rbutton">Font: +<span onclick="textsize(false)">Smaller</span> +<span onclick="textsize(true)">Larger</span> +</div> +<div> +<form id="loginform" action="/login" method="post"> +<div> +Name: <input type="text" name="name" id="input_name"> +Password: <input type="password" name="pw" id="input_pw"> +Choose game: <select name="game" id="input_game"> + <option label="Rogue V3" value="rogue3">Rogue V3</option> + <option label="Rogue V4" value="rogue4">Rogue V4</option> + <option label="Rogue V5" value="rogue5">Rogue V5</option> + <option label="Super-Rogue" value="srogue">Super-Rogue</option> +</select> +<input type="submit" value="Play" onclick="formlogin(event)"> +</div> +</form> +</div> +<div id="debug"> +<p>Debugging Output</p> +</div> +</body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/index-sh.html Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,91 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> +<html> +<head> +<title>WebTTY</title> +<script type="text/javascript" src="termemu.js"></script> +<script type="text/javascript" src="shterm.js"></script> +<link rel="stylesheet" type="text/css" href="tty.css"> +</head> +<body onload="setup()" onkeydown="sendkey(event)"> +<h1>WebTTY</h1> +<div id ="top"> + <span id="ttitle"></span> + <img src="/bell.png" alt="bell" id="bell"> +</div> +<div id="termwrap"> +Browsing with Javascript turned off? I sympathize. I didn't want Javascript to be necessary for WebTerm. Unfortunately, the only other way to make it work was Java applets. +</div> +<div class="keyrow"> + <div class="key" onclick="vkey('`')">`</div> + <div class="key" onclick="vkey('1')">1</div> + <div class="key" onclick="vkey('2')">2</div> + <div class="key" onclick="vkey('3')">3</div> + <div class="key" onclick="vkey('4')">4</div> + <div class="key" onclick="vkey('5')">5</div> + <div class="key" onclick="vkey('6')">6</div> + <div class="key" onclick="vkey('7')">7</div> + <div class="key" onclick="vkey('8')">8</div> + <div class="key" onclick="vkey('9')">9</div> + <div class="key" onclick="vkey('0')">0</div> + <div class="key" onclick="vkey('-')">-</div> + <div class="key" onclick="vkey('=')">=</div> + <div class="key" onclick="vkey('\b')" style="width: 2.5em">Bksp</div> +</div> +<div class="keyrow"> + <div class="key" onclick="vkey('\t')" style="width: 2.5em">Tab</div> + <div class="key" onclick="vkey('q')">Q</div> + <div class="key" onclick="vkey('w')">W</div> + <div class="key" onclick="vkey('e')">E</div> + <div class="key" onclick="vkey('r')">R</div> + <div class="key" onclick="vkey('t')">T</div> + <div class="key" onclick="vkey('y')">Y</div> + <div class="key" onclick="vkey('u')">U</div> + <div class="key" onclick="vkey('i')">I</div> + <div class="key" onclick="vkey('o')">O</div> + <div class="key" onclick="vkey('p')">P</div> + <div class="key" onclick="vkey('[')">[</div> + <div class="key" onclick="vkey(']')">]</div> + <div class="key" onclick="vkey('\\')">\</div> +</div> +<div class="keyrow"> + <div class="key" onclick="togglectrl()" id="ctrlkey">Ctrl</div> + <div class="key" onclick="vkey('a')">A</div> + <div class="key" onclick="vkey('s')">S</div> + <div class="key" onclick="vkey('d')">D</div> + <div class="key" onclick="vkey('f')">F</div> + <div class="key" onclick="vkey('g')">G</div> + <div class="key" onclick="vkey('h')">H</div> + <div class="key" onclick="vkey('j')">J</div> + <div class="key" onclick="vkey('k')">K</div> + <div class="key" onclick="vkey('l')">L</div> + <div class="key" onclick="vkey(';')">;</div> + <div class="key" onclick="vkey('\'')">'</div> + <div class="key" onclick="vkey('\n')" style="width: 4em">Ret</div> +</div> +<div class="keyrow"> + <div class="key" onclick="toggleshift()" id="shiftkey">Shift</div> + <div class="key" onclick="vkey('z')">Z</div> + <div class="key" onclick="vkey('x')">X</div> + <div class="key" onclick="vkey('c')">C</div> + <div class="key" onclick="vkey('v')">V</div> + <div class="key" onclick="vkey('b')">B</div> + <div class="key" onclick="vkey('n')">N</div> + <div class="key" onclick="vkey('m')">M</div> + <div class="key" onclick="vkey(',')">,</div> + <div class="key" onclick="vkey('.')">.</div> + <div class="key" onclick="vkey('/')">/</div> +</div> +<div class="keyrow"> + <div class="key" onclick="vkey(' ')" id="spacebar"></div> +</div> +<div class="rbutton" onclick="login()">Log in</div> +<div class="rbutton" onclick="stop()">Stop</div> +<div class="rbutton">Font: +<span onclick="textsize(false)">Smaller</span> +<span onclick="textsize(true)">Larger</span> +</div> +<div id="debug"> +<p>Debugging Output</p> +</div> +</body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ptyhelper.c Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,149 @@ +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <signal.h> +#include <pty.h> +#include <utmp.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <sys/select.h> +#include <termios.h> + +int got_sighup = 0; + +void handle_HUP(int signum) { + if (signum == SIGHUP) + got_sighup = 1; + return; +} + +int main(int argc, char *argv[]) { + + int ptymaster, ptyslave; /* File descriptors */ + int child; + int status, selstatus; + int w = 80, h = 24, t; + struct sigaction sighup_act; + fd_set readset; + struct timeval select_time; + char buf[4096]; + int nread; + char *penv, *ptmp; +#if 0 + struct termios ptysettings; +#endif + struct winsize ptysize; + + if (argc == 1) { + fprintf(stderr, "No command given.\n"); + exit(1); + } + + /* Set up the signal handler. */ + sighup_act.sa_handler = &handle_HUP; + sighup_act.sa_flags = SA_RESTART; + sigaction(SIGHUP, &sighup_act, NULL); + + /* Check the environment for configuration options. */ + penv = getenv("PTYHELPER"); + if (penv != NULL) { + t = strtol(penv, &ptmp, 10); + if (t > 0 && t < 256) + h = t; + if (*ptmp != '\0') { + penv = ptmp + 1; + t = strtol(penv, &ptmp, 10); + if (t > 0 && t < 256) + w = t; + } + } + /* Set up the size. */ + ptysize.ws_row = h; + ptysize.ws_col = w; + + /* Open a pty */ + if (openpty(&ptymaster, &ptyslave, NULL, NULL, &ptysize)) { + return 1; + } +#if 0 + /* Put it into raw mode. */ + tcgetattr(ptyslave, &ptysettings); + cfmakeraw(&ptysettings); + tcsetattr(ptyslave, TCSANOW, &ptysettings); +#endif + + /* Start the child */ + /* forkpty() might be more convenient. */ + if (!(child = fork())) { + /* Child process */ + login_tty(ptyslave); + close(ptymaster); + execvp(argv[1], argv + 1); + perror("execvp() failed"); + return 1; + } + close(ptyslave); + + while (1) { + /* Now do a select() over stdin and ptymaster, and write anything that + * appears to ptymaster and stdout respectively. */ + FD_ZERO(&readset); + FD_SET(0, &readset); + FD_SET(ptymaster, &readset); + select_time.tv_sec = 1; + select_time.tv_usec = 0; + selstatus = select(ptymaster + 1, &readset, NULL, NULL, &select_time); + if (selstatus > 0) { + /* TODO make sure it all gets written if a signal interrupts write(). */ + if (FD_ISSET(0, &readset)) { + nread = read(0, buf, 4096); + if (nread > 0) { + write(ptymaster, buf, nread); + } + } + if (FD_ISSET(ptymaster, &readset)) { + nread = read(ptymaster, buf, 4096); + if (nread > 0) { + write(1, buf, nread); + } + } + } + + /* Periodically check to see if we're done. */ + /* TODO: catch SIGCHLD and only wait() if it is delivered. */ + if (waitpid(child, &status, WNOHANG)) { + break; + } + + /* If node sighup's us, pass it along. */ + if (got_sighup) { + kill(child, SIGHUP); + } + } + + /* Get any leftover output and clean up. */ + /* FIXME looping over select() is pointless if there's only one fd that + * nothing's writing to. Just loop over read() until it's empty. */ + while (1) { + FD_ZERO(&readset); + FD_SET(ptymaster, &readset); + select_time.tv_sec = 0; + select_time.tv_usec = 0; + if (select(ptymaster + 1, &readset, NULL, NULL, &select_time) > 0) { + nread = read(ptymaster, buf, 4096); + if (nread > 0) { + write(1, buf, nread); + } + else + break; + } + else + break; + } + close(ptymaster); + + /* Return the child's exit status. */ + if (WIFEXITED(status)) + return WEXITSTATUS(status); + return 0; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/quickrypt.c Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,28 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <crypt.h> + +int main(int argc, char *argv[]) { + char clear[32], enc[120], *ptr; + fgets(&clear, 32, stdin); + if (!(ptr = strchr(&clear, '\n'))) + return 1; + else + *ptr = '\0'; + fgets(&enc, 120, stdin); + if (!(ptr = strchr(&enc, '\n'))) + return 1; + else + *ptr = '\0'; + ptr = crypt(clear, enc); + if (!strcmp(argv[argc - 1], "-s")) { + /* Option -s for "show": output the encrypted version. */ + printf("%s\n", ptr); + return 0; + } + /* Otherwise this is a check. */ + else if (!strcmp(ptr, enc)) + return 0; + return 1; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rlgterm.js Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,485 @@ +/* rlgterm.js: Roguelike Gallery's driver for termemu.js */ + +// A state machine that keeps track of polling the server. +var ajaxstate = { + state: 0, + timerID: null, + clear: function () { + if (this.timerID != null) { + window.clearTimeout(this.timerID); + this.timerID = null; + } + }, + set: function (ms) { + this.clear(); + this.timerID = window.setTimeout(getData, ms); + }, + gotdata: function () { + this.set(100); + this.state = 0; + }, + gotnothing: function () { + if (this.state == 0) { + this.set(100); + this.state = 1; + } + else if (this.state == 1) { + this.set(300); + this.state = 2; + } + else if (this.state == 2) { + this.set(1000); + this.state = 3; + } + else { + this.set(5000); + this.state = 3; + } + }, + posted: function () { + this.set(100); + this.state = 0; + } +}; + +function writeData(hexstr) { + var codenum; + var codes = []; + var nc; + var u8wait = 0; /* Stores bits from previous bytes of multibyte sequences. */ + var expect = 0; /* The number of 10------ bytes expected. */ + /* UTF-8 translation. */ + for (var i = 0; i < hexstr.length; i += 2) { + nc = Number("0x" + hexstr.substr(i, 2)); + if (nc < 0x7F) { + /* 0------- */ + codes.push(nc); + /* Any incomplete sequence will be discarded. */ + u8wait = 0; + expect = 0; + } + else if (nc < 0xC0) { + /* 10------ : part of a multibyte sequence */ + if (expect > 0) { + u8wait <<= 6; + u8wait += (nc & 0x3F); + expect--; + if (expect == 0) { + codes.push(u8wait); + u8wait = 0; + } + } + else { + /* Assume an initial byte was missed. */ + u8wait = 0; + } + } + /* These will all discard any incomplete sequence. */ + else if (nc < 0xE0) { + /* 110----- : introduces 2-byte sequence */ + u8wait = (nc & 0x1F); + expect = 1; + } + else if (nc < 0xF0) { + /* 1110---- : introduces 3-byte sequence */ + u8wait = (nc & 0x0F); + expect = 2; + } + else if (nc < 0xF8) { + /* 11110--- : introduces 4-byte sequence */ + u8wait = (nc & 0x07); + expect = 3; + } + else if (nc < 0xFC) { + /* 111110-- : introduces 5-byte sequence */ + u8wait = (nc & 0x03); + expect = 4; + } + else if (nc < 0xFE) { + /* 1111110- : introduces 6-byte sequence */ + u8wait = (nc & 0x01); + expect = 5; + } + else { + /* 1111111- : should never appear */ + u8wait = 0; + expect = 0; + } + /* Supporting all 31 bits is probably overkill... */ + } + termemu.write(codes); + return; +} + +/* Processes a message from the server, returning true or false if it was a + * data message with or without data, null if not data. */ +function processMsg(msg) { + var msglines = msg.split("\n"); + var havedata = null; + if (!msglines[0]) + return null; + if (msglines[0].charAt(0) == 'd') { + if (msglines[1]){ + writeData(msglines[1]); + havedata = true; + } + else { + havedata = false; + } + } + else if (msglines[0] == "E1") { + logout(); + } + else if (msglines[0].charAt(0) == "T") { + setTitle(msglines[1]); + } + else if (msglines[0] == "q1") { + logout(); + } + else { + debug(1, "Unrecognized server message " + msglines[0]); + } + return havedata; +} + +function getData() { + if (termemu.sessid == null) + return; + var datareq = new XMLHttpRequest(); + datareq.onreadystatechange = function () { + if (datareq.readyState == 4 && datareq.status == 200) { + var wasdata = processMsg(datareq.responseText); + if (wasdata != null) { + if (wasdata) + ajaxstate.gotdata(); + else + ajaxstate.gotnothing(); + } + return; + } + }; + datareq.open('POST', '/feed', true); + datareq.send("id=" + termemu.sessid); + return; +} + +function postResponseHandler() { + if (this.readyState == 4 && this.status == 200) { + // We might want to do something with wasdata someday. + var wasdata = processMsg(this.responseText); + ajaxstate.posted(); + return; + } +} + +function sendback(str) { + /* For responding to terminal queries. */ + var datareq = new XMLHttpRequest(); + datareq.onreadystatechange = postResponseHandler; + datareq.open('POST', '/feed', true); + datareq.send("id=" + termemu.sessid + "&keys=" + str); + return; +} + +/* ASCII values of keys 0-9. */ +var numShifts = [41, 33, 64, 35, 36, 37, 94, 38, 42, 40]; + +var keyHexCodes = { + init: function () { + this[KeyboardEvent.DOM_VK_RETURN] = ["0d", "0d"]; + this[KeyboardEvent.DOM_VK_SPACE] = ["20", "20"]; + this[KeyboardEvent.DOM_VK_TAB] = ["09", "09"]; + this[KeyboardEvent.DOM_VK_BACK_QUOTE] = ["60", "7e"]; + this[KeyboardEvent.DOM_VK_OPEN_BRACKET] = ["5b", "7b"]; + this[KeyboardEvent.DOM_VK_CLOSE_BRACKET] = ["5d", "7d"]; + this[KeyboardEvent.DOM_VK_BACK_SLASH] = ["5c", "7c"]; + this[KeyboardEvent.DOM_VK_SEMICOLON] = ["3b", "3a"]; + this[KeyboardEvent.DOM_VK_QUOTE] = ["27", "22"]; + this[KeyboardEvent.DOM_VK_COMMA] = ["2c", "3c"]; + this[KeyboardEvent.DOM_VK_PERIOD] = ["2e", "3e"]; + this[KeyboardEvent.DOM_VK_SLASH] = ["2f", "3f"]; + this[KeyboardEvent.DOM_VK_EQUALS] = ["3d", "2b"]; + this[KeyboardEvent.DOM_VK_SUBTRACT] = ["2d", "5f"]; + this[KeyboardEvent.DOM_VK_BACK_SPACE] = ["08", "08"]; + this[KeyboardEvent.DOM_VK_ESCAPE] = ["1b", "1b"]; + /* Multi-char control sequences! Neat! */ + this[KeyboardEvent.DOM_VK_PAGE_UP] = ["1b5b357e", "1b5b357e"]; + this[KeyboardEvent.DOM_VK_PAGE_DOWN] = ["1b5b367e", "1b5b367e"]; + this.appCursor(false); + this.appKeypad(false); + }, + appCursor: function (on) { + if (on) { + this[KeyboardEvent.DOM_VK_LEFT] = ["1b4f44", "1b4f44"]; + this[KeyboardEvent.DOM_VK_RIGHT] = ["1b4f43", "1b4f43"]; + this[KeyboardEvent.DOM_VK_UP] = ["1b4f41", "1b4f41"]; + this[KeyboardEvent.DOM_VK_DOWN] = ["1b4f42", "1b4f42"]; + this[KeyboardEvent.DOM_VK_END] = ["1b4f46", "1b4f46"]; + this[KeyboardEvent.DOM_VK_HOME] = ["1b4f48", "1b4f48"]; + } + else { + this[KeyboardEvent.DOM_VK_LEFT] = ["1b5b44", "1b5b44"]; + this[KeyboardEvent.DOM_VK_RIGHT] = ["1b5b43", "1b5b43"]; + this[KeyboardEvent.DOM_VK_UP] = ["1b5b41", "1b5b41"]; + this[KeyboardEvent.DOM_VK_DOWN] = ["1b5b42", "1b5b42"]; + this[KeyboardEvent.DOM_VK_END] = ["1b5b46", "1b5b46"]; + this[KeyboardEvent.DOM_VK_HOME] = ["1b5b48", "1b5b48"]; + } + }, + appKeypad: function (on) { + /* In theory, these should produce either numerals or the k[a-c][1-3] + * sequences. Since we can't count on the terminfo description actually + * containing those sequences, pretend they're just arrow keys etc. + */ + this[KeyboardEvent.DOM_VK_NUMPAD1] = ["1b4f46", "1b4f46"]; + this[KeyboardEvent.DOM_VK_NUMPAD2] = ["1b4f42", "1b4f42"]; + this[KeyboardEvent.DOM_VK_NUMPAD3] = ["1b5b367e", "1b5b367e"]; + this[KeyboardEvent.DOM_VK_NUMPAD4] = ["1b4f44", "1b4f44"]; + this[KeyboardEvent.DOM_VK_NUMPAD5] = ["1b5b45", "1b5b45"]; + this[KeyboardEvent.DOM_VK_NUMPAD6] = ["1b4f43", "1b4f43"]; + this[KeyboardEvent.DOM_VK_NUMPAD7] = ["1b4f48", "1b4f48"]; + this[KeyboardEvent.DOM_VK_NUMPAD8] = ["1b4f41", "1b4f41"]; + this[KeyboardEvent.DOM_VK_NUMPAD9] = ["1b5b357e", "1b5b357e"]; + return; + } +}; + +function sendkey(ev) { + if (termemu.sessid == null) + return; + var keynum = ev.keyCode; + var code; + if (keynum >= ev.DOM_VK_A && keynum <= ev.DOM_VK_Z) { + /* Letters. This assumes the codes are 65-90. */ + if (ev.ctrlKey) + keynum -= 64; + else if (!ev.shiftKey) + keynum += 32; + code = keynum.toString(16); + if (code.length < 2) + code = "0" + code; + } + else if (keynum >= ev.DOM_VK_0 && keynum <= ev.DOM_VK_9) { + /* The number row, NOT the numpad. */ + if (ev.shiftKey) { + code = numShifts[keynum - 48].toString(16); + } + else { + code = keynum.toString(16); + } + } + else if (keynum in keyHexCodes) { + if (ev.shiftKey) + code = keyHexCodes[keynum][1]; + else + code = keyHexCodes[keynum][0]; + } + else if (keynum == ev.DOM_VK_SHIFT || keynum == ev.DOM_VK_CONTROL || + keynum == ev.DOM_VK_ALT || keynum == ev.DOM_VK_CAPS_LOCK) { + return; + } + else { + debug(1, "Ignoring keycode " + keynum); + return; + } + if (termemu.sessid != null) + ev.preventDefault(); + var datareq = new XMLHttpRequest(); + datareq.onreadystatechange = postResponseHandler; + datareq.open('POST', '/feed', true); + datareq.send("id=" + termemu.sessid + "&keys=" + code); + return; +} + +var charshifts = { '-': "5f", '=': "2b", '[': "7b", ']': "7d", '\\': "7c", + ';': "3a", '\'': "22", ',': "3c", '.': "3e", '/': "3f", '`': "7e" +} + +function vkey(c) { + if (termemu.sessid == null) + return; + var keystr; + if (c.match(/^[a-z]$/)) { + if (termemu.ctrlp()) { + var n = c.charCodeAt(0) - 96; + keystr = n.toString(16); + if (keystr.length < 2) + keystr = "0" + keystr; + } + else if (termemu.shiftp()) + keystr = c.toUpperCase().charCodeAt(0).toString(16); + else + keystr = c.charCodeAt(0).toString(16); + } + else if (c.match(/^[0-9]$/)) { + if (termemu.shiftp()) + keystr = numShifts[c.charCodeAt(0) - 48].toString(16); + else + keystr = c.charCodeAt(0).toString(16); + } + else if (c == '\n') + keystr = "0a"; + else if (c == '\t') + keystr = "09"; + else if (c == '\b') + keystr = "08"; + else if (c == ' ') + keystr = "20"; + else if (c in charshifts) { + if (termemu.shiftp()) + keystr = charshifts[c]; + else + keystr = c.charCodeAt(0).toString(16); + } + else + return; + //writeData("Sending " + keystr); + var datareq = new XMLHttpRequest(); + datareq.onreadystatechange = postResponseHandler; + datareq.open('POST', '/feed', true); + datareq.send("id=" + termemu.sessid + "&keys=" + keystr); + return; +} + +function setup() { + keyHexCodes.init(); + termemu.init("termwrap"); + setTitle("Not connected."); + return; +} + +function toggleshift() { + termemu.toggleshift(); + keydiv = document.getElementById("shiftkey"); + if (termemu.shiftp()) + keydiv.className = "keysel"; + else + keydiv.className = "key"; + return; +} + +function togglectrl() { + termemu.togglectrl(); + keydiv = document.getElementById("ctrlkey"); + if (termemu.ctrlp()) + keydiv.className = "keysel"; + else + keydiv.className = "key"; + return; +} + +function formlogin(ev) { + ev.preventDefault(); + if (termemu.sessid != null) + return; + var formname = document.getElementById("input_name").value; + var formpass = document.getElementById("input_pw").value; + var formgame = document.getElementById("input_game").value; + var formdata = "game=" + encodeURIComponent(formgame) + "&name=" + encodeURIComponent(formname) + "&pw=" + encodeURIComponent(formpass); + var req = new XMLHttpRequest(); + req.onreadystatechange = function () { + if (req.readyState == 4 && req.status == 200) { + var datalines = req.responseText.split("\n"); + if (datalines[0] == 'l1') { + /* Success */ + termemu.sessid = datalines[1]; + setTitle("Playing as " + formname); + debug(1, "Logged in with id " + termemu.sessid); + document.getElementById("loginform").style.display = "none"; + getData(); + } + else { + debug(1, "Could not start game: " + datalines[1]); + document.getElementById("input_name").value = ""; + document.getElementById("input_pw").value = ""; + } + } + }; + req.open('POST', '/login', true); + req.send(formdata); + return; +} + +function logout() { + if (termemu.sessid == null) + return; + termemu.sessid = null; + setTitle("Game over."); + document.getElementById("loginform").style.display = "block"; + return; +} + +function stop() { + var req = new XMLHttpRequest(); + req.onreadystatechange = function () { + if (req.readyState == 4 && req.status == 200) { + processMsg(req.responseText); + return; + } + }; + req.open('POST', '/feed', true); + req.send("id=" + termemu.sessid + "&quit=quit"); + return; +} + +function debug(level, msg) { + if (level < debugSuppress) + return; + var msgdiv = document.createElement("div"); + var msgtext = document.createTextNode(msg); + msgdiv.appendChild(msgtext); + document.getElementById("debug").appendChild(msgdiv); + return; +} + +function textsize(larger) { + var cssSize = termemu.view.style.fontSize; + if (!cssSize) { + return; + } + var match = cssSize.match(/\d*/); + if (!match) { + return; + } + var csize = Number(match[0]); + var nsize; + if (larger) { + if (csize >= 48) + nsize = 48; + else if (csize >= 20) + nsize = csize + 4; + else if (csize >= 12) + nsize = csize + 2; + else if (csize >= 8) + nsize = csize + 1; + else + nsize = 8; + } + else { + if (csize <= 8) + nsize = 8; + else if (csize <= 12) + nsize = csize - 1; + else if (csize <= 20) + nsize = csize - 2; + else if (csize <= 48) + nsize = csize - 4; + else + nsize = 48; + } + document.getElementById("term").style.fontSize = nsize.toString() + "px"; + termemu.resize(); + debug(1, "Changing font size to " + nsize.toString()); + return; +} + +function bell(on) { + var imgnode = document.getElementById("bell"); + if (on) { + imgnode.style.visibility = "visible"; + window.setTimeout(bell, 1500, false); + } + else + imgnode.style.visibility = "hidden"; + return; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/shterm.js Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,478 @@ +// A state machine that keeps track of polling the server. +var ajaxstate = { + state: 0, + timerID: null, + clear: function () { + if (this.timerID != null) { + window.clearTimeout(this.timerID); + this.timerID = null; + } + }, + set: function (ms) { + this.clear(); + this.timerID = window.setTimeout(getData, ms); + }, + gotdata: function () { + this.set(100); + this.state = 0; + }, + gotnothing: function () { + if (this.state == 0) { + this.set(100); + this.state = 1; + } + else if (this.state == 1) { + this.set(300); + this.state = 2; + } + else if (this.state == 2) { + this.set(1000); + this.state = 3; + } + else { + this.set(5000); + this.state = 3; + } + }, + posted: function () { + this.set(100); + this.state = 0; + } +}; + +function writeData(hexstr) { + var codenum; + var codes = []; + var nc; + var u8wait = 0; /* Stores bits from previous bytes of multibyte sequences. */ + var expect = 0; /* The number of 10------ bytes expected. */ + /* UTF-8 translation. */ + for (var i = 0; i < hexstr.length; i += 2) { + nc = Number("0x" + hexstr.substr(i, 2)); + if (nc < 0x7F) { + /* 0------- */ + codes.push(nc); + /* Any incomplete sequence will be discarded. */ + u8wait = 0; + expect = 0; + } + else if (nc < 0xC0) { + /* 10------ : part of a multibyte sequence */ + if (expect > 0) { + u8wait <<= 6; + u8wait += (nc & 0x3F); + expect--; + if (expect == 0) { + codes.push(u8wait); + u8wait = 0; + } + } + else { + /* Assume an initial byte was missed. */ + u8wait = 0; + } + } + /* These will all discard any incomplete sequence. */ + else if (nc < 0xE0) { + /* 110----- : introduces 2-byte sequence */ + u8wait = (nc & 0x1F); + expect = 1; + } + else if (nc < 0xF0) { + /* 1110---- : introduces 3-byte sequence */ + u8wait = (nc & 0x0F); + expect = 2; + } + else if (nc < 0xF8) { + /* 11110--- : introduces 4-byte sequence */ + u8wait = (nc & 0x07); + expect = 3; + } + else if (nc < 0xFC) { + /* 111110-- : introduces 5-byte sequence */ + u8wait = (nc & 0x03); + expect = 4; + } + else if (nc < 0xFE) { + /* 1111110- : introduces 6-byte sequence */ + u8wait = (nc & 0x01); + expect = 5; + } + else { + /* 1111111- : should never appear */ + u8wait = 0; + expect = 0; + } + /* Supporting all 31 bits is probably overkill... */ + } + termemu.write(codes); + return; +} + +function getData() { + if (!termemu.alive) + return; + var datareq = new XMLHttpRequest(); + datareq.onreadystatechange = function () { + if (datareq.readyState == 4 && datareq.status == 200) { + var datalines = datareq.responseText.split("\n"); + if (!datalines[0]) { + return; + } + else if (datalines[0] == "E1") { + termemu.alive = false; + return; + } + else if (datalines[0].charAt(0) != 'd') { + return; + } + if (datalines[1]) { + writeData(datalines[1]); + ajaxstate.gotdata(); + } + else { + ajaxstate.gotnothing(); + } + return; + } + }; + datareq.open('GET', '/feed', true); + datareq.send(null); + return; +} + +function postResponseHandler() { + if (this.readyState == 4 && this.status == 200) { + var datalines = this.responseText.split("\n"); + if (!datalines[0]) + return; + else if (datalines[0] == "E1") { + termemu.alive = false; + return; + } + else if (datalines[0].charAt(0) != "d") + return; + /* It is a data message */ + if (datalines[1]) { + writeData(datalines[1]); + } + ajaxstate.posted(); + return; + } +} + +function sendback(str) { + /* For responding to terminal queries. */ + var datareq = new XMLHttpRequest(); + datareq.onreadystatechange = postResponseHandler; + datareq.open('POST', '/feed', true); + datareq.send("keys=" + str); + return; +} + +/* ASCII values of keys 0-9. */ +var numShifts = [41, 33, 64, 35, 36, 37, 94, 38, 42, 40]; + +var keyHexCodes = { + init: function () { + this[KeyboardEvent.DOM_VK_RETURN] = ["0d", "0d"]; + this[KeyboardEvent.DOM_VK_SPACE] = ["20", "20"]; + this[KeyboardEvent.DOM_VK_TAB] = ["09", "09"]; + this[KeyboardEvent.DOM_VK_BACK_QUOTE] = ["60", "7e"]; + this[KeyboardEvent.DOM_VK_OPEN_BRACKET] = ["5b", "7b"]; + this[KeyboardEvent.DOM_VK_CLOSE_BRACKET] = ["5d", "7d"]; + this[KeyboardEvent.DOM_VK_BACK_SLASH] = ["5c", "7c"]; + this[KeyboardEvent.DOM_VK_SEMICOLON] = ["3b", "3a"]; + this[KeyboardEvent.DOM_VK_QUOTE] = ["27", "22"]; + this[KeyboardEvent.DOM_VK_COMMA] = ["2c", "3c"]; + this[KeyboardEvent.DOM_VK_PERIOD] = ["2e", "3e"]; + this[KeyboardEvent.DOM_VK_SLASH] = ["2f", "3f"]; + this[KeyboardEvent.DOM_VK_EQUALS] = ["3d", "2b"]; + this[KeyboardEvent.DOM_VK_SUBTRACT] = ["2d", "5f"]; + this[KeyboardEvent.DOM_VK_BACK_SPACE] = ["08", "08"]; + this[KeyboardEvent.DOM_VK_ESCAPE] = ["1b", "1b"]; + this[KeyboardEvent.DOM_VK_PAGE_UP] = ["1b5b357e", "1b5b357e"]; + this[KeyboardEvent.DOM_VK_PAGE_DOWN] = ["1b5b367e", "1b5b367e"]; + this.appCursor(false); + this.appKeypad(false); + }, + /* Multi-char control sequences! Neat! */ + appCursor: function (on) { + /* Aren't special keys vile? */ + if (on) { + this[KeyboardEvent.DOM_VK_LEFT] = ["1b4f44", "1b4f44"]; + this[KeyboardEvent.DOM_VK_RIGHT] = ["1b4f43", "1b4f43"]; + this[KeyboardEvent.DOM_VK_UP] = ["1b4f41", "1b4f41"]; + this[KeyboardEvent.DOM_VK_DOWN] = ["1b4f42", "1b4f42"]; + this[KeyboardEvent.DOM_VK_END] = ["1b4f46", "1b4f46"]; + this[KeyboardEvent.DOM_VK_HOME] = ["1b4f48", "1b4f48"]; + } + else { + this[KeyboardEvent.DOM_VK_LEFT] = ["1b5b44", "1b5b44"]; + this[KeyboardEvent.DOM_VK_RIGHT] = ["1b5b43", "1b5b43"]; + this[KeyboardEvent.DOM_VK_UP] = ["1b5b41", "1b5b41"]; + this[KeyboardEvent.DOM_VK_DOWN] = ["1b5b42", "1b5b42"]; + this[KeyboardEvent.DOM_VK_END] = ["1b5b46", "1b5b46"]; + this[KeyboardEvent.DOM_VK_HOME] = ["1b5b48", "1b5b48"]; + } + }, + appKeypad: function (on) { + /* In theory, these should produce either numerals or the k[a-c][1-3] + * sequences. Since we can't count on the terminfo description actually + * containing those sequences, pretend they're just arrow keys etc. + */ + this[KeyboardEvent.DOM_VK_NUMPAD1] = ["1b4f46", "1b4f46"]; + this[KeyboardEvent.DOM_VK_NUMPAD2] = ["1b4f42", "1b4f42"]; + this[KeyboardEvent.DOM_VK_NUMPAD3] = ["1b5b367e", "1b5b367e"]; + this[KeyboardEvent.DOM_VK_NUMPAD4] = ["1b4f44", "1b4f44"]; + this[KeyboardEvent.DOM_VK_NUMPAD5] = ["1b5b45", "1b5b45"]; + this[KeyboardEvent.DOM_VK_NUMPAD6] = ["1b4f43", "1b4f43"]; + this[KeyboardEvent.DOM_VK_NUMPAD7] = ["1b4f48", "1b4f48"]; + this[KeyboardEvent.DOM_VK_NUMPAD8] = ["1b4f41", "1b4f41"]; + this[KeyboardEvent.DOM_VK_NUMPAD9] = ["1b5b357e", "1b5b357e"]; + return; + } +}; + +function sendkey(ev) { + var keynum = ev.keyCode; + var code; + if (keynum >= ev.DOM_VK_A && keynum <= ev.DOM_VK_Z) { + /* Letters. This assumes the codes are 65-90. */ + if (ev.ctrlKey) + keynum -= 64; + else if (!ev.shiftKey) + keynum += 32; + code = keynum.toString(16); + if (code.length < 2) + code = "0" + code; + } + else if (keynum >= ev.DOM_VK_0 && keynum <= ev.DOM_VK_9) { + /* The number row. */ + if (ev.shiftKey) { + code = numShifts[keynum - 48].toString(16); + } + else { + code = keynum.toString(16); + } + } + else if (keynum in keyHexCodes) { + if (ev.shiftKey) + code = keyHexCodes[keynum][1]; + else + code = keyHexCodes[keynum][0]; + } + else if (keynum == ev.DOM_VK_SHIFT || keynum == ev.DOM_VK_CONTROL || + keynum == ev.DOM_VK_ALT || keynum == ev.DOM_VK_CAPS_LOCK) { + return; + } + else { + debug(1, "Ignoring keycode " + keynum); + return; + } + if (termemu.alive) + ev.preventDefault(); + var datareq = new XMLHttpRequest(); + datareq.onreadystatechange = postResponseHandler; + datareq.open('POST', '/feed', true); + datareq.send("keys=" + code); + //dkey(code); + return; +} + +var charshifts = { '-': "5f", '=': "2b", '[': "7b", ']': "7d", '\\': "7c", + ';': "3a", '\'': "22", ',': "3c", '.': "3e", '/': "3f", '`': "7e" +} + +function vkey(c) { + var keystr; + if (c.match(/^[a-z]$/)) { + if (termemu.ctrlp()) { + var n = c.charCodeAt(0) - 96; + keystr = n.toString(16); + if (keystr.length < 2) + keystr = "0" + keystr; + } + else if (termemu.shiftp()) + keystr = c.toUpperCase().charCodeAt(0).toString(16); + else + keystr = c.charCodeAt(0).toString(16); + } + else if (c.match(/^[0-9]$/)) { + if (termemu.shiftp()) + keystr = numShifts[c.charCodeAt(0) - 48].toString(16); + else + keystr = c.charCodeAt(0).toString(16); + } + else if (c == '\n') + keystr = "0a"; + else if (c == '\t') + keystr = "09"; + else if (c == '\b') + keystr = "08"; + else if (c == ' ') + keystr = "20"; + else if (c in charshifts) { + if (termemu.shiftp()) + keystr = charshifts[c]; + else + keystr = c.charCodeAt(0).toString(16); + } + else + return; + //writeData("Sending " + keystr); + var datareq = new XMLHttpRequest(); + datareq.onreadystatechange = postResponseHandler; + datareq.open('POST', '/feed', true); + datareq.send("keys=" + keystr); + return; +} + +function setup() { + keyHexCodes.init(); + termemu.init("termwrap"); + setTitle("Not connected."); + return; +} + +function toggleshift() { + termemu.toggleshift(); + keydiv = document.getElementById("shiftkey"); + if (termemu.shiftp()) + keydiv.className = "keysel"; + else + keydiv.className = "key"; + return; +} + +function togglectrl() { + termemu.togglectrl(); + keydiv = document.getElementById("ctrlkey"); + if (termemu.ctrlp()) + keydiv.className = "keysel"; + else + keydiv.className = "key"; + return; +} + +function login() { + if (termemu.alive) + return; + var req = new XMLHttpRequest(); + req.onreadystatechange = function () { + if (req.readyState == 4 && req.status == 200) { + var datalines = req.responseText.split("\n"); + if (datalines[0] == 'l1') { + /* Success */ + termemu.alive = true; + setTitle("Logged in"); + debug(1, "Logged in with id " + datalines[1]); + getData(); + return; + } + return; + } + }; + req.open('POST', '/login', true); + req.send("login=login"); + return; +} + +function stop() { + var req = new XMLHttpRequest(); + req.onreadystatechange = function () { + if (req.readyState == 4 && req.status == 200) { + var datalines = req.responseText.split("\n"); + /* Figure out whether or not it worked. */ + termemu.alive = false; + return; + } + }; + req.open('POST', '/feed', true); + req.send("quit=quit"); + return; +} + +function setTitle(tstr) { + var titlespan = document.getElementById("ttitle"); + var tnode = document.createTextNode(tstr); + if (titlespan.childNodes.length == 0) + titlespan.appendChild(tnode); + else + titlespan.replaceChild(tnode, titlespan.childNodes[0]); + return; +} + +function debug(level, msg) { + if (level < debugSuppress) + return; + var msgdiv = document.createElement("div"); + var msgtext = document.createTextNode(msg); + msgdiv.appendChild(msgtext); + document.getElementById("debug").appendChild(msgdiv); + return; +} + +/* This should be a termemu method. */ +function textsize(larger) { + var cssSize = document.getElementById("term").style.fontSize; + if (!cssSize) + return; + var match = cssSize.match(/\d*/); + if (!match) + return; + var csize = Number(match[0]); + var nsize; + if (larger) { + if (csize >= 48) + nsize = 48; + else if (csize >= 20) + nsize = csize + 4; + else if (csize >= 12) + nsize = csize + 2; + else if (csize >= 8) + nsize = csize + 1; + else + nsize = 8; + } + else { + if (csize <= 8) + nsize = 8; + else if (csize <= 12) + nsize = csize - 1; + else if (csize <= 20) + nsize = csize - 2; + else if (csize <= 48) + nsize = csize - 4; + else + nsize = 48; + } + document.getElementById("term").style.fontSize = nsize.toString() + "px"; + termemu.resize(); + debug(1, "Changing font size to " + nsize.toString()); + return; +} + +function bell(on) { + var imgnode = document.getElementById("bell"); + if (on) { + imgnode.style.visibility = "visible"; + window.setTimeout(bell, 1500, false); + } + else + imgnode.style.visibility = "hidden"; + return; +} + +function dkey(codestr) { + var dstr = "Keystring: "; + for (var i = 0; i < codestr.length; i += 2) { + code = Number("0x" + codestr.substr(i, 2)); + if (code < 32 || (code >= 127 && code < 160)) + dstr += "\\x" + code.toString(16); + else + dstr += String.fromCharCode(code); + } + debug(1, dstr); + return; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/termemu.js Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,1105 @@ +/* termemu.js: a mostly xterm-compatible terminal emulator for a webpage */ +/* SELF-HOSTING 2011-09-23 */ + +// How detailed the debugging should be. +var debugSuppress = 1; +// Some char values. +var csiPre = [63, 62, 33]; +var csiPost = [36, 34, 39, 32]; +function csiFinal(code) { + /* @A-Z */ + if (code >= 64 && code <= 90) + return true; + /* `a-z{| */ + if (code >= 96 && code <= 124) + return true; + return false; +} +var esc7ctl = [68, 69, 72, 77, 78, 79, 80, 86, 87, 88, 90, 91, 92, 93, 94, 95]; +var escSingle = [55, 56, 61, 62, 70, 99, 108, 109, 110, 111, 124, 125, 126]; +var escDouble = [32, 35, 37, 40, 41, 42, 43, 45, 46, 47]; + +var decChars = {96: 0x2666, 97: 0x2592, 102: 0xB0, 103: 0xB1, + 106: 0x2518, 107: 0x2510, 108: 0x250C, 109: 0x2514, + 110: 0x253C, 111: 0x23BA, 112: 0x23BB, 113: 0x2500, + 114: 0x23BC, 115: 0x23BD, 116: 0x251C, 117: 0x2524, + 118: 0x2534, 119: 0x252C, 120: 0x2502, 121: 0x2264, + 122: 0x2265, 123: 0x03C0, 124: 0x2260, 125: 0xA3, 126: 0xB7}; + +/* Not everything that should be saved by DECSC has been implemented yet. */ +function Cursor(src) { + if (src) { + this.x = src.x; + this.y = src.y; + this.bold = src.bold; + this.inverse = src.inverse; + this.uline = src.uline; + this.fg = src.fg; + this.bg = src.bg; + this.cset = src.cset; + } + else { + this.x = 0; + this.y = 0; + this.bold = false; + this.inverse = false; + this.uline = false; + this.fg = null; + this.bg = null; + this.cset = "B"; + } + return; +} + +// An object representing the terminal emulator. +var termemu = { + sessid: null, // Session key assigned by the server + /* Some elements of the page. */ + inwrap: null, // A non-table div wrapping the screen + view: null, // The div holding the terminal screen + screen: null, // The div representing the active screen area + normbuf: null, // The normal screen buffer + altbuf: null, // The alternate screen buffer + histbuf: null, // The screen history buffer + fgColor: "#b2b2b2", // Default color for text + bgColor: "black", // Default background color + c: null, // Contains cursor position and text attributes + offedge: false, // Going off the edge doesn't mean adding a new line + clearAttrs: function () { + /* Make sure to reset ALL attribute properties and NOTHING else. */ + this.c.bold = false; + this.c.inverse = false; + this.c.uline = false; + this.c.fg = null; + this.c.bg = null; + }, + saved: null, // saved cursor + normc: null, // Stores the normal screen buffer cursor when using altbuf + ansicolors: ["#000000", "#b21818", "#18b218", "#b26818", "#1818b2", + "#b218b2", "#18b2b2", "#b2b2b2"], + brightcolors: ["#686868", "#ff5454", "#54ff54", "#ffff54", "#5454ff", + "#ff54ff", "#54ffff", "#ffffff"], + cssColor: function (fg) { + /* returns a CSS color specification for the text or background */ + var n; + var fallback; + var cube6 = ["00", "5f", "87", "af", "d7", "ff"]; + if (this.c.inverse) + fg = !fg; + if (fg) { + n = this.c.fg; + fallback = this.fgColor; + if (n == null) + return fallback; + } + else { + n = this.c.bg; + fallback = this.bgColor; + if (n == null) + return fallback; + } + if (n < 0) + return fallback; + else if (n < 8) { + if (this.c.bold && fg) + return this.brightcolors[n]; + else + return this.ansicolors[n]; + } + else if (n < 16) + return this.brightcolors[n - 8]; + else if (n < 232) { + var r = cube6[Math.floor((n - 16) / 36)]; + var g = cube6[Math.floor((n - 16) / 6) % 6]; + var b = cube6[(n - 16) % 6]; + return "#" + r + g + b; + } + else if (n < 256) { + var colstr = ((n - 232) * 10 + 8).toString(16); + if (colstr.length < 2) + colstr = "0" + colstr; + return "#" + colstr + colstr + colstr; + } + else + return fallback; + }, + scrT: 0, // top and bottom of scrolling region + scrB: 23, + // These keyboard-related things don't really belong here. + shift: false, + shiftp: function () { + return this.shift; + }, + toggleshift: function () { + this.shift = !this.shift; + }, + ctrl: false, + ctrlp: function () { + return this.ctrl; + }, + togglectrl: function () { + this.ctrl = !this.ctrl; + }, + init: function (divID) { + /* Makes a div full of character cells. */ + if (this.screen != null) + return; + var owrap = document.getElementById(divID); + if (!owrap) + return; + while (owrap.firstChild != null) + owrap.removeChild(owrap.firstChild); + this.c = new Cursor(null); + /* Create the contents of the terminal div */ + this.inwrap = document.createElement("div"); + this.inwrap.id = "inwrap"; + owrap.appendChild(this.inwrap); + var termdiv = document.createElement("div"); + termdiv.id = "term"; + termdiv.style.fontSize = "12px"; + this.inwrap.appendChild(termdiv); + /* Set up the screen buffers */ + this.histbuf = document.createElement("div"); + this.histbuf.id = "histbuf"; + termdiv.appendChild(this.histbuf); + this.normbuf = document.createElement("div"); + this.normbuf.id = "normbuf"; + termdiv.appendChild(this.normbuf); + for (var row = 0; row < 24; row++) { + this.normbuf.appendChild(this.makeRow()); + } + this.altbuf = document.createElement("div"); + this.altbuf.id = "altbuf"; + termdiv.appendChild(this.altbuf); + this.altbuf.style.display = "none"; + /* altbuf will be filled when it is used. */ + /* Attach them. */ + this.view = termdiv; + this.screen = this.normbuf; + this.resize(); + this.cmove(0, 0); + }, + valign: function () { + if (this.screen == this.normbuf) + this.inwrap.scrollTop = this.histbuf.clientHeight; + }, + resize: function () { + var owrap = document.getElementById("termwrap"); + /* Set the height up properly. */ + this.inwrap.style.height = this.screen.scrollHeight.toString() + "px"; + this.valign(); + // Figure out how wide the vertical scrollbar is. + var dwid = this.inwrap.offsetWidth - this.inwrap.clientWidth; + // And resize accordingly. + this.inwrap.style.width = (this.view.scrollWidth + dwid).toString() + "px"; + owrap.style.width = this.inwrap.offsetWidth.toString() + "px"; + return; + }, + comseq: [], // Part of an impending control sequence + flipCursor: function () { + /* Swaps the text and background colors of the active location. */ + /* This will change when other cursor styles are supported. */ + if (this.c.x != null && this.c.y != null) { + var oldcell = this.screen.childNodes[this.c.y].childNodes[this.c.x]; + var tempswap = oldcell.style.color; + oldcell.style.color = oldcell.style.backgroundColor; + oldcell.style.backgroundColor = tempswap; + } + return; + }, + saveCursor: function () { + this.saved = new Cursor(this.c); + return; + }, + restoreCursor: function () { + if (!this.saved) { + this.cmove(0, 0); + this.c = new Cursor(null); + } + else { + this.cmove(this.saved.y, this.saved.x); + this.c = new Cursor(this.saved); + } + return; + }, + toAltBuf: function () { + if (this.screen == this.altbuf) + return; + while (this.altbuf.firstChild != null) + this.altbuf.removeChild(this.altbuf.firstChild); + for (var i = 0; i < 24; i++) { + this.altbuf.appendChild(this.makeRow()); + } + this.normc = new Cursor(this.c); + this.altbuf.style.display = "table-row-group"; + this.normbuf.style.display = "none"; + this.histbuf.style.display = "none"; + this.screen = this.altbuf; + debug(0, "Altbuf with charset " + this.c.cset); + return; + }, + toNormBuf: function () { + if (this.screen == this.normbuf) + return; + this.altbuf.style.display = "none"; + this.normbuf.style.display = "table-row-group"; + this.histbuf.style.display = "table-row-group"; + this.screen = this.normbuf; + this.valign(); + /* The cursor isn't actually at this position in normbuf, but cmove will + * flip it anyway. Flip it again to compensate. */ + this.flipCursor(); + this.cmove(this.normc.y, this.normc.x); + this.c = new Cursor(this.normc); + }, + cmove: function (y, x) { + /* Move the cursor. NOTE coords are [row, col] as in curses. */ + /* If x or y is null, that coordinate is not altered. */ + /* Sanity checks and initializations. */ + if (x == null) { + if (this.c.x != null) + x = this.c.x; + else + return; + } + else { + this.offedge = false; + if (x < 0) + x = 0; + else if (x > 79) + x = 79; + } + if (y == null) { + if (this.c.y != null) + y = this.c.y; + else + return; + } + else if (y < 0) + y = 0; + else if (y > 23) + y = 23; + /* Un-reverse video the current location. */ + this.flipCursor(); + this.c.x = x; + this.c.y = y; + /* Reverse-video the new location. */ + this.flipCursor(); + return; + }, + historize: function (n) { + if (n < 0 || n >= this.screen.childNodes.length) + return; + var oldrow = this.screen.childNodes[n]; + if (this.screen != this.altbuf && this.scrT == 0) { + this.histbuf.appendChild(oldrow); + } + else { + this.screen.removeChild(oldrow); + } + /* These may not be the correct heights... */ + this.inwrap.style.height = this.screen.clientHeight.toString() + "px"; + this.valign(); + }, + scroll: function (lines) { + if (lines == 0) + return; + var count; + if (lines > 0) + count = lines; + else + count = -lines; + this.flipCursor(); + while (count > 0) { + var blankrow = this.makeRow(); + /* Careful with the order */ + if (lines > 0) { + if (this.scrB == 23) + this.screen.appendChild(blankrow); + else + this.screen.insertBefore(blankrow, this.screen.childNodes[this.scrB + + 1]); + this.historize(this.scrT); + } + else { + /* Historize here? */ + this.screen.removeChild(this.screen.childNodes[this.scrB]); + this.screen.insertBefore(blankrow, this.screen.childNodes[this.scrT]); + } + count--; + } + this.valign(); // needed? + this.flipCursor(); + return; + }, + newline: function (doReturn) { + if (this.c.y == this.scrB) + this.scroll(1) + else if (this.c.y < 23) + this.cmove(this.c.y + 1, null); + /* If the cursor is at the bottom but outside the scrolling region, + * nothing can be done. */ + if (doReturn) { + this.cmove(null, 0); + } + }, + antinewline: function () { + if (this.c.y == this.scrT) + this.scroll(-1); + else if (this.c.y > 0) + this.cmove(this.c.y - 1, null); + }, + advance: function () { + if (this.c.x < 79) + this.cmove(null, this.c.x + 1); + else { + this.offedge = true; + } + }, + placechar: function (str) { + if (this.offedge) { + this.newline(true); + } + var nextch = str.charAt(0); + var newcell = this.makeCell(nextch); + var rowdiv = this.screen.childNodes[this.c.y]; + rowdiv.replaceChild(newcell, rowdiv.childNodes[this.c.x]); + this.flipCursor(); // The replace removed the cursor. + /* Update the cursor. */ + this.advance(); + }, + reset: function () { + /* Reset ALL state, hopefully in the right order. */ + /* TODO test this and compare it with xterm */ + this.toNormBuf(); + this.clearAttrs(); + this.c.cset = 'B'; + this.cmove(0, 0); + this.saved = null; + this.normc = null; + this.scrT = 0; + this.scrB = 23; + while (this.histbuf.firstChild != null) { + this.histbuf.removeChild(this.histbuf.firstChild); + } + for (var i = 0; i < 24; i++) { + this.screen.replaceChild(this.makeRow(), this.screen.childNodes[i]); + } + this.flipCursor(); // make it appear in the new row + return; + }, + write: function (codes) { + //dchunk(codes); + for (var i = 0; i < codes.length; i++) { + /* First see if there's an incomplete command sequence waiting. */ + if (this.comseq.length > 0) { + if (this.comseq.length == 1 && this.comseq[0] == 27) { + /* Just ESC */ + if (codes[i] == 55) { + /* ESC 7 : save cursor */ + this.saveCursor(); + this.comseq = []; + } + else if (codes[i] == 56) { + /* ESC 8 : restore cursor */ + this.restoreCursor(); + this.comseq = []; + } + else if (codes[i] == 61) { + /* ESC = : application keypad */ + keyHexCodes.appKeypad(true); + this.comseq = []; + } + else if (codes[i] == 62) { + /* ESC > : normal keypad */ + keyHexCodes.appKeypad(false); + this.comseq = []; + } + else if (codes[i] == 68) { + /* ESC D = IND */ + this.newline(false); + this.comseq = []; + } + else if (codes[i] == 69) { + /* ESC E = NEL */ + this.newline(true); + this.comseq = []; + } + else if (codes[i] == 77) { + /* ESC M = RI */ + this.antinewline(); + this.comseq = []; + } + else if (codes[i] == 91) { + /* ESC [ = CSI */ + this.comseq[0] = 155; + } + else if (codes[i] == 93) { + /* ESC [ = OSC */ + this.comseq[0] = 157; + } + else if (codes[i] == 99) { + /* ESC c = reset */ + this.reset(); + this.comseq = []; + } + else if (escSingle.indexOf(codes[i]) >= 0) { + /* An unimplemented two-char sequence. */ + debug(1, "Unimplemented sequence ESC " + codes[i].toString(16)); + this.comseq = []; + } + else if (escDouble.indexOf(codes[i]) >= 0) { + /* A three-char sequence. */ + this.comseq.push(codes[i]); + } + else { + /* Nothing else is implemented yet. */ + debug(1, "Unrecognized sequence ESC " + codes[i].toString(16)); + this.comseq = []; + } + } + else if (this.comseq.length == 2 && this.comseq[0] == 27) { + /* An ESC C N sequence. Not implemented. Doesn't check validity + * of N. */ + if (this.comseq[1] == 40) { + if (codes[i] == 48) { + this.c.cset = "0"; + debug(0, "Switching to DEC graphics."); + } + else if (codes[i] == 66) { + this.c.cset = "B"; + debug(0, "Switching to ASCII."); + } + else { + debug(1, "Unimplemented character set: " + + String.fromCharCode(codes[i])); + } + debug(0, "cset is now: " + this.c.cset); + } + else + debug(1, "Unknown sequence ESC " + + String.fromCharCode(this.comseq[1]) + " 0x" + + codes[i].toString(16)); + this.comseq = []; + } + else if (this.comseq[0] == 157) { + /* Commands beginning with OSC */ + /* Check for string terminator */ + if (codes[i] == 7 || codes[i] == 156 || (codes[i] == 92 && + this.comseq[this.comseq.length - 1] == 27)) { + if (codes[i] == 92 && this.comseq[this.comseq.length - 1] == 27) + this.comseq.pop(); + debug(0, "Got " + (this.comseq.length - 1) + "-byte OSC sequence"); + this.oscProcess(); + this.comseq = []; + } + else + this.comseq.push(codes[i]); + } + else if (this.comseq[0] == 155) { + /* Commands starting with CSI */ + // End at first char that's not numeric ; ? > ! $ " space ' + // i.e. letter @ ` lbrace | + // ?>! must come directly after CSI + // $"'space must come directly before terminator + // FIXME put this checking code into csiProcess + if (csiPre.indexOf(codes[i]) >= 0) { + if (this.comseq.length > 1) { + /* Chars in csiPre can only occur right after the CSI */ + debug(1, "Invalid CSI sequence: misplaced prefix"); + this.comseq = []; + } + else + this.comseq.push(codes[i]); + } + else if (csiPost.indexOf(this.comseq[this.comseq.length - 1]) >= 0 && + !csiFinal(codes[i])) { + /* Chars is csiPost must come right before the final char */ + debug(1, "Invalid CSI sequence: misplaced postfix"); + this.comseq = []; + } + else if ((codes[i] >= 48 && codes[i] <= 57) || codes[i] == 59 || + csiPost.indexOf(codes[i]) >= 0) { + /* Numbers and ; can go anywhere */ + this.comseq.push(codes[i]); + } + else if (csiFinal(codes[i])) { + this.comseq.push(codes[i]); + this.csiProcess(); + this.comseq = []; + } + else { + debug(1, "Invalid CSI sequence: unknown code " + codes[i].toString(16)); + this.comseq = []; + } + } + else { + debug(1, "Unknown sequence with " + this.comseq[0].toString(16)); + this.comseq = []; + } + continue; + } + /* Treat it as a single character. */ + if (codes[i] == 5) { + sendback("06"); + } + else if (codes[i] == 7) { + /* bell */ + bell(true); + } + else if (codes[i] == 8) { + /* backspace */ + if (this.offedge) + this.offedge = false; + else if (this.c.x > 0) + this.cmove(null, this.c.x - 1); + } + else if (codes[i] == 9) { + /* tab */ + var xnew; + if (this.c.x < 79) { + xnew = 8 * (Math.floor(this.c.x / 8) + 1); + if (xnew > 79) + xnew = 79; + this.cmove(null, xnew); + } + else { + this.offedge = true; + } + } + else if (codes[i] >= 10 && codes[i] <= 12) { + /* newline, vertical tab, form feed */ + if (this.offedge) + this.newline(true); + else + this.newline(false); + } + else if (codes[i] == 13) { + /* carriage return \r */ + this.cmove(null, 0); + } + else if (codes[i] == 14) { + /* shift out */ + // Currently assuming that G1 is DEC Special & Line Drawing + this.c.cset = "0"; + debug(0, "Using DEC graphics charset."); + } + else if (codes[i] == 15) { + /* shift in */ + // Currently assuming that G0 is ASCII + this.c.cset = "B"; + debug(0, "Using ASCII charset."); + } + else if (codes[i] == 27) { + /* escape */ + this.comseq.push(codes[i]); + } + else if (codes[i] < 32 || (codes[i] >= 127 && codes[i] < 160)) { + /* Some kind of control character. */ + debug(1, "Unprintable character 0x" + codes[i].toString(16)); + } + else { + /* If it's ASCII, it's printable; take a risk on anything higher */ + if ((this.c.cset == "0") && (codes[i] in decChars)) { + // DEC special character set + this.placechar(String.fromCharCode(decChars[codes[i]])); + } + else { + this.placechar(String.fromCharCode(codes[i])); + } + } + } + return; + }, + csiProcess: function () { + /* Processes the CSI sequence in this.comseq */ + var c = this.comseq[this.comseq.length - 1]; + if (this.comseq[0] != 155 || !csiFinal(c)) + return; + var comstr = ""; + for (var i = 1; i < this.comseq.length; i++) + comstr += String.fromCharCode(this.comseq[i]); + debug(0, "CSI sequence: " + comstr); + var reCSI = /^([>?!])?([0-9;]*)([ "$'])?([A-Za-z@`{|])$/; + var matchCSI = comstr.match(reCSI); + if (!matchCSI) { + debug(1, "Unrecognized CSI sequence: " + comstr); + return; + } + var prefix = null; + if (matchCSI[1]) + prefix = matchCSI[1]; + var postfix = null; + if (matchCSI[3]) + postfix = matchCSI[3]; + var params = []; + if (matchCSI[2]) { + var numstrs = matchCSI[2].split(";"); + for (var i = 0; i < numstrs.length; i++) { + if (numstrs[i]) + params.push(Number(numstrs[i])); + else + params.push(null); + } + } + /* Useful if expecting a single parameter which is a count. */ + var count = 1; + if (params[0]) + count = params[0]; + /* The final character determines the action. */ + if (c == 64) { + /* @ - insert spaces at cursor */ + if (prefix || postfix) { + debug(1, "Invalid CSI @ sequence: " + comstr); + return; + } + /* The cursor stays still, but characters move out from under it. */ + this.flipCursor(); + var rowdiv = this.screen.childNodes[this.c.y]; + var newspace; + while (count > 0) { + newspace = this.makeCell(' '); + rowdiv.insertBefore(newspace, rowdiv.childNodes[this.c.x]); + rowdiv.removeChild(rowdiv.lastChild); + count--; + } + /* Finally, put the cursor back. */ + this.flipCursor(); + } + else if (c >= 65 && c <= 71) { + /* A - up, B - down, C - forward, D - backward */ + /* E - next line, F - previous line, G - to column */ + if (prefix || postfix) { + debug(1, "Invalid CSI sequence: " + comstr); + return; + } + /* These may be out of range, but cmove will take care of that. */ + if (c == 65) + this.cmove(this.c.y - count, null); + else if (c == 66) + this.cmove(this.c.y + count, null); + else if (c == 67) + this.cmove(null, this.c.x + count); + else if (c == 68) + this.cmove(null, this.c.x - count); + else if (c == 69) + this.cmove(this.c.y + count, 0); + else if (c == 70) + this.cmove(this.c.y - count, 0); + else if (c == 71) + this.cmove(null, count - 1); + } + else if (c == 72) { + /* H - move */ + var x = 0; + var y = 0; + if (prefix || postfix) { + debug(1, "Invalid CSI H sequence: " + comstr); + return; + } + if (params[0]) + y = params[0] - 1; + if (params[1]) + x = params[1] - 1; + if (y > 23) + y = 23; + if (x > 79) + x = 79; + debug(0, "Moving to row " + y + ", col " + x); + this.cmove(y, x); + } + else if (c == 73) { + /* I - move forward by tabs */ + var x = this.c.x; + if (prefix || postfix) { + debug(1, "Invalid CSI I sequence: " + comstr); + return; + } + while (count > 0) { + x = 8 * (Math.floor(x / 8) + 1); + if (x > 79) { + x = 79; + break; + } + count--; + } + this.cmove(null, x); + } + else if (c == 74) { + /* J - erase display */ + var start; + var end; + var cols; + if (prefix == '?') + debug(1, "Warning: CSI ?J not implemented"); + else if (prefix || postfix) { + debug(1, "Invalid CSI J sequence: " + comstr); + return; + } + if (!params[0]) { + /* Either 0 or not given */ + start = this.c.y + 1; + end = 23; + cols = 1; + } + else if (params[0] == 1) { + start = 0; + end = this.c.y - 1; + cols = -1; + } + else if (params[0] == 2) { + start = 0; + end = 23; + cols = 0; + } + else { + debug(1, "Unimplemented parameter in CSI J sequence: " + comstr); + return; + } + for (var nrow = start; nrow <= end; nrow++) { + this.screen.replaceChild(this.makeRow(), this.screen.childNodes[nrow]); + } + if (cols != 0) { + /* Otherwise, the whole screen was erased and the active row doesn't + * need special treatment. */ + var cursrow = this.screen.childNodes[this.c.y]; + for (var ncol = this.c.x; ncol >= 0 && ncol < 80; ncol += cols) { + cursrow.replaceChild(this.makeCell(' '), cursrow.childNodes[ncol]); + } + } + this.offedge = false; + /* Always flip after replacing the active position. */ + this.flipCursor(); + } + else if (c == 75) { + /* K - erase line */ + /* The ? is for an erase method that respects an "uneraseable" attribute, + * which isn't implemented, so the methods are equivalent for now. */ + var start; + var end; + if (prefix == '?') + debug(1, "Warning: CSI ?K not implemented"); + else if (prefix || postfix) { + debug(1, "Invalid CSI K sequence: " + comstr); + return; + } + /* 0 (default): right, 1: left, 2: all. Include cursor position. */ + if (params[0] == 1) { + start = 0; + end = this.c.x; + } + else if (params[0] == 2) { + start = 0; + end = 79; + } + else { + start = this.c.x; + end = 79; + } + var rowdiv = this.screen.childNodes[this.c.y]; + for (var i = start; i <= end; i++) { + rowdiv.replaceChild(this.makeCell(' '), rowdiv.childNodes[i]); + } + /* Deleting stuff tends to clear this */ + this.offedge = false; + /* The active position is always cleared, so the cursor must be made + * visible again. */ + this.flipCursor(); + } + else if (c == 76 || c == 77) { + /* L - insert lines at the current position. + * M - delete current lines */ + if (prefix || postfix) { + debug(1, "Invalid CSI sequence: " + comstr); + return; + } + /* CSI LM have no effect outside of the scrolling region */ + if (this.c.y < this.scrT || this.c.y > this.scrB) + return; + this.flipCursor(); + while (count > 0) { + var blankrow = this.makeRow(); + if (c == 76) { + this.historize(this.scrB); + this.screen.insertBefore(blankrow, this.screen.childNodes[this.c.y]); + } + else { + if (this.scrB == 23) + this.screen.appendChild(blankrow); + else + this.screen.insertBefore(blankrow, this.screen.childNodes[this.scrB + + 1]); + this.historize(this.c.y); + } + count--; + } + /* It doesn't seem necessary to reset this, but xterm does it. */ + this.offedge = false; + this.flipCursor(); + } + else if (c == 80) { + /* P - delete at active position, causing cells on the right to shift. */ + if (prefix || postfix) { + debug(1, "Invalid CSI P sequence: " + comstr); + return; + } + var cursrow = this.screen.childNodes[this.c.y]; + while (count > 0) { + cursrow.removeChild(cursrow.childNodes[this.c.x]); + cursrow.appendChild(this.makeCell(' ')); + count--; + } + this.offedge = false; + this.flipCursor(); + } + else if (c == 83 || c == 84) { + /* S - scroll up, T - scroll down */ + if (prefix || postfix) { + debug(1, "Invalid CSI sequence: " + comstr); + return; + } + if (c == 83) + this.scroll(count); + else + this.scroll(-count); + } + else if (c == 88) { + /* X - erase characters */ + if (prefix || postfix) { + debug(1, "Invalid CSI sequence: " + comstr); + return; + } + var row = this.screen.childNodes[this.c.y]; + for (var i = 0; i < count && this.c.x + i < 80; i++) { + row.replaceChild(this.makeCell(' '), row.childNodes[this.c.x + i]); + } + this.flipCursor(); + } + else if (c == 90) { + /* Z - tab backwards */ + var x = this.c.x; + if (prefix || postfix) { + debug(1, "Invalid CSI Z sequence: " + comstr); + return; + } + while (count > 0) { + x = 8 * (Math.ceil(x / 8) - 1); + if (x < 0) { + x = 0; + break; + } + count--; + } + this.cmove(null, x); + } + else if (c == 96) { + /* ` - go to col */ + if (prefix || postfix) { + debug(1, "Invalid CSI ` sequence: " + comstr); + return; + } + this.cmove(null, count - 1); + } + else if (c == 100) { + /* d - go to row */ + if (prefix || postfix) { + debug(1, "Invalid CSI d sequence: " + comstr); + return; + } + this.cmove(count - 1, null); + } + else if (c == 102) { + /* f - move */ + var x = 0; + var y = 0; + if (prefix || postfix) { + debug(1, "Invalid CSI f sequence: " + comstr); + return; + } + if (params[0]) + y = params[0] - 1; + if (params[1]) + x = params[1] - 1; + this.cmove(y, x); + } + else if (c == 104) { + /* h - set modes */ + if (prefix != '?') { + debug(1, "Unimplemented CSI sequence: " + comstr); + return; + } + for (var i = 0; i < params.length; i++) { + if (params[i] == 1) { + keyHexCodes.appCursor(true); + } + else if (params[i] == 1048) { + this.saveCursor(); + } + else if (params[i] == 1049) { + this.toAltBuf(); + } + else { + debug(1, "Unimplemented CSI ?h parameter: " + params[i]); + } + } + } + else if (c == 108) { + /* l - reset modes */ + if (prefix != '?') { + debug(1, "Unimplemented CSI sequence: " + comstr); + return; + } + for (var i = 0; i < params.length; i++) { + if (params[i] == 1) { + keyHexCodes.appCursor(false); + } + else if (params[i] == 1048) { + this.restoreCursor(); + } + else if (params[i] == 1049) { + this.toNormBuf(); + } + else { + debug(1, "Unimplemented CSI ?l parameter: " + params[i]); + } + } + } + else if (c == 109) { + /* m - character attributes */ + if (params.length == 0) + this.clearAttrs(); + for (var i = 0; i < params.length; i++) { + if (params[i] == null || params[i] == 0) + this.clearAttrs(); + else if (params[i] == 1) + this.c.bold = true; + else if (params[i] == 4) + this.c.uline = true; + else if (params[i] == 7) + this.c.inverse = true; + else if (params[i] == 22) + this.c.bold = false; + else if (params[i] == 24) + this.c.uline = false; + else if (params[i] == 27) + this.c.inverse = false; + else if (params[i] >= 30 && params[i] <= 37) + this.c.fg = params[i] - 30; + else if (params[i] == 39) + this.c.fg = null; + else if (params[i] >= 40 && params[i] <= 47) + this.c.bg = params[i] - 40; + else if (params[i] == 49) + this.c.bg = null; + else if (params[i] >= 90 && params[i] <= 97) + this.c.fg = params[i] - 82; + else if (params[i] >= 100 && params[i] <= 107) + this.c.bg = params[i] - 92; + else if (params[i] == 38 && params[i + 1] == 5) { + if (i + 2 < params.length && params[i+2] != null && params[i+2] < 256) + this.c.fg = params[i+2]; + i += 2; + } + else if (params[i] == 48 && params[i + 1] == 5) { + if (i + 2 < params.length && params[i+2] != null && params[i+2] < 256) + this.c.bg = params[i+2]; + i += 2; + } + else + debug(1, "Unimplemented CSI m parameter: " + params[i]); + } + } + else if (c == 114) { + /* r - set scrolling region */ + var t = 0; + var b = 23; + if (params[0] && params[0] <= 23) + t = params[0] - 1; + if (params[1] && params[1] <= 24) + b = params[1] - 1; + if (b <= t) + return; + this.scrT = t; + this.scrB = b; + this.cmove(0, 0); + } + else { + debug(1, "Unimplemented CSI sequence: " + comstr); + } + return; + }, + oscProcess: function () { + var numstr = ""; + var i; + for (i = 1; i < this.comseq.length; i++) { + if (this.comseq[i] >= 48 && this.comseq[i] <= 57) + numstr += String.fromCharCode(this.comseq[i]); + else + break; + } + if (this.comseq[i] != 59) { + debug(1, "Invalid OSC sequence"); + return; + } + var codenum = Number(numstr); + var msgstr = ""; + i++; + while (i < this.comseq.length) { + msgstr += String.fromCharCode(this.comseq[i]); + i++; + } + if (codenum == 0 || codenum == 2) { + setTitle(msgstr); + } + else + debug(1, "Unimplemented OSC command " + codenum + " " + msgstr); + return; + }, + makeCell: function(c) { + var tnode; + if (c && c.charAt && c.charAt(0)) + tnode = document.createTextNode(c.charAt(0)); + else + tnode = document.createTextNode(' '); + var cell = document.createElement("span"); + cell.className = "termcell"; + /* cssColor will handle reverse-video and bold->bright */ + cell.style.color = this.cssColor(true); + cell.style.backgroundColor = this.cssColor(false); + if (this.c.bold) + cell.style.fontWeight = "bold"; + if (this.c.uline) + cell.style.textDecoration = "underline"; + cell.appendChild(tnode); + return cell; + }, + makeRow: function() { + var blankrow = document.createElement("div"); + blankrow.className = "termrow"; + for (var i = 0; i < 80; i++) + blankrow.appendChild(this.makeCell(' ')); + return blankrow; + } +}; + +function setTitle(tstr) { + var titlespan = document.getElementById("ttitle"); + var tnode = document.createTextNode(tstr); + if (titlespan.childNodes.length == 0) + titlespan.appendChild(tnode); + else + titlespan.replaceChild(tnode, titlespan.childNodes[0]); + return; +} + +function dchunk(codes) { + var dstr = "Chunk: "; + for (var i = 0; i < codes.length; i++) { + if (codes[i] < 32 || (codes[i] >= 127 && codes[i] < 160)) + dstr += "\\x" + codes[i].toString(16); + else + dstr += String.fromCharCode(codes[i]); + } + debug(1, dstr); + return; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tty.css Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,90 @@ +div#top { + font-size: 1.2em; + text-align: center; + margin: 0.2em; +} +img#bell { + visibility: hidden; + margin-left: 2em; +} +div.keyrow { + font-size: 1.2em; +} +div.keyrow > div:first-child { + clear: left; +} +div.key { + width: 2em; + height: 2em; + float: left; + border: 2px solid black; + margin: 0.2em; + text-align: center; +} +div.keysel { + width: 2em; + height: 2em; + float: left; + border: 2px solid black; + margin: 0.2em; + text-align: center; + background-color: #C0FFC0; +} +div#shiftkey { + width: 4em; +} +div#ctrlkey { + width: 3em; +} +div#spacebar { + width: 8em; + margin-left: 12em; +} +div#termwrap { + margin: 0.5em auto; + padding: 1em; + background-color: #808080; + border: 0.25em solid #202020; + border-radius: 1em; +} +div#inwrap { + overflow-y: scroll; +} +div#term { + display: table; + font-size: 12px; + font-family: monospace; + white-space: pre; +} +div#term > div { + display: table-row-group; +} +div.termrow { + display: table-row; +} +span.termcell { + display: table-cell; +} +div.rbutton { + float: right; + clear: right; + border: 2px solid black; + text-align: center; + margin: 0.2em; + font-size: 1.2em; + padding: 0.1em; +} +div.rbutton > span { + border: 1px solid black; + padding: 0 0.1em; +} +div#debug { + width: 100%; + height: 10em; + overflow: scroll; + white-space: pre; + clear: both; +} +div#debug > div { + font-family: monospace; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/webtty.js Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,329 @@ +#!/usr/bin/env node +var http = require('http'); +var url = require('url'); +var path = require('path'); +var fs = require('fs'); +//var tty = require("tty"); +var child_process = require("child_process"); + +var serveStaticRoot = "/home/elwin/hk/nodejs/rlg/s/"; +var sessions = {}; + +/* Constructor for TermSessions. Note that it opens the terminal and + * adds itself to the sessions dict. + */ +function TermSession(sessid) { + //var pterm = tty.open("/bin/bash"); + this.child = child_process.spawn("./ptywreck/ptyhelperC", ["bash"]); + var ss = this; + /* Eventually we'll need to make sure the sessid isn't in use yet. */ + this.sessid = sessid; + //this.ptmx = pterm[0]; + //this.child = pterm[1]; + this.alive = true; + this.data = []; + this.child.stdout.on("data", function (buf) { + ss.data.push(buf); + }); + this.child.stderr.on("data", function (buf) { + ss.data.push(buf); + }); + this.child.on("exit", function (code, signal) { + ss.exitcode = (code != null ? code : 255); + ss.alive = false; + /* Wait for all the data to get collected */ + setTimeout(ss.cleanup, 1000); + }); + this.write = function (data) { + if (this.alive) + this.child.stdin.write(data); + /* Otherwise, throw some kind of exception? */ + }; + this.read = function () { + if (this.data.length == 0) + return null; + var pos = 0; + var i = 0; + for (i = 0; i < this.data.length; i++) + pos += this.data[i].length; + var nbuf = new Buffer(pos); + var tptr; + pos = 0; + while (this.data.length > 0) { + tptr = this.data.shift(); + tptr.copy(nbuf, pos); + pos += tptr.length; + } + return nbuf; + }; + this.close = function () { + if (this.alive) + this.child.kill('SIGHUP'); + }; + this.cleanup = function () { + /* Call this when the child is dead. */ + if (this.alive) + return; + //ss.ptmx.destroy(); + /* Give the client a chance to read any leftover data. */ + if (ss.data.length > 0) + setTimeout(ss.remove, 8000); + else + ss.remove(); + }; + this.remove = function () { + delete sessions[ss.sessid]; + console.log("Session " + this.sessid + " removed."); + }; + sessions[sessid] = this; +} + +function randkey() { + rnum = Math.floor(Math.random() * 65536 * 65536); + hexstr = rnum.toString(16); + while (hexstr.length < 8) + hexstr = "0" + hexstr; + return hexstr; +} + +/* Returns a list of the cookies in the request, obviously. */ +function getCookies(req) { + cookies = []; + if ("cookie" in req.headers) { + cookstrs = req.headers["cookie"].split("; "); + for (var i = 0; i < cookstrs.length; i++) { + eqsign = cookstrs[i].indexOf("="); + if (eqsign > 0) { + name = cookstrs[i].slice(0, eqsign).toLowerCase(); + val = cookstrs[i].slice(eqsign + 1); + cookies[name] = val; + } + else if (eqsign < 0) + cookies[cookstrs[i]] = null; + } + } + return cookies; +} + +function urlDec(encstr) { + var decstr = ""; + var tnum; + for (var i = 0; i < encstr.length; i++) + { + if (encstr.charAt(i) == "+") + decstr += " "; + else if (encstr.charAt(i) == "%") + { + tnum = Number("0x" + encstr.slice(i + 1, 2)); + if (!isNaN(tnum) && tnum >= 0) + decstr += String.fromCharCode(tnum); + i += 2; + } + else + decstr += encstr.charAt(i); + } + return decstr; +} + +/* Returns the contents of a form */ +function getFormValues(formtext) { + var pairstrs = formtext.split("&"); + var data = {}; + for (var i = 0; i < pairstrs.length; i++) + { + var eqsign = pairstrs[i].indexOf("="); + if (eqsign > 0) { + rawname = pairstrs[i].slice(0, eqsign); + rawval = pairstrs[i].slice(eqsign + 1); + name = urlDec(rawname); + val = urlDec(rawval); + if (!(name in data)) + data[name] = []; + data[name].push(val); + } + } + return data; +} + +function login(req, res, formdata) { + var resheaders = {'Content-Type': 'text/plain'}; + var sessid = randkey(); + var nsession = new TermSession(sessid); + resheaders["Set-Cookie"] = "ID=" + sessid; + res.writeHead(200, resheaders); + res.write("l1\n" + sessid + "\n"); + res.end(); + console.log("Started new session with key " + sessid + ", pid " + nsession.child.pid); + return; +} + +function findTermSession(req) { + var cookies = getCookies(req); + if ("id" in cookies) { + var sessid = cookies["id"]; + if (sessid in sessions) { + return sessions[sessid]; + } + } + return null; +} + +function serveStatic(req, res, fname) { + var nname = path.normalize(fname); + if (nname == "" || nname == "/") + nname = "index.html"; + if (nname.match(/\/$/)) + path.join(nname, "index.html"); /* it was a directory */ + var realname = path.join(serveStaticRoot, nname); + var extension = path.extname(realname); + path.exists(realname, function (exists) { + var resheaders = {}; + if (!exists || !extension || extension == ".html") + resheaders["Content-Type"] = "text/html"; + else if (extension == ".png") + resheaders["Content-Type"] = "image/png"; + else if (extension == ".css") + resheaders["Content-Type"] = "text/css"; + else if (extension == ".js") + resheaders["Content-Type"] = "text/javascript"; + else if (extension == ".svg") + resheaders["Content-Type"] = "image/svg+xml"; + else + resheaders["Content-Type"] = "application/octet-stream"; + if (exists) { + /* Not nice, not sensible. First see if it's readable, then respond + * 200 or 500. Don't throw nasty errors. */ + res.writeHead(200, resheaders); + fs.readFile(realname, function (error, data) { + if (error) throw error; + res.write(data); + res.end(); + }); + } + else { + res.writeHead(404, resheaders); + res.write("<html><head><title>" + nname + "</title></head>\n<body><h1>" + nname + " Not Found</h1></body></html>\n"); + res.end(); + } + }); + return; +} + +function readFeed(res, term) { + res.writeHead(200, { "Content-Type": "text/plain" }); + if (term) { + var result = term.read(); + if (result == null) + resultstr = ""; + else + resultstr = result.toString("hex"); + res.write("d" + resultstr.length.toString() + "\n" + resultstr + "\n"); + } + else { + //console.log("Where's the term?"); + res.write("d0\n\n"); + } +} + +var errorcodes = [ "Generic Error", "Not logged in", "Invalid data" ]; + +function sendError(res, ecode) { + res.writeHead(200, { "Content-Type": "text/plain" }); + if (ecode < errorcodes.length && ecode > 0) + res.write("E" + ecode + '\n' + errorcodes[ecode] + '\n'); + else + res.write("E0\nGeneric Error\n"); + res.end(); +} + +function handler(req, res) { + /* default headers for the response */ + var resheaders = {'Content-Type': 'text/html'}; + /* The request body will be added to this as it arrives. */ + var reqbody = ""; + var formdata; + + /* Register a listener to get the body. */ + function moredata(chunk) { + reqbody += chunk; + } + req.on('data', moredata); + + /* This will send the response once the whole request is here. */ + function respond() { + var target = url.parse(req.url).pathname; + var cterm = findTermSession(req); + /* First figure out if the client is POSTing to a command interface. */ + if (req.method == 'POST') { + formdata = getFormValues(reqbody); + if (target == '/feed') { + if (!cterm) { + sendError(res, 1); + return; + } + if (formdata["quit"] == "quit") { + /* The client wants to quit. */ + // FIXME need to send a message back to the client + cterm.close(); + } + else if (formdata["keys"]) { + /* process the keys */ + hexstr = formdata["keys"][0].replace(/[^0-9a-f]/gi, ""); + if (hexstr.length % 2 != 0) { + sendError(res, 2); + return; + } + keybuf = new Buffer(hexstr, "hex"); + cterm.write(keybuf); + } + readFeed(res, cterm); + res.end(); + } + else if (target == "/login") { + login(req, res, formdata); + } + else { + res.writeHead(405, resheaders); + res.end(); + } + } + else if (req.method == 'GET' || req.method == 'HEAD') { + if (target == '/feed') { + if (!cterm) { + sendError(res, 1); + return; + } + readFeed(res, cterm); + res.end(); + } + /* Default page, create a new term */ + /* FIXME New term not created anymore, is a special case still needed? */ + else if (target == '/') { + serveStatic(req, res, "/"); + } + else /* Go look for it in the filesystem */ + serveStatic(req, res, target); + } + else { /* Some other method */ + res.writeHead(501, resheaders); + res.write("<html><head><title>501</title></head>\n<body><h1>501 Not Implemented</h1></body></html>\n"); + res.end(); + } + return; + } + req.on('end', respond); + +} + +process.on("exit", function () { + for (var sessid in sessions) { + if (sessions[sessid].alive) + sessions[sessid].child.kill('SIGHUP'); + } + console.log("Quitting..."); + return; +}); + +process.env["TERM"] = "xterm-256color"; +http.createServer(handler).listen(8080, "127.0.0.1"); +console.log('Server running at http://127.0.0.1:8080/');
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/webttyd.js Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,531 @@ +#!/usr/bin/env node + +// If you can't quite trust node to find it on its own +var localModules = '/usr/local/lib/node_modules/'; +var http = require('http'); +var url = require('url'); +var path = require('path'); +var fs = require('fs'); +//var tty = require("tty"); +var child_process = require('child_process'); +var daemon = require(path.join(localModules, "daemon")); + +var chrootDir = "/var/dgl/"; +var dropToUID = 501; +var dropToGID = 501; +var serveStaticRoot = "/var/www/"; // inside the chroot +var passwdfile = "/dgldir/dgl-login"; +var sessions = {}; + +var games = { + "rogue3": { + "name": "Rogue V3", + "uname": "rogue3", + "path": "/bin/rogue3" + }, + "rogue4": { + "name": "Rogue V4", + "uname": "rogue4", + "path": "/bin/rogue4" + }, + "rogue5": { + "name": "Rogue V5", + "uname": "rogue5", + "path": "/bin/rogue5" + }, + "srogue": { + "name": "Super-Rogue", + "uname": "srogue", + "path": "/bin/srogue" + } +}; + +/* Constructor for TermSessions. Note that it opens the terminal and + * adds itself to the sessions dict. It currently assumes the user has + * been authenticated. + */ +function TermSession(game, user, files) { + /* First make sure starting the game will work. */ + if (!(game in games)) { + // TODO: throw an exception instead + return null; + } + /* This order seems to best avoid race conditions... */ + this.alive = false; + this.sessid = randkey(); + while (this.sessid in sessions) { + this.sessid = randkey(); + } + /* Grab a spot in the sessions table. */ + sessions[this.sessid] = this; + /* TODO handle tty-opening errors */ + //var pterm = tty.open(games[game].path, ["-n", user.toString()]); + /* TODO make argument-finding into a method */ + args = [games[game].path, "-n", user.toString()]; + this.child = child_process.spawn("/bin/ptyhelper", args); + var ss = this; + //this.ptmx = pterm[0]; + //this.child = pterm[1]; + this.alive = true; + this.data = []; + this.lock = files[0]; + fs.writeFile(this.lock, this.child.pid.toString() + '\n80\n24\n', "utf8"); + this.record = fs.createWriteStream(files[1], { mode: 0664 }); + /* END setup */ + function ttyrec_chunk(buf) { + var ts = new Date(); + var chunk = new Buffer(buf.length + 12); + /* TTYREC headers */ + chunk.writeUInt32LE(Math.floor(ts.getTime() / 1000), 0); + chunk.writeUInt32LE(1000 * (ts.getTime() % 1000), 4); + chunk.writeUInt32LE(buf.length, 8); + buf.copy(chunk, 12); + ss.data.push(chunk); + ss.record.write(chunk); + } + this.child.stdout.on("data", ttyrec_chunk); + this.child.stderr.on("data", ttyrec_chunk); + this.child.on("exit", function (code, signal) { + ss.exitcode = (code != null ? code : 255); + ss.alive = false; + fs.unlink(ss.lock); + /* Wait for all the data to get collected */ + setTimeout(ss.cleanup, 1000); + }); + this.write = function (data) { + if (this.alive) + this.child.stdin.write(data); + /* Otherwise, throw some kind of exception? */ + }; + this.read = function () { + if (this.data.length == 0) + return null; + var pos = 0; + var i = 0; + for (i = 0; i < this.data.length; i++) + pos += this.data[i].length - 12; + var nbuf = new Buffer(pos); + var tptr; + pos = 0; + while (this.data.length > 0) { + tptr = this.data.shift(); + tptr.copy(nbuf, pos, 12); + pos += tptr.length - 12; + } + return nbuf; + }; + this.close = function () { + if (this.alive) + this.child.kill('SIGHUP'); + }; + this.cleanup = function () { + /* Call this when the child is dead. */ + if (this.alive) + return; + //ss.ptmx.destroy(); + ss.record.end(); + /* Give the client a chance to read any leftover data. */ + if (ss.data.length > 0) + setTimeout(ss.remove, 8000); + else + ss.remove(); + }; + this.remove = function () { + delete sessions[ss.sessid]; + console.log("Session " + this.sessid + " removed."); + }; +} + +/* A few utility functions */ +function timestamp() { + dd = new Date(); + sd = dd.toISOString(); + sd = sd.slice(0, sd.indexOf(".")); + return sd.replace("T", "."); +} + +function randkey() { + rnum = Math.floor(Math.random() * 65536 * 65536); + hexstr = rnum.toString(16); + while (hexstr.length < 8) + hexstr = "0" + hexstr; + return hexstr; +} + +/* Returns a list of the cookies in the request, obviously. */ +function getCookies(req) { + cookies = []; + if ("cookie" in req.headers) { + cookstrs = req.headers["cookie"].split("; "); + for (var i = 0; i < cookstrs.length; i++) { + eqsign = cookstrs[i].indexOf("="); + if (eqsign > 0) { + name = cookstrs[i].slice(0, eqsign).toLowerCase(); + val = cookstrs[i].slice(eqsign + 1); + cookies[name] = val; + } + else if (eqsign < 0) + cookies[cookstrs[i]] = null; + } + } + return cookies; +} + +function urlDec(encstr) { + var decstr = ""; + var tnum; + for (var i = 0; i < encstr.length; i++) + { + if (encstr.charAt(i) == "+") + decstr += " "; + else if (encstr.charAt(i) == "%") + { + tnum = Number("0x" + encstr.slice(i + 1, 2)); + if (!isNaN(tnum) && tnum >= 0) + decstr += String.fromCharCode(tnum); + i += 2; + } + else + decstr += encstr.charAt(i); + } + return decstr; +} + +/* Returns the contents of a form */ +function getFormValues(formtext) { + var pairstrs = formtext.split("&"); + var data = {}; + for (var i = 0; i < pairstrs.length; i++) + { + var eqsign = pairstrs[i].indexOf("="); + if (eqsign > 0) { + rawname = pairstrs[i].slice(0, eqsign); + rawval = pairstrs[i].slice(eqsign + 1); + name = urlDec(rawname); + val = urlDec(rawval); + if (!(name in data)) + data[name] = []; + data[name].push(val); + } + } + return data; +} + +function auth(username, password) { + // Real authentication not implemented + return true; +} + +function login(req, res, formdata) { + if (!("game" in formdata)) { + sendError(res, 2, "No game specified."); + return; + } + else if (!("name" in formdata)) { + sendError(res, 2, "Username not given."); + return; + } + else if (!("pw" in formdata)) { + sendError(res, 2, "Password not given."); + return; + } + var username = formdata["name"][0]; + var password = formdata["pw"][0]; + var gname = formdata["game"][0]; + if (!(gname in games)) { + sendError(res, 2, "No such game: " + gname); + console.log("Request for nonexistant game \"" + gname + "\""); + return; + } + var progressdir = "/dgldir/inprogress-" + games[gname].uname; + + // This sets up the game once starting is approved. + function startgame() { + var ts = timestamp(); + var lockfile = path.join(progressdir, username + ":node:" + ts + ".ttyrec"); + var ttyrec = path.join("/dgldir/ttyrec", username, gname, ts + ".ttyrec"); + var nsession = new TermSession(gname, username, [lockfile, ttyrec]); + if (nsession) { + /* Technically there's a race condition for the "lock"file, but since + * it requires the user deliberately starting two games at similar times, + * it's not too serious. We can't get O_EXCL in Node anyway. */ + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.write("l1\n" + nsession.sessid + "\n"); + res.end(); + console.log("%s playing %s (key %s, pid %d)", username, gname, + nsession.sessid, nsession.child.pid); + } + else { + sendError(res, 5, "Failed to open TTY"); + console.log("Unable to allocate TTY for " + gname); + } + } + function checkit(code, signal) { + // check the password + if (code != 0) { + sendError(res, 3); + console.log("Password check failed for user " + username); + return; + } + // check for an existing game + fs.readdir(progressdir, function(err, files) { + if (!err) { + var fre = RegExp("^" + username + ":"); + for (var i = 0; i < files.length; i++) { + if (files[i].match(fre)) { + sendError(res, 4, null); + return; + } + } + } + // If progressdir isn't readable, start a new game anyway. + startgame(); + }); + } + /* Look for the user in the password file */ + fs.readFile(passwdfile, "utf8", function(err, data) { + if (err) { + sendError(res, 3); + console.log("Can't authenticate: " + err.toString()); + return; + } + var dlines = data.split('\n'); + for (var n = 0; n < dlines.length; n++) { + var fields = dlines[n].split(':'); + if (fields[0] == username) { + // check the password with the quickrypt utility + checker = require('child_process').spawn("/bin/quickrypt") + checker.on("exit", checkit); + checker.stdin.end(password + '\n' + fields[2] + '\n', "utf8"); + return; + } + } + sendError(res, 3); + console.log("Attempted login by nonexistent user " + username); + }); + return; +} + +function logout(term, res) { + if (!term.alive) { + sendError(res, 1, null); + return; + } + cterm.close(); + var resheaders = {'Content-Type': 'text/plain'}; + res.writeHead(200, resheaders); + res.write("q1\n\n"); + res.end(); + return; +} + +function findTermSession(formdata) { + if ("id" in formdata) { + var sessid = formdata["id"][0]; + if (sessid in sessions) { + return sessions[sessid]; + } + } + return null; +} + +function serveStatic(req, res, fname) { + var nname = path.normalize(fname); + if (nname == "" || nname == "/") + nname = "index.html"; + if (nname.match(/\/$/)) + path.join(nname, "index.html"); /* it was a directory */ + var realname = path.join(serveStaticRoot, nname); + var extension = path.extname(realname); + path.exists(realname, function (exists) { + var resheaders = {}; + if (!exists || !extension || extension == ".html") + resheaders["Content-Type"] = "text/html"; + else if (extension == ".png") + resheaders["Content-Type"] = "image/png"; + else if (extension == ".css") + resheaders["Content-Type"] = "text/css"; + else if (extension == ".js") + resheaders["Content-Type"] = "text/javascript"; + else if (extension == ".svg") + resheaders["Content-Type"] = "image/svg+xml"; + else + resheaders["Content-Type"] = "application/octet-stream"; + if (exists) { + fs.readFile(realname, function (error, data) { + if (error) { + res.writeHead(500, {}); + res.end(); + } + else { + res.writeHead(200, resheaders); + res.write(data); + res.end(); + } + }); + } + else { + res.writeHead(404, resheaders); + res.write("<html><head><title>" + nname + "</title></head>\n<body><h1>" + nname + " Not Found</h1></body></html>\n"); + res.end(); + } + }); + return; +} + +function readFeed(res, term) { + if (term) { + var result = term.read(); + res.writeHead(200, { "Content-Type": "text/plain" }); + if (result == null) + resultstr = ""; + else + resultstr = result.toString("hex"); + if (result == null && !term.alive) { + /* Child has terminated and data is flushed. */ + res.write("q1\n\n"); + } + else + res.write("d" + resultstr.length.toString() + "\n" + resultstr + "\n"); + res.end(); + } + else { + //console.log("Where's the term?"); + sendError(res, 1, null); + } +} + +var errorcodes = [ "Generic Error", "Not logged in", "Invalid data", + "Login failed", "Already playing", "Game launch failed" ]; + +function sendError(res, ecode, msg) { + res.writeHead(200, { "Content-Type": "text/plain" }); + if (ecode < errorcodes.length && ecode > 0) { + var emsg = errorcodes[ecode]; + if (msg) + emsg += ": " + msg; + res.write("E" + ecode + '\n' + emsg + '\n'); + } + else + res.write("E0\nGeneric Error\n"); + res.end(); +} + +function handler(req, res) { + /* default headers for the response */ + var resheaders = {'Content-Type': 'text/html'}; + /* The request body will be added to this as it arrives. */ + var reqbody = ""; + var formdata; + + /* Register a listener to get the body. */ + function moredata(chunk) { + reqbody += chunk; + } + req.on('data', moredata); + + /* This will send the response once the whole request is here. */ + function respond() { + formdata = getFormValues(reqbody); + var target = url.parse(req.url).pathname; + var cterm = findTermSession(formdata); + /* First figure out if the client is POSTing to a command interface. */ + if (req.method == 'POST') { + if (target == '/feed') { + if (!cterm) { + sendError(res, 1, null); + return; + } + if ("quit" in formdata) { + /* The client wants to terminate the process. */ + logout(cterm, res); + } + else if (formdata["keys"]) { + /* process the keys */ + hexstr = formdata["keys"][0].replace(/[^0-9a-f]/gi, ""); + if (hexstr.length % 2 != 0) { + sendError(res, 2, "incomplete byte"); + return; + } + keybuf = new Buffer(hexstr, "hex"); + cterm.write(keybuf); + } + readFeed(res, cterm); + } + else if (target == "/login") { + login(req, res, formdata); + } + else { + res.writeHead(405, resheaders); + res.end(); + } + } + else if (req.method == 'GET' || req.method == 'HEAD') { + if (target == '/feed') { + if (!cterm) { + sendError(res, 1, null); + return; + } + readFeed(res, cterm); + } + /* Default page, create a new term */ + /* FIXME New term not created anymore, is a special case still needed? */ + else if (target == '/') { + serveStatic(req, res, "/"); + } + else /* Go look for it in the filesystem */ + serveStatic(req, res, target); + } + else { /* Some other method */ + res.writeHead(501, resheaders); + res.write("<html><head><title>501</title></head>\n<body><h1>501 Not Implemented</h1></body></html>\n"); + res.end(); + } + return; + } + req.on('end', respond); + +} + +process.on("exit", function () { + for (var sessid in sessions) { + if (sessions[sessid].alive) + sessions[sessid].child.kill('SIGHUP'); + } + console.log("Quitting..."); + return; +}); + +/* Initialization STARTS HERE */ +process.env["TERM"] = "xterm-256color"; + +if (process.getuid() != 0) { + console.log("Not running as root, cannot chroot."); + process.exit(1); +} +try { + process.chdir(chrootDir); +} +catch (err) { + console.log("Cannot enter " + chrootDir + " : " + err); + process.exit(1); +} +try { + daemon.chroot(chrootDir); +} +catch (err) { + console.log("chroot to " + chrootDir + " failed: " + err); + process.exit(1); +} +try { + // drop gid first, that requires UID=0 + process.setgid(dropToGID); + process.setuid(dropToUID); +} +catch (err) { + console.log("Could not drop permissions: " + err); + process.exit(1); +} + +http.createServer(handler).listen(8080, "127.0.0.1"); +console.log('webttyd running at http://127.0.0.1:8080/');