From 40ae26e3fc7045518345ee5de0654b9e9c46541e Mon Sep 17 00:00:00 2001 From: Dimytch Date: Tue, 14 May 2024 17:18:04 +0200 Subject: [PATCH] =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=BD=D0=BE=D0=BE=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=BD=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/examples/acquira_dead.rb | 9 ++ spec/examples/bunny_client.rb | 107 +++++++++++++++ spec/examples/cp_host.rb | 176 ++++++++++++++++++++++++ spec/examples/default.rb | 7 + spec/examples/extended_array.rb | 12 ++ spec/examples/extended_hash.rb | 218 ++++++++++++++++++++++++++++++ spec/examples/extended_string.rb | 7 + spec/examples/glutton.rb | 96 +++++++++++++ spec/examples/http-request.rb | 45 ++++++ spec/examples/input_data_error.rb | 79 +++++++++++ spec/examples/janitor.rb | 49 +++++++ spec/examples/main.rb | 56 ++++++++ spec/examples/multi_io.rb | 5 + spec/examples/spec_helper.rb | 122 +++++++++++++++++ spec/examples/telegram.rb | 14 ++ spec/examples/vtb.rb | 16 +++ 16 files changed, 1018 insertions(+) create mode 100644 spec/examples/acquira_dead.rb create mode 100644 spec/examples/bunny_client.rb create mode 100755 spec/examples/cp_host.rb create mode 100644 spec/examples/default.rb create mode 100644 spec/examples/extended_array.rb create mode 100644 spec/examples/extended_hash.rb create mode 100644 spec/examples/extended_string.rb create mode 100644 spec/examples/glutton.rb create mode 100644 spec/examples/http-request.rb create mode 100644 spec/examples/input_data_error.rb create mode 100644 spec/examples/janitor.rb create mode 100755 spec/examples/main.rb create mode 100644 spec/examples/multi_io.rb create mode 100644 spec/examples/spec_helper.rb create mode 100644 spec/examples/telegram.rb create mode 100644 spec/examples/vtb.rb diff --git a/spec/examples/acquira_dead.rb b/spec/examples/acquira_dead.rb new file mode 100644 index 0000000..f0c996d --- /dev/null +++ b/spec/examples/acquira_dead.rb @@ -0,0 +1,9 @@ +require 'securerandom' +require 'erb' + +module Responder + @answer = YAML.load ERB.new( File.read "#{ $project_root }/responders/acquira-dead.yml" ).result + def Responder.answer(body, in_fmt = :plain, out_fmt = :plain) + return [ @answer['answer']['code'], @answer['answer']['headers'], @answer['answer']['body'] ] + end +end \ No newline at end of file diff --git a/spec/examples/bunny_client.rb b/spec/examples/bunny_client.rb new file mode 100644 index 0000000..57c3947 --- /dev/null +++ b/spec/examples/bunny_client.rb @@ -0,0 +1,107 @@ +require 'bunny' +require 'json' + +class BunnyClient < Bunny::Consumer + ## + # Router::Connection, String|Array, Router::Action, params: Hash == секция роутов, + # если есть tmout: для ожидания сообщения -- значит работает один раз и выходит. + # def initialize( mqconn, listen_key, action, params: {}, tmout: nil, queuename: nil ) + def initialize( cfg, worker ) + @cfg = cfg + @action = worker + @status = :idle + @cancelled = false + @conn = @channel = @queue = @exchange = nil + at_exit{ self.close } + super( channel, queue, "#{ Cfg.app.id }.#{ @worker.class.to_s }", false, false ) + if cfg.routing_key.nil? # только подключиться к кролику + exchange + else # поднять клиента и слушать ключ + # channel, queue, consumer_tag = channel.generate_consumer_tag, no_ack = false, exclusive = false, arguments = {} + on_delivery do |a, b, c| + @status = :job + @worker.arbeiten!( a, b.to_hash, c ) + @status = :idle + end + queue.subscribe_with self + Log.info{"Подслушиваю ключ #{ lk } на обменнике #{ exn } в очереди #{ queue.name }."} + queue.bind( @exchange, routing_key: lk ) + end + Thread.current.name = @action.class.to_s + Log.debug{ self.inspect } + end + + def connection + if ! @conn || @conn.closed? + Log.info{"#{ self.class.name } подключение #{ self.inspect }."} + @conn = Bunny.new @cfg.conn.merge( logger: MQLog ) + @conn.start + end + @conn + rescue Bunny::ChannelAlreadyClosed => e + Log.warn{ "#{ self.class.to_s } переподключение #{ short_log }." } + @conn = nil + sleep( Cfg.app.tmout.mq_connection_retry || 0.5 ) + retry + end + + def channel( reset: false ) + if ! @channel || @channel.closed? + @channel = connection.create_channel + end + @channel + end + + def queue + if @queue.nil? || cancelled? + @queue = channel.queue( @cfg.queue, cfg.rmq.defaults.queue ) + end + @queue + rescue Bunny::ChannelAlreadyClosed, Bunny::NotFound => e + Log.warn{"Channel or queue is closed."} + @channel = @exchange = nil + sleep( Cfg.app.tmout.mq_connection_retry || 0.5 ) + retry + rescue Bunny::PreconditionFailed => e + Log.fatal { e.inspect } + exit 2 + end + + def exchange + @exchange = channel.exchange( @cfg.exchange, cfg.rmq.defaults.exchange ) + rescue Bunny::ChannelAlreadyClosed => e + Log.warn{"Exchange #{ xname } is closed."} + @channel = @exchange = nil + sleep( Cfg.app.tmout.mq_connection_retry || 0.5 ) + retry + rescue Bunny::PreconditionFailed => e + Log.fatal { e.inspect } + exit 1 + end + + def vhost; channel.connection.vhost; end + def cancelled?; @cancelled; end + def idle?; @status == :idle; end + + def handle_cancellation(_) + Log.warn{ "Выключается #{ self.inspect }"} + @cancelled = true + end + + def close; self.cancel rescue nil; end + + + def run! + end + + # послать zak:json по указанному адресу + def say( rkey, zak ) + exchange.publish( zak.to_json, { routing_key: rkey } ) + end + + def inspect + out = "Потреблятель AMQP: #{ @action.nil? ? 'nil' : @action.name } conn:#{ @mqconn.inspect }, q:#{ @queue.name }-> x:#{ @route_params[:exchange] }" + out += ", одноразовый (#{ @tmout }сек.) " if @tmout.present? + out + end +end diff --git a/spec/examples/cp_host.rb b/spec/examples/cp_host.rb new file mode 100755 index 0000000..ac5f090 --- /dev/null +++ b/spec/examples/cp_host.rb @@ -0,0 +1,176 @@ +#!/usr/bin/env ruby +=begin +Порядок действий: + +01 Зайти по ssh на источник +02 Забрать список контейнеров и отфильтровать те, где есть session_id и они есть в списке сопоставления +03 НЦ, для каждого sessionId +05 Скопировать папку хрома на назначение +06 Запустить контейнер на назначении, подставив скопированную папку из 05, с той же версией браузера +07 Записать на STDOUT новый session_id и md5 хэш для GGR +09 ~~Остановить контейнер источника~~ +10 КЦ + +Чтобы подключиться к докерному unixsocket на источнике + +а) соединяем и пробрасываем порт: удалённый localhost:2375 <-> локальный localhost:8100 +б) удалённо запускаем nc 2375 > fifo > docker.sock > fifo > nc 2375 +в) отдаём команды докеру на локальный tcp порт 8100 + +Докер на назначении слушает на порту №2375 +=end + +raise unless RUBY_VERSION =~ /^3\./ +ENV['OPENSSL_CONF'] = "#{ __dir__ }/add_legacy.cnf" + +if ARGV.count != 1 + puts <<~HELP + + Копирует докеры с вебдрайвером с одного хоста на другой. + Запуск: + ./cp_host.rb customerIds.csv > newconfig.csv + Где: + customerIds.csv — файл соответствий customer_id;session_id;username;src_ip;username;dstIP + newconfig.csv — результат копирования: customerId;newSessionId;md5hash=ip+session + Также см. cfg.yml + + Подсказка: + Можно копировать контейнеры из одного источника на разные назначения. + + HELP + exit 1 +end + +require 'rubygems' +require 'bundler/setup' +require 'yaml' +require 'json' +require 'logger' +require 'ed25519' +require 'bcrypt_pbkdf' +require 'net/http' +require 'digest' +require 'fileutils' +require 'base64' +require_relative './util.rb' + +init_cfg() +sessions = {} +File.readlines(ARGV.first).each do |line| + next if line =~ /^\s*$/ + cuid, sid, usrc, srcip, udst, dstip = line.chomp.gsub(/;$/, "").split(/\s*;\s*/) + k = [srcip, dstip, usrc, udst] + data = { customerId: cuid, sessionId: sid } + sessions[k] ||= [] + sessions[k] << data + end +FileUtils.rm_f "/tmp/copy_data.log" + +# по хостам +sessions.each do |ips, idlist| + src_ip, dstIP, usrc, udst = ips + $cfg[:log].info "#{ src_ip } -> #{ dstIP }" + [src_ip, dstIP].each{|ip| raise_exit( get_ssh_redirections(), "Не доступен #{ ip }" ) unless check_connection( ip ) } + $cfg[:log].info " Пингуются и входится." + # подготовка + sshSrcURI = "#{ usrc }@#{ src_ip }" + sshDstURI = "#{ udst }@#{ dstIP }" + redir_src_all = spawn("ssh -f -n -A -L#{$cfg[:src][:docker]}:#{$cfg[:src][:sock]} -L#{$cfg[:src][:selenix]}:localhost:4444 -N #{ sshSrcURI }") + redir_dst_all = spawn("ssh -f -n -A -L#{$cfg[:dst][:docker]}:localhost:2375 -L#{$cfg[:dst][:selenix]}:localhost:4444 -N #{ sshDstURI }") + $cfg[:log].info " Перенаправления портов запущены." + sleep 1 + redir_ssh_pids = [redir_src_all, redir_dst_all] + + system "ssh #{ sshDstURI } 'mkdir -p #{ $cfg[:dst][:basepath] }; sudo systemctl start selenix'" + src_list = JSON.parse Net::HTTP.get(URI("http://localhost:#{$cfg[:src][:docker]}/containers/json")), symbolize_names: true + + # по сессиям-контейнерам + idlist.each do |u| + $cfg[:log].info " session #{ u[:sessionId] }" + userHome = "#{ $cfg[:dst][:basepath] }/home_#{ u[:customerId] }" + container = src_list.find{|rec| u[:sessionId] == parse_session_id(rec) } || next + browser, version = (container[:Image] =~ /^selenoid\/vnc_(\w+):([0-9\.]+)$/) && [$1, $2] || next + browserInfo = JSON.parse( + Net::HTTP.get( + URI("http://localhost:#{$cfg[:src][:selenix]}/wd/hub/session/#{ u[:sessionId] }")), + symbolize_names: true + ) + srcChromeFolder = browserInfo[:value][:chrome][:userDataDir] + srcChromeBaseFolder = srcChromeFolder[/\/([a-z\._\-A-Z0-9]+)$/, 1] + exec_in_container( sshSrcURI, u[:sessionId], "bash -c 'sync;sync'" ) + source_size = exec_in_container( sshSrcURI, u[:sessionId], "bash -c 'du -sh #{ srcChromeFolder }'")[/^\s*(\w+)/, 1] + + $cfg[:log].info " #{ userHome }; #{ browser }:#{ version }; #{ srcChromeBaseFolder }: #{ source_size }" + + File.write "/tmp/copy_data.sh", <<~ECOPYF + #/usr/bin/bash + set -e + ssh #{ sshDstURI } "mkdir -p '#{ userHome }/.config'" + curl --connect-timeout #{ $cfg[:curl][:connect_tmout] } \\ + -m #{ $cfg[:curl][:copy_tmout] } \\ + -s 'http://localhost:#{ $cfg[:src][:docker] }/containers/#{ u[:sessionId] }/archive?path=#{ srcChromeFolder }' | \\ + gzip -c| \\ + ssh #{ sshDstURI } "gzip -dc -|tar Cxf '#{ userHome }/.config' -" + + ssh #{ sshDstURI } " \ + cd '#{ userHome }/.config' && \ + rm -rf google-chrome && \ + mv '#{ srcChromeBaseFolder }' google-chrome + rm -f google-chrome/SingletonLock google-chrome/SingletonCookie google-chrome/SingletonSocket + " + ECOPYF + FileUtils.chmod 0700, "/tmp/copy_data.sh" + $cfg[:log].info " Запускаю копирование профиля. Лог в /tmp/copy_data.log" + assert( system("/tmp/copy_data.sh >> /tmp/copy_data.log 2>&1"), "Ошибка выполнения копирования. см. /tmp/copy_data.{sh,log}" ) + FileUtils.rm "/tmp/copy_data.sh" + $cfg[:log].info " Копирование #{ srcChromeBaseFolder } завершено. Создаю контейнер." + + nc = create_req( + URI("http://localhost:#{ $cfg[:dst][:selenix] }/wd/hub/session"), + build_conf(userHome, browser, version).to_json, + $cfg[:max_attempt]) + assert( nc, "Ошибка создания контейнера" ) + $cfg[:log].info " Создан контейнер с сессией #{ nc[:sessionId] }" + + dstContainerInfo = get_container_info( nc[:sessionId], "localhost", $cfg[:dst][:docker] ) + vncDstPort = dstContainerInfo[:NetworkSettings][:Ports][:"5900/tcp"].first[:HostPort].to_i + webdriverDstPort = dstContainerInfo[:NetworkSettings][:Ports][:"4444/tcp"].first[:HostPort].to_i + redir_ssh_pids << spawn("ssh -f -n -A -L#{ vncDstPort }:localhost:#{ vncDstPort } -L#{ webdriverDstPort }:localhost:#{ webdriverDstPort } -N #{ sshDstURI }") + $cfg[:log].info " Нажимаю [ВВОД]" + vnc_exec( 'localhost', vncDstPort, 'selenoid' ){|vnc| vnc.key_press :return } +# sleep 5 +# session_check = Net::HTTP.get_response(URI "http://localhost:#{ $cfg[:dst][:selenix] }/wd/hub/session/#{ nc[:sessionId] }/window/handles" ) +# if session_check.code.to_i > 299 +# $cfg[:log].error "Проверка показала, что не получилось. #{ session_check.code }. #{ session_check.body.inspect }. Фотаю экран." +# else +# $cfg[:log].info "Проверка показала, что всё хорошо. #{ session_check.body.inspect }. Фотаю экран." +# end +# screenshot('localhost', webdriverDstPort, nc[:sessionId], dstIP) + puts "#{ u[:customerId] };#{ nc[:sessionId] };#{ ggr_hash(dstIP, nc[:sessionId]) }" + +# $cfg[:log].info " Убиваю Хром." +# vnc_exec( 'localhost', vncDstPort, 'selenoid' ){|vnc| vnc.key_down :left_control; vnc.key_press 'w'; vnc.key_up :left_control } +# exec_in_container sshDstURI, dstContainerInfo[:Id], "bash -c 'pidwait #{ browser }'" +# sleep 0.3 +# $cfg[:log].info " Создаю новую сессию." +# newDstSession = start_session( 'localhost', webdriverDstPort, build_conf(userHome, browser, version) ) +# post( URI("http://localhost:#{ $cfg[:dst][:docker] }/containers/#{ dstContainerInfo[:Id] }/rename?name=#{ newDstSession[:sessionId] }"), +# { name: newDstSession[:sessionId] }.to_json, +# { 'Content-Type' => 'application/x-www-form-urlencoded' } ) +# exec_in_container sshDstURI, dstContainerInfo[:Id], "bash -c 'pidwait #{ browser }'" +# sleep 0.33 +# $cfg[:log].info " Новая сессия #{ newDstSession[:sessionId] }" +# screenshot('localhost', $cfg[:dst][:selenix], newDstSession[:sessionId], dstIP) +# puts "#{ u[:customerId] };#{ newDstSession[:sessionId] };#{ ggr_hash(dstIP, newDstSession[:sessionId]) }" + end ### по сессиям-контейнерам + + $cfg[:log].info <<~ELOG + Список контейнеров на #{ dstIP } + --- + #{ `ssh #{ sshDstURI } "docker ps -a"`.chomp } + --- + ELOG + $cfg[:log].info "Конец копирования хоста #{ src_ip } в #{ dstIP }." + killall redir_ssh_pids + get_ssh_redirections() + +end ### по хостам diff --git a/spec/examples/default.rb b/spec/examples/default.rb new file mode 100644 index 0000000..ffaab99 --- /dev/null +++ b/spec/examples/default.rb @@ -0,0 +1,7 @@ +module Responder + def Responder.answer(body, in_fmt = :plain, out_fmt = :plain) + [ 200, + { 'Content-Type' => 'text/plain;charset=UTF-8' }, + ['OK'] ] + end +end diff --git a/spec/examples/extended_array.rb b/spec/examples/extended_array.rb new file mode 100644 index 0000000..ca51214 --- /dev/null +++ b/spec/examples/extended_array.rb @@ -0,0 +1,12 @@ +# encoding: UTF-8 +class Array + def deep_transform!(&block) + self.map do |e| + case e + when Array then e.deep_transform!(&block) + when Hash then e.deep_transform_values!(&block) + else yield(e) + end + end + end +end diff --git a/spec/examples/extended_hash.rb b/spec/examples/extended_hash.rb new file mode 100644 index 0000000..131f1d6 --- /dev/null +++ b/spec/examples/extended_hash.rb @@ -0,0 +1,218 @@ +# encoding: UTF-8 +require 'gyoku' +require 'iconv' +require_relative 'extended_string' + +class Hash + def deep_clone = (Marshal.load Marshal.dump self) + + def to_format(t = :xml, enc = 'utf-8') + %i{xml json msgpack}.include?(t) ? send("encode_to_#{ t }", enc ) : nil + end + def encode_to_json(enc = 'utf-8') = self.to_json.encode(enc) # здесь какая-то загадка + + def encode_to_xml(enc = 'utf-8') + <<~EENCXML + + #{ Gyoku.xml(self.deep_clone.deep_transform_values!{|v| v.nil? ? '' : v }, { key_converter: :none, unwrap: true, element_form_default: true }) } + + EENCXML + end + def encode_to_msgpack(enc = 'utf-8') = self.to_msgpack + + def recursive_key?(key) + if key.is_a?(Regexp) then + kx = [] + each do |k, v| + if v.is_a?(Hash) + kx += v.recursive_key?(key) || [] + elsif k.is_a?(Array) + else + kx << k if k =~ key + end + end + return kx.empty? ? nil : kx + else + each{|k, v| return { k => v } if k == key || ( v.is_a?(Hash) && v.recursive_key?(key) ) } + end + return nil + end + def recursive_val(key) + self.each do |k, v| + return v if k == key || ( v.is_a?(Hash) && (v = v.recursive_val(key)) ) + end + return nil + end + # https://github.com/rails/rails/blob/55f9b8129a50206513264824abb44088230793c2/activesupport/lib/active_support/core_ext/hash/keys.rb + # Returns a new hash with all keys converted using the +block+ operation. + # hash = { name: 'Rob', age: '28' } + # hash.transform_keys { |key| key.to_s.upcase } # => {"NAME"=>"Rob", "AGE"=>"28"} + # If you do not provide a +block+, it will return an Enumerator + # for chaining with other methods: + # hash.transform_keys.with_index { |k, i| [k, i].join } # => {"name0"=>"Rob", "age1"=>"28"} + def transform_keys + return enum_for(:transform_keys) { size } unless block_given? + result = {} + each_key do |key| + result[yield(key)] = self[key] + end + result + end + # Destructively converts all keys using the +block+ operations. + # Same as +transform_keys+ but modifies +self+. + def transform_keys! + return enum_for(:transform_keys!) { size } unless block_given? + keys.each do |key| + self[yield(key)] = delete(key) + end + self + end + # Returns a new hash with all keys converted to strings. + # hash = { name: 'Rob', age: '28' } + # hash.stringify_keys + # # => {"name"=>"Rob", "age"=>"28"} + def stringify_keys + transform_keys(&:to_s) + end + # Destructively converts all keys to strings. Same as + # +stringify_keys+, but modifies +self+. + def stringify_keys! + transform_keys!(&:to_s) + end + # Returns a new hash with all keys converted to symbols, as long as + # they respond to +to_sym+. + # hash = { 'name' => 'Rob', 'age' => '28' } + # hash.symbolize_keys + # # => {:name=>"Rob", :age=>"28"} + def symbolize_keys = (transform_keys { |key| key.to_sym rescue key }) + + alias_method :to_options, :symbolize_keys + # Destructively converts all keys to symbols, as long as they respond + # to +to_sym+. Same as +symbolize_keys+, but modifies +self+. + def symbolize_keys! = (transform_keys! { |key| key.to_sym rescue key }) + + alias_method :to_options!, :symbolize_keys! + # Validates all keys in a hash match *valid_keys, raising + # +ArgumentError+ on a mismatch. + # Note that keys are treated differently than HashWithIndifferentAccess, + # meaning that string and symbol keys will not match. + # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age" + # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'" + # { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing + def assert_valid_keys(*valid_keys) + valid_keys.flatten! + each_key do |k| + unless valid_keys.include?(k) + raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}") + end + end + end + # Returns a new hash with all keys converted by the block operation. + # This includes the keys from the root hash and from all + # nested hashes and arrays. + # hash = { person: { name: 'Rob', age: '28' } } + # hash.deep_transform_keys{ |key| key.to_s.upcase } + # # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}} + def deep_transform_keys(&block) + _deep_transform_keys_in_object(self, &block) + end + # Destructively converts all keys by using the block operation. + # This includes the keys from the root hash and from all + # nested hashes and arrays. + def deep_transform_keys!(&block) + _deep_transform_keys_in_object!(self, &block) + end + # Returns a new hash with all keys converted to strings. + # This includes the keys from the root hash and from all + # nested hashes and arrays. + # hash = { person: { name: 'Rob', age: '28' } } + # hash.deep_stringify_keys + # # => {"person"=>{"name"=>"Rob", "age"=>"28"}} + def deep_stringify_keys + deep_transform_keys(&:to_s) + end + # Destructively converts all keys to strings. + # This includes the keys from the root hash and from all + # nested hashes and arrays. + def deep_stringify_keys! + deep_transform_keys!(&:to_s) + end + + # Returns a new hash with all keys converted to symbols, as long as + # they respond to +to_sym+. This includes the keys from the root hash + # and from all nested hashes and arrays. + # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } } + # hash.deep_symbolize_keys + # # => {:person=>{:name=>"Rob", :age=>"28"}} + def deep_symbolize_keys + deep_transform_keys { |key| key.to_sym rescue key } + end + # Destructively converts all keys to symbols, as long as they respond + # to +to_sym+. This includes the keys from the root hash and from all + # nested hashes and arrays. + def deep_symbolize_keys! + deep_transform_keys! { |key| key.to_sym rescue key } + end + + def deep_normalize_keys + deep_transform_keys { |key| key.underscore_unless_caps.to_sym rescue key } + end + def deep_normalize_keys! + deep_transform_keys! { |key| key.underscore_unless_caps.to_sym rescue key } + end + def deep_camelize_keys + deep_transform_keys { |key| key.camelcase_unless_caps.to_sym rescue key } + end + + def deep_transform_values!(&block) + self.each do |k, v| + case v + when Array then self[k] = v.deep_transform!(&block) + when Hash then self[k] = v.deep_transform_values!(&block) + else self[k] = yield(v) + end + end + end + def deep_merge!(other) + self.merge!(other){|k, x, y| mp k, x, y} + end + private + def mp(key, v1, v2) + if v1.is_a?(Hash) && v2.is_a?(Hash) + v1.merge!(v2){|k,x,y| mp k, x, y } + elsif v1.is_a?(Hash) + v1 + else + v2 + end + end + + # support methods for deep transforming nested hashes and arrays + def _deep_transform_keys_in_object(object, &block) + case object + when Hash + object.each_with_object({}) do |(key, value), result| + result[yield(key)] = _deep_transform_keys_in_object(value, &block) + end + when Array + object.map { |e| _deep_transform_keys_in_object(e, &block) } + else + object + end + end + + def _deep_transform_keys_in_object!(object, &block) + case object + when Hash + object.keys.each do |key| + value = object.delete(key) + object[yield(key)] = _deep_transform_keys_in_object!(value, &block) + end + object + when Array + object.map! { |e| _deep_transform_keys_in_object!(e, &block) } + else + object + end + end +end diff --git a/spec/examples/extended_string.rb b/spec/examples/extended_string.rb new file mode 100644 index 0000000..dd48f3b --- /dev/null +++ b/spec/examples/extended_string.rb @@ -0,0 +1,7 @@ +# encoding: UTF-8 +class String + def camelcase = self.split(/_/).map(&:capitalize).join + def camelcase_unless_caps = (self.camelcase if self =~ /[a-z]/) + def underscore = (self.gsub(/([A-Z])/, '_\1').gsub(/^_/,'').downcase) + def underscore_unless_caps = (self.underscore unless self =~ /^[A-Z]+$/) +end diff --git a/spec/examples/glutton.rb b/spec/examples/glutton.rb new file mode 100644 index 0000000..33cf67f --- /dev/null +++ b/spec/examples/glutton.rb @@ -0,0 +1,96 @@ +# encoding: UTF-8 +require 'json' +require 'nori' +require 'msgpack' +require 'iconv' +require_relative 'input_data_error' +require_relative 'extended_hash' + +# 1. Опознать кодировку, перевести в utf-8 при необходимости +# 2. Опознать формат, перевести в hash +module Glutton + + def Glutton.process_x( fmt, data ) + Glutton.send "process_#{ fmt }", data + end + + def Glutton.process_json( data ) + JSON.parse( data ) + rescue JSON::ParserError => e + raise InputFormatError.new(e.message) + end + + def Glutton.process_xml( data ) + # Защита от кулхацкеров + if data =~ /]+>|xsi:schemaLocation\s*=|]+>\n*/i,'') + end + + def Glutton.process_msgpack( data ) + MessagePack.unpack( data ) + end + + def Glutton.process_urlenc( data ) + Hash[ *URI::decode( data ).split(/[&=]/) ] + rescue + Hash[ *data.split('&').collect{|i| i.split('=',2).map{|j| j.empty? ? nil : j }} ] + end + + def Glutton.process_plain( data ) + data.force_encoding 'UTF-8' + end + + def Glutton.determine_input_enc( content_type ) + e = content_type ? ( content_type.split( /charset=/ )[1] || 'utf-8' ) : 'utf-8' + Encoding.find e + return e + end + + def Glutton.determine_output_enc( env ) + e = env.key?('HTTP_ACCEPT_CHARSET') ? ( env['HTTP_ACCEPT_CHARSET'].split(',')[0] || 'utf-8' ) : 'utf-8' + Encoding.find e + return e + end + + def Glutton.determine_format( hdr ) + puts "got hdr #{ hdr }" + case hdr + when /(application|text)\/xml/ then :xml + when /(application|text)\/json/ then :json + when /application\/((x-)?msgpack|octet-stream)/ then :msgpack + when /x-www-form-urlencoded/ then :urlenc + else + :plain + end + end + + def Glutton.format2header( fmt ) + { xml: 'text/xml', json: 'application/json', msgpack: 'application/x-msgpack', plain: 'text/plain' }[fmt] + end + + def Glutton.read_and_decode( io, enc ) + # binding.pry + body = io.read + raise DataError.new if body.nil? || body.length < 10 + # body передадим на GW, если что + body = body.force_encoding('utf-8') + body.gsub!("\xEF\xBB\xBF".force_encoding("UTF-8"), '') if enc =~ /^UTF|^SCSU|^BOCU-1|^GB-18030/i + io.rewind + return body + end + + def Glutton.cleaning(data) + if kx = data.recursive_key?(/[^a-zA-Z0-9_\.]/) + raise InputDataHack.new("Found invalid key(s)"){ kx.inspect } + end + k = data.keys & %w[request Request] + raise DataError.new if k.empty? + data = data[k.first] + %w[id, Id, ID].each{ |x| k.delete x } + data + end + +end diff --git a/spec/examples/http-request.rb b/spec/examples/http-request.rb new file mode 100644 index 0000000..1cde6e8 --- /dev/null +++ b/spec/examples/http-request.rb @@ -0,0 +1,45 @@ +# Обработать-распарсить тело, положить в payload +require_relative 'glutton' + +class HTTPRequest + attr_reader :params, :payload, :output_enc, :input_enc, :content_type + def initialize( rack_request ) + @request = rack_request + @content_type = Glutton.determine_format( @request.content_type ) + unpack_body + end + + def params = @request.params + def request_method = @request.request_method + def path = @request.path + def fullpath = @request.fullpath + def post? = (@request.request_method == 'POST') + def get? = (@request.request_method == 'GET') + + private + def unpack_body + @request.body.rewind + @payload = Glutton.process_x( + @content_type, + @request.body.read.to_s ) + end + + def deconstruct_keys( ks ) + { + request: @request, + request_method: request_method(), + path: path(), + fullpath: fullpath(), + params: params(), + output_enc: @output_enc || 'utf-8', + input_enc: @input_enc || 'utf-8', + content_type: @content_type, + paylod: @payload || unpack_body(), + is_post: post?(), + is_get: get?() + } + end + + def to_hash = deconstruct_keys() + +end diff --git a/spec/examples/input_data_error.rb b/spec/examples/input_data_error.rb new file mode 100644 index 0000000..7bc84f0 --- /dev/null +++ b/spec/examples/input_data_error.rb @@ -0,0 +1,79 @@ +# encoding: UTF-8 +class EError < StandardError + attr_reader :visa_code, :correlation_id, :errmsg + def initialize( msg = 'Errors happens.', + correlation_id = nil, + visa_code = 5, + errmsg = 'E' + ) + @visa_code = visa_code + @correlation_id = correlation_id + @logmsg ||= nil + @errmsg = errmsg + Steward.log.warn{ "#{ self.class }#{ ": '#{ @logmsg }" if @logmsg && ! @logmsg.empty? } VISA code: #{ visa_code }. CorrelationID: #{ correlation_id }. #{ self.message }#{ ' ' + yield if block_given? }" } + super msg + end +end + +class DataError < EError + def initialize(msg = 'Invalid data supplied.', correlation_id = nil, visa_code = 6, errmsg = 'E_FORMAT_ERROR') + super + end +end + +class InputFormatError < EError + def initialize(msg = 'Input format is not recognized.', correlation_id = nil, visa_code = 6, errmsg = 'E_FORMAT_UNKNOWN') + super + end +end + +class InputEncodingError < EError + def initialize(msg = 'Input encoding is not recognized.', correlation_id = nil, visa_code = 6, errmsg = 'E_ENCODING') + super + end +end + +class InputDataHack < EError + def initialize(msg = "Invalid input data.", correlation_id = nil, visa_code = 6, errmsg = 'E_UNKNOWN0' ) + @logmsg = "На нас напали! correlation_id: #{ correlation_id }. #{ msg }" + super + end +end + +class UnknownError < EError + def initialize(msg = 'Unknown error.', correlation_id = nil, visa_code = 5, errmsg = 'E_UNKNOWN') + @logmsg = Thread.current.backtrace.join("\n") + super + end +end + +class InvalidRoute < EError + def initialize(msg = "I've lost the route!", correlation_id = nil, visa_code = -6, errmsg = 'E_NO_ROUTE') + super + end +end + +class GetAnswerLater < EError + def initialize(msg = "Timeout reading from service.", correlation_id = nil, visa_code = 9, errmsg = 'OK_PROCESSING') + super + end +end + +class InternalError < EError + def initialize(msg = "Something strange happened.", correlation_id = nil, visa_code = 5, errmsg = 'E_FATAL') + @logmsg = self.message + "\n" + Thread.current.backtrace.join("\n") + super + end +end + +class AccessDenied < EError + def initialize(msg = "Anonymous access denied.", correlation_id = nil, visa_code = -17, errmsg = 'E_ACCESS_DENIED') + super + end +end + +class EGWTimeout < EError + def initialize(msg = "Timeout reading from GW.", correlation_id = nil, visa_code = -4, errmsg = 'E_GW_TIMEOUT' ) + super + end +end diff --git a/spec/examples/janitor.rb b/spec/examples/janitor.rb new file mode 100644 index 0000000..599b954 --- /dev/null +++ b/spec/examples/janitor.rb @@ -0,0 +1,49 @@ +# encoding: UTF-8 +# Чтение настроек, создание необходимых папок, создаёт $log +require 'fileutils' +require 'pathname' +require 'yaml' + +module Janitor + # Добавляет глобальные переменные для доступа к общим ресурсам + # Логирование: $log.info{"Вывод в лог"} + # Лог по умолчанию: STDERR + # Доступ к настройкам: $cfg['name'] + # Доступ к базе: $db + def Janitor.load_setup + print "Janitor is loading settings..." + $project_root = Pathname( __FILE__ ).dirname.expand_path.to_s.freeze + $runmode = ( ENV['PROXY_RUN_ENVIRONMENT'] || :development ).to_sym + $cfg = ( YAML.load_file( "#{ $project_root.to_s }/settings.yml" )[$runmode].merge( + YAML.load_file("#{ $project_root .to_s }/info/about.yml")['public'] ) ).freeze + $log = + case $cfg[:log] + when 'syslog' + require 'syslog/logger' + Syslog::Logger.new "service.#{ $cfg['name'] }", Syslog::LOG_PID | Syslog::LOG_DAEMON | Syslog::LOG_LOCAL0 + when 'stdout' + require 'logger' + Logger.new STDOUT + when nil, 'stderr' + require 'logger' + Logger.new STDERR + else + require 'logger' + Logger.new File.open($cfg[:log], 'w') + end + + $log.info { "#{ $cfg['name'] } #{$cfg[:ver]}, pid: #{ Process.pid } loaded" } + $log.info { 'loaded' } + print " загрузил.\n\n" + end + + def reconfigure + $log.info{ 'reloading settings'} + Janitor.load_setup + end + + def end_of_service = $log.warn{"ligh off"} + +end + +Janitor.load_setup diff --git a/spec/examples/main.rb b/spec/examples/main.rb new file mode 100755 index 0000000..22d6818 --- /dev/null +++ b/spec/examples/main.rb @@ -0,0 +1,56 @@ +#!/usr/bin/env ruby + +require 'rubygems' +require 'bundler/setup' +require 'rack' +require 'app-config' +require 'app-logger' +# require_relative 'http-request' + +App::Config.init approot: Pathname(__dir__).expand_path +App::Logger.new + +if ARGV[0] =~ /^--?h/ + puts <<~EHELP + Отвечает на все запросы по HTTP структурой {"responce":{"message":"pong"}} + Если задан параметр - ищёт файл с кодом в папке responders и отвечает + форматом и методом: + out_fmt, answer = Responder.responce(body, in_fmt, out_fmt) + где body - исходный расшифрованный запрос + остальные параметры являются необязательными рекомендациями и равны :plain + +EHELP + exit +end +x = ARGV[0] || 'default' +# puts "Загружаю ответчик responders/#{ x }.rb" +# require_relative "responders/#{ x }.rb" + +# use Rack::ShowExceptions +# use Rack::Reloader +body = '' +Log.info "Соломенный бычок зашёл." +app = Proc.new do |env| +# require 'pry-byebug' +# binding.pry + # request = HTTPRequest.new Rack::Request.new( env ) + Log.add Logger::UNKNOWN, <<~EINPUT + #{ env.inspect() } + body: #{ env["rack.input"].read } +EINPUT + #{ request.request_method } #{ request.fullpath } + #{ env.select{|k,v| k =~ /^[A-Z_]+$/ }.inspect } + #{ request.payload.inspect } + # puts env.inspect + # in_fmt = Glutton.determine_format env['CONTENT_TYPE'] + # out_fmt = (in_fmt == :urlenc) ? :xml : in_fmt + # body = Glutton.process_x in_fmt, body + # answer = Responder.answer request + # answer + # ['200', {'Content-Type' => Glutton.format2header( out_fmt ) }, answer ] + # puts "Принял в #{ env['CONTENT_TYPE'] }, отвечаю #{ answer }\n-----\n\n" + ['200', {'Content-Type' => "text/plain" }, "Хорошо" ] +end + +Rack::Handler::Thin.run app, Port: (Cfg.http.port || 4000), Host: (Cfg.http.host || '0.0.0.0') +Log.info "Соломенный бычок вышел." diff --git a/spec/examples/multi_io.rb b/spec/examples/multi_io.rb new file mode 100644 index 0000000..5f2a7ea --- /dev/null +++ b/spec/examples/multi_io.rb @@ -0,0 +1,5 @@ +class Tee + def initialize(*targets) = (@targets = targets) + def write(*args) = @targets.each{|t| t.write args} + def close = @targets.each(&:close) +end diff --git a/spec/examples/spec_helper.rb b/spec/examples/spec_helper.rb new file mode 100644 index 0000000..89cfd3b --- /dev/null +++ b/spec/examples/spec_helper.rb @@ -0,0 +1,122 @@ +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +system('export PROXY_RUN_ENVIRONMENT=test') +require 'rack/test' +require 'pry' +require 'iconv' +require 'rchardet' +require 'nori' +require 'gyoku' +require_relative '../main.rb' +#require "moqueue" +#overload_amqp + +def app + Confidant.new +end + +RSpec.configure do |config| + include Rack::Test::Methods + config.before(:suite) do + puts 'Запускаю соломенного бычка' + system 'bundle exec rackup -E test -D -P tmp/honeypot.pid honeypot.ru' + end + + config.after(:suite) do + puts 'Останавливаю соломенного бычка' + system 'cat tmp/honeypot.pid | xargs kill' + end + + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. + + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/rspec-fail.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + # config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = 'doc' + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +end diff --git a/spec/examples/telegram.rb b/spec/examples/telegram.rb new file mode 100644 index 0000000..208ee36 --- /dev/null +++ b/spec/examples/telegram.rb @@ -0,0 +1,14 @@ +require 'json' + +module Responder + @root = Pathname( __FILE__ ).dirname.parent.expand_path.freeze + + module_function + def answer(request) + if request.post? && request.path =~ /(setWebhook|sendMessage)$/ + [ 200, { 'Content-Type' => 'application/json' }, [{ ok: true, result: true, description: 'ok' }.to_json] ] + else + [ 502, { 'Content-Type' => 'application/json' }, [{ ok: false, result: false, description: 'Webhook was not set' }.to_json] ] + end + end +end diff --git a/spec/examples/vtb.rb b/spec/examples/vtb.rb new file mode 100644 index 0000000..092ad49 --- /dev/null +++ b/spec/examples/vtb.rb @@ -0,0 +1,16 @@ +require_relative '../http-request' + +module Responder + @root = Pathname( __FILE__ ).dirname.parent.expand_path.freeze + + module_function + def answer( request ) # <= HTTPRequest; => [ code, { headers }, [ body ] ] + case request + in request_method: 'GET' + [ 200, {}, [ 'not implemented' ] ] + in request_method: 'POST' + [ 201, {}, [ 'not implemented' ] ] + else + [ 404, {}, [ 'not found' ] ] + end +end