Мой опенсорс
Aug. 13th, 2009 11:49 amПисал недавно web-приложение - этакий мостик или конвертор с web-запросов на внутреннюю учётную систему - и обратно. На своём любимом Ruby on Rails, есссно. Веб-запросы идут с платёжных терминалов и касаются финансовых операций, потому там применяется повсеместный SSL для защиты от их подслушивания и подделки. В первоначальной спецификации писалось, что web-сервер должен не только отдавать свой SSL-сертификат, но и запрашивать и проверять сертификат клиента. Прочитав маны Апача на mod_ssl, я решил, что мне нужна директива 'SSLVerifyClient required'. Но применить эту директиву в контексте единственной директории, в которой будет крутиться защищённое web-приложение, не удалось (apache 2.2.11, Ubuntu 9.04). Судя по логам отладки, в начале Апач устанавливает SSL-сессию только с серверным сертификатом, не требуя клиентского, а когда получает GET с URLом защищённого приложения, инициирует SSL-renegotiation и только тогда запрашивает сертификат клиента. На этом этапе сессия с большинством броузеров затыкается и через некоторое время отваливается по тайм-ауту. Я нашёл Апачевский баг 47055, немножко похожий на то, обо что я споткнулся. А также бумажку с ApacheCon-EU-2009 (по ключевым словам deciphering mod_ssl), в которой пишется, что в продвинутых вопросах применения SSL по-прежнему много кривизны то там, то сям (и не только в одном Apache). Да и как SSL может быть ровным, ведь он же designed by committee, притом комитет, должно быть, был такого высокого уровня, что всяким инженерам и технарям там уже просто не нашлось места. Не смог я сделать верификацию клиента и через stunnel -v2 -d. stunnel не делает renegotiation посреди сессии, зато у него какие-то странные понятия насчёт persistent SSL-сессий и их связи с HTTP-keepalive. В итоге всех эти мытарств решили слегка переделать спецификацию. От верификации клиента на уровне SSL отказаться, зато сделать цифровую подпись клиентских запросов и ответов web-приложения, для проверки подлинности того и другого (вдобавок к обмену ключами и шифрации трафика за счёт серверного SSL-сертификата). Для каждого web-запроса указали, значения каких параметров и в каком порядке склеить в одну строку через ';' для проверки их подписи (присылаемой отдельным параметром в 16-ричном виде), а для ответов в формате xml добавили параметр в рамку <response sign=...></response> , в котором должна храниться подпись его innerHTMLа, тоже в 16-ричном виде. У Ruby есть довольно красивая и вполне ООшная, хотя и не слишком подробно документированная, привязка к OpenSSL, и в Инете нашлось несколько образцов её применения, которые я творчески переработал для своих нужд. В результате появилась пара простых функций :
Внутренняя учётная система у конечных заказчиков, хммм... своеобразная. "Финансы без проблем" называется. С ней нужно общаться, кидая файлы запросов определённого формата с раширением .IN в сетевую шару и ожидая появления файлов .OUT . Притом русские буквы в ответах идут в кодировке cp866. И там ещё есть файлик UPTI со временной меткой, определяющий, была ли обновлена база со времени того или иного запроса, или ответ на него можно взять из кэша.
Ну и появились такие функции, соответственно :
Легаси и экзотика, конечно же, но я смог к ней приспособиться :>
И ещё вам отсыплю свою наработочку, которая у меня родилась, когда пришлось обрабатывать временнЫе интервалы. Типа, когда у такого-то работника запланированы рабочие часы, какие из этих часов уже заняты под какую-либо работу, а какие ещё можно занять. Предполагается, что обрабатываемый объект представИм в виде массива или списка непустых и непересекающихся временнЫх интервалов, отсортированных по возрастанию. И я нарисовал функции, чтоб эти объекты теоретико-множественно объединять, пересекать и вычитать.
Даже нарисовал класс-обёртку для всего этого, с переопределёнными операциями. Правда, так им и не воспользовался. Там и класс-то не нужен. В функциях и так полный duck-typing.
Захотелось всё написать элегантненько-функциональненько-рекурсивненько и почти без сайд-эффектов, насколько это возможно в Ruby. Присваивания каждой переменной - только однократные, например.
И да, функции, которые _less - служебные. предназначены для операций с отсортированными аргументами и напрямую вызываться не должны.
Класс так и остался недорисованным, там ещё надо переопределить операторы [] и []= и перекидывать их на @content. Или все непонятые методы перекидывать на @content ?
В общем, так и живу. Берусь только за те затеи, в которых могу выбрать приемлемый для себя инструмент. Или в которых премлемый инструмент уже выбрали за меня.
Кроме Ruby, нормально отношусь к Perlу с его CPANом и к /bin/sh (иногда могу написать нечто довольно сложное на шелле в качестве главного языка). В подобающих случаях допускаю применение Си.
Хотел бы наработать у себя побольше навыков применения Pythonа и его фреймворков, но пока не удавалось встрять в готовые проекты на них.
C++, C#, Javу - не люблю. Как и PHP и ASP[.Net]. По разным причинам, заслуживающим отдельного рассказа.
Трогаю их только по большой нужде. Когда другого пути нет. Когда на горизонте маячит угроза голода и бедности :>
Как в анекдоте про мужчин и коз на далёком острове :>
require 'openssl'Поначалу было немножко боязно, т.к. система получалась совсем гетерогенной - пиплы с терминальной стороны писали вообще на .Netе и свой код мне не показывали. Но старые добрые pack/unpack не подвели и в этот раз. Наследие Перла, обкатанное долгими годами :> Не совсем понятно, почему unpack возвращает массив, а pack требует массива на вход, но пришлось написать [0] и .to_a, чтоб всех удовлетворить. Также, возможно, вместо pnlist.map{... params ...} можно было бы написать params.values_at(pnlist)
pubkey = OpenSSL::X509::Certificate.new(File.read('remote.cer')).public_key privkey = OpenSSL::PKey::RSA.new(File.read('server.key')) def sign_xml (s) # form a signed xml reply with signature in hexnums '<response sign="' + privkey.sign(OpenSSL::Digest::SHA1.new, s).unpack('H*')[0] + '">' + s + '</response>' end def verify_params (pnlist) # get values of named params in given order, join them by ';', get signature from hexnums and verify them all pubkey.verify(OpenSSL::Digest::SHA1.new, params[:sign].to_a.pack('H*'), pnlist.map{|i|params[i]}.join(';')) end
Внутренняя учётная система у конечных заказчиков, хммм... своеобразная. "Финансы без проблем" называется. С ней нужно общаться, кидая файлы запросов определённого формата с раширением .IN в сетевую шару и ожидая появления файлов .OUT . Притом русские буквы в ответах идут в кодировке cp866. И там ещё есть файлик UPTI со временной меткой, определяющий, была ли обновлена база со времени того или иного запроса, или ответ на него можно взять из кэша.
Ну и появились такие функции, соответственно :
require 'iconv' def decodeFBP(fo) # split fields, convert text from cp866 to utf8, return them as list of lists File.open(fo) do |f| return f.readlines.map { |z| z.split(/\xFA/).map { |s| Iconv.conv("UTF-8", "cp866", s) } } end end QN="/home/mwg/queue-Diler/" # queue/mailbox/exchange dir, usually mounted by smb from windows server FU=QN+"UPTI" # file with last DB modification timestamp, used for refreshing saved replies def queryFBP (fn, parmlist) # send ФБП query as a list of parameters in specified file name, wait for reply, decode and return it fo=QN+fn+".OUT" if test(?f, fo) # outfile exists return decodeFBP(fo) if test(?f, FU) && test(?<, FU, fo) # outfile does not need refresh File.delete(fo) # remove it before refreshing end fi=QN+fn+".IN" fi0 = fi + '0' File.open(fi0, 'w') {|f| f.write(parmlist.join("\xFA")+"\n") } # form a FBP query, probably should add encoding from utf8 to cp866 if Russian letters are used File.rename(fi0, fi) # commit - send it sleep 1 until test(?f, fo) # wait for answer to appear decodeFBP(fo) end
Легаси и экзотика, конечно же, но я смог к ней приспособиться :>
И ещё вам отсыплю свою наработочку, которая у меня родилась, когда пришлось обрабатывать временнЫе интервалы. Типа, когда у такого-то работника запланированы рабочие часы, какие из этих часов уже заняты под какую-либо работу, а какие ещё можно занять. Предполагается, что обрабатываемый объект представИм в виде массива или списка непустых и непересекающихся временнЫх интервалов, отсортированных по возрастанию. И я нарисовал функции, чтоб эти объекты теоретико-множественно объединять, пересекать и вычитать.
Даже нарисовал класс-обёртку для всего этого, с переопределёнными операциями. Правда, так им и не воспользовался. Там и класс-то не нужен. В функциях и так полный duck-typing.
Захотелось всё написать элегантненько-функциональненько-рекурсивненько и почти без сайд-эффектов, насколько это возможно в Ruby. Присваивания каждой переменной - только однократные, например.
И да, функции, которые _less - служебные. предназначены для операций с отсортированными аргументами и напрямую вызываться не должны.
#Let's represent finite nonempty interval sets as arrays of ranges. #Let's assume that within each set, ranges do not intersect #and are stored in ascending order #Let's use half-open ranges, i.e. containing its begin but not its end, #like in C++ STL. #And let's don't store empty ranges like [a,a) #We can have ranges of integers, floats, strings or times. #Each could have its own use. #now some non-class functions : def rs_union_less (a, b) # precondition : both a and b non-empty, ha,*ta=a # a.first.begin < b.first.begin hb,*tb=b # filter empty heads return rs_union(ta,b) if ha.begin == ha.end return rs_union(a,tb) if hb.begin == hb.end # simple case : head of a does not intersect with head of b, # so it goes into the result unchanged return [ha] + rs_union(ta,b) if ha.end < hb.begin # here, head of b is entirely contained in the head of a, so we just drop it return rs_union(a,tb) if ha.end >= hb.end return rs_union(ta,[ha.begin...hb.end]+tb) # take head of a, end # merge it to head of b and recurse def rs_union(a,b) # set union or merge. what is in either of sets # simple cases return b if a.empty? return a if b.empty? return a.first.begin < b.first.begin ? rs_union_less(a,b) : rs_union_less(b,a) end def rs_merge_less(a,b) ha,*ta=a # a.first.begin < b.first.begin hb,*tb=b # filter empty heads return rs_merge(ta,b) if ha.begin == ha.end return rs_merge(a,tb) if hb.begin == hb.end return [ha] + rs_merge(ta,b) end def rs_merge(a,b) # like set union, but do not merge adjacent ranges # simple cases return b if a.empty? return a if b.empty? return a.first.begin < b.first.begin ? rs_merge_less(a,b) : rs_merge_less(b,a) end def rs_intersect_less(a,b) # precondition : both a and b non-empty, ha,*ta=a # a.first.begin < b.first.begin hb,*tb=b # filter empty heads return rs_intersect(ta,b) if ha.begin == ha.end return rs_intersect(a,tb) if hb.begin == hb.end # simple case : head of a does not go into the result return rs_intersect(ta,b) if ha.end <= hb.begin # here, head of b is entirely contained in the head of a, return [hb] + rs_intersect(a,tb) if ha.end >= hb.end return [hb.begin...ha.end]+rs_intersect(ta,b) # take head of a, end # intersect it to head of b and recurse def rs_intersect(a,b) # set intersection. what is in both sets # simple case return [] if a.empty? || b.empty? return a.first.begin < b.first.begin ? rs_intersect_less(a,b) : rs_intersect_less(b,a) end def rs_subtract(a,b) # set difference. what's in a but not in b return a if a.empty? || b.empty? ha,*ta=a hb,*tb=b # filter empty heads return rs_subtract(ta,b) if ha.begin == ha.end return rs_subtract(a,tb) if hb.begin == hb.end if ha.begin < hb.begin # a starts earlier than b return [ha]+rs_subtract(ta,b) if ha.end <= hb.begin # head of a does not intersect with head of b return [ha.begin...hb.begin]+rs_subtract(ta,b) if ha.end <= hb.end # shorten head of a at the end [ha.begin...hb.begin]+rs_subtract([hb.end...ha.end]+ta,tb) # take a middle out of head of a, and cancel head of b else # a starts later than b return rs_subtract(a,tb) if hb.end <= ha.begin # heads do not intersect return rs_subtract(ta,b) if ha.end <= hb.end # head of a is cancelled entirely rs_subtract([hb.end...ha.end]+ta,tb) # head of a is shortened by head of b end end # such that rs_subtract([10...100],[50...60]) == [10...50,60...100] def rs_xor(a,b) # that's trivial : what's in one of the sets but not in both return rs_subtract(rs_union(a,b),rs_intersect(a,b)) end class RangeSet attr_reader :content def initialize(a) if a.is_a? Array @content = a else @content=[a] # for initializers like single range (a...b) end end def self.c(b) # pick @content from RangeSet, or use Array as is. return b.content if b.is_a? RangeSet return [b] if b.is_a? Range b end def -(b) return RangeSet.new(rs_subtract(@content, self.class.c(b))) end def +(b) return RangeSet.new(rs_union(@content, c(b))) end def *(b) return RangeSet.new(rs_intersect(@content, c(b))) end def ^(b) return RangeSet.new(rs_xor(@content, c(b))) end end
Класс так и остался недорисованным, там ещё надо переопределить операторы [] и []= и перекидывать их на @content. Или все непонятые методы перекидывать на @content ?
В общем, так и живу. Берусь только за те затеи, в которых могу выбрать приемлемый для себя инструмент. Или в которых премлемый инструмент уже выбрали за меня.
Кроме Ruby, нормально отношусь к Perlу с его CPANом и к /bin/sh (иногда могу написать нечто довольно сложное на шелле в качестве главного языка). В подобающих случаях допускаю применение Си.
Хотел бы наработать у себя побольше навыков применения Pythonа и его фреймворков, но пока не удавалось встрять в готовые проекты на них.
C++, C#, Javу - не люблю. Как и PHP и ASP[.Net]. По разным причинам, заслуживающим отдельного рассказа.
Трогаю их только по большой нужде. Когда другого пути нет. Когда на горизонте маячит угроза голода и бедности :>
Как в анекдоте про мужчин и коз на далёком острове :>
(upd: вот блин, залил исходники подсвеченные со стилями, а ЖЖшка стили и подсветку убрала. причём при редактировании заметки всё стильное и цветное. как его правильно в ЖаЖу заливать ?)
no subject
Date: 2009-08-13 01:00 pm (UTC)А пишу, чтоб денег заработать, для чего ж ещё :>
Совершенствование скиллзов - это так, сайд-эффект :>