WebSSHっぽいことをしてみる。

WebSocket経由でSSHの操作を行います。 といっても、今回行うのはChromeのSource ShellのようにWebブラウザをSSHとして使うのではなく、 あくまでも、ウェブ経由でサーバ側のSSHプロセスを操作するというものです。

今回参考にしたのは、githubにあるこのリポジトリ
https://github.com/aluzzardi/wssh
WSSHってなんかマルウェアの名前みたい、というか使い方によってはまるっきりマルウェアなんでしょうけど...
ここで必要なの物は以下の3つ、なんとすべてpythonパッケージで構築ができます。 ・Webサーバ -> Flask ・WebSocket -> genevt ・sshクライアント -> paramiko Flaskは軽量なWebフレームワークライブラリ(有名どころでいうと、djangoRuby on Rails見たいなもの)
geventは並行ライブラリ、socketを利用したネットワークアプリケーションを作成するときに便利です。
paramikoはpythonSSH接続ができるようになるライブラリ、pythonのttyライブラリと組み合わせることで、 linuxSSHクライアントと同等の動作が可能になります。

Flask

基本的な使い方はこう

from flask import Flask, request, abort, render_template
from jinja2 import FileSystemLoader
import os
app = Flask(__name__) #インスタンスの作成

@app.route('/')#ルートにアクセスした場合の処理
def index():
    return render_template('index.html') #index.htmlがブラウザに表示される。

if __name__ == "__main__":
    #rootpathの指定と、何かしらファイルを参照する際のtemplatesフォルダを指定する。
    #index.htmlはtemplatesフォルダに入れておくと、render_template(~~~)で簡単に参照できる。
    root_path = os.path.dirname('/')
    app.jinja_loader = FileSystemLoader(os.path.join(root_path,'templates'))
    app.run() #サーバの立ち上げ

デフォルトではlocalhost:5000向けにサーバが立ち上がります。 テンプレートディレクトリの指定などはdjangoに似ていてわかりやすいです。

gevent

pythonでウェブソケットを実装しようとして始めて知ったが、このgeventがかなり使いやすいです。 基本的には、複数の処理を並列して実行可能にするライブラリと考えて良いと思います。 バックエンドで何らかの待ちうけ処理を行い、かつ、その裏で他のプログラムを実行したい場合に超有効。 ネットワークアプリなんかは典型的な例です。 今回はwebsocket -> sshssh -> websocketの入出力をgeventで並列実行する。 簡単に使い方を紹介。

from flask import Flask, request, abort, render_template
from werkzeug.exceptions import BadRequest
from gevent import monkey
monkey.patch_all()
import gevent

import socket
import time

app = Flask(__name__)


class WSSHBridge(object):
    def __init__(self,websocket):
        self._websocket = websocket
        self._tasks = []

    def _forward_inbound(self):
        try:
            while True:
                data = self._websocket.receive()
                if not data:
                    return
                print data
        finelly:
            self.close()
    
    def _forward_outbound(self):
        try:
            while True:
                time.sleep(5)
                self._websocket.send("return message")

    def _bridge(self):
        self._tasks = [
            gevent.spawn(self._forward_inbound),
            gevent.spawn(self._forward_outbound)
        ]
        gevent.joinall(self._tasks)
    
@app.route('/')
def index():
    bridge = WSSHBridge(request.environ['wsgi.websocket'])
    bridge._bridge()

if __name__ == "__main__":
    #app.run()
    from gevent.pywsgi import WSGIServer
    from geventwebsocket.handler import WebSocketHandler
    from jinja2 import FileSystemLoader
    import os
    root_path = os.path.dirname('/')
    app.jinja_loader = FileSystemLoader(os.path.join(root_path,'templates'))
    http_server = WSGIServer(('0.0.0.0',5000),app,log=None,handler_class=WebSocketHandler)
    try:
        http_server.serve_forever()
    except KeyboardInterrupt:
        pass

上記はwebsocketでクライアントから送られてくるメッセージをprintで標準出力し、サーバ側は5秒おきにOKの返事を返す仕組みです。
geventで並列動作しているので、両者が独立して動くきます。
(何のためのプログラムなんでしょうか...まあなんかいろいろできそうですね。) wscatを使えば簡単にウェブソケットを試せるのでお勧めです。
(ちょっと試験したいだけなのに、javascriptで書くはめんどくさいので。。。)

wscat --connect ws://localhost:5000/

ssh

いよいよSSH接続をして見ます。 pythonSSH接続をするにはparamikoがお勧めです。
何よりも簡単だし、SSHのプロセスを理解していない私でも使えます。 ちなみに、paramikoを使えばSCPやSFTPなんかもできちゃいます。 下記は指定のサーバにssh接続して、コマンドを実行する例。

import paramiko
from paramiko import PasswordRequiredException
from paramiko.dsskey import DSSKey
from paramiko.rsakey import RSAKey
from paramiko.ssh_exception import SSHException


def open(self, hostname, port=22, username=None,password=None,private_key=None, key_passphrase=None, allow_agent=False,timeout=None):
    pkey=None
    self._ssh.connect(
        hostname=hostname,
        port=port,
        username=username,
        password=password,
        pkey=pkey,
        timeout=timeout,
        allow_agent=allow_agent,
        look_for_keys=False)


ssh = pramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
open('localhost',22,'root','password')
stdin, stdout, stderr = ssh.exec_command('ls -l')

set_missing_host_key_policyは接続した際に、サーバホストキーがなかった場合の挙動を定義しています。 よく、最初にサーバへsshするときに、「~~~~yes no」みたいに聞かれますよね? あれです。今回はここで処理がとまると面倒なので、paramiko.AutoAddPolicy()で自動的にホストキーを追加しています。 いつもの質問に自動的にyesって答えるということです。
connect()でSSHのコネクションを張ります。 connectの中のパラメータはサーバ側に設定したものにあわせて設定、今回は鍵の設定をしていないのでほとんどNoneですが
最後、コマンドの実行はssh.exec_command()です。
実行結果はstdoutに帰ってきます。

Websocket経由でSSH

パーツが揃ったので、いよいよ本題の「WebSocket経由でSSH」を実行してみます。
といってもやることは簡単で、websocketで受け取った入力をコマンドとしてparamikoで実行して、 その結果をまたwebsocketでクライアントに返すだけです。
下記のプログラムを実行すると、5000番ポートでHTTPリクエストを待ち受け、ブラウザ(index.html)とpythonプロセスとの間でwebsocketを使ったやり取りをしています。

from gevent import monkey
monkey.patch_all()

from flask import Flask, request, abort, render_template
from werkzeug.exceptions import BadRequest
import gevent
from gevent.socket import wait_read, wait_write

import paramiko
from paramiko import PasswordRequiredException
from paramiko.dsskey import DSSKey
from paramiko.rsakey import RSAKey
from paramiko.ssh_exception import SSHException

import socket
import time

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/web/')
def webS():
    bridge = WSSHBridge(request.environ['wsgi.websocket'])
    bridge.execute()

class WSSHBridge(object):
    def __init__(self,websocket):
        self._websocket = websocket
        self._ssh = paramiko.SSHClient()
        self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        self.open('localhost',22,'root','password')
        self._transport = self._ssh.get_transport()
        self._channel = self._transport.open_session()
        self._channel.get_pty('xterm')
        self._tasks = []

    def open(self, hostname, port=22, username=None,password=None,private_key=None, key_passphrase=None, allow_agent=False,timeout=None):
        pkey = None
        self._ssh.connect(
            hostname=hostname,
            port=port,
            username=username,
            password=password,
            pkey=pkey,
            timeout=timeout,
            allow_agent=allow_agent,
            look_for_keys=False)

    def _forward_inbound(self):
        try:
            while True:
                data = self._websocket.receive()
                self._channel = self._transport.open_session()
                self._channel.get_pty('xterm')
                self._channel.exec_command(data)
                while True:
                    data = self._channel.recv(1024)
                    self._websocket.send(data)
                    if len(data) == 0:
                        break
        finally:
            self.close()

    def _bridge(self):
        self._channel.setblocking(False)
        self._channel.settimeout(0.0)
        self._tasks = [
            gevent.spawn(self._forward_inbound)
        ]
        gevent.joinall(self._tasks)

    def execute(self):
        self._bridge()
        self._channel.close()

    def close(self):
        gevent.killall(self._tasks, block = True)
        self._tasks = []
        self._ssh.close()

if __name__ == "__main__":
    from gevent.pywsgi import WSGIServer
    from geventwebsocket.handler import WebSocketHandler
    from jinja2 import FileSystemLoader
    import os
    root_path = os.path.dirname('/')
    app.jinja_loader = FileSystemLoader(os.path.join(root_path,'templates'))
    http_server = WSGIServer(('0.0.0.0',5000),app,log=None,handler_class=WebSocketHandler)
    try:
        http_server.serve_forever()
    except KeyboardInterrupt:
        pass

さらに、このpythonプロセスとwebsocketで通信するjavascriptのプログラム(index.html)が以下
これはただ単純にwebsocketのチャットプログラムで、チャット相手がSSHサーバになったと考えればよい感じ。

<!DOCTYPR html>
<html>
<head>
    <title>WebSSH</title>
    <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    <script type="text/javascript">
        var uri = "ws://"+ location.host +"/web/";
        console.log(uri);
        var webSocket = null;

        function init(){
            $("[data-name='message']").keypress(press);
            open();
        }

        function open(){
            if(webSocket == null){
                webSocket = new WebSocket(uri);
                webSocket.onopen = onOpen;
                webSocket.onmessage=onMessage;
                webSocket.onclose = onClose;
                webSocket.onerror = onError;
            }
        }

        function onOpen(event){
            chat("connected");
        }
        function onMessage(event){
            if(event && event.data){
                chat(event.data);
            }
        }

        function onError(event){
            chat("error");
        }

        function onClose(event){
            chat("connection closed. "+ event.code);
            webSocket = null;
            setTimeout("open()",3000);
        }

        function press(event){
            if(event && event.which == 13){
                $("[data-name='chat']").append("<div>root@ubuntu# "+$("[data-name='message']").val()+"</div>");
                var message = $("[data-name='message']").val();
                if(message && webSocket){
                    webSocket.send(""+message);
                    $("[data-name='message']").val("");
                }
            }
        }

        function chat(message){
            var chats = $("[data-name='chat']").find("div");
            while (chats.length >= 100){
                chats = chats.last().remove();
            }
            var msgtag = $('<div>').text(message);
            $("[data-name='chat']").append(msgtag);
        }
        $(init);
    </script>
</head>
<style>
body{
    background-color:#000000;
    color:#00ff00;
    font-family:'arial black';
}
input[type="text"], textarea {
    background-color : #000000;
    color:#00ff00;
    font-family:'arial black';
}
input{
    border:#000000;
}
</style>
<body>
    <div data-name="chat"></div>
    root@ubuntu#
    <input type="text" data-name="message" size="100"/>
    <hr />
</body>
</html>

こんな感じで動きます。ttyを転送してるわけじゃないから、viエディタとか使えませんけどね... f:id:m-masataka:20161102170735p:plain

vim tabキーをスペース4つにする。

vimの設定

ubuntuの場合/etc/vim/vimrcに設定を入れる。
(ここを編集すると全ユーザでvimの設定が変更されるので注意)

augroup vimrcEx
  au BufRead * if line("'\"") > 0 && line("'\"") <= line("$") |
  \ exe "normal g`\"" | endif
augroup END

set tabstop=4
"set autoindent
set expandtab
set shiftwidth=4

OpenWRT Closs Compile メモ

OpenWRTをインストールできたので、OpenWRT上で動くアプリケーションをコンパイルする。
OpenWRTが動いているルータ(TP-Link WDR4300 N750)はディスク容量もメモリもかなり少ないため、ルータ上でプログラムをコンパイルすることができない。 そのため、別のLinuxサーバでクロスコンパイル環境を構築し、ソースコードをビルドする必要がある。
今回はOpenWRT上で最新版のHostapdを動かすことを目標に、クロスコンパイルを実行していく。

OpenWRT SDKの準備

OpenWRT SDKとはOpenWRT用のクロスコンパイル環境が一式そろったものである。 それぞれのバージョン・アーキテクチャごとにSDKが用意されているので、自分のバージョンに合ったものをダウンロードする。
(ファームウェアイメージと同じ場所にある)

linux上でファイルを解凍。   名前が長いので変更しました。

root@ubuntu # tar -xvf OpenWrt-SDK-15.05-ar71xx-generic_gcc-4.8-linaro_uClibc-0.9.33.2.Linux-x86_64.tar.bz2
    ...
    ...
    ...
root@ubuntu # mv OpenWrt-SDK-15.05-ar71xx-generic_gcc-4.8-linaro_uClibc-0.9.33.2.Linux-x86_64.tar.bz2 openwrt
root@ubuntu # ls -al
    drwxr-xr-x 12 root root     4096 Aug 17 14:40 openwrt

実はクロスコンパイル環境はこれで準備完了。
SDKが用意されているのは便利ですね。
本来ならば、自分の環境のSDKはgitのソースから自分でビルドする必要があります。
やり方は公式ページのDeveloper Guideを参照
https://wiki.openwrt.org/doc/playground/developer

"Hello,World!"のコンパイル

まずは簡単なプログラムからビルドする。

root@ubuntu # cd openwrt
root@ubuntu # mkdir helloworld
root@ubuntu # cd helloworld

適当に作業用ディレクトリを作成し、その中にc言語のプログラムを作成

#include <stdio.h>
int main(void)
{
        printf("Hello World\n");
        return 0;
}

このコードをコンパイルするのだが、Host環境のgcc(/usr/bin/gcc)でコンパイルしてはいけない。
今回はSDKの中にあるgccコンパイルを実行する。

root@ubuntu # ../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/bin/mips-openwrt-linux-gcc -o helloworld.o helloworld.c
root@ubuntu # ls
    helloworld.o helloworld.c 
root@ubuntu # ./helloworld.o
    -su: ./helloworld.o: cannot execute binary file: Exec format error

出来上がったファイルをHost環境で実行するとformat errorで実行できない。 これはOpenWRT環境で動くファイルなので当然である。 出来上がったファイルをOpenWRT化されているルータに移す。

root@ubuntu # scp helloworld.o root@192.168.1.1:

OpenWRTルータ内でファイルを実行

root@OpenWRT # chmod a+x helloworld.o
root@OpenWRT # ./helloworld.o
     Hello World

実行できた。

Hostapdのコンパイル

OpenWRTで使われていたhostapdは最新versionでは無く、hostapd_cliも使えない。
何か開発しようとすると使い勝手が悪いので、今回は最新版のhostapdをOpenWRT上で動かせるようにコンパイルを行う。

hostapdをダウンロードして解凍。わかりやすいようにopenwrtディレクトリ配下に入れておく。

root@ubuntu # wget https://w1.fi/releases/hostapd-2.5.tar.gz
root@ubuntu # tar -zxvf hostapd-2.5.tar.gz
root@ubuntu # mc hostapd-2.5 openwrt/hostapd

ほとんどのMakefile

ifndef CC
CC=/usr/bin/gcc
endif
...
...
...

という感じで、明示的にコンパイラを指定しない限りはHostのコンパイラを使用するようになっている。
したがってMakefileがクロスコンパイル環境のgccを読み込んでくれるようにCCを明示しておく。

root@ubuntu # export OPENWRT_HOME=[openwrt直下のディレクトリ]
root@ubuntu # export CC=$OPENWRT_HOME/staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/bin/mips-openwrt-linux-gcc

さて、
これでmakeを実行すればhostapdのコンパイル完了。
というわけにはいかなかった。
hostapdのmakeopenwrt/hostapd/hostapd/直下で行う。

root@ubuntu # cd hostapd/hostapd/
root@ubuntu # make
mips-openwrt-linux-gcc: warning: environment variable 'STAGING_DIR' not defined

まずこんなエラーがたくさん出てくるので、言われているとおりにSTAGING_DIRを設定する。

root@ubuntu # export STAGING_DIR=$OPENWRT_HOME/staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2

再びmake

root@ubuntu # make
....
....
  CC  ../src/drivers/driver_hostap.c
../src/drivers/driver_nl80211.c:17:31: fatal error: netlink/genl/genl.h: No such file or directory
 #include <netlink/genl/genl.h>
                               ^
compilation terminated.
make: *** [../src/drivers/driver_nl80211.o] Error 1

やはりというか、途中でとまりました。 クロスコンパイル環境なので、いろいろとファイルが足りないことがある。 今回のようにヘッダーファイルが無いだけならHost環境の/usr/include/などからひっぱてくればよい。
以下のようにssl系とnetlink系のヘッダファイルが無いというエラーがでたので、これらを引っ張ってくる。

root@ubuntu # cp -r /usr/include/netlink/ ../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/usr/include/
root@ubuntu # cp -r /usr/include/openssl/ ../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/usr/include/
root@ubutnu # cp /usr/include/x86_64-linux-gnu/openssl/opensslconf.h ../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/usr/include/openssl/

問題なのは共有ライブラリが無かった場合。

root@ubuntu # make 
.....
.....
../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/bin/../lib/gcc/mips-openwrt-linux-uclibc/4.8.3/../../../../mips-openwrt-linux-uclibc/bin/ld: cannot find -lnl
../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/bin/../lib/gcc/mips-openwrt-linux-uclibc/4.8.3/../../../../mips-openwrt-linux-uclibc/bin/ld: cannot find -lssl
../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/bin/../lib/gcc/mips-openwrt-linux-uclibc/4.8.3/../../../../mips-openwrt-linux-uclibc/bin/ld: cannot find -lcrypto
collect2: error: ld returned 1 exit status
make: *** [hostapd] Error 1

普通このようなエラーが出た場合はapt-getとかでライブラリをとってくれば解決なのだが、クロスコンパイル環境では頑張ってネットからライブラリを探すか、ソースからビルドする必要がある。
ちなみに共有ライブラリは

  • -lnl => libnl.so
  • -lssl => libssl.so
  • -lcrypto => libcrypto.so

というように読み替える。この.soファイル(バイナリファイル)を参照できるようにする必要がある。   libssl.solibcrypto.soはOpenwrt SDKの中に見つかったので、参照できる場所にコピーする。

root@ubuntu # ls -l ../../staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libssl*
lrwxrwxrwx 1 root root     15 Jul 25  2015 ../../staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libssl.so -> libssl.so.1.0.0
-r-xr-xr-x 1 root root 381267 Jul 25  2015 ../../staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libssl.so.1.0.0
root@ubuntu # ls -l ../../staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libcrypto*
lrwxrwxrwx 1 root root      18 Jul 25  2015 ../../staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libcrypto.so -> libcrypto.so.1.0.0
-r-xr-xr-x 1 root root 1765806 Jul 25  2015 ../../staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libcrypto.so.1.0.0
root@ubuntu # cp ../../staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libssl.so.1.0.0 ../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/usr/lib/
root@ubuntu # cp ../../staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libcrypto.so.1.0.0 ../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/usr/lib/
root@ubuntu # cd ../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/usr/lib/libcrypto.so.1.0.0
root@ubuntu # ln -s libssl.so.1.0.0 libssl.so
root@ubuntu # ln -s libcrypto.so.1.0.0 libcrypto.so

共有ライブラリは新しいバージョンの.soファイルにリンクする形になっているので、ここでも同じ形式で管理するのがよい。

libnl.soは見つからない。調べてみると、OpenWRT自体libnlは以下のライブラリしか扱っていないらしい。 Name Size Description libnl-core 37K Common code for all netlink libraries libnl-genl 8K Generic Netlink Library Functions libnl-nf 25K Netfilter Netlink Library Functions libnl-route 91K Routing Netlink Library Functions

libnl-3を使ってもコンパイルが失敗する。 仕方が無いので、libnlのversion1のソースをダウンロードしてコンパイルすることにする。

root@ubuntu # wget https://www.infradead.org/~tgr/libnl/files/libnl-1.1.4.tar.gz
root@ubuntu # tar -zxvf libnl-1.1.4.tar.gz
root@ubuntu # cd libnl-1.1.4/
root@ubuntu # ./configure
root@ubuntu # make 

これもちゃんとクロスコンパイル環境のgccコンパイルする。 出来上がった.soファイルをその他2つと同じようにSTAGING_DIR/usr/lib/の中に入れて再びmakeをすればOK。
あとは出来上がったhostapdhostapd_cliをOpenWRTルータに移して実行できる。

結構長かったけど、お疲れ様でした。

makeファイルをデバック

makeファイルをデバックしたいときはこんな感じでコンソールに出力できる。
読み込んでいるライブラリとかをまとめて確認したいときは便利。 dummy := $(shell echo 表示したい内容 1>&2)

Go でAPIを書く

目標は
JSONを受け取る→{何らかの処理}→処理結果をJSON形式で返す
これだけのAPIを作ります。 以下のページを参考にしました。 thenewstack.io

パッケージ

まずはインポートするパッケージ

    import (
            "fmt"  
            "html"  
            "log" 
            "net/http"  
            "encoding/json"  
            "github.com/gorilla/mux"  
    )

"fmt"は標準出力に関するパッケージ。
"net/http"は簡単にhttpサーバを実装してくれるパッケージ。
"html"はhtml文をトークン化&解析するもの。
"encoding/json"はスライスなどをJSONエンコードしてくれるパッケージ。
そして最後に"github.com/gorilla/mux"。これは外部パッケージで、httpリクエストが来た時に、リクエストをGoで定義された関数までルーティングしてくれる。無くてもよいのだけど、とても便利なパッケージである。
外部パッケージを使う場合は現在プログラムを書いているディレクトリで

    go get

を実行する必要があります。 ※"gorilla/mux"を入れない場合はデフォルトのmux(multiplexer)によってルーティングを書くのだけど、少しめんどくさそう...詳しく知りたい人は以下を参照

m0t0k1ch1st0ry.com

メイン文

メイン文はこんな感じ。

    func main(){
            router := mux.NewRouter().StrictSlash(true)
            router.HandleFunc("/",Index)
            router.HandleFunc("/todos/{todoId}",TodoShow)
            log.Fatal(http.ListenAndServe(":8080",router))
    }

外部パッケージの"gorilla/mux"を使ってhttpルータを定義します。
router.HandleFuncによってhttpリクエストをそれぞれの関数にルーティングしてあげます。

    router.HandleFunc("{リクエストURL}",{関数名})

って感じの使い方です。
http.ListenAndServe(":8080",router)で8080番ポートでhttpサーバを起動させます。ここの第2引数routerの部分はhandlerを入れる部分、"gorilla/mux"等のhttpルータを使ってなければnilで埋めれば良いらしい。

関数の作成

    func Index(w http.ResponseWriter, r *http.Request){
            fmt.Fprintf(w,"Hello, %q",html.EscapeString(r.URL.Path))
    }
    func TodoShow(w http.ResponseWriter , r *http.Request){
            vars := mux.Vars(r)
            todoId := vars["todoId"]
            fmt.Fprintln(w,"Todo show:",todoId)
    }

今回はリクエストに応じて2つの関数を作りました。(参考ページに乗っているサンプル通りだけど...)
はじめの関数IndexはURLのパスを返すだけの関数。ここにhtmlのレスポンスを書けば、ウェブページが作れる。
2つ目の関数TodoShowはmain文で受け取ったURLの{todoId}を表示する関数になっている。
URLの{todoId}の部分は任意の文字列を入れることができる。
たとえば http://localhost:8080/todos/hogehoge というようにリクエストを送ると、todoIdの部分にはhogehogeが入ってきて、Todo show:hogehogeという文字列が出力される。

JSONでデータを受け取る

ここからは少し応用編
JSON形式のデータを受け取る部分に入る。
http.requestの中で送られてくるデータはhttp.Request.Bodyの中に入っている。 今回はBodyの中身を一度Stringで読み込んで、それをJSONファイルとしてデコードする。(あまりスマートじゃない気がする)

    import (
            "fmt"
            "html"
            "log"
            "net/http"
            "encoding/json"
            "io"
            "bufio"
     
            "github.com/gorilla/mux"
    )
    
    type POSTJSON struct{
            Board string "json:\"board\""
            Player int "json:\"player\""
    }
       
    func TodoIndex(w http.ResponseWriter,r *http.Request){
            request := ""
            rb := bufio.NewReader(r.Body)
            for {
                    s, err := rb.ReadString('\n')
                    request = request + s
                    if err == io.EOF { break }
            }
            u := new(POSTJSON)
            json.Unmarshal(([]byte)(request),u)
            fmt.Println(u.Board)
    }

まず、importパッケージの中に"io""bufio"を追加している(データ読み込みのため)。
forの中身でbufio.NewReader(r.Body)をstringへ変換している。ここまででstringにデータを変換できたので、無理してJSONとして読み込む必要はないきがするが、、、
JSONデータを受けるためには、受け皿になる構造体が必要なので、type POSTJSON structでそのPOSTJSON(名前は何でも良い)構造体を定義している。
u := new(POSTJSON)によって構造体を初期化。
ここで重要な関数Unmarshalの登場。この関数により、stringをbyteに変換し構造体の中にマップする。
これで、送られてきたJSONデータを構造体へと変換することができたので、データへの簡単なアクセスが可能になった。 ちなみに、htmlのFORMで送られてきたデータを受け取りたいだけなら

    r.ParseForm()

でリクエストの中身を解析して、

    for k, v := range r.Form {
            fmt.Println("key:", k)
            fmt.Println("val:", strings.Join(v, ""))
        }

こんな感じで中身を取り出すことができるらしい。JSONでもこんな感じでできないか探してます。

フォームの入力を処理する | build-web-application-with-golang

JSONデータを返す

あとはJSONデータを返すだけ。これは先ほどとは逆に構造体→JSONという形で変換ができます。

    type Todo struct{
            Name string
            Completed bool
            Num int
    }
    type Todos []Todo

    func TodoIndex(w http.ResponseWriter,r *http.Request){
        type Todos []Todo
    
        todos := Todos{
                    Todo{Name: "Host meetup", Completed: true , Num: 1},
                    Todo{Name: "KEET", Completed: false , Num: 2},
            }
            json.NewEncoder(w).Encode(todos)
    }

上記では構造体配列を使っている。このようにjson.NewEncoder(w).Encode(todos)により構造体配列も簡単にJSONに変換することができる。さらに、http.ResponseWriterに変換したJSONを入れているので、リクエスト先にJSONでレスポンスが返される。

これでJSONを受け取ってJSONを返すAPIが完成?!
お疲れ様でした。

TP-Link WDR4300 N750にOpenWRTを入れてみる。

前回、Buffalo WSR-600DHPをOpenWRT化することに成功したが、Wi-Fi設定の部分で見事に躓いてしまった。
* WSR-600DHPはMediaTekチップセット(MT7603E)を使っており、このチップセット対応のファームウェアがWSR-600DHP用に作成されていない。つまり、WSR-600DHPで5GHz帯のWi-Fiを使いたいのであれば、自分でファームウェアをビルドする必要がある。(難しかったので断念...) そこで、Buffalo ルータはサクッと諦めて、別のルータを購入しOpenWRT化する。
今回使ったのはTP-Link WDR4300 N750
(5GHz帯がOpenWRTで使用できるルータはかなり少ないし、日本で売っているのかは謎。TP-Linkの最新の無線LANルータはArcher 7である。Archer 7チップセットファームウェアとしてAth10kを使用しているが、Ath10kはまだOpenWRTでサポートされていない。よって、今回はTP-Linkの最新版からは3世代ほど前の型であるWDR4300を使用した。)

WDR4300のWebUIからOpenWRT化 → 失敗

OpenWRTの公式ページにはTP-LinkのWebUIからFirmware UpgradeでOpenWRTの...factory.binファイルを選択し、Upgradeボタンを押すとOpenWRTにアップグレードされるよと書いてある。
しかし、TP-linkはそんなに甘くは無いんだな。 見事に失敗

Upgrade unsuccessfully because the version of the upgraded file was incorrect. Please check the file name.  

こんなエラーメッセージが....
どうしたものかなこれ
いろんなバージョン.binファイルを試したが、同じエラーが出続ける。挙句の果てにはTP-Link公式ページからダウンロードしてきたファームウェアもアップグレード失敗(T.T)....
軽く怒りがこみ上げてくるが、こんな事では諦めない。

TFTPサーバからOpenWRT化 → 失敗

続いてはTFTPサーバからのブートを試す。

assign 192.168.0.66 to your local network interface (the router uses 192.168.0.86)
publish a firmware image via tftp: cp openwrt-ar71xx-generic-tl-wdr4300-v1-squashfs-factory.bin/srv/tftp/wdr4300v1_tp_recovery.bin
configure your tftp server
wait for the firmware transfer (about 20s), firmware flash (about 90s) and subsequent reboot (about 30s)

OpenWRTの公式にはこんなことが書いてあるので、その通りに実装(Tftp64っていうソフトでWindowsを簡単にTFTPサーバにできます。)
が、ぜんぜんだめ

[http://www.friedzombie.com/tplink-stripped-firmware/]

この記事によると、TP-Linkは最近になって正規のファームウェアしかアップデートできないようなってるようである...
これは難敵、YouTubeでアップデート手順を見てみると、

  • ルータをこじ開け
  • 無理やりシリアルコンソールを繋ぐ
  • シリアルポートからブートを強制中断
  • 無理やりブート対象を書き換え

なんていう荒業をしている動画があった
(できればそんなことやりたくないね。)


Bricked TP-Link WDR4300 Router Recovery Using UART Serial Converter Part #1

さらに検索検索。

ネット上にこんな一言が、
「DD-DRTにならアップグレードできるよ」

そんなことがあるのか?

DD-WRT化の後にOpenWRT化 → 成功

とりあえず、DD-WRTのbinファイルを公式ホームページから取ってくる。よくよくネットを見ていると、最新バージョンより2~3個前のほうが安定しているらしいので、最新よりも2~3個前のDD-WRTのbinファイルを取ってくる。DD-WRTはファームの作成年月日が細かく分かれていて、使いやすそう。
※同じTP-Linkルータでも、ファームウェアversionが違うことがあるらしく(今回はver1.7)versionによってアップデートできるファームウェアが違うっぽい。今回は下記のDD-WRTファームウェアでアップデートできたが、Versionによっていろいろ試す必要がありそう。

[ftp://ftp.dd-wrt.com/betas/2016/07-01-2016-r30082/tplink_tl-wdr4300v1/]

今回使用したのは上記のURLの中にある"factory-to-ddwrt-us.bin"っていうファイル。これをTP-LinkのWebUIからアップデート。

なんとアップデート成功。後は下記のリンクのやり方にしたがって、ファームをDD-WRTからOpenWRTに書き換えるだけ。

[http://www.madox.net/blog/2013/03/10/how-to-install-openwrt-on-a-tl-wr703n-that-came-with-dd-wrt/]

で、telnet 192.168.1.1でDD-WRTコマンドラインに入れるのだけれど、sshで入りたかったらUIで設定できるみたい。 あとは、wgetかなんかでopenwrtのファームウェアをダウンロードして(....factory.binとかいうやつ)
以下のコマンドでファームウェアアップグレード

root@DD-WRT:/tmp# mtd -r write openwrt-ar71xx-generic-tl-wdr4300n-v1-squashfs-factory.bin linux
Unlocking linux ...
Writing from openwrt-ar71xx-generic-tl-wr703n-v1-squashfs-factory.bin to linux ... [e]
Connection closed by foreign host.

http://d.hatena.ne.jp/from_kyushu/20080917/1221581890

また、アップグレードしたい場合は

sysupgrade -n openwrt-ramips-mt7621-wsr-600-squashfs-sysupgrade.bin

非常に長かったが、最後はあっけなかった。お疲れ様でした。

OpenWRT wireless 設定編

OpenWRTのWiFiを設定してみる

Buffalo WSR-600DHPをOpenWRT化することができたので、早速動かしてみようと思ったが、無線機能が動いていない!?
ここからWi-Fiができるようになるまで奮闘する。
※奮闘した結果、まだ動いていませんorz....... OpenWRTの操作の参考になればと思います。 OpenWRT wiki にある通りに

#uci show wireless

uciを見ても何も表示されない。/etc/config/wirelessに設定を書く見たいだけど、そもそも/etc/config/wirelessのファイルが無かった。 これでは何もおきないはずなので、とりあえずこのファイルを作成することに。
wikiではこんな感じになってる。

#config 'wifi-device' 'wl0'
    option 'type'    'broadcom'
    option 'channel' '6's

typeとかwifi-deviceとかは自分の環境に合わせる必要がありそう。 必要そうなパッケージは入れておく。

#opkg install pciutils
#opkg install iwinfo

とりあえずルータの持っているwireless deviceを調べてみる。

root@OpenWrt:~# lspci
00:00.0 PCI bridge: Device 0e8d:0801 (rev 01)
00:01.0 PCI bridge: Device 0e8d:0801 (rev 01)
01:00.0 Network controller: Ralink corp. RT3091 Wireless 802.11n 1T/2R PCIe
02:00.0 Network controller: MEDIATEK Corp. Device 7603

チップセットは正しく認識されている様子。
「Ralink corp. RT3091 Wireless」用のモジュールをインストール

root@OpenWrt:~# opkg install kmod-rt2800-pci wireless-tools

上記でインストールされない場合は

https://downloads.openwrt.org/chaos_calmer/15.05.1/ramips/mt7621/packages/base/ からkmod-rt2800-pci_3.18.23+2015-03-09-3_ramips_1004kc.ipkをダウンロードして
同じく" opkg install ファイル名 "でインストールできるはず。
iw listコマンドでデバイスが正しく認識されていることがわかる。

root@OpenWrt:~# iw list
Wiphy phy0
        max # scan SSIDs: 4
        max scan IEs length: 2257 bytes
        Retry short limit: 7
        Retry long limit: 4
        Coverage class: 0 (up to 0m)
        Available Antennas: TX 0 RX 0
        Supported interface modes:
        .....
        .....

さらにlsmodで今まで「mac80211 381619 1 mt76pci」だったのが
「mac80211 381619 4 rt2800lib」になっていることが確認できる。
これで無線が使用できる。
はずだったが....
wireless signal がゼロになっているorz
以下デバックのために便利だったコマンド一覧

uci set wireless.@wifi-device[0].disabled=0; uci commit wireless; wifi
iw
iwconfig
iwinfo
iw dev wlan info
iw phy phy0 list
wifi up
wifi down
dmesg
logread /dev/log
uci show wireless

Buffalo WSR-600DHPにOpenWRTを入れる

ヨドバシカメラで購入したバッファロールータをOpenWRT化してみる

 SDNコントローラからコントロール可能な無線APが作りたかったので、OpenWRTを使ってみることにする。はじめはHostapdを使ってWhitebox switch的なものを作ろうとしていたが、2.4Ghzと5Ghzの両方をしゃべれるチップセットがかなり入手しにくかったのであきらめてOpenWRTを使うことにする。

OpenWRTにはOpen vSwichを入れることができるので、OpenFlowと連携が可能になる。また、市販のルータを使って実装できるのでかなり便利。

自分のルータがOpenWRTに対応しているかどうかはここで見ることができる。

https://wiki.openwrt.org/toh/start

 ここではBuffaloのWSR-600DHPを使用する。

buffalo.jp

準備として以下のことを行う。

 OpenWRTのファームウェアのダウンロード

OpenWRTのファームウェアは以下の公式サイトからダウンロードできる。

https://openwrt.org/

先ほどの対応機器一覧の画面から自身のルータを選び、その中にリンクのあるbinファイルをダウンロード。 公式サイトを読むと、ダウンロードしたbinファイルをルータの管理画面(WebUI)の「ファームウェア更新」のところでアップロードすればOpenWRTになると書いてあるが、今回のBuffaloルータだとそれが失敗してしまった。ほかのファームウェアでもやってみてだめだったので、別の方法を探すことにする。 参考にしたのは以下のサイト

http://www.srchack.org/article.php?story=20160503003415488

このサイトではtftpサーバからルータを起動しているようだ。

 サイトにしたがってopenwrt-ramips-mt7621-wsr-600-initramfs-kernel.binを入手

以下のサイトにあった。(自力で探しても見つからなかった....)

http://www.srchack.org/pub/openwrt/wsr-600dhp/r49177/openwrt-ramips-mt7621-wsr-600-initramfs-kernel.bin

「Tftp64」というWindowsftpサーバに変えてくれるツールを使う。

その後は下記の手順に従う。

  • 上記でダウンロードしたbinファイルの名前をfirmware_WSR-600DHP.ramに変更する。
  • tftpサーバを(アドレス:192.168.11.168)で公開する(サーバ上に上記.ramファイルを置く)
  • Windows PC(tftpサーバ)とバッファロールータをLANケーブルでつなげる。
  • Buffalloルータの電源を「AOSS」ボタンを押しながら入れる

これでBuffalloルータがOpenWRTとして起動してくれる。

あとはtelnetでルータに入り(192.168.1.1)以下のコマンドを実行

# fw_setenv dual_image 0

# sysupgrade -n openwrt-ramips-mt7621-wsr-600-squashfs-sysupgrade.bin

openwrt-ramips-mt7621-wsr-600-squashfs-sysupgrade.binはアップグレード用のファイルなので

https://downloads.openwrt.org/chaos_calmer/15.05/ramips/mt7621/

このあたりから適宜ダウンロードしてルータに配置する。

 最後にsshでログインできるようにrootパスワードを設定

 # passwd root

 

 

 

iptables -t nat -D POSTROUTING 1

 

iptables -t nat -A POSTROUTING -p TCP -s 192.168.122.0/24 -d 153.153.145.156/32 -j MASQUERADE --to-ports 1024-65535

ip netns exec test ip route

ip netns exec test route add default gw 192.168.122.1