ハニーポットを自宅に設置しVPS経由で公開する
下記を読んで自分もハニーポットを植えてみたいと思いました。
どうせなら色々なハニーポット試したいし、攻撃の様子もいい感じに可視化したい。
だったらT-Potを植えればいいじゃない!ということで環境を構築してみました。
環境構成
環境構築のしやすさと運用コストの観点から下記の構成を取りました。
VPSを経由して自宅のサーバを公開する方法です。
補足
・クラウドに設置するのが一番楽だが、T-Potの動作条件を満たそうと思ったらレンタル料が高額になってしまう。(RAMがネック)
・VPSを使わない場合、インターネット側から自宅サーバへの通信を通すのが難しい。
(参考)T-Potの動作条件(Standard Installation)
・6-8GB RAM
・128GB SSD
- system-requirements
注意事項
・事前にVPSの利用規約をお確かめください。
・運用には細心の注意を払ってください。
・当サイトに掲載された内容によって生じた損害等の一切の責任を負いかねますので、ご了承ください。
T-Potのインストール
インストール方法はいくつか公開されており、ネットで検索した感じPrebuilt ISO Imageを使う方が多いようです。
が、自分の環境ではPrebuilt ISO Imageだとインストールが上手くいかなかった(パーティションを認識してくれずエラー終了する)ので、Post-Install Userでインストールしました。
サーバとして用意した実機はLiva-Zです。
8Gメモリ x 2枚、120GB SSDを追加しました。
Debianのインストール
T-PotのREADMEにはDebian 9.7 (Stretch)が要件に書いてありますが、私はDebian 10.3を使いました。(よく確認せず最新をダウンロードしてしまった)
余計なエラー等にハマりたくない方は、なるべく要件に合わせたほうがよいでしょう。
概要
(1)Debianのインストール用ISOイメージをダウンロードしてメディアに焼く。
(2)インストール用メディアを用意したサーバに接続してメディアからブート。
(3)インストールガイドに従ってインストール実施。
今回、インストール時のパーティション構成は"/"にディスクの全容量を割り当てました。
T-Potのハニーポットはdockerコンテナで構成されており、デフォルトでは実データが/var/lib/docker配下に格納されます。
何も考えず/varにパーティションをきると/varがすぐにディスク100%になってしまうので注意です。
Debianのインストールはこちらから。
Downloading Debian CD/DVD images via HTTP/FTP
T-Potのダウンロード&インストール
下記記載の手順にしたがって実行します。
gitが必要なので事前にインストールしましょう。
https://github.com/dtag-dev-sec/tpotce#post-install-user
# apt install git # git clone https://github.com/dtag-dev-sec/tpotce # cd tpotce/iso/installer/ # ./install.sh --type=user
エディションはStandardを選択しました。
WEBの管理画面(Kibana)にログインするためのユーザ名+パスワードを設定します。
実行したシェルスクリプトが終了し再起動すればT-Potの完成です。
VPS-IX2105間のIPsec-VPN
VPSにSoftEther VPNをインストールして、下記を参考にして同様に設定していきます。
NEC ルータ等からの EtherIP を用いた VPN 接続方法 - SoftEther VPN プロジェクト
正常にVPNが張れているかコマンド等で確認しましょう。
IX2105-HOME(config)# show ike sa ISAKMP SA - 1 configured, 1 created Local address is 192.168.0.2, port is 4500 Remote address is 203.0.113.1, port is 4500 IKE policy name is ike-policy Direction is initiator Initiator's cookie is 0x**************** Responder's cookie is 0x**************** Exchange type is aggressive mode NAT-Traversal RFC3947 NAT detected at local side State is established Authentication method is pre-shared Encryption algorithm is aes-128 Hash algorithm is sha1 DH group is modp1024, lifetime is 550 seconds #ph1 success: 1, #ph1 failure: 0 #ph1 hash err: 0, #ph1 timeout: 0, #ph1 resend: 0 #ph2 success: 1, #ph2 failure: 0 #ph2 hash err: 0, #ph2 timeout: 0, #ph2 resend: 0 IX2105-HOME(config)# show ipsec sa IPsec SA - 1 configured, 2 created Interface is Tunnel0.0 Key policy map name is ipsec-map UDP encapsulation Tunnel mode, 4-over-4, autokey-map Local address is 192.168.0.2, port is 4500 Remote address is 203.0.113.1, port is 4500 Outgoing interface is GigaEthernet0.0 Interface MTU is 1422, path MTU is 1500 Inbound: ESP, SPI is 0x********(********) Transform is ESP-AES-128-HMAC-SHA-96 Remaining lifetime is 1445 seconds Replay detection support is on Outbound: ESP, SPI is 0x********(********) Transform is ESP-AES-128-HMAC-SHA-96 Remaining lifetime is 1445 seconds Replay detection support is on Perfect forward secrecy is off
VPSのファイアウォール
ひとまず、SSHのハニーポットであるCowrieだけ試したいので、T-Potへの転送は22番ポートだけ設定しています。
また、T-Potの管理用ポート(64294, 64295, 64297)へのアクセスはVPS経由 + VPS側で自宅のIPアドレスのみ受け付けるようにフィルタするようにしました。
$ sudo firewall-cmd --list-all public (active) target: default icmp-block-inversion: no interfaces: eth0 sources: services: ssh ports: protocols: masquerade: no forward-ports: port=22:proto=tcp:toport=22:toaddr=192.168.1.3 sourceports: icmp-blocks: echo-request rich rules: rule family="ipv4" source address="198.51.100.1" forward-port port="64294" protocol="tcp" to-port="64294" to-addr="192.168.1.3" rule family="ipv4" source address="198.51.100.1" forward-port port="64297" protocol="tcp" to-port="64297" to-addr="192.168.1.3" rule family="ipv4" source address="198.51.100.1" forward-port port="64295" protocol="tcp" to-port="64295" to-addr="192.168.1.3" rule family="ipv4" source address="198.51.100.1" port port="500" protocol="udp" accept rule family="ipv4" source address="198.51.100.1" port port="4500" protocol="udp" accept
動作確認
構築が終わったら、T-Potにアクセスできるか動作確認してみましょう。
下記は5日間程度動かしてみたときの様子です。
参考
- SoftEther VPN プロジェクト - SoftEther VPN プロジェクト
- Downloading Debian CD/DVD images via HTTP/FTP
- 【ハニーポット構築】初心者でもできた!ハニーポットの構築① ハニーポットの選定と場所の選定 - サイバーセキュリティはじめました
- GitHub - dtag-dev-sec/tpotce: 🍯 T-Pot - The All In One Honeypot Platform 🐝
- 集合住宅 LAN の内側にサーバを置いた話 (自宅サーバの思い出 Advent Calendar 2016) - Qiita
Linux PCのデスクトップ画面をブラウザから監視する
やりたいこと
こんな感じでブラウザから外部のPC(Linux)のモニターに表示している内容をできる限りリアルタイムで表示したい。
HLS(HTTP Live Streaming)
HLS(HTTP Live Streaming)を使えば、多少遅延はあるがほぼリアルタイムの映像をみることができるようです。Youtubeとかで良くあるLive配信的なやつですね。
具体的には、ffmpegを使ってLinux PCのモニターの表示内容(X11)を録画 + HLS用ファイルとして出力 + Nginx等のWebサーバを立てて、HLS用ファイルにアクセス可能にします。
ただ、HLSはAppleが開発した仕組みであり、現時点で標準サポートしているブラウザSafariだけっぽいです。
が、世の中には優秀な素晴らしい方々がおり、Video.jsを使えば他のブラウザでもLive Stremingの動画再生が可能になります(感謝)。
実施例
ここでは、Virtual-BOXにCentOS7(GUIモード) + WEBサーバ + HLSファイルの出力処理を構築 --> ホスト側のブラウザからCentOSのWEBサーバにアクセスし、CentOSのGUI画面を表示してみます。
事前準備
まずは必要なパッケージを取得します。
nginxのインストール
$ sudo vim /etc/yum.repos.conf.d/nginx.repo $ cat /etc/yum.repos.conf.d/nginx.repo name=nginx repo baseurl=http://nginx.org/packages/centos/7/$basearch/ gpgcheck=0 enabled=1 $ sudo yum -y install nginx
ffmpegのインストール
$ sudo yum -y install epel-release http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-5.el7.nux.noarch.rpm $ sudo yum -y install ffmpeg
WEBサーバの設定
今回の例ではhttp://<IPアドレス>/hlsにアクセスした際に動画が見えるようにします。
必要なDirectoryを掘ります。
$ sudo mkdir /var/www/html/hls $ sudo mkdir /var/www/html/hls/static/js $ sudo mkdir /var/www/html/hls/static/css
VideoJsを配置します。
$ cd /var/www/html/hls $ tree └── static ├── css │ └── video-js.min.css └── js ├── video.min.js ├── videojs-contrib-hls.min.js └── videojs-contrib-media-sources.min.js
index.htmlを作成します。
$ sudo vim /var/www/html/hls/index.html $ cat /var/www/html/hls/index.html <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>Video.jsのお試し</title> <link href="http://192.168.56.103/hls/static/css/video-js.min.css" rel="stylesheet"> <script src="./static/js/video.min.js"></script> <script src="./static/js/videojs-contrib-media-sources.min.js"></script> <script src="./static/js/videojs-contrib-hls.min.js"></script> </head> <body> <h2>ffmpeg + HLSでデクストップ画面をモニタリング</h2> <video id="test" class="video-js vjs-default-skin vjs-big-play-centered" controls preload="auto" width="640" height="360" data-setup="{}"> <source src="./output.m3u8" type="application/x-mpegURL"> </video> </body> </html>
nginxのconfigを編集します。
$ sudo vim /etc/nginx/conf/default.conf $ cat /etc/nginx/conf/default.conf ...(途中略)... location / { root /var/www/html; index index.html; } ...(途中略)...
ポート80番号を開放します。
$ sudo firewall-cmd --add-service=http --zone=public --permanent $ sudo firewall-cmd --reload
nginxを起動します。
$ sudo systemctl start nginx
HLS開始
下記のコマンドでCentOSのGUI画面を録画しHLS形式のファイルで出力します。
$ cd /var/www/html/hls $ sudo ffmpeg -f x11grab -r 60 -s 800x600 -i :0.0+0,0 -vcodec h264 -b:v 250K -pix_fmt yuv420p -segment_format mpegts -hls_time 5 -hls_list_size 5 -hls_flags delete_segments output.m3u8
実行結果
ChromeからHLS配信用サーバ(Virtual-BOX上のCentOS)にアクセスしてみます。
20秒〜30秒の遅延はありますが、いい感じに監視できていますね。
※動画は面倒なのでgif画像です。
その他の方法
遅延を避けるには、WebRTCといった技術を駆使すればできるらしいです。
ビデオチャットとかのイメージですね。
◆構成
仮想Webカメラ(v4l2loopback) + シグナリングサーバ
◆内容
監視対象のPCに仮想的なWebカメラ(v4l2loopback)導入する、そしてWebカメラにffmpegで映像を流し込み、Webカメラで撮影している内容をシグナリングサーバに送信する。
監視を行うクライアントはブラウザからシグナリングサーバにアクセスして映像を見る。
...みたいな構成を組めばできるようです。Node.JsやWebSocket等の知識が必要になります
ちょっとばかり調べてみましたが、何言ってるか良く分からなかったので断念しました...
参考
・https://medium.com/@alexcambose/webcam-live-streaming-with-websockets-and-base64-64b1b4992db8
・https://github.com/umlaeute/v4l2loopback
リンク
・https://tools.ietf.org/html/rfc8216
・https://videojs.com
sshでログインしている端末の出力をのぞき見る
やりたいこと
- 自分は端末Aでログインしている状態。
端末Aから別の端末である端末Bのコマンドの入力、応答をのぞき見たい。 - できればC言語で実装したい
実現方法
ググってみたところ、perl製のttylogを使ったり、shellスクリプトで頑張ればできるようです。
どちらも共通するのはstraceの出力を加工していること。
sshdに対してstraceを実行し、read(fd=13)の第2引数を取っています。
参考
orebibou.com
strace自体はptraceを使って実装されているので、マネすればc言語でもいけそうです。
ptraceについて
manページ見てもよくわからなかったのですが、下記の流れで実装すれば良いみたいです。
attach ↓ option設定 ↓ システムコールをcatch ↓ レジスタから引数を解析 - システムコールの番号、引数はレジスタ(struct user_regs_struct)で判別する。<br> - readの第2引数(buf)の中身を取得するにはPTRACE_PEEKDATAを使う。
X86のレジスタ対応表(とりあえずreadの捕捉に必要なものだけ)
種目 | レジスタ(メンバ名) |
---|---|
システムコールの番号 | orig_rax |
リターン値 | rax |
第1引数 | rdi |
第2引数 | rsi |
第3引数 | rdx |
ソースコード
ttytrace.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/ptrace.h> #include <sys/wait.h> #include <sys/user.h> #include <sys/syscall.h> #define FD_TERMINAL (13) static int read_args(pid_t pid, long addr, size_t size) { int rc = 0; long dat = 0; size_t datsz = 0; for (int i = 0; i < size; i+= sizeof(long)) { errno = 0; dat = ptrace(PTRACE_PEEKDATA, pid, addr + i, NULL); int err = errno; if (err != 0) { perror("ptrace"); rc = -err; break; } if (i + sizeof(long) > size) { write(1, (void*)&dat, size % sizeof(long)); } else { write(1, (void*)&dat, sizeof(long)); } } return rc; } static int get_syscall_args_read( pid_t pid, struct user_regs_struct *regs) { int rc = 0; #if defined(__x86_64__) /* skip other than "read" */ if (!((regs->orig_rax == SYS_read) && (regs->rdi == FD_TERMINAL) && ((ssize_t)regs->rax > 0))) { goto end; } rc = read_args(pid, (long)regs->rsi, (size_t)regs->rax); if (rc < 0) { goto end; } #endif end: return rc; } static void start_trace(pid_t pid) { int rc = 0; int status = 0; struct user_regs_struct regs; rc = ptrace(PTRACE_ATTACH, pid, NULL, NULL); if (rc < 0) { perror("ptrace"); goto end; } ptrace(PTRACE_SETOPTIONS, pid, NULL, PTRACE_O_TRACESYSGOOD); while (1) { ptrace(PTRACE_SYSCALL, pid, NULL, NULL); rc = waitpid(pid, &status, 0); if (rc < 0) { perror("waitpid"); goto detach_end; } if (WIFEXITED(status)) { break; } rc = ptrace(PTRACE_GETREGS, pid, NULL, ®s); if (rc < 0) { perror("ptrace"); goto detach_end; } rc = get_syscall_args_read(pid, ®s); if (rc < 0) { goto detach_end; } } detach_end: ptrace(PTRACE_DETACH, pid, NULL, NULL); end: return; } int main(int argc, char *argv[]) { if (argc != 2) { printf("%s <pid>\n", argv[0]); exit(EXIT_FAILURE); } pid_t pid = atoi(argv[1]); start_trace(pid); return 0; }
ビルド
gcc -o ttytrace ttytrace.c -std=gnu99
実行
監視したい端末のsshdのpidを指定する。
実行にはroot権限が必要です。
sudo ./ttytrace <sshdのpid>
実行に成功していれば、ttylogとかと同様に監視対象の端末での操作内容がそのまま表示されます。
意外と短い実装で済んでよかった。
Javascriptでテトリスを作る
Javascriptのサンプルとしてテトリスは定番ですが、そういえば今まで挑戦してなかったの作ってみました。
canvasを使って落下ブロック(テトリミノ)が7種、色が6種のザ定番ぽいものを作ります。
完成品
Chromeでしか動作確認してません。
ソースコード
<!DOCTYPE HTML> <html> <head> <meta charset="UTF-8"> </head> <body> <!-- canvas --> <canvas id="tetris" width="300" height="600"></canvas> <script type="text/javascript"> (function() { /* 全体のコンフィグ */ var CONFIG = { 'width' : 0, /* 横幅 */ 'hegith' : 0, /* 縦幅 */ 'xsqs' : 10, /* 横方眼数 */ 'ysqs' : 20, /* 縦方眼数 */ 'color' : "rgb(65,65,65)", /* 盤面の色 */ 'grid_color' : "black", /* 格子の色 */ 'grid_width' : 2, /* 格子の幅 */ }; /* 方眼 */ var SQUARES; var squre = function() { this.color = 0; }; /* 色リスト */ var COLOR = { "default" : CONFIG.color, "red" : "rgb(255,0,0)", "green" : "rgb(0,255,0)", "blue" : "rgb(0,0,255)", "yellow" : "rgb(255,255,0)", "cyan" : "rgb(0,255,255)", "magenta" : "rgb(255,0,255)", }; var COLOR_LIST = [ COLOR.red, COLOR.green, COLOR.blue, COLOR.yellow, COLOR.cyan, COLOR.magenta, ]; /* ブロックの定義 */ var BLOCK; var block = function () { this.color = COLOR_LIST[Math.floor(Math.random() * (COLOR_LIST.length))]; this.shape = BLOCK_LIST[Math.floor(Math.random() * (BLOCK_LIST.length))]; this.x = (CONFIG.xsqs / 2) - 1; this.y = 0; return this; }; var BLOCK_BOX = [ [0,0,0,0], [0,1,1,0], [0,1,1,0], [0,0,0,0] ]; var BLOCK_BAR = [ [1,1,1,1], [0,0,0,0], [0,0,0,0], [0,0,0,0] ]; var BLOCK_LL = [ [1,1,1,0], [1,0,0,0], [0,0,0,0], [0,0,0,0] ]; var BLOCK_LN = [ [1,0,0,0], [1,1,1,0], [0,0,0,0], [0,0,0,0] ]; var BLOCK_NL = [ [1,0,0,0], [1,1,0,0], [0,1,0,0], [0,0,0,0] ]; var BLOCK_NN = [ [0,1,0,0], [1,1,0,0], [1,0,0,0], [0,0,0,0] ]; var BLOCK_T = [ [1,0,0,0], [1,1,0,0], [1,0,0,0], [0,0,0,0] ]; var BLOCK_LIST = [ BLOCK_BOX, BLOCK_BAR, BLOCK_LL, BLOCK_LN, BLOCK_NL, BLOCK_NN, BLOCK_T, ]; /* 描画コンテキスト */ var CTX; /* タイマー */ var fallTimer; /* キー入力 */ var KEY_DOWN = { "a" : 65, /* left */ "s" : 83, /* right */ "d" : 68, /* down */ "f" : 70, /* turn right */ "left" : 37, "right" : 39, "down" : 40, "space" : 32, /* turn right */ }; /* 初期化処理 */ function initialize() { /* canvasの取得 */ CTX = document.getElementById("tetris").getContext('2d'); if (!CTX) { return false; } CONFIG.width = CTX.canvas.width; CONFIG.height = CTX.canvas.height; /* keydown event */ document.onkeydown = moveBlock; /* 盤面の生成 */ SQUARES = createSquares(CONFIG); /* 落下ブロックの生成 */ BLOCK = block(); /* テトリスの起動 */ drawTetris(CTX, CONFIG, SQUARES, BLOCK); fallTimer = setInterval(tetris, 400, CTX, CONFIG, SQUARES, BLOCK); } /* ブロックの移動 */ function moveBlock() { var key = event.keyCode; switch(key) { case KEY_DOWN.a : case KEY_DOWN.left : moveLeftBlock(); break; case KEY_DOWN.s : case KEY_DOWN.right : moveRightBlock(); break; case KEY_DOWN.f : case KEY_DOWN.space : turnRightBlock(); break; case KEY_DOWN.d : case KEY_DOWN.down : moveDownBlock(); break; default: break; } } /* 左に移動 */ function moveLeftBlock() { var isEnabeMove = true; for (var y = 0 ; y < BLOCK.shape[0].length; y++) { for (var x = 0; x < BLOCK.shape.length; x++) { if (BLOCK.shape[x][y] == 0) { continue; } /* これ以上左に行けない */ if (((BLOCK.x + x) <= 0) || ((SQUARES[BLOCK.x + x - 1][BLOCK.y + y].color != CONFIG.color))) { isEnabeMove = false; break; } } } if (isEnabeMove) { BLOCK.x--; } } /* 右に移動 */ function moveRightBlock() { isEnabeMove = true; for (var y = 0 ; y < BLOCK.shape[0].length; y++) { for (var x = 0; x < BLOCK.shape.length; x++) { if (BLOCK.shape[x][y] != 0) { /* これ以上右に行けない */ if (((BLOCK.x + x) >= (CONFIG.xsqs - 1)) || ((SQUARES[BLOCK.x + x + 1][BLOCK.y + y].color != CONFIG.color))) { isEnabeMove = false; break; } } } } if (isEnabeMove) { BLOCK.x++; } }; /* 下に加速 */ function moveDownBlock() { if (!isLanded(CONFIG, SQUARES, BLOCK)) { BLOCK.y++; } }; /* ターンできるか */ function isEnableToTurn(tmp) { for (var x = 0; x < BLOCK.shape.length; x++) { for (var y = 0; y < BLOCK.shape[0].length; y++) { if (tmp[x][y] != 0) { if (((BLOCK.x + x) < 0) || (CONFIG.xsqs <= (BLOCK.x + x)) || ((BLOCK.y + y) < 0) || (CONFIG.ysqs <= (BLOCK.y + y)) || SQUARES[BLOCK.x + x][BLOCK.y + y].color != CONFIG.color) { return false; } } } } return true; } /* ブロックの右回転 */ function turnRightBlock() { var tmp = Array(BLOCK.shape.length); for (var x = 0; x < BLOCK.shape.length; x++) { tmp[x] = Array(BLOCK.shape[0].length); } for (var x = 0; x < BLOCK.shape.length; x++) { for (var y = 0; y < BLOCK.shape[0].length; y++) { tmp[x][y] = BLOCK.shape[y][BLOCK.shape[0].length - 1 - x]; } } if (isEnableToTurn(tmp)) { BLOCK.shape = tmp; } } /* 方眼の作成 */ function createSquares(c) { var sqrs = new Array(c.ysqs); for (var y = 0; y < c.ysqs; y++) { sqrs[y] = new Array(c.xsqs); } for (var x = 0; x < c.xsqs; x++) { for (var y = 0; y < c.ysqs; y++) { sqrs[x][y] = new squre(); sqrs[x][y].color = c.color; } } return sqrs; } /* 盤面の描画 */ function drawTetris(ctx, c, sqrs, blk) { drawBack(ctx, c); drawSquare(ctx, c, sqrs); drawBlock(ctx, c, blk); drawGrid(ctx, c); } /* 背景の描画 */ function drawBack(ctx, c) { ctx.fillStyle = c.color; ctx.fillRect(0, 0, c.width, c.height); ctx.strokeStyle = c.grid_color; ctx.lineWidth = c.grid_width; ctx.beginPath(); ctx.stroke(); } /* 格子(グリッド)の描画 */ function drawGrid(ctx, c) { ctx.beginPath(); /* 縦のグリッド線 */ for(var x = c.width/c.xsqs; x < c.width; x+= c.width/c.xsqs) { ctx.moveTo(x, 0); ctx.lineTo(x, c.height); } /* 横のグリッド線 */ for(var y = c.height/c.ysqs; y < c.height; y+= c.height/c.ysqs) { ctx.moveTo(0, y); ctx.lineTo(c.width, y); } ctx.stroke(); } /* 方眼の描画 */ function drawSquare(ctx, c, blocks) { var x_sp; /* 描画開始座標(x) */ var y_sp; /* 描画開始座標(y) */ var w; /* 横幅 */ var h; /* 縦幅 */ ctx.beginPath(); for (var x = 0; x < c.xsqs; x++) { for (var y = 0; y < c.ysqs; y++) { if (blocks[x][y].color == COLOR.default) { continue; } w = (c.width/c.xsqs); h = (c.height/c.ysqs); x_sp = x * w; y_sp = y * h; ctx.fillStyle = blocks[x][y].color; ctx.fillRect(x_sp, y_sp, w, h); } } ctx.stroke(); }; /* 落下ブロックの描画 */ function drawBlock(ctx, c, blk) { var width = c.width/c.xsqs; var height = c.height/c.ysqs; ctx.fillStyle = blk.color; for (var x = 0; x < blk.shape.length; x++) { for (var y = 0; y < blk.shape[0].length; y++) { if (blk.shape[x][y] != 0) { if (((blk.x + x) >= 0) && ((blk.x + x) <= c.xsqs) && ((blk.y + y) >= 0) && ((blk.y + y) <= c.ysqs)) { ctx.fillRect((blk.x + x) * width, (blk.y + y) * height, width, height); } } } } }; /* ブロックの着地 */ function addBlock2Board(c, squres, blk) { /* 盤面に着色 */ for (var x = 0; x < blk.shape.length; x++) { for (var y = 0; y < blk.shape[0].length; y++) { if (blk.shape[x][y] == 0) { continue; } if (blk.x + x <= c.xsqs && blk.y + y <= c.ysqs) { squres[blk.x + x][blk.y + y].color = blk.color; } } } } /* ブロックの落下 */ function isLanded(c, squres, blk) { /* 着地判定 */ for (var x = 0; x < blk.shape.length; x++) { for (var y = 0; y < blk.shape[0].length; y++) { if (blk.shape[x][y] != 0) { /* 地面と接触 */ if ((blk.y + y) == (c.ysqs - 1)) { return true; } /* 既設ブロックと接触 */ if (squres[blk.x + x][blk.y + y + 1].color != c.color) { return true; } } } } /* Not着地 */ return false; } /* GameOverですか? */ function isGameOver(c, squres) { for (var x = c.xsqs/2 - 2 ; x < c.xsqs/2 + 2; x++) { if (squres[x][0].color != c.color) { return true; } } return false; } /* 行削除 */ function eraseLine(c, squres) { for (var y = 0; y < c.ysqs; y++) { /* is enable to erase ? */ var flag = true; for (var x = 0; x < c.xsqs; x++) { if (squres[x][y].color == c.color) { flag = false; break; } } if(!flag) { continue; } /* do to erase */ for (var x = 0; x < c.xsqs; x++) { squres[x][y].color = c.color; } if (y != 0) { for (var y1 = y; y1 != 0; y1--) { for (var x = 0; x < c.xsqs; x++) { squres[x][y1].color = squres[x][y1-1].color; } } } } } /* main処理 */ function tetris(ctx, c, squres, blk) { if (isLanded(c, squres, blk)) { /* ブロックの着地 */ addBlock2Board(c, squres, blk); /* 削除処理 */ eraseLine(c, squres); /* Game Over ? */ if (isGameOver(c,squres)) { drawTetris(ctx, c, squres, blk); clearInterval(fallTimer); } else { /* 新しいブロックの生成 */ blk = block(); } } else { /* 落下 */ blk.y++; } /* 盤面の描画 */ drawTetris(ctx, c, squres, blk); } window.addEventListener('load', initialize, false); } )(); </script> <div>a : 左に移動</div> <div>s : 右に移動</div> <div>d : 下に移動</div> <div>f : 右に回転</div> </body> </html>
もう少しシンプルに作れたかも。
Python3サンプルコード集(その2)
Pythonのサンプルコードです。
ネットワーク系が中心となります。
書かなくなるとすぐ忘れてしまうので備忘録として残しておきます。
全てクラアント-サーバの通信のサンプルです。
TCP
tcp_client.py
import os import sys import socket if __name__ == '__main__': address = ('127.0.0.1', 50000) datasize = 4096 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(address) msg = 'Hello TCP' s.send(msg.encode()) s.close()
tcp_server.py
import os import sys import socket if __name__ == '__main__': address = ('127.0.0.1', 50000) datasize = 4096 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(address) s.listen(1) client, addr = s.accept() data = client.recv(datasize) sys.stdout.write("receive : {}\n".format(data.decode())) s.close()
UDP
udp_client.py
import os import sys import socket if __name__ == '__main__': server_address = ('127.0.0.1', 50001) datasize = 4096 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) msg = 'Hello UDP' s.sendto(msg.encode(), server_address) s.close()
udp_server.py
import os import sys import socket if __name__ == '__main__': bind_address = ('127.0.0.1', 50001) datasize = 4096 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind(bind_address) data, client = s.recvfrom(datasize) sys.stdout.write("receive : {}\n".format(data.decode())) s.close()
UNIXドメイン(DATAGRAM型)
unix_dgram_client.py
import os import sys import socket if __name__ == '__main__': socket_path = './unix_dgram' s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) s.connect(socket_path) msg = 'Hello unix domain (dgram)' s.send(msg.encode()) s.close()
unix_dgram_server.py
import os import sys import socket if __name__ == '__main__': socket_path = './unix_dgram' datasize = 4096 s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) s.bind(socket_path) data, client = s.recvfrom(datasize) sys.stdout.write("received : {}\n".format(data.decode())) os.unlink(socket_path) s.close()
UNIXドメイン(STREAM型)
unix_stream_client.py
import os import sys import socket if __name__ == '__main__': socket_path = './unix_stream' s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.connect(socket_path) msg = 'Hello unix domain (stream)' s.send(msg.encode()) s.close()
unix_stream_server.py
import os import sys import socket if __name__ == '__main__': socket_path = './unix_stream' datasize = 4096 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.bind(socket_path) s.listen(1) client, addr = s.accept() data = client.recv(datasize) sys.stdout.write("received : {}\n".format(data.decode())) os.unlink(socket_path) s.close()
TCP(selectで待つ)
tcp_select.py
import os import sys import socket import select if __name__ == '__main__': address = ('127.0.0.1', 50000) datasize = 4096 maxconn = 10 sockets = [] s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(address) s.listen(maxconn) sockets.append(s) try: readfds = list(sockets) while True: sys.stdout.write("waiting...\n") r,w,x = select.select(readfds, [], []) for s in r: if s in sockets: conn, addr = s.accept() readfds.append(conn) sys.stdout.write("connected.\n") else: data = s.recv(datasize) if len(data) == 0: sys.stdout.write("disconnected.\n") readfds.remove(s) s.close() break sys.stdout.write("received {}\n".format(data.decode())) finally: s.close()
TCP-UDP-UNIXを全てselectで待つ
all_select.py
import os import sys import socket import select import signal if __name__ == '__main__': address1 = ('127.0.0.1', 50000) address2 = ('127.0.0.1', 50001) socket_path1 = "./unix_stream" socket_path2 = "./unix_dgram" datasize = 4096 maxconn = 1 sockets = [] conn_unix = None conn_tcp = None tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcp.bind(address1) tcp.listen(maxconn) udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp.bind(address2) unix_s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) unix_s.bind(socket_path1) unix_s.listen(maxconn) unix_d = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) unix_d.bind(socket_path2) sockets.append(tcp) sockets.append(udp) sockets.append(unix_s) sockets.append(unix_d) try: readfds = list(sockets) while True: sys.stdout.write("waiting...\n") r,w,x = select.select(readfds, [], []) for s in r: if s in sockets: if s == tcp: conn_tcp, addr_tcp = s.accept() readfds.append(conn_tcp) sys.stdout.write("tcp connected.\n") if s == udp: data, client = s.recvfrom(datasize) sys.stdout.write("received {}\n".format(data.decode())) if s == unix_s: conn_unix, addr_unix = s.accept() readfds.append(conn_unix) sys.stdout.write("unix connected.\n") if s == unix_d: data, client = s.recvfrom(datasize) sys.stdout.write("received {}\n".format(data.decode())) else: if s == conn_tcp: data = s.recv(datasize) if len(data) == 0: sys.stdout.write("tcp disconnected.\n") readfds.remove(s) s.close() break sys.stdout.write("received {}\n".format(data.decode())) if s == conn_unix: data = s.recv(datasize) if len(data) == 0: sys.stdout.write("unix disconnected.\n") readfds.remove(s) s.close() break sys.stdout.write("received {}\n".format(data.decode())) except KeyboardInterrupt: tcp.close() udp.close() os.unlink(socket_path1) os.unlink(socket_path2) unix_s.close() unix_d.close()
エラー処理は上手く書けないのでごめんなさい。
Nginx+uWSGI+viewvcでSubversionのリポジトリをブラウザから閲覧する
ViewVC: Repository Browsingを使ってLinux(CentOS7)上に構築したリポジトリをブラウザから参照可能にしてみます。
これで、Linuxが使えないポンコツな上司から修正内容を問われても、URLをコピペしてメールするだけで済むようになります(?)
Git環境向けはまた今度。
事前準備
必要なパッケージを先にインストールしておきます。
$ sudo yum -y install python python-devel svn subversion-python policycoreutils-devel $ sudo yum -y groupinstall "Development Tools"
参照するリポジトリ
今回のテスト用に/opt/svn/reposを作ります。
$ sudo mkdir -p /opt/svn/ $ cd /opt/svn/ $ sudo svnadmin create repos $ sudo chown -R user:user repos/
コミットログも見れるか確認するため作成したリポジトリに適当なファイルを追加します。
$ cd ~/ $ svn co file:///opt/svn/repos $ cd repos $ mkdir -p branches tag trunk $ echo "Hello World" > trunk/README $ svn add * $ svn commit -m "Create New Repository."
Nginxのインストール
Nginxをyumでインストールするため、リポジトリにnginxのパッケージ取得先を追記します。
$ sudo vi /etc/yum.repos.d/nginx.repo $ cat /etc/yum.repos.d/nginx.repo [nginx] name=nginx repo baseurl=http://nginx.org/packages/mainline/centos/7/$basearch/ gpgcheck=0 enabled=0
以下のコマンドでインストールできればOK
$ sudo yum -y --enablerepo=nginx install nginx
続いて、nginxのコンフィグです。
/viewvcでアクセスした場合にuWSGIに処理させるようにします。
/viewvc-staticはviewvcのロゴ等の画像ファイルを参照するための記述です。
$ sudo vi /etc/nginx/conf.d/default.conf $ diff -u /etc/nginx/conf.d/default.conf.orig /etc/nginx/conf.d/default.conf --- /etc/nginx/conf.d/default.conf.orig 2018-09-23 19:20:22.455255538 +0900 +++ /etc/nginx/conf.d/default.conf 2018-09-23 20:58:28.830092221 +0900 @@ -41,5 +41,14 @@ #location ~ /\.ht { # deny all; #} + + location /viewvc { + include uwsgi_params; + uwsgi_pass 127.0.0.1:3031; + } + + location /viewvc-static { + alias /usr/local/viewvc/templates/docroot; + } }
このままではファイアウォールとSELinuxに接続が弾かれるので予めポートの開放とSELinuxを無効化しておきます。
ファイアウォールの設定
$ sudo firewall-cmd --permanent --add-port=3031/tcp --zone=public $ sudo firewall-cmd --permanent --zone public --add-service http $ sudo firewall-cmd --reload
SELinuxの無効化
sudo semanage permissive -a httpd_t
設定が終ったらnginxを起動します。
$ sudo systemctl enable nginx $ sudo systemctl start nginx
uWSGIのインストール
公式の方法はこちら。
今回、uWSGIはpipでインストールするので、まずはpipを使えるようにします。
$ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py $ sudo python get-pip.py
pipが準備できたらuwsgiを追加します。
$ sudo pip install uwsgi
追加できたら、systemctlで起動できるように下記ファイルを作ります。
/etc/uwsgi/uwsgi.ini
$ sudo mkdir -p /etc/uwsgi $ sudo vi /etc/uwsgi/uwsgi.ini $ cat /etc/uwsgi/uwsgi.ini [uwsgi] socket = 127.0.0.1:3031 wsgi-file = /usr/local/viewvc/bin/wsgi/viewvc.wsgi uid = nginx gid = nginx log = /var/log/uwsgi master = true
$ sudo vim /etc/systemd/system/uwsgi.service $ cat /etc/systemd/system/uwsgi.service [Unit] Description=uWSGI After=syslog.target [Service] ExecStart=/usr/bin/uwsgi --ini /etc/uwsgi/uwsgi.ini Restart=always KillSignal=SIGQUIT Type=notify StandardError=syslog NotifyAccess=all [Install] WantedBy=multi-user.target
設定が終ったら起動します。
$ sudo systemctl enable uwsgi $ sudo systemctl start uwsgi
Viewvcのインストール
こちらから最新版を取ってきます。
$ curl http://www.viewvc.org/downloads/viewvc-1.1.26.tar.gz -o viewvc-1.1.26.tar.gz $ tar xvf viewvc-1.1.26.tar.gz $ cd xvf viewvc-1.1.26 $ sudo ./viewvc-install <インストール先について質問されるが全て<Enter>を押下>
ViewVCのパスとコンフィグをNginx、uWSGIに記載した内容に合わせます。
所有者もnginxに変更します。
$ cd /usr/local $ sudo chown -R nginx:nginx viewvc-1.1.26/ $ sudo ln -s viewvc-1.1.26 viewvc $ sudo chown nginx:nginx viewvc
$ cd /usr/local/viewvc $ sudo vi viewvc.conf $ diff -u viewvc.conf.orig viewvc.conf --- viewvc.conf.orig 2018-09-23 19:28:57.955853140 +0900 +++ viewvc.conf 2018-09-23 20:18:35.987344907 +0900 @@ -109,7 +109,7 @@ ## svn_roots = svnrepos: /opt/svn/, ## anotherrepos: /usr/local/svn/repos2 ## -#svn_roots = +svn_roots = viewvc:/opt/svn/repos ## root_parents: Specifies a list of directories under which any ## number of repositories may reside. You can specify multiple root @@ -658,7 +658,7 @@ ## still be based on the global default template set per 'template_dir' ## above, not on 'template_dir' as overridden for a given root. ## -#docroot = +docroot = /viewvc-static ## show_subdir_lastmod: Show last changelog message for CVS subdirectories ##
結果
ブラウザにURLを入力してリポジトリの内容が参照できたら成功です。
今回の例では、"http://(サーバーのIPアドレス)/viewvc "にアクセスします。
参考
https://pip.pypa.io/en/stable/
https://uwsgi-docs.readthedocs.io/en/latest/index.html
https://subversion.apache.org/
ZAIFのAPIで仮想通貨の積立スクリプトを作る(python)
pythonのお勉強の一環として、ちょこちょこ仮想通貨の自動取引の実装にトライしております。
今回はzaifでBTC/BCH/ETH/MONA/XEM(NEM)を500円分買い注文するプログラムを組んでみます。
作ったスクリプトとcronやsystemdのタイマーと組み合わせることで定期積立を実現できます。
はじめに
zaifを選択する理由は、国内の取引所の中ではAPIで操作できる通貨種類が多いからです。
現在日本円で手に入る主要な仮想通貨としては、
- Bitcoin(BTC)
- Bitcoin Cash(BCH)
- Ethereum(ETH)
- Ethereum classic(ETC)
- XEM(NEM)
- Monacoin(MONA)
- Ripple(XRP)
- Lisk(LSK)
このあたりでしょうか。
Zaifは大体カバーできてますね。
かつ販売ではなく取引ができるのでAPIのpairに指定できるというわけです。
(BitFlyerとCoinCheckはBitcoinしかpairに設定できないはず)
また、zaifには積立のサービスがありますが少額だと手数料の割合がそこそこ高いので自分で実装するとお得です。ただしセキュリティ面については自己責任で。
実装方針
売りの実装は難しい(利益や損切りを考えると面倒)ので、まずは買いの実装だけやってみます。
バグって誤発注するのが怖いので予算は少額に設定しておきます。
- 定期的に一定額(500円で買える分だけ)を購入する(積立)
- 指値はpythonスクリプトを実行した時点の最終取引額
- 最小注文単位の金額が500円を超える場合は注文しない
- 約定判定はなし
実装にあたっては下記のAPIを使用します。
currency_pairs — Zaif api document v1.1.1 ドキュメント
last_price — Zaif api document v1.1.1 ドキュメント
trade — Zaif api document v1.1.1 ドキュメント
APIを利用するには事前にアカウント設定から取引用のキーを取得しておく必要があります。セキュリティ確保のため送信元IPアドレスの制限もしておくと良いでしょう。
ソースコード
zaif_fund.py
# -*- coding: utf-8 -*- import sys import json import requests import hmac import hashlib import time from datetime import datetime import urllib.parse import math class zaifApi: def __init__(self, key, key_secret, endpoint): self.key = key self.key_secret = key_secret self.endpoint = endpoint def get(self, path): nonce = int(datetime.now().strftime('%s')) text = nonce + self.endpoint + path signature = hmac.new( bytes(self.key_secret.encode('ascii')), bytes(text.encode('ascii')), hashlib.sha256).hexdigest() return requests.get( self.endpoint + path , headers = self.__get_header(self.key, nonce, signature)) def post(self, method, params): nonce = int(datetime.now().strftime('%s')) payload = { "method": method, "nonce": nonce, } payload.update(params) encoded_payload = urllib.parse.urlencode(payload) return requests.post( self.endpoint, data = payload, headers = self.__get_header(encoded_payload)) def delete(self,path): nonce = str(int(time.time())) text = nonce + self.endpoint + path signature = hmac.new( bytes(self.key_secret.encode('ascii')), bytes(text.encode('ascii')), hashlib.sha256).hexdigest() return requests.delete( self.endpoint+path, headers = self.__get_header(self.key, nonce)) def __get_header(self, params): signature = hmac.new( bytearray(self.key_secret.encode('utf-8')), digestmod=hashlib.sha512) signature.update(params.encode('utf-8')) return { 'key': self.key, 'sign': signature.hexdigest() } def getLastPrice(pair): urlbase = 'https://api.zaif.jp/api/1/last_price/' response = requests.get(urlbase+pair) if response.status_code != 200: return None last_price = response.json() return last_price['last_price'] def getItemUnitMin(pair): urlbase = 'https://api.zaif.jp/api/1/currency_pairs/' response = requests.get(urlbase+pair) if response.status_code != 200: return None currency_pair = response.json() return currency_pair[0]['item_unit_min'] if __name__ == '__main__': argv = sys.argv argc = len(argv) if (argc != 3): print('usage: python %s <key> <secret key>' % argv[0]) quit() key = argv[1] key_secret = argv[2] endpoint = 'https://api.zaif.jp/tapi' # 予算 budget_jpy = 500 # 取引対象の通貨 pairs = [ 'btc_jpy', 'bch_jpy', 'eth_jpy', 'xem_jpy', 'mona_jpy', ] # Instance zaif = zaifApi(key,key_secret,endpoint) for p in pairs: # 認証に使うnonceのための遅延処理 time.sleep(1.1) # 取引通貨の通貨情報を取得 item_unit_min = getItemUnitMin(p) if (item_unit_min == None): # 失敗した場合は中断 continue # 最終取引価格を取得 last_price = getLastPrice(p) if (last_price == None): # 失敗した場合は中断 continue # 注文価格が予算を超えた場合は中断 if ((item_unit_min * last_price) > budget_jpy): continue # 注文量の計算 # buget_jpy / last_price = 123.456789, item_unit_min = 0.01の場合, amount = 123.45になるような計算を行う # floadの場合, 最小桁付近で計算誤差がでるためroundで桁を調整する amount = round((math.floor((budget_jpy / last_price) / item_unit_min) * item_unit_min), int(math.log10(1/item_unit_min))) # 小数点を使わない場合は整数に変換する if (amount.is_integer()): amount = int(amount) if (last_price.is_integer()): last_price = int(last_price) # リクエストのパラメータ設定 method = 'trade' params = { 'currency_pair': p, 'action' : 'bid', 'price' : last_price, 'amount': amount, } print(params) print('total = {0:.2f} yen' .format(amount*last_price)) # 注文の送信 response = zaif.post(method, params) if response.status_code != 200: continue print(response.json()) print('')
実行結果
$ /usr/local/bin/python3 zaif_fund.py usage: python zaif_fund.py <key> <secret key> $ /usr/local/bin/python3 zaif_fund.py 'あなたのAPIキー' 'あなたのプライベートキー' {'amount': 0.0005, 'currency_pair': 'btc_jpy', 'action': 'bid', 'price': 936820} total = 468.41 yen {'return': {'order_id': 0, 'remains': 0.0, 'received': 0.0005, 'funds': {'保有資産の一覧'}}, 'success': 1} {'amount': 0.0031, 'currency_pair': 'bch_jpy', 'action': 'bid', 'price': 159200} total = 493.52 yen {'return': {'order_id': 0, 'remains': 0.0, 'received': 0.0030907, 'funds': {'保有資産の一覧'6}}, 'success': 1} {'amount': 0.0065, 'currency_pair': 'eth_jpy', 'action': 'bid', 'price': 76800} total = 499.20 yen {'return': {'order_id': xxxxxxxxx, 'remains': 0.0065, 'received': 0.0, 'funds': {'保有資産の一覧'}}, 'success': 1} {'amount': 13.6, 'currency_pair': 'xem_jpy', 'action': 'bid', 'price': 36.55} total = 497.08 yen {'return': {'order_id': xxxxxxxxx, 'remains': 13.6, 'received': 0.0, 'funds': {'保有資産の一覧'}}, 'success': 1} {'amount': 1, 'currency_pair': 'mona_jpy', 'action': 'bid', 'price': 442.5} total = 442.50 yen {'return': {'order_id': xxxxxxxxx, 'remains': 1.0, 'received': 0.0, 'funds': {'保有資産の一覧'}}, 'success': 1}
応答のremainsが0.0以外のものは、板に注文が残っていて、0.0の場合は既に取引が完了しています。
約定の判定処理はありませんが、上げトレンドが続くようなことでもない限り大体翌日ぐらいまでには約定する気がします。
今のところ2ヶ月ほど週1回の頻度で実行してますが、注文が未約定のまま残ったことは無いです。
仮想通貨を取引所に置きっぱなしが怖い人は送金するAPIの組み合わせることで別walletへの退避も自動で出来ますね。