Skip to content
This repository
Browse code

mod_ssl: Added mod_ssl, enables ssl certs per site. Removed ssl from …

…the core. Tuned dispatch rules for more secure usage. Fixes #434. Fixes #433.
  • Loading branch information...
commit 54e60f6e6b397e52f29a65ab0d4f3276f3f44656 1 parent 9051de1
Marc Worrell mworrell authored
5 include/zotonic.hrl
@@ -222,3 +222,8 @@
222 222 -define(zDebug(Msg, Context), z:debug(Msg, [{module, ?MODULE}, {line, ?LINE}], Context)).
223 223 -define(zInfo(Msg, Context), z:info(Msg, [{module, ?MODULE}, {line, ?LINE}], Context)).
224 224 -define(zWarning(Msg, Context), z:warning(Msg, [{module, ?MODULE}, {line, ?LINE}], Context)).
  225 +
  226 +-define(zDebug(Msg, Args, Context), z:debug(Msg, Args, [{module, ?MODULE}, {line, ?LINE}], Context)).
  227 +-define(zInfo(Msg, Args, Context), z:info(Msg, Args, [{module, ?MODULE}, {line, ?LINE}], Context)).
  228 +-define(zWarning(Msg, Args, Context), z:warning(Msg, Args, [{module, ?MODULE}, {line, ?LINE}], Context)).
  229 +
5 include/zotonic_notifications.hrl
@@ -21,12 +21,15 @@
21 21 %% | {ok, #dispatch_match{}}
22 22 %% | {ok, #dispatch_redirect{}}
23 23 %% | undefined.
24   --record(dispatch, {host, path=[], method='GET', is_ssl=false}).
  24 +-record(dispatch, {host, path=[], method='GET', protocol=http}).
25 25
26 26 -record(dispatch_redirect, {location, is_permanent=false}).
27 27 -record(dispatch_match, {dispatch_name, mod, mod_opts=[], path_tokens=[], bindings=[], app_root="", string_path=""}).
28 28
29 29
  30 +%% @doc Modify cookie options, used for setting http_only and secure options. (foldl)
  31 +-record(cookie_options, {name, value}).
  32 +
30 33 % 'module_ready' - Sent when modules have changed, z_module_indexer reindexes all modules' templates, actions etc.
31 34
32 35 %% @doc A module has been activated and started. (notify)
16 modules/mod_admin/dispatch/dispatch
... ... @@ -1,14 +1,14 @@
1 1 %% -*- mode: erlang -*-
2 2 %% Admin dispatch rules
3 3 [
4   - {admin, ["admin"], controller_admin, []},
5   - {admin_logon, ["admin", "logon"], controller_logon, [{template, "admin_logon.tpl"}]},
6   - {admin_overview_rsc, ["admin", "overview"], controller_admin, [{template, "admin_overview.tpl"}, {selected, "overview"}]},
7   - {admin_media, ["admin", "media"], controller_admin, [{template, "admin_media.tpl"}, {selected, "media"}]},
  4 + {admin, ["admin"], controller_admin, []},
  5 + {admin_logon, ["admin", "logon"], controller_logon, [{template, "admin_logon.tpl"}, {ssl,true}]},
  6 + {admin_overview_rsc, ["admin", "overview"], controller_admin, [{template, "admin_overview.tpl"}, {selected, "overview"}]},
  7 + {admin_media, ["admin", "media"], controller_admin, [{template, "admin_media.tpl"}, {selected, "media"}]},
8 8
9   - {admin_edit_rsc, ["admin", "edit", id], controller_admin_edit, []},
10   - {admin_referrers, ["admin", "referrer", id], controller_admin_referrers, []},
11   - {admin_media_preview, ["admin", "media", "preview", id], controller_admin_media_preview, []},
  9 + {admin_edit_rsc, ["admin", "edit", id], controller_admin_edit, []},
  10 + {admin_referrers, ["admin", "referrer", id], controller_admin_referrers, []},
  11 + {admin_media_preview, ["admin", "media", "preview", id], controller_admin_media_preview, []},
12 12
13   - {admin_status, ["admin", "status"], controller_admin, [{template, "admin_status.tpl"}, {selected, "status"}]}
  13 + {admin_status, ["admin", "status"], controller_admin, [{template, "admin_status.tpl"}, {selected, "status"}]}
14 14 ].
2  modules/mod_admin_config/dispatch/dispatch
... ... @@ -1,5 +1,5 @@
1 1 %% -*- mode: erlang -*-
2 2 %% Dispatch rule for overview of configuration settings.
3 3 [
4   - {admin_config, ["admin", "config"], controller_admin_config, []}
  4 + {admin_config, ["admin", "config"], controller_admin_config, [{ssl,true}]}
5 5 ].
2  modules/mod_admin_identity/dispatch/admin_dispatch
... ... @@ -1,4 +1,4 @@
1 1 %% -*- mode: erlang -*-
2 2 [
3   - {admin_user, ["admin", "users"], controller_admin, [{template, "admin_users.tpl"}, {selected, "users"}]}
  3 + {admin_user, ["admin", "users"], controller_admin, [{template, "admin_users.tpl"}, {selected, "users"}, {ssl, true}]}
4 4 ].
2  modules/mod_authentication/dispatch/dispatch
... ... @@ -1,5 +1,5 @@
1 1 %% -*- mode: erlang -*-
2 2 [
3 3 {logon, ["logon"], controller_logon, [{ssl, true}]},
4   - {logoff, ["logoff"], controller_logoff, []}
  4 + {logoff, ["logoff"], controller_logoff, [{ssl, true}]}
5 5 ].
7 modules/mod_backup/dispatch/backup
... ... @@ -1,12 +1,13 @@
1 1 %% -*- mode: erlang -*-
2 2 [
3   - {admin_backup, [ "admin", "backup" ], controller_admin_backup, []},
4   - {admin_backup_revision, [ "admin", "backup", id ], controller_admin_backup_revision, []},
  3 + {admin_backup, [ "admin", "backup" ], controller_admin_backup, [{ssl, any}]},
  4 + {admin_backup_revision, [ "admin", "backup", id ], controller_admin_backup_revision, [{ssl, any}]},
5 5
6 6 {backup_download, [ "backup", '*' ], controller_file_readonly,
7 7 [
8 8 {root, [{module, mod_backup}]},
9 9 {content_disposition, attachment},
10   - {use_cache, false}
  10 + {use_cache, false},
  11 + {ssl, any}
11 12 ]}
12 13 ].
26 modules/mod_base/dispatch/dispatch
@@ -5,7 +5,7 @@
5 5 {comet, ["comet"], controller_comet, [{ssl, any}, {no_session, true}]},
6 6
7 7 %% Comet sub-domain connection, used with long polls from the browser.
8   - {comet, ["comet", "subdomain"], controller_template, [{template, "comet_subdomain.tpl"}]},
  8 + {comet, ["comet", "subdomain"], controller_template, [{template, "comet_subdomain.tpl"}, {ssl, any}]},
9 9
10 10 %% WebSocket connection.
11 11 {websocket, ["websocket"], controller_websocket, [{ssl, any}, {no_session, true}]},
@@ -17,7 +17,7 @@
17 17 {close_connection, ["close-connection"], controller_close_connection, [{ssl, any}, {no_session, true}]},
18 18
19 19 %% The id controller redirects depending on the accept header sent by the user agent.
20   - {id, ["id", id], controller_id, []},
  20 + {id, ["id", id], controller_id, [{ssl, any}]},
21 21
22 22 %% CSS and Javascript files from the "lib" module folder. Possibly more than one file combined in one request.
23 23 {lib, ["lib",'*'], controller_lib, [ {use_cache, false}, {ssl, any} ]},
@@ -36,7 +36,8 @@
36 36 [
37 37 {path, id},
38 38 {use_cache, false},
39   - {content_disposition, attachment}
  39 + {content_disposition, attachment},
  40 + {ssl, any}
40 41 ]},
41 42
42 43 %% Download of an image, attached to a media rsc
@@ -44,26 +45,29 @@
44 45 [
45 46 {path, id},
46 47 {use_cache, false},
47   - {content_disposition, inline}
  48 + {content_disposition, inline},
  49 + {ssl, any}
48 50 ]},
49 51
50 52 %% Inline display of original uploaded files. Assumes the files are in the root folder.
51 53 {media_inline, ["media","inline",'*'], controller_file_readonly,
52 54 [
53 55 {use_cache, false},
54   - {content_disposition, inline}
  56 + {content_disposition, inline},
  57 + {ssl, any}
55 58 ]},
56 59
57 60 %% Download of original uploaded files. Assumes the files are in the root folder.
58 61 {media_attachment, ["media","attachment",'*'], controller_file_readonly,
59 62 [
60 63 {use_cache, false},
61   - {content_disposition, attachment}
  64 + {content_disposition, attachment},
  65 + {ssl, any}
62 66 ]},
63 67
64 68 %% API access
65   - {api, ["api",module,method], controller_api, []},
66   - {api, ["api",module], controller_api, []},
  69 + {api, ["api",module,method], controller_api, [{ssl, any}]},
  70 + {api, ["api",module], controller_api, [{ssl, any}]},
67 71
68 72 %% Serves the favicon.ico from "lib/images/favicon.ico" in the modules.
69 73 {favicon, ["favicon.ico"], controller_file_readonly,
@@ -76,9 +80,9 @@
76 80 ]},
77 81
78 82 %% User Agent handling
79   - {ua_probe, ["useragent","probe.js"], controller_user_agent_probe, []},
80   - {ua_select, ["useragent","select", ua_class], controller_user_agent_select, []},
81   - {ua_select, ["useragent","select"], controller_user_agent_select, []},
  83 + {ua_probe, ["useragent","probe.js"], controller_user_agent_probe, [{ssl, any}]},
  84 + {ua_select, ["useragent","select", ua_class], controller_user_agent_select, [{ssl, any}]},
  85 + {ua_select, ["useragent","select"], controller_user_agent_select, [{ssl, any}]},
82 86
83 87 %% robots.txt - simple allow all file
84 88 {robots_txt, ["robots.txt"], controller_file_readonly,
10 modules/mod_development/dispatch/development
... ... @@ -1,11 +1,11 @@
1 1 %% -*- mode: erlang -*-
2 2 [
3   - {admin_development, ["admin", "development"], controller_admin, [{template, "admin_development.tpl"}, {selected, "development"}]},
  3 + {admin_development, ["admin", "development"], controller_admin, [{template, "admin_development.tpl"}, {selected, "development"}, {ssl,true}]},
4 4
5   - {admin_development_templates, ["admin", "development", "templates"], controller_admin, [{template, "admin_development_templates.tpl"}, {selected, "development"}]},
  5 + {admin_development_templates, ["admin", "development", "templates"], controller_admin, [{template, "admin_development_templates.tpl"}, {selected, "development"}, {ssl,true}]},
6 6
7   - {wmtrace_conf, ["wmtrace_conf"], controller_wmtrace_conf, []},
  7 + {wmtrace_conf, ["wmtrace_conf"], controller_wmtrace_conf, [{ssl,true}]},
8 8
9   - {wmtrace, ["wmtrace"], controller_wmtrace, []},
10   - {wmtrace, ["wmtrace", '*'], controller_wmtrace, []}
  9 + {wmtrace, ["wmtrace"], controller_wmtrace, [{ssl,true}]},
  10 + {wmtrace, ["wmtrace", '*'], controller_wmtrace, [{ssl,true}]}
11 11 ].
6 modules/mod_facebook/dispatch/dispatch
... ... @@ -1,6 +1,6 @@
1 1 %% -*- mode: erlang -*-
2 2 [
3   - {admin_facebook, ["admin", "facebook"], controller_admin_facebook, []},
4   - {facebook_authorize, ["facebook", "authorize"], controller_facebook_authorize, []},
5   - {facebook_redirect, ["facebook", "redirect"], controller_facebook_redirect, []}
  3 + {admin_facebook, ["admin", "facebook"], controller_admin_facebook, [{ssl,true}]},
  4 + {facebook_authorize, ["facebook", "authorize"], controller_facebook_authorize, [{ssl,any}]},
  5 + {facebook_redirect, ["facebook", "redirect"], controller_facebook_redirect, [{ssl,any}]}
6 6 ].
2  modules/mod_oauth/dispatch/dispatch
@@ -7,5 +7,5 @@
7 7 {oauth_authorize, ["oauth", "authorize"], controller_oauth_authorize, []},
8 8 {oauth_finish, ["oauth", "authorize", "finished"], controller_template, [ {template, "oauth_authorize_finished.tpl"} ]},
9 9
10   - {admin_oauth, ["admin", "oauth", "apps"], controller_oauth_apps, []}
  10 + {admin_oauth, ["admin", "oauth", "apps"], controller_oauth_apps, [{ssl,true}]}
11 11 ].
4 modules/mod_rest/dispatch/dispatch_rest
... ... @@ -1,4 +1,4 @@
1 1 [
2   - {rest_rsc, ["rest", "rsc", format, id], controller_rest_rsc, []},
3   - {rest_rsc, ["rest", "rsc", id], controller_rest_rsc, []}
  2 + {rest_rsc, ["rest", "rsc", format, id], controller_rest_rsc, [{ssl,any}]},
  3 + {rest_rsc, ["rest", "rsc", id], controller_rest_rsc, [{ssl,any}]}
4 4 ].
321 modules/mod_ssl/mod_ssl.erl
... ... @@ -0,0 +1,321 @@
  1 +%% @author Marc Worrell <marc@worrell.nl>
  2 +%% @copyright 2012 Marc Worrell
  3 +%%
  4 +%% @doc SSL pages for Zotonic.
  5 +
  6 +%% Copyright 2012 Marc Worrell
  7 +%%
  8 +%% Licensed under the Apache License, Version 2.0 (the "License");
  9 +%% you may not use this file except in compliance with the License.
  10 +%% You may obtain a copy of the License at
  11 +%%
  12 +%% http://www.apache.org/licenses/LICENSE-2.0
  13 +%%
  14 +%% Unless required by applicable law or agreed to in writing, software
  15 +%% distributed under the License is distributed on an "AS IS" BASIS,
  16 +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17 +%% See the License for the specific language governing permissions and
  18 +%% limitations under the License.
  19 +
  20 +-module(mod_ssl).
  21 +
  22 +-mod_title("HTTPS / SSL").
  23 +-mod_description("Provides https/ssl connectivity.").
  24 +-mod_provides([ssl]).
  25 +-mod_prio(100).
  26 +
  27 +-author('Marc Worrell <marc@worrell.nl>').
  28 +
  29 +-behaviour(supervisor).
  30 +
  31 +-export([
  32 + start_link/1,
  33 + init/1
  34 + ]).
  35 +
  36 +-export([
  37 + observe_module_activate/2,
  38 + pid_observe_dispatch_rules/4,
  39 + observe_cookie_options/3,
  40 +
  41 + cert_files/1,
  42 + check_certs/1
  43 + ]).
  44 +
  45 +-include_lib("zotonic.hrl").
  46 +-include_lib("wm_host_dispatch_list.hrl").
  47 +
  48 +start_link(Args) ->
  49 + supervisor:start_link(?MODULE, Args).
  50 +
  51 +
  52 +init(_Args) ->
  53 + {ok, {{one_for_one, 20, 10}, []}}.
  54 +
  55 +
  56 +%% @doc Ensure that all session and autologon cookies are set to 'secure' (ssl only).
  57 +observe_cookie_options(#cookie_options{name=Name}, Options, Context) ->
  58 + case is_ssl(Context) andalso is_secure_cookie(Name) of
  59 + true ->
  60 + case z_convert:to_bool(m_config:get_value(mod_ssl, is_secure, Context)) of
  61 + true -> [{secure, true} | proplists:delete(secure, Options)];
  62 + false -> Options
  63 + end;
  64 + false ->
  65 + Options
  66 + end.
  67 +
  68 +is_ssl(Context) ->
  69 + wrq:is_ssl(z_context:get_reqdata(Context)).
  70 +
  71 +is_secure_cookie("z_sid") -> true;
  72 +is_secure_cookie("z_logon") -> true;
  73 +is_secure_cookie(_) -> false.
  74 +
  75 +
  76 +%% @doc Start the SSL listener.
  77 +%% TODO: better is to make the startup a process linked in our own supervisor
  78 +observe_module_activate(#module_activate{module=?MODULE, pid=SupPid}, Context) ->
  79 + spawn_link(fun() -> check_start_ssl(SupPid, Context) end);
  80 +observe_module_activate(#module_activate{}, _Context) ->
  81 + ok.
  82 +
  83 +%% @doc Filter all dispatch rules, set the correct protocol options
  84 +% TODO: use the public port, not the listen port
  85 +pid_observe_dispatch_rules(_Pid, dispatch_rules, #wm_host_dispatch_list{dispatch_list=Dispatch} = HD, Context) ->
  86 + {ok, SSLPort} = get_ssl_port(Context),
  87 + Port = get_http_port(Context),
  88 + IsSecure = z_convert:to_bool(m_config:get_value(mod_ssl, is_secure, false, Context)),
  89 + Dispatch1 = map_ssl_option(IsSecure, Port, SSLPort, Dispatch, []),
  90 + HD#wm_host_dispatch_list{dispatch_list=Dispatch1}.
  91 +
  92 +
  93 +map_ssl_option(_IsSecure, _Port, _SSLPort, [], Acc) ->
  94 + lists:reverse(Acc);
  95 +map_ssl_option(IsSecure, Port, SSLPort, [{DispatchName, PathSchema, Mod, Props}|Rest], Acc) ->
  96 + Props1= case proplists:get_value(ssl, Props) of
  97 + any ->
  98 + [{protocol, keep} | Props];
  99 + true ->
  100 + [{protocol, {https, SSLPort}} | Props ];
  101 + false ->
  102 + [{protocol, {http, Port}} | Props ];
  103 + undefined ->
  104 + case IsSecure of
  105 + true -> [{protocol, keep} | Props];
  106 + false -> Props
  107 + end
  108 + end,
  109 + map_ssl_option(IsSecure, Port, SSLPort, Rest, [{DispatchName, PathSchema, Mod, Props1}|Acc]).
  110 +
  111 +
  112 +
  113 +check_start_ssl(SupPid, Context) ->
  114 + case is_running(SupPid) of
  115 + false -> start_ssl(SupPid, Context);
  116 + true -> ok
  117 + end.
  118 +
  119 +is_running(SupPid) ->
  120 + supervisor:which_children(SupPid) =/= [].
  121 +
  122 +
  123 +start_ssl(SupPid, Context) ->
  124 + ?zInfo("SSL: starting (~p)", [SupPid], Context),
  125 + case check_certs(Context) of
  126 + {ok, Certs} ->
  127 + case ensure_port(Context) of
  128 + {ok, Port} -> start_listener(SupPid, Port, Certs, Context);
  129 + {error, Reason} -> ?zWarning("SSL: Can't find port: ~p", [Reason], Context)
  130 + end;
  131 + {error, Reason} ->
  132 + ?zWarning("SSL: problem with certificates: ~p", [Reason], Context)
  133 + end.
  134 +
  135 +
  136 +%% @doc Add the SSL listeners to the mod_ssl supervisor
  137 +start_listener(SupPid, SSLPort, Certs, Context) ->
  138 + HttpIp = z_config:get(listen_ip),
  139 + SSLOpts = [
  140 + {port, SSLPort},
  141 + {ssl, true},
  142 + {ssl_opts, Certs},
  143 + {dispatcher, z_sites_dispatcher},
  144 + {dispatch_list, []},
  145 + {backlog, z_config:get(inet_backlog)}
  146 + ],
  147 + Name4 = z_utils:name_for_host("mochiweb_v4_ssl", Context),
  148 + Ipv4 = {
  149 + webmachine_mochiweb_v4_ssl,
  150 + {webmachine_mochiweb, start, [Name4, [{ip, HttpIp}|SSLOpts]]},
  151 + permanent, 5000, worker, dynamic
  152 + },
  153 + ?zInfo("SSL: starting IPv4 listener on ~p:~p", [HttpIp, SSLPort], Context),
  154 + {ok, _} = supervisor:start_child(SupPid, Ipv4),
  155 + %% When binding to all IP addresses ('any'), also bind for ipv6 addresses
  156 + case HttpIp of
  157 + any ->
  158 + case ipv6_supported() of
  159 + true ->
  160 + Name6 = z_utils:name_for_host("mochiweb_v6_ssl", Context),
  161 + Ipv6 = {
  162 + webmachine_mochiweb_v6_ssl,
  163 + {webmachine_mochiweb, start, [Name6, [{ip, any6}|SSLOpts]]},
  164 + permanent, 5000, worker, dynamic
  165 + },
  166 + ?zInfo("SSL: starting IPv6 listener on any:~p", [SSLPort], Context),
  167 + {ok, _} = supervisor:start_child(SupPid, Ipv6),
  168 + ok;
  169 + false ->
  170 + ok
  171 + end;
  172 + _ ->
  173 + ok
  174 + end.
  175 +
  176 +
  177 +%% @doc Check if the localhost can bind to ipv6 addresses
  178 +ipv6_supported() ->
  179 + case (catch inet:getaddr("localhost", inet6)) of
  180 + {ok, _Addr} -> true;
  181 + {error, _} -> false
  182 + end.
  183 +
  184 +
  185 +get_http_port(Context) ->
  186 + Hostname = z_context:hostname_port(Context),
  187 + case lists:takewhile(fun(C) -> C /= $: end, Hostname) of
  188 + [$:|Port] -> z_convert:to_integer(Port);
  189 + _ -> 80
  190 + end.
  191 +
  192 +get_ssl_port(Context) ->
  193 + case z_convert:to_integer(m_config:get_value(mod_ssl, port, Context)) of
  194 + N when is_integer(N), N > 0 -> {ok, N};
  195 + _ -> ensure_port(Context)
  196 + end.
  197 +
  198 +%% @doc Find the port for the SSL listener
  199 +ensure_port(Context) ->
  200 + case m_config:get_value(mod_ssl, listen_port, Context) of
  201 + undefined ->
  202 + find_random_port(Context);
  203 + Port ->
  204 + case catch z_convert:to_integer(Port) of
  205 + N when is_integer(N) ->
  206 + {ok, N};
  207 + _ ->
  208 + find_random_port(Context)
  209 + end
  210 + end.
  211 +
  212 +
  213 +find_random_port(Context) ->
  214 + InUse = site_ssl_ports(),
  215 + case find_next_port(InUse) of
  216 + {ok, Port} ->
  217 + m_config:set_value(mod_ssl, listen_port, z_convert:to_binary(Port), Context),
  218 + {ok, Port};
  219 + Error ->
  220 + Error
  221 + end.
  222 +
  223 +site_ssl_ports() ->
  224 + [ z_convert:to_integer(m_config:get_value(mod_ssl, listen_port, C)) || C <- z_sites_manager:get_site_contexts() ].
  225 +
  226 +
  227 +%% @doc Find the next unused port, try opening a listener.
  228 +find_next_port(InUse) ->
  229 + case gen_tcp:listen(0, []) of
  230 + {ok, Fd} ->
  231 + {ok, Port} = inet:port(Fd),
  232 + gen_tcp:close(Fd),
  233 + case lists:member(Port, InUse) of
  234 + false -> {ok, Port};
  235 + true -> find_next_port(InUse)
  236 + end;
  237 + {error, _} = Error ->
  238 + Error
  239 + end.
  240 +
  241 +
  242 +%% @doc Check if all certificates are available in the site's ssl directory
  243 +check_certs(Context) ->
  244 + {ok, Opts} = cert_files(Context),
  245 + case filelib:is_file(proplists:get_value(certfile, Opts)) of
  246 + false ->
  247 + case filelib:is_file(proplists:get_value(keyfile, Opts)) of
  248 + false ->
  249 + generate_self_signed(Opts, Context);
  250 + true ->
  251 + {error, {missing_certfile, proplists:get_value(certfile, Opts)}}
  252 + end;
  253 + true ->
  254 + case filelib:is_file(proplists:get_value(keyfile, Opts)) of
  255 + false ->
  256 + {error, {missing_pemfile, proplists:get_value(keyfile, Opts)}};
  257 + true ->
  258 + check_keyfile(Opts)
  259 + end
  260 + end.
  261 +
  262 +check_keyfile(Opts) ->
  263 + Filename = proplists:get_value(keyfile, Opts),
  264 + case file:read_file(Filename) of
  265 + {ok, <<"-----BEGIN PRIVATE KEY", _/binary>>} ->
  266 + {error, {need_rsa_private_key, Filename, "use: openssl rsa -in sitename.key -out sitename.pem"}};
  267 + {ok, Bin} ->
  268 + case public_key:pem_decode(Bin) of
  269 + [] -> {error, {no_private_keys_found, Filename}};
  270 + _ -> {ok, Opts}
  271 + end;
  272 + Error ->
  273 + {error, {cannot_read_pemfile, Filename, Error}}
  274 + end.
  275 +
  276 +
  277 +generate_self_signed(Opts, Context) ->
  278 + PemFile = proplists:get_value(keyfile, Opts),
  279 + case filelib:ensure_dir(PemFile) of
  280 + ok ->
  281 + KeyFile = filename:rootname(PemFile) ++ ".key",
  282 + CertFile = proplists:get_value(certfile, Opts),
  283 + Hostname = z_context:hostname(Context),
  284 + Command = "openssl req -x509 -nodes"
  285 + ++ " -days 3650"
  286 + ++ " -subj '/CN="++Hostname++"'"
  287 + ++ " -newkey rsa:2048 "
  288 + ++ " -keyout "++KeyFile
  289 + ++ " -out "++CertFile,
  290 + lager:info("SSL: ~p", [Command]),
  291 + Result = os:cmd(Command),
  292 + lager:info("SSL: ~p", [Result]),
  293 + case file:read_file(KeyFile) of
  294 + {ok, <<"-----BEGIN PRIVATE KEY", _/binary>>} ->
  295 + os:cmd("openssl rsa -in "++KeyFile++" -out "++PemFile),
  296 + {ok, Opts};
  297 + {ok, <<"-----BEGIN RSA PRIVATE KEY", _/binary>>} ->
  298 + file:rename(KeyFile, PemFile),
  299 + {ok, Opts};
  300 + _Error ->
  301 + {error, "Could not generate self signed certificate"}
  302 + end;
  303 + {error, _} = Error ->
  304 + {error, {ensure_dir, Error, PemFile}}
  305 + end.
  306 +
  307 +
  308 +cert_files(Context) ->
  309 + SSLDir = filename:join(z_path:site_dir(Context), "ssl"),
  310 + Sitename = z_convert:to_list(z_context:site(Context)),
  311 + Files = [
  312 + {certfile, filename:join(SSLDir, Sitename++".crt")},
  313 + {keyfile, filename:join(SSLDir, Sitename++".pem")},
  314 + {password, m_config:get_value(mod_ssl, password, "", Context)}
  315 + ],
  316 + CaCertFile = filename:join(SSLDir, Sitename++".ca.crt"),
  317 + case filelib:is_file(CaCertFile) of
  318 + false -> {ok, Files};
  319 + true -> {ok, [{cacertfile, CaCertFile} | Files]}
  320 + end.
  321 +
4 modules/mod_twitter/dispatch/dispatch
... ... @@ -1,5 +1,5 @@
1 1 %% -*- mode: erlang -*-
2 2 [
3   - {twitter_authorize, ["twitter", "authorize"], controller_twitter_authorize, []},
4   - {twitter_redirect, ["twitter", "redirect"], controller_twitter_redirect, []}
  3 + {twitter_authorize, ["twitter", "authorize"], controller_twitter_authorize, [{ssl,any}]},
  4 + {twitter_redirect, ["twitter", "redirect"], controller_twitter_redirect, [{ssl,any}]}
5 5 ].
13 src/support/z.erl
@@ -36,10 +36,13 @@
36 36
37 37 debug/2,
38 38 debug/3,
  39 + debug/4,
39 40 info/2,
40 41 info/3,
  42 + info/4,
41 43 warning/2,
42   - warning/3
  44 + warning/3,
  45 + warning/4
43 46 ]).
44 47
45 48 -include_lib("zotonic.hrl").
@@ -86,19 +89,27 @@ debug_msg(Module, Line, Msg) ->
86 89 %% @doc Log a debug message, with extra props.
87 90 debug(Msg, Context) -> log(debug, Msg, [], Context).
88 91 debug(Msg, Props, Context) -> log(debug, Msg, Props, Context).
  92 +debug(Msg, Args, Props, Context) -> log(debug, Msg, Args, Props, Context).
89 93
90 94 %% @doc Log an informational message.
91 95 info(Msg, Context) -> log(info, Msg, [], Context).
92 96 info(Msg, Props, Context) -> log(info, Msg, Props, Context).
  97 +info(Msg, Args, Props, Context) -> log(info, Msg, Args, Props, Context).
93 98
94 99 %% @doc Log a warning.
95 100 warning(Msg, Context) -> log(warning, Msg, [], Context).
96 101 warning(Msg, Props, Context) -> log(warning, Msg, Props, Context).
  102 +warning(Msg, Args, Props, Context) -> log(warning, Msg, Args, Props, Context).
97 103
98 104
  105 +log(Type, Msg, Args, Props, Context) ->
  106 + Msg1 = lists:flatten(io_lib:format(Msg, Args)),
  107 + log(Type, Msg1, Props, Context).
  108 +
99 109 log(Type, Msg, Props, Context) ->
100 110 Msg1 = erlang:iolist_to_binary(Msg),
101 111 Line = proplists:get_value(line, Props, 0),
102 112 Module = proplists:get_value(module, Props, unknown),
103 113 error_logger:info_msg("[~p] ~p @ ~p:~p ~s~n", [Context#context.host, Type, Module, Line, binary_to_list(Msg1)]),
104 114 z_notifier:notify({log, #log_message{type=Type, message=Msg1, props=Props, user_id=z_acl:user(Context)}}, Context).
  115 +
4 src/support/z_context.erl
@@ -981,8 +981,10 @@ set_cookie(Key, Value, Options, Context) ->
981 981 {domain, _} -> Options;
982 982 none -> [{domain, z_context:cookie_domain(Context)}|Options]
983 983 end,
  984 + Options2 = z_notifier:foldl(#cookie_options{name=Key, value=Value}, Options1, Context),
984 985 RD = Context#context.wm_reqdata,
985   - Hdr = mochiweb_cookies:cookie(Key, Value, Options1),
  986 + Hdr = mochiweb_cookies:cookie(Key, Value, Options2),
  987 + ?DEBUG(Hdr),
986 988 RD1 = wrq:merge_resp_headers([Hdr], RD),
987 989 z_context:set_reqdata(RD1, Context).
988 990
96 src/support/z_sites_dispatcher.erl
@@ -68,12 +68,12 @@ update_dispatchinfo() ->
68 68 %% | {Mod, ModOpts, HostTokens, Port, PathTokens, Bindings, AppRoot, StringPath}
69 69 %% | handled
70 70 dispatch(Host, Path, ReqData) ->
71   - Method = wrq:method(ReqData),
72   - IsSSL = wrq:is_ssl(ReqData),
73 71 % Classify the user agent
74 72 {ok, ReqDataUA} = z_user_agent:set_class(ReqData),
  73 + Method = wrq:method(ReqData),
  74 + Protocol = case wrq:is_ssl(ReqData) of true -> https; false -> http end,
75 75 % Find a matching dispatch rule
76   - DispReq = #dispatch{host=Host, path=Path, method=Method, is_ssl=IsSSL},
  76 + DispReq = #dispatch{host=Host, path=Path, method=Method, protocol=Protocol},
77 77 case gen_server:call(?MODULE, DispReq) of
78 78 {no_dispatch_match, MatchedHost, NonMatchedPathTokens, Bindings} when MatchedHost =/= undefined ->
79 79 {ok, ReqDataHost} = webmachine_request:set_metadata(zotonic_host, MatchedHost, ReqDataUA),
@@ -160,10 +160,10 @@ init(_Args) ->
160 160 %% {stop, Reason, Reply, State} |
161 161 %% {stop, Reason, State}
162 162 %% @doc Match a host/path to the dispatch rules. Return a match result or a no_dispatch_match tuple.
163   -handle_call(#dispatch{host=HostAsString, path=PathAsString, method=Method, is_ssl=IsSSL}, _From, State) ->
  163 +handle_call(#dispatch{host=HostAsString, path=PathAsString, method=Method, protocol=Protocol}, _From, State) ->
164 164 Reply = case get_host_dispatch_list(HostAsString, State#state.rules, State#state.fallback_site, Method) of
165 165 {ok, Host, DispatchList} ->
166   - case wm_dispatch(IsSSL, HostAsString, Host, PathAsString, DispatchList) of
  166 + case wm_dispatch(Protocol, HostAsString, Host, PathAsString, DispatchList) of
167 167 {redirect, _ProtocolAsString, _Hostname} = R ->
168 168 R;
169 169 {no_dispatch_match, UnmatchedPathTokens, Bindings} ->
@@ -176,9 +176,9 @@ handle_call(#dispatch{host=HostAsString, path=PathAsString, method=Method, is_ss
176 176 Host}
177 177 end;
178 178 {redirect, Hostname} ->
179   - ProtocolAsString = case IsSSL of
180   - true ->"https";
181   - false -> "http"
  179 + ProtocolAsString = case Protocol of
  180 + https ->"https";
  181 + http -> "http"
182 182 end,
183 183 {redirect, ProtocolAsString, Hostname};
184 184 no_host_match ->
@@ -239,10 +239,9 @@ code_change(_OldVsn, State, _Extra) ->
239 239
240 240 %% @doc Collect all dispatch rules for all sites, normalize and filter them.
241 241 collect_dispatchrules() ->
242   - Rules = [ fetch_dispatchinfo(Site) || Site <- z_sites_manager:get_sites() ],
243   - Rules1 = filter_ssl(z_config:get(ssl), Rules),
244   - Rules2 = normalize_streamhosts(Rules1),
245   - compile_regexps_hosts(Rules2).
  242 + Rules = [ filter_rules(fetch_dispatchinfo(Site), Site) || Site <- z_sites_manager:get_sites() ],
  243 + Rules1 = normalize_streamhosts(Rules),
  244 + compile_regexps_hosts(Rules1).
246 245
247 246
248 247 %% @doc Fetch dispatch rules for a specific site.
@@ -466,26 +465,9 @@ make_streamhost(Hostname) ->
466 465 false.
467 466
468 467
469   -%% @doc When SSL is not enabled, then remove any ssl option from all dispatch rules.
470   -filter_ssl(true, Rules) ->
471   - Rules;
472   -filter_ssl(_, Rules) ->
473   - filter_ssl_hosts(Rules).
474   -
475   - filter_ssl_hosts(DLs) ->
476   - filter_ssl_hosts(DLs, []).
477   -
478   - filter_ssl_hosts([], Acc) ->
479   - lists:reverse(Acc);
480   - filter_ssl_hosts([#wm_host_dispatch_list{dispatch_list=DispatchList} = DL|Rest], Acc) ->
481   - DispatchList1 = filter_ssl_hosts_dispatch(DispatchList, []),
482   - filter_ssl_hosts(Rest, [DL#wm_host_dispatch_list{dispatch_list=DispatchList1}|Acc]).
483   -
484   - filter_ssl_hosts_dispatch([], Acc) ->
485   - lists:reverse(Acc);
486   - filter_ssl_hosts_dispatch([{DispatchName, PathSchema, Mod, Props}|Rest], Acc) ->
487   - Props1 = proplists:delete(ssl, Props),
488   - filter_ssl_hosts_dispatch(Rest, [{DispatchName, PathSchema, Mod, Props1}|Acc]).
  468 +%% @doc Filter all rules, also used to set/reset protocol (https) options.
  469 +filter_rules(Rules, Site) ->
  470 + z_notifier:foldl(dispatch_rules, Rules, z_context:new(Site)).
489 471
490 472
491 473 %%%%%%% Adapted version of Webmachine dispatcher %%%%%%%%
@@ -511,16 +493,16 @@ filter_ssl(_, Rules) ->
511 493 -define(SEPARATOR, $\/).
512 494 -define(MATCH_ALL, '*').
513 495
514   -%% @spec wm_dispatch(IsSSL, HostAsString, Host::atom(), Path::string(), DispatchList::[matchterm()]) ->
  496 +%% @spec wm_dispatch(Protocol, HostAsString, Host::atom(), Path::string(), DispatchList::[matchterm()]) ->
515 497 %% dispterm() | dispfail()
516 498 %% @doc Interface for URL dispatching.
517 499 %% See also http://bitbucket.org/justin/webmachine/wiki/DispatchConfiguration
518   -wm_dispatch(IsSSL, HostAsString, Host, PathAsString, DispatchList) ->
  500 +wm_dispatch(Protocol, HostAsString, Host, PathAsString, DispatchList) ->
519 501 Context = z_context:new(Host),
520 502 Path = string:tokens(PathAsString, [?SEPARATOR]),
521 503 IsDir = lists:last(PathAsString) == ?SEPARATOR,
522 504 {Path1, Bindings} = z_notifier:foldl(#dispatch_rewrite{is_dir=IsDir, path=PathAsString}, {Path, []}, Context),
523   - try_path_binding(IsSSL, HostAsString, Host, DispatchList, Path1, Bindings, extra_depth(Path1, IsDir), Context).
  505 + try_path_binding(Protocol, HostAsString, Host, DispatchList, Path1, Bindings, extra_depth(Path1, IsDir), Context).
524 506
525 507 % URIs that end with a trailing slash are implicitly one token
526 508 % "deeper" than we otherwise might think as we are "inside"
@@ -530,37 +512,37 @@ extra_depth(_Path, true) -> 1;
530 512 extra_depth(_, _) -> 0.
531 513
532 514
533   -try_path_binding(_IsSSL, _HostAsString, _Host, [], PathTokens, Bindings, _ExtraDepth, _Context) ->
  515 +try_path_binding(_Protocol, _HostAsString, _Host, [], PathTokens, Bindings, _ExtraDepth, _Context) ->
534 516 {no_dispatch_match, PathTokens, Bindings};
535   -try_path_binding(IsSSL, HostAsString, Host, [{DispatchName, PathSchema, Mod, Props}|Rest], PathTokens, Bindings, ExtraDepth, Context) ->
  517 +try_path_binding(Protocol, HostAsString, Host, [{DispatchName, PathSchema, Mod, Props}|Rest], PathTokens, Bindings, ExtraDepth, Context) ->
536 518 case bind(Host, PathSchema, PathTokens, Bindings, 0, Context) of
537 519 {ok, Remainder, NewBindings, Depth} ->
538   - case {proplists:get_value(ssl, Props), IsSSL} of
539   - {undefined, true} ->
540   - %Port = z_config:get(listen_port),
541   - %Host1 = set_port(Port, HostAsString),
542   - {Host1, _Port} = split_host(HostAsString),
543   - {redirect, "http", Host1};
544   - {false, true} ->
545   - %Port = z_config:get(listen_port),
546   - %Host1 = set_port(Port, HostAsString),
  520 + case proplists:get_value(protocol, Props) of
  521 + % Force switch to normal http protocol
  522 + undefined when Protocol =/= http ->
  523 + {Host1, HostPort} = split_host(HostAsString),
  524 + Host2 = add_port(http, Host1, HostPort),
  525 + {redirect, "http", Host2};
  526 +
  527 + % Force switch to other (eg. https) protocol
  528 + {NewProtocol, NewPort} when NewProtocol =/= Protocol ->
547 529 {Host1, _Port} = split_host(HostAsString),
548   - {redirect, "http", Host1};
549   - {true, false} ->
550   - %Port = z_config:get(listen_port_ssl),
551   - %Host1 = set_port(Port, HostAsString),
552   - {Host1, _Port} = split_host(HostAsString),
553   - {redirect, "https", Host1};
  530 + Host2 = add_port(NewProtocol, Host1, NewPort),
  531 + {redirect, z_convert:to_list(NewProtocol), Host2};
  532 +
  533 + % 'keep' or correct protocol
554 534 _ ->
555   - {DispatchName, Mod, Props, Remainder, NewBindings, calculate_app_root(Depth + ExtraDepth), reconstitute(Remainder)}
  535 + {DispatchName, Mod, Props, Remainder, NewBindings,
  536 + calculate_app_root(Depth + ExtraDepth),
  537 + reconstitute(Remainder)}
556 538 end;
557 539 fail ->
558   - try_path_binding(IsSSL, HostAsString, Host, Rest, PathTokens, Bindings, ExtraDepth, Context)
  540 + try_path_binding(Protocol, HostAsString, Host, Rest, PathTokens, Bindings, ExtraDepth, Context)
559 541 end.
560 542
561   -% set_port(Port, Host) ->
562   -% {Host_, _OldPort} = split_host(Host),
563   -% Host_ ++ [$: | integer_to_list(Port)].
  543 +add_port(http, Host, 80) -> Host;
  544 +add_port(https, Host, 443) -> Host;
  545 +add_port(_, Host, Port) -> Host++[$:|z_convert:to_list(Port)].
564 546
565 547 bind(_Host, [], [], Bindings, Depth, _Context) ->
566 548 {ok, [], Bindings, Depth};
54 src/zotonic_sup.erl
... ... @@ -1,8 +1,8 @@
1 1 %% @author Marc Worrell <marc@worrell.nl>
2   -%% @copyright 2009 Marc Worrell
  2 +%% @copyright 2009-2012 Marc Worrell
3 3 %% @doc Supervisor for the zotonic application.
4 4
5   -%% Copyright 2009 Marc Worrell
  5 +%% Copyright 2009-2012 Marc Worrell
6 6 %%
7 7 %% Licensed under the Apache License, Version 2.0 (the "License");
8 8 %% you may not use this file except in compliance with the License.
@@ -126,66 +126,40 @@ init([]) ->
126 126 false -> z_config:get_dirty(listen_port);
127 127 Anyport -> list_to_integer(Anyport)
128 128 end,
129   - WebPortSSL = case os:getenv("ZOTONIC_PORT_SSL") of
130   - false -> z_config:get_dirty(listen_port_ssl);
131   - Anyport_ -> list_to_integer(Anyport_)
132   - end,
  129 +
133 130 z_config:set_dirty(listen_ip, WebIp),
134 131 z_config:set_dirty(listen_port, WebPort),
135   - z_config:set_dirty(listen_port_ssl, WebPortSSL),
136 132
137 133 WebConfig = [
138 134 {dispatcher, z_sites_dispatcher},
139 135 {dispatch_list, []},
140 136 {backlog, z_config:get_dirty(inet_backlog)}
141   - ],
  137 + ],
142 138
143   - NonSSLOpts = [{port, WebPort}],
144   - SSLCertOpts = [{certfile, z_config:get_dirty(ssl_certfile)},
145   - {keyfile, z_config:get_dirty(ssl_keyfile)},
146   - {password, z_config:get_dirty(ssl_password)}],
147   - SSLCertOpts2 = case z_config:get_dirty(ssl_cacertfile) of
148   - undefined -> SSLCertOpts;
149   - CACertFile -> [{cacertfile, CACertFile} | SSLCertOpts]
150   - end,
151   - SSLOpts = [
152   - {port, WebPortSSL},
153   - {ssl, true},
154   - {ssl_opts, SSLCertOpts2}
155   - ],
156   -
157   - IPv4Opts = [{ip, WebIp}], % Listen to the ip address and port for all sites.
158   - IPv6Opts = [{ip, any6}],
  139 + % Listen to the ip address and port for all sites.
  140 + IPv4Opts = [{port, WebPort}, {ip, WebIp}],
  141 + IPv6Opts = [{port, WebPort}, {ip, any6}],
159 142
160 143 % Webmachine/Mochiweb processes
161   - [IPv4Proc, IPv4SSLProc, IPv6Proc, IPv6SSLProc] =
  144 + [IPv4Proc, IPv6Proc] =
162 145 [[{Name,
163 146 {webmachine_mochiweb, start,
164 147 [Name, Opts]},
165 148 permanent, 5000, worker, dynamic}]
166 149 || {Name, Opts}
167   - <- [{webmachine_mochiweb, IPv4Opts ++ NonSSLOpts ++ WebConfig},
168   - {webmachine_mochiweb_ssl, IPv4Opts ++ SSLOpts ++ WebConfig},
169   - {webmachine_mochiweb_v6, IPv6Opts ++ NonSSLOpts ++ WebConfig},
170   - {webmachine_mochiweb_v6_ssl, IPv6Opts ++ SSLOpts ++ WebConfig}]],
  150 + <- [{webmachine_mochiweb, IPv4Opts ++ WebConfig},
  151 + {webmachine_mochiweb_v6, IPv6Opts ++ WebConfig}]],
171 152
172 153 %% When binding to all IP addresses ('any'), bind separately for ipv6 addresses
173 154 EnableIPv6 = case WebIp of
174 155 any -> ipv6_supported();
175 156 _ -> false
176 157 end,
177   - EnableSSL = z_config:get_dirty(ssl),
178   -
  158 +
179 159 Processes1 =
180   - case {EnableIPv6, EnableSSL} of
181   - {true, true} ->
182   - Processes ++ IPv4Proc ++ IPv4SSLProc ++ IPv6Proc ++ IPv6SSLProc;
183   - {true, false} ->
184   - Processes ++ IPv4Proc ++ IPv6Proc;
185   - {false, true} ->
186   - Processes ++ IPv4Proc ++ IPv4SSLProc;
187   - {false, false} ->
188   - Processes ++ IPv4Proc
  160 + case EnableIPv6 of
  161 + true -> Processes ++ IPv4Proc ++ IPv6Proc;
  162 + false -> Processes ++ IPv4Proc
189 163 end,
190 164
191 165 init_webmachine(),

0 comments on commit 54e60f6

Please sign in to comment.
Something went wrong with that request. Please try again.