Веб-программирование в Erlang
Нам понадобится веб-сервер YAWS (Yet Another Web Server).
Обладатели Убунты могут пакетно установить yaws:
apt-get build-dep erlang yaws
В большинстве линуксовых дистрибутивов yaws прийдется ставить из исходников, которые лежат
тут.
YAWS имеет следуюшие достоинства:
скорость
масштабируемость
генерация статического и динамического контента
виртуальный хостинг
отладчик
кеширование
ssl
аутентификация
сессии
модульная архитектура
веб-сокеты
Когда вы установите yaws из исходников с помощью команды make install,
конфиг будет установлен по умолчанию в /usr/local/etc/yaws/yaws.conf.
Лог файлы лежат в каталоге logdir = /usr/local/var/log/yaws.
Дополнительные бинарные сборки можно складывать в ebin_dir = /usr/local/lib/yaws/examples/ebin.
Для файлов с расширением .hrl определен каталог include_dir = /usr/local/lib/yaws/examples/include.
Конфигурация самого сервера
<server localhost>
port = 8000
listen = 127.0.0.1
docroot = /home/yaws/www
</server>
Запуск сервера в интерактивном режиме:
yaws -i
Динамический контент в yaws реализован с помощью расширения .yaws. Страница с таким расширением может содержать
как статический html-код, так и динамический эрланговский, обрамленный с помощью тегов <erl> ... </erl>,
внутри которого обязательно должна быть эрланговская функция out.
Есть два варианта вызова функции out. Первый - она возвращает кортеж .{html, String},
где String - это html-код:
<html>
<h1> Example 1 </h1>
<erl>
out(A) ->
Headers = A#arg.headers,
{html, io_lib:format("You say that you’re running ~p",
[Headers#headers.user_agent])}.
</erl>
</html>
Такая страница выводит что-то типа:
Example 1
You say that you are running "Mozilla/5.0 (X11; Linux i686; rv:31.0) Gecko/20100101 Firefox/31.0"
Второй вариант вызова out - функция out возвращает кортеж{ehtml, EHTML}:
<erl>
{ehtml, {table, [{bgcolor, grey}],
[
{tr, [],
[
{td, [], "1"},
{td, [], "2"},
{td, [], "3"}
]
},
{tr, [],
[{td, [{colspan, "3"}], "444"}]}]}}.
<erl>
Такой код вернет следующий html:
<table bgcolor="grey">
<tr>
<td> 1 </td
<td> 2 </td>
<td> 3 </td>
</tr>
<tr>
<td colspan="3"> 444 </td>
</tr>
</table>
Во втором случае возможен дополнительный вызов других эрланговских функций.
Параметр #arg содержит в себе многие подробности о клиентском запросе - заголовки, данные, пути и т.д. -
вот полный список:
-record(arg, {
clisock, % the socket leading to the peer client
client_ip_port, % {ClientIp, ClientPort} tuple
headers, % headers
req, % request
orig_req, % original request
clidata, % The client data (as a binary in POST requests)
server_path, % The normalized server path
% (pre-querystring part of URI)
querydata, % For URIs of the form ...?querydata
% equiv of cgi QUERY_STRING
appmoddata, % (deprecated - use pathinfo instead) the remainder
% of the path leading up to the query
docroot, % Physical base location of data for this request
docroot_mount, % virtual directory e.g /myapp/ that the docroot
% refers to.
fullpath, % full deep path to yaws file
cont, % Continuation for chunked multipart uploads
state, % State for use by users of the out/1 callback
pid, % pid of the yaws worker process
opaque, % useful to pass static data
appmod_prepath, % (deprecated - use prepath instead) path in front
% of:
prepath, % Path prior to ’dynamic’ segment of URI.
% or .yaws,.php,.cgi,.fcgi etc script file.
pathinfo % Set to ’/d/e’ when calling c.yaws for the request
% http://some.host/a/b/c.yaws/d/e
% equiv of cgi PATH_INFO
}).
-record(http_request,{method,
path,
version}).
-record(headers, {
connection,
accept,
host,
if_modified_since,
if_match,
if_none_match,
if_range,
if_unmodified_since,
range,
referer,
user_agent,
accept_ranges,
cookie = [],
keep_alive,
location,
content_length,
content_type,
content_encoding,
authorization,
transfer_encoding,
x_forwarded_for,
other = []
% misc other headers
}).
Вывести это на экран с помощью ehtml можно так:
<html>
<h2> The Arg </h2>
<p>This page displays the Arg #argument structure
supplied to the out/1 function.
<erl>
out(A) ->
Peer = A#arg.client_ip_port,
Req = A#arg.req,
H = yaws_api:reformat_header(A#arg.headers),
{ehtml,
[{h5,[], "The headers passed to us were:"},
{hr,[],[]},
{ol, [],lists:map(fun(S) -> {li,[], {p,[],S}} end,H)},
{h5, [], "The request"},
{ul,[],
[{li,[], f("method: ~s", [Req#http_request.method])},
{li,[], f("path: ~p", [Req#http_request.path])},
{li,[], f("version: ~p", [Req#http_request.version])}]},
{hr,[],[]},
{h5, [], "Other items"},
{ul,[],
[{li, [], f("Peer: ~p", [Peer])},
{li,[], f("docroot: ~s", [A#arg.docroot])},
{li,[], f("fullpath: ~s", [A#arg.fullpath])}]},
{hr,[],[]},
{h5, [], "Parsed query data"},
{pre,[], f("~p", [yaws_api:parse_query(A)])},
{hr,[],[]},
{h5,[], "Parsed POST data "},
{pre,[], f("~p", [yaws_api:parse_post(A)])}]}.
</erl>
Здесь используются несколько функций из модуля yaws_api.
Распарсить GET-запрос можно с помощью функции yaws_api:parse_query.
Распарсить POST-запрос можно так:
out(A) ->
L = yaws_api:parse_post(A),
{html, f("~p", [L])}
Сделать upload файла на сервер можно так:
out(A) ->
Form =
{form, [{enctype, "multipart/form-data"},
{method, post},
{action, "file_upload_form.yaws"}],
[{input, [{type, submit}, {value, "Upload"}]},
{input, [{type,file}, {width, "50"}, {name, foo}]}]},
{ehtml, {html,[], [{h2,[], "A simple file upload page"},
Form]}}.
yaws поддерживает куки, которые лежат в основе сессий. Для работы с сессиями есть несколько функций:
yaws_api:new_cookie_session(Opaque) - создает сессию
yaws_api:cookieval_to_opaque(Cookie)
yaws_api:replace_cookie_session(Cookie, NewOpaque)
yaws_api:delete_cookie_session(Cookie) - удаляет сессию
Проверку можно сделать так:
-record(session, {user,
passwd,
udata = []}).
get_cookie_val(CookieName, Arg) ->
H = Arg#arg.headers,
yaws_api:find_cookie_val(CookieName, H#headers.cookie).
check_cookie(A, CookieName) ->
case get_cookie_val(CookieName, A) of
[] ->
{error, "not logged in"};
Cookie ->
yaws_api:cookieval_to_opaque(Cookie)
end.
Редирект на другую страицу делается так:
<erl>
out(Arg) ->
URL = "http://www.erlang.org",
{redirect, URL}.
</erl>
Аутентификацию можно организовать следующим образом: имеется стартовая страница с формой,
на которой пользователь набирает логин с паролем, после чего перенаправляется на другую страницу,
которая обрабатывает запрос. Стартовая страница login.yaws:
<erl>
out(A) ->
{ehtml,
{html,[],
[{h2, [], "Login page"},
{hr},
{form, [{action,"/login_post.yaws"},{method,post}],
[{p,[], "Username"}, {input, [{type,text},{name,uname}]},
{p,[],"Password"}, {input, [{type,password},{name,passwd}]},
{input, [{type,submit},{value,"Login"}]},
{input, [{type,hidden},{name,url}, {value, A#arg.state}]}]}]}}.
</erl>
Вторая страница login_post.yaws, обрабатывающая запрос:
<erl>
-include("myapp.hrl").
%% myapp.hrl нужно прописать в yaws.conf
kv(K,L) ->
{value, {K, V}} = lists:keysearch(K,1,L), V.
out(A) ->
L = yaws_api:parse_post(A),
User = kv(user, L),
Pwd = kv(passwd, L),
case myapp:auth(User, Pwd) of
ok ->
S = #session{user = User,
passwd = Pwd,
udata = []},
%% создаем новую сессию
Cookie = yaws_api:new_cookie_session(S),
[{redirect_local, kv(url, L)},
yaws_api:set_cookie("myapp_sid",Cookie,[])]
Err ->
{ehtml,
{html, [],
{p, [], f("Bad login: ~p",[Err])}}}
end.
</erl>
Appmods позволяет в yaws контролировать адресную строку и делать редирект.
Работает это следующим образом: допустим, на сервере загружается адрес:
http://localhost:8000/my-path/my_url.html
Если мы в конфиге пропишем секцию appmods:
<server localhost>
...
appmods = <my_path, my_appmod>
</server>
то вместо загрузки страницы my_url.html будет вызвана функция my_appmod:out.
Нужно положить рядом со страницей my_url.html эрланговский модуль my_appmod.erl,
в котором нужно реализовать функцияю out:
-module(my_appmod).
-compile(export_all).
out(A) ->
{ehtml,
[{p,[],
...
Если мы хотим обрабатывать все урлы:
<server localhost>
...
appmods = </, my_appmod >
</server>
Если мы хотим для некоторых путей оставить обычную обработку:
<server localhost>
...
appmods = </, my_appmod exclude_paths icons js top/static>
</server>
Пример
Резюмируя все вышесказанное, рассмотрим простое веб-приложение, которое можно найти
в исходниках yaws - оно называется shoppingcart. Стартовая страница index.yaws веб-приложения содержит
форму авторизации, после прохождения которой вы попадаете на страницу, предлагающую вам купить товар.
В приложении всего 4 страницы:
index.yaws
loginpost.yaws
logout.yaws
shopcart_form.yaws
На всех страницах контент генерится динамически, и все страницы выглядят похожим образом -
в них совершенно отсутствует статический html-код:
<erl>
out(A) ->
case shopcart:top(A) of
ok ->
shopcart:index(A);
X ->
X
end.
</erl>
И рядом лежит эрланговский модуль shopcart.erl, в котором реализована вся эта динамика.
На каждой странице вначале всегда вызывается функция top(), которая проверяет куки, и если не находит их,
то перенаправляет пользователя на страницу авторизации:
top(A) ->
case check_cookie(A) of
{ok, _Session, _Cookie} ->
ok;
{error, _Reason} ->
login(A)
end.
Функция Login, в которой используется Ehtml:
login(A) ->
CSS = css_head("Shopcart"),
Head = head_status("Not lgged in"),
Top = toprow(),
Login =
{ehtml,
[{h2, [], "Shopcart login"},
{form, [{method, get},
{action, "loginpost.yaws"}],
[
{p, [], "Username"},
{input, [{name, user},
{type, text},
{value, "Joe Junk shopper"},
{size, "48"}]},
{p, [], "Password"},
{input, [{name, password},
{type, text},
{value, "xyz123"},
{size, "48"}]},
{input, [{type, submit},
{value, "Login"}]},
{input, [{name, url},
{type, hidden},
{value, xpath((A#arg.req)#http_request.path, A)}]}
]
}
]},
[CSS, Head, Top, Login, bot(), break].
|