Fragments of verbose memory

冗長な記憶の断片 - Web技術のメモをほぼ毎日更新(準備中)

Jan 29, 2021 - コメント - 日記

shell-jsonrpcをバイナリ対応してみた

以前の記事、shellコマンドをJSON-RPC経由で実行する にて作ったshell-jsonrpc ですが、ふとバイナリファイルの扱いがどうなっているか不安になって調べてみました→だめでした、ので改良してみます。

$ curl http://0.0.0.0:8999 -d '{"jsonrpc":"2.0", "id": "abc", "method":"shell", "params": [{"command": "cat a.png"}] }'|jq .

例外が発生します

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[2021-01-28 00:16:54] ERROR JSON::GeneratorError: source sequence is illegal/malformed utf-8
        /Users/tumf/.rvm/gems/ruby-2.7.2/gems/multi_json-1.15.0/lib/multi_json/adapters/json_common.rb:19:in `to_json'
        /Users/tumf/.rvm/gems/ruby-2.7.2/gems/multi_json-1.15.0/lib/multi_json/adapters/json_common.rb:19:in `dump'
        /Users/tumf/.rvm/gems/ruby-2.7.2/gems/multi_json-1.15.0/lib/multi_json/adapter.rb:25:in `dump'
        /Users/tumf/.rvm/gems/ruby-2.7.2/gems/multi_json-1.15.0/lib/multi_json.rb:139:in `dump'
        /Users/tumf/.rvm/gems/ruby-2.7.2/gems/jimson-0.11.0/lib/jimson/server.rb:112:in `process'
        /Users/tumf/.rvm/gems/ruby-2.7.2/gems/jimson-0.11.0/lib/jimson/server.rb:87:in `call'
        /Users/tumf/.rvm/gems/ruby-2.7.2/gems/rack-1.6.13/lib/rack/handler/webrick.rb:88:in `service'
        /Users/tumf/.rvm/rubies/ruby-2.7.2/lib/ruby/2.7.0/webrick/httpserver.rb:140:in `service'
        /Users/tumf/.rvm/rubies/ruby-2.7.2/lib/ruby/2.7.0/webrick/httpserver.rb:96:in `run'
        /Users/tumf/.rvm/rubies/ruby-2.7.2/lib/ruby/2.7.0/webrick/server.rb:307:in `block in start_thread'

binding.pryで覗いてみると標準出力がバイナリになっていますね。

そこでバイナリを判別して、Base64エンコードするようにしてみます。バイナリ判定アルゴリズムはptools を参考にしました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
require 'bundler/setup'
require 'jimson'
require 'json'
require "open3"
require 'base64'

# https://github.com/djberg96/ptools/blob/master/lib/ptools.rb#L77
def binary?(s, percentage = 0.30)
  s = s.encode('US-ASCII', :undef => :replace).split(//)
  ((s.size - s.grep(" ".."~").size) / s.size.to_f) > percentage
rescue Encoding::InvalidByteSequenceError =>e
  true
end

class MyHandler
  extend Jimson::Handler 
  
  def shell *params
    params.collect { |param|
      c = param["command"]
      i = if param["stdin_binary"]
            Base64.decode64(param["stdin"])
          else
            param["stdin"]
          end
      o, e, s = Open3.capture3(c, stdin_data: i)

      if binary?(o)
        o = Base64.encode64(o) 
      else
        json = JSON.parse(o) rescue nil
      end
      { command: c, "stdout.json": json, stdout: o, stderr: e, status: { exitstatus: s.to_i, pid: s.pid } }
    }
  rescue =>e
    p e
    p e.backtrace
    raise
  end
end

server = Jimson::Server.new(MyHandler.new)
server.start # serve with webrick on http://0.0.0.0:8999/

しかしこのバイナリー判定30%以上の非ASCIIがあるかどうかでバイナリと判定するっていうの面白いですね・・・

# https://github.com/djberg96/ptools/blob/master/lib/ptools.rb#L77
def binary?(s, percentage = 0.30)
  s = s.encode('US-ASCII', :undef => :replace).split(//)
  ((s.size - s.grep(" ".."~").size) / s.size.to_f) > percentage
rescue Encoding::InvalidByteSequenceError =>e
  true
end

さて、これで冒頭のテストをします

$ curl http://0.0.0.0:8999 -d '{"jsonrpc":"2.0", "id": "abc", "method":"shell", "params": [{"command": "cat a.png"}] }'|jq .
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  "jsonrpc": "2.0",
  "result": [
    {
      "command": "cat",
      "stdout.json": null,
      "stdout": "iVBORw0KGgoAAAANSUhEUgAABgQAAALmCAIAAAAC...UeAHs3Qd8\n",
      "stderr": "",
      "status": {
        "exitstatus": 0,
        "pid": 53175
      }
    }
  ],
  "id": "abc"
}

うまくいきました👍