ちょっとしたことでプロキシサーバが必要になりそうで(TCPがプロキシできればよい)、そういえばNettyにプロキシのサンプル実装があったなぁと思い、これをGroovyスクリプトに移植してみました。
Netty Documentation
http://netty.io/wiki/
Package io.netty.example.proxy
http://netty.io/4.0/xref/io/netty/example/proxy/package-summary.html
久々にNettyを使ったコードを書いたというか、Netty 4系になってから直接Nettyのコードを扱うのは初めてだと思います。
ほぼベタベタの移植ですが、コマンドライン引数処理をCliBuilderにしたり、ところどころGroovyっぽくして書き直しました。もともと4つクラス定義されていたのですが、全部分解して1スクリプトにしています。
結果はこちら。
netty_proxy.groovy
@Grab('io.netty:netty-all:4.0.26.Final') import io.netty.bootstrap.* import io.netty.buffer.Unpooled import io.netty.channel.* import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.socket.SocketChannel import io.netty.channel.socket.nio.NioServerSocketChannel import io.netty.handler.logging.* def cli = new CliBuilder(usage: "groovy ${new File(this.class.protectionDomain.codeSource.location.file).name} [options]") cli.localport(args: 1, argName: 'localport', 'bind local-port') cli.remotehost(args: 1, required: true, argName: 'remote-host', 'proxy target remote host') cli.remoteport(args: 1, required: true, argName: 'remote-port', 'proxy target remote port') def options = cli.parse(args) if (!options) { System.exit(1) } def localPort = options.localport ? options.localport as int : 8080 def remoteHost = options.remotehost def remotePort = options.remoteport as int def bossGroup = new NioEventLoopGroup() def workerGroup = new NioEventLoopGroup() try { def serverBootstrap = new ServerBootstrap() serverBootstrap .group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(hexDumpProxyInitializer(remoteHost, remotePort)) .childOption(ChannelOption.AUTO_READ, false) .bind(localPort).sync().channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully() workerGroup.shutdownGracefully() } def hexDumpProxyInitializer(remoteHost, remotePort) { new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) { ch.pipeline().addLast( new LoggingHandler(LogLevel.INFO), hexDumpProxyFrontendHandler(remoteHost, remotePort)) } } } def hexDumpProxyFrontendHandler(remoteHost, remotePort) { new ChannelInboundHandlerAdapter() { def volatile Channel outboundChannel @Override public void channelActive(ChannelHandlerContext ctx) { def inboundChannel = ctx.channel() def bootstrap = new Bootstrap() bootstrap .group(inboundChannel.eventLoop()) .channel(ctx.channel().getClass()) .handler(hexDumpProxyBackendHandler(inboundChannel)) .option(ChannelOption.AUTO_READ, false) def f = bootstrap.connect(remoteHost, remotePort) outboundChannel = f.channel() f.addListener( { future -> if (future.isSuccess()) { // connection complete start to read first data inboundChannel.read() } else { // Close the connection if the connection attempt has failed. inboundChannel.close() } } as ChannelFutureListener) } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { if (outboundChannel.isActive()) { outboundChannel .writeAndFlush(msg) .addListener( { future -> if (future.isSuccess()) { // was able to flush out data, start to read the next chunk ctx.channel().read() } else { future.channel().close() } } as ChannelFutureListener) } } @Override public void channelInactive(ChannelHandlerContext ctx) { if (outboundChannel != null) { closeOnFlush(outboundChannel); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace() closeOnFlush(ctx.channel()) } } } def hexDumpProxyBackendHandler(inboundChannel) { new ChannelInboundHandlerAdapter() { @Override public void channelActive(ChannelHandlerContext ctx) { ctx.read() ctx.write(Unpooled.EMPTY_BUFFER) } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { inboundChannel .writeAndFlush(msg) .addListener( { future -> if (future.isSuccess()) { ctx.channel().read() } else { future.channel().close() } } as ChannelFutureListener) } @Override public void channelInactive(ChannelHandlerContext ctx) { closeOnFlush(inboundChannel) } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace() closeOnFlush(ctx.channel()) } } } /** * Closes the specified channel after all queued write requests are flushed. */ def closeOnFlush(ch) { if (ch.isActive()) { ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE) } }
バインドするローカルポート、プロキシ先のリモートホスト、リモートポートを引数に取ります。ローカルポートのみ任意引数で、デフォルトは8080を使うようにしました。
リモートホスト、リモートポートは必須なので、何も指定しないとusageを出力して終了します。
$ groovy netty_proxy.groovy error: Missing required options: remotehost, remoteport usage: groovy netty_proxy.groovy [options] -localport <localport> bind local-port -remotehost <remote-host> proxy target remote host -remoteport <remote-port> proxy target remote port
では、ちょっと使ってみます。
自分のはてなのブログを見てみましょう。
$ groovy netty_proxy.groovy --remotehost d.hatena.ne.jp --remoteport 80 5 02, 2015 9:50:41 午後 io.netty.handler.logging.LoggingHandler channelRegistered 情報: [id: 0xe579c81c] REGISTERED 5 02, 2015 9:50:41 午後 io.netty.handler.logging.LoggingHandler bind 情報: [id: 0xe579c81c] BIND(0.0.0.0/0.0.0.0:8080) 5 02, 2015 9:50:41 午後 io.netty.handler.logging.LoggingHandler channelActive 情報: [id: 0xe579c81c, /0:0:0:0:0:0:0:0:8080] ACTIVE
結果は長いので端折りますが、プロキシできているようです。
※Hostヘッダの指定が必要
$ curl -H 'Host: d.hatena.ne.jp' http://localhost:8080/Kazuhira/ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html xmlns:og="http://ogp.me/ns#"> <head> <meta http-equiv="Content-Type" content="text/html; charset=euc-jp"> <meta http-equiv="Content-Style-Type" content="text/css"> <meta http-equiv="Content-Script-Type" content="text/javascript"> <title>CLOVER</title> <link rel="start" href="./" title="CLOVER"> <link rel="help" href="/help" title="�إ���"> <link rel="prev" href="/Kazuhira/?of=5" title="����5��ʬ"> <link rel="stylesheet" href="http://d.st-hatena.com/statics/css/base.css?845afa966323b83dfb25972c9ad94129f2586c5e" type="text/css" media="all"> <link rel="stylesheet" href="http://d.st-hatena.com/statics/css/headerstyle_bl.css?031d9f2299a3235ea02565e84936ee0c39ee837b" type="text/css" media="all"> <link rel="stylesheet" href="http://d.st-hatena.com/statics/theme/monotone-flower/monotone-flower.css?ce325ac2661691df491af4bf345b96f01406af7f" type="text/css" media="all"> <link rel="alternate" type="application/rss+xml" title="RSS" href="http://d.hatena.ne.jp/Kazuhira/rss"> <link rel="alternate" type="application/rss+xml" title="RSS 2.0" href="http://d.hatena.ne.jp/Kazuhira/rss2"> 〜省略〜
この時、起動したプロキシ側にはPipelineにLoggingHandlerが入っているので、こんな感じでダンプされます。
5 02, 2015 9:54:23 午後 io.netty.handler.logging.LoggingHandler logMessage 情報: [id: 0x2c4662e4, /0:0:0:0:0:0:0:0:8080] RECEIVED: [id: 0xc936e659, /127.0.0.1:34328 => /127.0.0.1:8080] 5 02, 2015 9:54:23 午後 io.netty.handler.logging.LoggingHandler channelRegistered 情報: [id: 0xc936e659, /127.0.0.1:34328 => /127.0.0.1:8080] REGISTERED 5 02, 2015 9:54:23 午後 io.netty.handler.logging.LoggingHandler channelActive 情報: [id: 0xc936e659, /127.0.0.1:34328 => /127.0.0.1:8080] ACTIVE 5 02, 2015 9:54:23 午後 io.netty.handler.logging.LoggingHandler logMessage 情報: [id: 0xc936e659, /127.0.0.1:34328 => /127.0.0.1:8080] RECEIVED(87B) +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 47 45 54 20 2f 4b 61 7a 75 68 69 72 61 2f 20 48 |GET /Kazuhira/ H| |00000010| 54 54 50 2f 31 2e 31 0d 0a 55 73 65 72 2d 41 67 |TTP/1.1..User-Ag| |00000020| 65 6e 74 3a 20 63 75 72 6c 2f 37 2e 33 35 2e 30 |ent: curl/7.35.0| |00000030| 0d 0a 41 63 63 65 70 74 3a 20 2a 2f 2a 0d 0a 48 |..Accept: */*..H| |00000040| 6f 73 74 3a 20 64 2e 68 61 74 65 6e 61 2e 6e 65 |ost: d.hatena.ne| |00000050| 2e 6a 70 0d 0a 0d 0a |.jp.... | +--------+-------------------------------------------------+----------------+ 5 02, 2015 9:54:24 午後 io.netty.handler.logging.LoggingHandler logMessage 情報: [id: 0xc936e659, /127.0.0.1:34328 => /127.0.0.1:8080] WRITE(1024B) +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d |HTTP/1.1 200 OK.| |00000010| 0a 44 61 74 65 3a 20 53 61 74 2c 20 30 32 20 4d |.Date: Sat, 02 M| |00000020| 61 79 20 32 30 31 35 20 31 32 3a 35 34 3a 32 31 |ay 2015 12:54:21| |00000030| 20 47 4d 54 0d 0a 53 65 72 76 65 72 3a 20 41 70 | GMT..Server: Ap| |00000040| 61 63 68 65 0d 0a 58 2d 53 65 72 76 65 72 3a 20 |ache..X-Server: | |00000050| 64 69 61 72 79 62 61 63 6b 65 6e 64 37 39 0d 0a |diarybackend79..| |00000060| 58 2d 72 75 6e 74 69 6d 65 3a 20 34 39 31 6d 73 |X-runtime: 491ms| 〜省略〜
他には、うちのローカルにはMySQLが入っているので、これをプロキシしてみましょう。
$ groovy netty_proxy.groovy --remotehost localhost --remoteport 3306 5 02, 2015 9:56:05 午後 io.netty.handler.logging.LoggingHandler channelRegistered 情報: [id: 0x1b2d1b0a] REGISTERED 5 02, 2015 9:56:05 午後 io.netty.handler.logging.LoggingHandler bind 情報: [id: 0x1b2d1b0a] BIND(0.0.0.0/0.0.0.0:8080) 5 02, 2015 9:56:05 午後 io.netty.handler.logging.LoggingHandler channelActive 情報: [id: 0x1b2d1b0a, /0:0:0:0:0:0:0:0:8080] ACTIVE
ローカルの3306をターゲットに指定。
mysqlコマンドで、ローカルの8080ポートに対して普通に使えます。
※「localhost」指定はダメ
$ mysql --host=127.0.0.1 --port=8080 -ukazuhira -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 8 Server version: 5.6.23 MySQL Community Server (GPL) Copyright (c) 2000, 2015, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> SHOW DATABASES; +--------------------+ | Database | +--------------------+ | information_schema | | practice | +--------------------+ 2 rows in set (0.00 sec) mysql>
単に写しただけですが、スクリプトひとつで動かせるので良いかなーと思うのですが。
一応、Gistにも置いています。
https://gist.github.com/kazuhira-r/7e7c514f25c06cd41407