2009-03-23

CL-WHO macros

CL-WHO is a library, that performs HTML generation from s-expressions (which I'll call pseudo-html forms). As for now it has one major shortcoming due to the way the input is processed by the WITH-HTML-OUTPUT macro: it doesn't support using macros, that expand to pseudo-html forms. (And the reason for it is that the input is first subjected to rule-based transformations to allow intermixing pseudo-html and lisp forms).

Let's look at the simple example...

First we just process plain pseudo-html:
CL-USER> (with-html-output-to-string (s)
(:p "a"))
"<p>a</p>"

Then we mix it with Lisp code with the help of using a special symbol HTM to hint WITH-HTML-OUTPUT, where to switch:
CL-USER> (with-html-output-to-string (s)
(:ul (dolist (a '(1 2 3))
(htm (:li (str a))))))
"<ul><li>1</li><li>2</li><li>3</li></ul>"

Now we try to define a macro:
CL-USER> (defmacro test (a)
`(htm (:li (str ,a))))
TEST
CL-USER> (with-html-output-to-string (s)
(:ul (dolist (a '(1 2 3))
(test a))))

But the following code will cause UNDEFINED-FUNCTION error ("The function :LI is undefined.") Why? Because the translation of pseudo-html s-expressions would be already finished by the time of macroexpansion of TEST, so it will be expanded not inside (:ul (dolist (a '(1 2 3)) ...), but inside calls to WRITE-STRING.

It's definitely unlispy (not to be able to use macros anywhere you want :), so from the beginning of using CL-WHO I tried to find a work-around. (And I guess, many did the same). At least in the mailing list I turned out to be not the only one, who proposed something...

Below 3 solutions, that I've tried (in chronological order) are described. Each implements a distinct approach to combining pseudo-html forms with macros.
  1. Delimited macroexpansion: introduce an additional special symbol (in the lines of already present HTM, STR, ESC and FMT) -- EMB, which will instruct CL-WHO to MACROEXPAND-1 the enclosed form during input transformation.

    This is the example (based on the example from CL-WHO manual):
    (defmacro embed-td (j)
    `(:td :bgcolor (if (oddp ,j) "pink" "green")
    (fmt "~@R" (1+ ,j))))

    (with-html-output-to-string (*http-stream*)
    (:table :border 0 :cellpadding 4
    (loop for i below 25 by 5
    do (htm
    (:tr :align "right"
    (loop for j from i below (+ i 5)
    do (htm (emb (embed-td j)))))))))

    I implemented it by tweaking the WITH-HTML-OUTPUT underlying input transforming function TREE-TO-TEMPLATE:
    (defun tree-to-template (tree)
    "Transforms an HTML tree into an intermediate format - mainly a
    flattened list of strings. Utility function used by TREE-TO-COMMANDS-AUX."
    (loop for element in tree
    nconc (cond ((or (keywordp element)
    (and (listp element)
    (keywordp (first element)))
    (and (listp element)
    (listp (first element))
    (keywordp (first (first element)))))
    ;; normal tag
    (process-tag element #'tree-to-template))
    ((listp element)
    ;; most likely a normal Lisp form (check if we
    ;; have nested HTM subtrees) or an EMB form
    (list
    (if (eql (car element) 'emb)
    (replace-htm (list 'htm (macroexpand-1 (cadr element)))
    #'tree-to-template)
    (replace-htm element #'tree-to-template))))
    (t
    (if *indent*
    (list +newline+ (n-spaces *indent*) element)

    This is a simple and undertested solution, so it, probably, may fail in some corner cases. At least I couldn't make it reliably work, when I tried to combine it with PARENSCRIPT code (see http://common-lisp.net/pipermail/cl-who-devel/2008-July/000351.html). So it's just a direction (yet a quite possible one). But I decided to leave it aside to try the second approach...

  2. Preliminary macroexpansion: don't modify CL-WHO, but build on top of it a macro infrastructure to expand the macros, before the whole form is passed to WITH-HTML-OUTPUT.

    After thinking about a problem for some time I came to a conclusion, that it would be better to treat CL-WHO as just a good processor for static pseudo-html data, and on top of it build macros, which will allow eventually to add macroexpansion ability. For that let's consider such model: if we want to use macros, we define an "endpoint", at which they should be expanded (the function, that generates HTML after all). I called this endpoint WHO-PAGE. It will function as DEFMACRO, taking a backquote template, in which we can explicitly control evaluation of forms.
    (defmacro def-who-page (name (&optional stream) pseudo-html-form)
    "Creates a function to generate an HTML page with the use of
    WITH-HTML-OUTPUT macro, in which pseudo-html forms, constructed with
    DEF-WHO-MACRO, can be embedded. If STREAM is nil/not supplied
    WITH-HTML-OUTPUT-TO-STRING to a throwaway string will be used,
    otherwise -- WITH-HTML-OUTPUT"
    `(macrolet ((who-tmp ()
    `(,@',(if stream
    `(with-html-output (,stream nil :prologue t))
    `(with-html-output-to-string
    (,(gensym) nil :prologue t)))
    ,,pseudo-html-form)))
    (defun ,name ()
    (who-tmp))))

    The definition of this HTML-generation functions uses macrolet to expand the macros before passing the body to CL-WHO. And the special macros, that will be used inside the template, could be defined like this:
    (defmacro def-who-macro (name (&rest args) pseudo-html-form)
    "A macro for use with CL-WHO's WITH-HTML-OUTPUT."
    `(defmacro ,name (,@args)
    `'(htm ,,pseudo-html-form)))

    But that's not all. Sometimes we will need to evaluate the arguments, passed to who-macro (to be able to accept pseudo-html forms as arguments), so a complimentary utility can be added (it's named by the PARENSCRIPT naming convention):
    (defmacro def-who-macro* (name (&rest args) pseudo-html-form)
    "Who-macro, which evaluates its arguments (like an ordinary function,
    which it is in fact.
    Useful for defining syntactic constructs"
    `(defun ,name (,@args)
    ,pseudo-html-form))

    The example usage can be this:
    (def-who-macro title (for &optional (show-howto? t))
    "A title with a possibility to show howto-link by the side"
    `(:div :class "title"
    (:h2 :class "inline"
    "→ " ,(ie for)) " "
    (when ,show-howto?
    (how-to-use ,for)) (:br)))

    (def-who-macro* user-page (title &key head-form onload body-form)
    `(:html
    (:head (:title ,(ie title))
    (:link :rel "stylesheet" :type "text/css"
    :href "/css/user.css")
    (:script :src "/js/user.js" :type "text/javascript"
    "")
    ,head-form)
    (:body :onload ,onload
    ,(header-box)
    (:table ,@(wide-class)
    ,body-form)
    ,(footer-box))))

    I could successfully use this approach and have added some additional features to it (docstings and once-only evaluation, for example). Actually, I've built a whole dynamic web-site on it, and then faced a problem. As the code of the system grew quite complex with several layers of who-macros, SBCL started to lack resources to compile it. More specifically, upon re-compilation of the system it sometimes fell into heap dump. We tried to investigate the problem, but couldn't dig deep enough to find it's cause (or just lacked enough knowledge to properly debug it).

    But the possibility of this solution indeed shows the nature of Lisp: virtually everything can be changed and all the boilerplate for that can be made transparent to the end-user. Alas it also shows, that this flexibility comes at a cost: it's pretty hard to build a robust and simple system, that will handle all the possible corner cases.

  3. Transparent macroexpansion (idea and basic implementation by Victor Kryukov): the last approach (which I use now) was proposed in CL-WHO mailing list. It's in some sense the most obvious one: modify CL-WHO rules to accommodate macroexpansion.

    Here's the implementation of this idea, that I use, which is tightly based on the one, proposed by Victor:
    (defparameter *who-macros* (make-array 0 :adjustable t :fill-pointer t)
    "Vector of macros, that MACROEXPAND-TREE should expand.")

    (defun macroexpand-tree (tree)
    "Recursively expand all macro from *WHO-MACROS* list in TREE."
    (apply-to-tree
    (lambda (subtree)
    (macroexpand-tree (macroexpand-1 subtree)))
    (lambda (subtree)
    (and (consp subtree)
    (find (first subtree) *who-macros*)))
    tree))

    (defmacro def-internal-macro (name attrs &body body)
    "Define internal macro, that will be added to *WHO-MACROS*
    and macroexpanded during W-H-O processing.
    Other macros can be defined with DEF-INTERNAL-MACRO, but a better way
    would be to add additional wrapper, that will handle such issues, as
    multiple evaluation of macro arguments (frequently encountered) etc."
    `(eval-when (:compile-toplevel :load-toplevel :execute)
    (prog1 (defmacro ,name ,attrs
    ,@body)
    (unless (find ',name *who-macros*)
    ;; the earlier the macro is defined, the faster it will be found
    ;; (optimized for frequently used macros, like the inernal ones,
    ;; defined first)
    (vector-push-extend ',name *who-macros*)))))

    ;; basic who-macros

    (def-internal-macro htm (&rest rest)
    "Defines macroexpasion for HTM special form."
    (tree-to-commands rest '*who-stream*))

    (def-internal-macro str (form &rest rest)
    "Defines macroexpansion for STR special form."
    (declare (ignore rest))
    (let ((result (gensym)))
    `(let ((,result ,form))
    (when ,result (princ ,result *who-stream*)))))

    (def-internal-macro esc (form &rest rest)
    "Defines macroexpansion for ESC special form."
    (declare (ignore rest))
    (let ((result (gensym)))
    `(let ((,result ,form))
    (when ,form (write-string (escape-string ,result)
    *who-stream*)))))
    (def-internal-macro fmt (form &rest rest)
    "Defines macroexpansion for FMT special form."
    `(format *who-stream* ,form ,@rest))

    An interesting side-effect here is, that now HTM (et al.) becomes not just a symbol, but a normal macro, so this will work out of the box:
    CL-USER> (with-html-output-to-string (s)
    (:ul (dolist (a '(1 2 3))
    (test a))))

    And a user-level DEF-WHO-MACRO, that incorporates ONCE-ONLY (which is regularly needed for this domain) and docstring support can be defined like this:
    (defmacro def-who-macro (name attrs &body body)
    "Define a macro, that will be expanded by W-H-O.
    /(Its name is added to *WHO-MACROS*).
    Body is expected to consist of an optional doctring and declaration,
    followed by a single backquoted WHO template.
    All regular, optional and keyword arguments are wrapped in ONCE-ONLY
    and can't contain pseudo-html forms."
    `(eval-when (:compile-toplevel :load-toplevel :execute)
    (unless (find ',name *who-macros*)
    (vector-push-extend ',name *who-macros*))
    (defmacro ,name ,attrs
    ,@(when (cdr body) ; docstring
    (butlast body))
    ,(let ((attrs (if-it (or (position '&rest attrs) (position '&body attrs))
    (subseq attrs 0 it)
    attrs)))
    (if attrs
    `(once-only ,(mapcar #`(car (as 'list _))
    (remove-if #`(char= #\& (elt (format nil "~a"
    (car (mklist _)))
    0))
    attrs))
    `(htm ,,(last1 body)))
    (last1 body))))))

I hope the above explanations and examples can be grasped easily. And moreover, that they will be useful for those, who are searching for the solution to the same problem. Still, if questions arise, be free to ask them.
If someone uses a different approach, I'm very interested to hear about it.

PS. Btw, Edi Weitz mentioned in the mailing list, that he is preparing a new version of the library. As he'd said, that addition of macroexpansion was on his TODO list, it will be interesting to see, what the new CL-WHO will hold...

2009-03-10

О самоподписных сертификатах

Написано для: habrahabr.ru

В связи с моим участием в проекте fin-ack.com постоянно сталкиваюсь с подобными замечаниями:
я не доверяю вашому самоподписному сертификату, почему вы не купите «нормальный» сертификат?

Как по мне, это один из случаев недопонимания и предрассудков, которых так много в отношении безопасности в Интернете. (Вроде знаменитых "Хакеров, крекеров, спамов, куки" :). Хочу разобрать его с двух точек зрения: как человека, некоторое время проработавшего в сфере защиты информации в банке и имевшего дело с большинством аспектов информационной безопасности, и как человека, занимающегося разработкой и развитием интернет-сервиса.

Но сперва отвечу на вопрос, почему у нас нет "нормального" сертификата? (На самом деле, с недавнего времени есть :) Самая главная причина в том, что в нашем списке приоритетов этот пункт стоял на N-ном месте, и только сейчас все N-1 предыдущих пунктов были выполнены. Когда работаешь над новым проектом, всегда приходится от чего-то отказываться, потому что ресурсы, прежде всего временные, ограничены...

А почему же он стоял аж на N-ном месте?
Во-первых, зачем вообще нужен сертификат SSL? Для того, чтобы зашифровать HTTP-соединение между браузером и сайтом, по которому будет передаваться пароль и какие-то другие конфиденциальные данные. Что изменится, если сертификат не подписан доверенным центром сертификации? Ничего! Соединение все равно будет зашифрованно точно также. Единственная возможная проблема: атака человек-посредине, которая в Интернете обычно является phishing'ом или pharming'ом.
  • При фишинге пользователя перенаправляют на сайт с похожим URL. При этом в браузере обязательно появится предупреждение про сертификат (такое же предупреждение появляется и при первом заходе на реальный сайт с самоподписным сертификатом).

    В общем-то, в этой ситуации достаточно просто посмотреть к какому домену относится сертификат, и если это именно тот домен, на который вы хотели попасть, добавить сертификат в доверенные. После этого любое сообщение о недоверенном сертификате для данного сайта можно воспринимать как тревожный звоночек.

  • Отличие фарминга в том, что в данном случае пользователь попадет как-бы на тот сайт, на который хотел (судя по URL). Впрочем, ему также как и при фишинге будет показано сообщение о недоверенном сертификате.

Но многие вкладывают в сертификат SSL больший смысл:
...Если же сертификат выдан каким-нибудь Verisign-ом (для примера), то это некая "гарантия" что за этим сайтом стоит настоящая организация/частное лицо и уж как минимум "есть с кого спросить в случае чего". Т.е. вообще это как гарантия "серьезности" намерений владельцев сайта.

Мы прекрасно понимаем, что такое мнение имеет право на жизнь. Но ведь все не так просто. Ничто не мешает купить сертификат у Verisign или другого вендора на липовую контору или подставные личные данные. Они не могут проверить наличие у клиента юридических оснований выдавать себя за условные ООО "Рога и копыта" из г.Пермь, Россйская Федерация. Единственное, что проверяется при выпуске сертификата — это то, принадлежит ли вам домен, для которого вы его запрашиваете.
Так что, как по мне, покупка сертфиката у Verisign'а — это всего-навсего демонстрация того, что компания готова выбросить 500$ и несколько человеко-часов, а то и больше на утрясение всех организационных вопросов, вместо того, чтобы потратить это время и деньги на разработку новых возможностей или же реальное улучшение безопасности системы. Вообще, Verisign — это для банков. Есть другие вендоры, с которыми проще и дешевле (пример — ниже).

Но, самое главное, другое. Любая компьютерная система уязвима настолько, насколько уязвимо ее наименее защищенное звено. Хороший подход к безопасности — это всегда комплекс мер, в котором нужно учесть все риски и уделить каждому должное внимание. Попробую перечислить основные риски безопасноти пользовательских данных для стандартного Интернет-проекта, имеющего дело с личной информацией (веб-почта, личная бухгалтерия и пр.) в порядке их важности:
  1. Не достаточно продуманная система доступа к конфиденциальным данным, которая имеет дыры

  2. Проблемы в работе ПО, которое используется системой (ОС, веб-сервер, реализация протоколов шифрования), позволяющие осуществить взлом

  3. Атаки типа человек-посредине, социальная инженерия

Фишинг/фарминг (человек-посредине), по моему мнению, один из наименее важных рисков, поскольку его намного труднее осуществить, его быстро перекроют и, поэтому, такая атака имеет смысл только для систем с очень большим количеством пользователей, из которых можно быстро выудить очень ценные данные (классический пример: интернет-банкинг). По сравнению с этим намного проще запустить сканер уязвимостей и обнаружить, что в системе используется старая версия OpenSSH или на Windows не установлена какая-то заплатка (к нам каждый день стучатся тысячи таких тестировщиков :). Или обнаружить какую-то XSS или SQL-injection уязвимость. Это не говоря о более сложных проблемах создания безопасных Интернет-систем, таких как, например, корректное использование сессий (и куки) для аутентификации. Именно этому нужно уделять внимание в первую очередь!

Еще один аспект безопасности, связанный с сертификатами. Будь он самоподписным или выданным Verisign'ом, все равно с ним ассоциирован секретный ключ, который нужно где-то хранить. Более того, он постоянно используется веб-сервером при открытии HTTPS-соединений, т.е. его нельзя применить один раз при включнии питания, сохранить на флешку и спрятать в сейф. Что будет, если кто-то завладеет ключом? (программист, который имел доступ к серверу, взломщик или еще кто-то). В идеале, этот ключ зашифрован, но при желании и наличии ресурсов его можно расшифровать (и сейчас это дешевле, чем организовать фишинг-атаку). А ведь мы не учли, что некоторые веб-сервера или реверс-прокси вообще не умеют работать с зашифрованными ключами. А еще ведь пароль может быть захардкожен где-то в тексте программы или скрипта, который ее запускает... Так что то, что на каком-то сайте красуется бирочка, что его SSL сертификат подписан Verisign, не дает никакой гарантии, что в один прекрасный день не появится фарминг-аналог, использующий тот же сертификат с украденным секретным ключом.

Тут я даже не вспоминаю о таких аспектах, связанных с системой PKI, как особенности ее поддержки на разных специфических платформах, таких как j2me...

Резюме: есть вещи, которые, в целом, правильные, но не всегда стоят затраченных усилий. Концентрация стартапов должна быть на другом, а мелочи, подобные "правильным" сертификатам должны идти вторым эшелоном. Сначала, как говорят американцы, нужно "get the product right". Всему свое время.

P.S. Собственно говоря, я понимаю, что чем пытатся изменить общественное мнение, проще под него подстроится, поэтому у нас уже есть "правильный" сертификат (время подошло). Кстати говоря, который стоит в 10 раз дешевле, чем большинство (спасибо GoDaddy!). Цель данной статьи в первую очередь в том, чтобы еще раз коснуться неисчерпаемой темы информационной безопасности в Интернете и постараться правильно расставить акценты в одном из ее аспектов.