Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

tty.js

코드가 많은 사이트다. 그래 아래처럼 웹에서 직접 코드를 수정도 하고 실행도 할 수 있도록 코드 실행기를 붙였다.
package main
import (
    "crypto/sha1"
    "fmt"
)

func main() {
    s := "sha1 this string"
    h := sha1.New()
    
    h.Write([]byte(s))
    
    bs := h.Sum(nil)
    
    fmt.Println(s)
    fmt.Printf("%x\n", bs)
    
}
	
		
유용하긴 한데, 코드를 실행하고 그 결과를 화면에 출력하는 방식이라서 표준입력이나 python interactive mode등을 사용 할 수 없다. 그래서 웹 터미널 애플리케이션을 찾아보기로 했다.

구글님에게 물어 물어서 gotty tty.js 두 개의 애플리케이션을 찾았다. gotty는 go 언어 기반이고 tty.js는 node 기반이다. 둘 다 웹 터미널을 제공하는 단일 애플리케이션으로 훌륭하긴 하지만, 웹 터미널을 내 사이트에 붙여야 했기 때문에 좀 더 간단하고 수정하기 편한 녀석을 선택해야 했다. 일단 tty.js를 선택했다. 대략 아래와 같은 일을 하는 애플리케이션 이다.

 ttyjs 스크린샷

구성

아래와 같이 구성하기로 했다.

 tty.js 구성
  1. gowiki는 일반 HTML 컨텐츠와 tty.js를 위한 javascript와 css, html 코드를 제공한다.
  2. Nginx로 reverse proxy 한다. 일반 HTML 컨텐츠는 gowiki로 터미널은 웹소켓을 이용 tty.js 서버로 프락시 한다.
  3. tty.js는 터미널 명령과 함께 컨테이너를 interactive 모드로 실행한다. 파이슨의 경우에는 python을 실행한다.

테스트

우분투 리눅스에 node와 tty.js를 설치한다.
# apt-get update
# apt-get install -y nodejs
# apt-get install -y npm
# ln -s /usr/bin/nodejs /usr/bin/node
# npm install tty.js

테스트를 위해서 간단한 애플리케이션을 만들고 실행 했다.
// 파일 이름 : app.js
var tty = require('tty.js');

var app = tty.createServer({
  shell: 'bash',
  port: 8000
});

app.get('/foo', function(req, res, next) {
  res.send('bar');
});

app.listen();

# node app.js
[tty.js] Listening on port 8000.
[tty.js] 0 Session 0 created.
[tty.js] 0 Created pty (id: /dev/pts/0, master: 18, pid: 1840).

joinc에 설치

이렇게 해서 tty.js를 띄웠다. 지금 띄운건 tty.js 튜토리얼 페이지에 나온 예제고, 독립적인 웹 애플리케이션으로 뜬다. 나는 웹 터미널기능만 떼어내서 joinc에 붙여야 했다. 문제는 내가 node.js를 전혀 모른다는 거다("Hello world"를 찍어본 적은 있다.). 혹시나 해서 js 파일들을 열어봤는데, 역시나 코드를 따라가기가 힘들었다.

node.js를 배워가면서 수정 할 수 있을까란 생각에 코드를 좀 읽었다. 뭔가 코드가 낯설다. 찾아봤더니 프레임워크로 express를 사용하고 있다. 그래서 node를 배워가면서 수정하는 건 포기했다. 이 것 때문에 프레임워크까지 공부할 수는 없는 노릇이니까(언젠가 공부 해야 할 날이 올지도 모르지만)

그래서 웹 관련된 잡지식들을 이용 해서 필요한 부분만 짜집기 하기로 했다.

프론트앤드

내가 하려는 핵심 작업은 두 가지로 정리 할 수 있다.
  1. 프론트앤드 : joinc에 붙여야 하기 때문에.
  2. 백앤드 서버 : tty.js의 셈플은 설정기반으로 돌아간다. 설정파일에 어떤 명령어를 실행할 것인지가 설정돼 있다. 나는 컨테이너를 다양한 옵션으로 실행하길 원했다. 예컨데 pythn interactive shell 이나 sqlite interactive shell을 토미널로 제공하고 싶었다.
프론트앤드 요소의 분리는 간단하다. 그냥 예제에 사용한 HTML 페이지에 있는 것들만 따로 빼면 된다.
<!doctype html>
<title>tty.js</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="user.css">

<h1>tty.js</h1>

<div id="help">
  <p>Click the titlebar to drag.</p>
  <p>Double-click titlebar to maximize.</p>
  <p>Click the lower-right corner to resize.</p>
  <p>Click the tilde to open a new tab.</p>
  <p>Click the tilde with a modifier to close the window.</p>
</div>

<button id="open">Open Terminal</button>
<button id="lights">Light Switch</button>

<script src="socket.io/socket.io.js"></script>
<script src="term.js"></script>
<script src="options.js"></script>
<script src="tty.js"></script>
<script src="user.js"></script>
js 파일과 css 파일들을 따로 추출해서 joinc 사이트에 박아 넣았다. 어려울게 없었다. 다음 websocket에 연결하는 부분을 수정해야 한다. 유저가 해당 페이지에 연결하면 즉시 socket.io를 이용해서 tty.js 서버에 연결을 한다.이때 tty.js가 실행할 값을 websocket handshake 파라메터로 넘기도록 수정했다. tty.js는 docker로 파라메터의 명령을 실행한다. 아래와 같이 socket.io.js 파일을 수정했다.
  io.connect = function (host, details) {
    var uri = io.util.parseUri(host)
      , uuri
      , socket;

    if (global && global.location) {
      uri.protocol = uri.protocol || global.location.protocol.slice(0, -1);
      uri.host = uri.host || (global.document
        ? global.document.domain : global.location.hostname);
      uri.port = uri.port || global.location.port;
    }

    uuri = io.util.uniqueUri(uri);

    var options = {
        host: uri.host
      , secure: 'https' == uri.protocol
      , port: uri.port || ('https' == uri.protocol ? 443 : 80)
      , query: "type="+$("#shell").val() || ''    // 이 부분을 수정했다.
    };
    // ......
jquery를 이용해서 id.sehll의 값을 type파라메터로 넘기는 코드다.

백앤드

유저가 "터미널 실행" 버튼을 클릭하면, pty.fork() 메서드를 이용해서 프로세스를 실행한다. pty.fork() 메서드를 아래와 같이 수정했다.
Session.prototype.handleCreate = function(cols, rows, func) {
  var self = this
    , terms = this.terms
    , conf = this.server.conf
    , socket = this.socket;
    // ......

  shell = socket.handshake.query.type
  term = pty.fork("docker", ["run", "--net=none", "--cpu-period=100000", "--cpu-quota=50000", "-m=256m", "--rm", "-it", "ubuntu", shell], {
    name: conf.termName,
    cols: cols,
    rows: rows,
    cwd: conf.cwd || process.env.HOME
  });
이제 docker를 실행하고 python3 interactive 터미널을 웹에서 볼 수 있다.

손봐야 할 것들

python으로 할 수 있는 모든 것을 할 수 있다. 어차피 컨테이너로 격리되기 때문에 별 상관 없겠지라고 생각 할 수 있겠으나 디스크를 모두 써버리는등의 코드를 실행 할 수 있다. 결국 volume도 격리 해야 한다. btrfs나 zfs 등으로 파일시스템에 대한 quota를 설정하거나 제한된 크기의 블럭디바이스(qemu image등의)를 제공해야 한다.

그리고 ubuntu 이미지는 너무 많은 것을 할 수 있다. 용도에 맞는 최소한의 기능만을 가진 컨테이너를 제공해야 한다. busybox같은 이미지를 커스터마이징 해서 사용해야 할 것이다.

이래 저래 신경쓸 것들이 상당히 많다.