diff options
-rw-r--r-- | src/ejabberd_acme.erl | 104 |
1 files changed, 51 insertions, 53 deletions
diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 517fdf10a..b8671b75d 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -1,7 +1,7 @@ -module (ejabberd_acme). -export([ scenario/3 - , scenario0/0 + , scenario0/1 , directory/1 , get_account/3 , new_account/4 @@ -14,12 +14,15 @@ -include_lib("public_key/include/public_key.hrl"). -define(REQUEST_TIMEOUT, 5000). % 5 seconds. --define(DIRURL, "directory"). --define(REGURL, "/acme/reg/"). --define(DEFAULT_KEY_FILE, "private_key_temporary"). +-type nonce() :: string(). +-type url() :: string(). +-type proplist() :: [{_, _}]. +-type jws() :: map(). +-spec directory(url()) -> + {ok, map(), nonce()} | {error, _}. directory(DirURL) -> Options = [], HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], @@ -27,66 +30,63 @@ directory(DirURL) -> {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> %% Decode the json string {Directories} = jiffy:decode(Body), - StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || + StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || {X,Y} <- Directories], % Find and save the replay nonce Nonce = get_nonce(Head), %% Return Map of Directories NewDirs = maps:from_list(StrDirectories), {ok, NewDirs, Nonce}; - {ok, {{_, Code, _}, Head, _Body}} -> + {ok, {{_, Code, _}, _Head, _Body}} -> ?ERROR_MSG("Got unexpected status code from <~s>: ~B", [DirURL, Code]), - Nonce = get_nonce(Head), - {error, unexpected_code, Nonce}; + {error, unexpected_code}; {error, Reason} -> ?ERROR_MSG("Error requesting directory from <~s>: ~p", [DirURL, Reason]), {error, Reason} end. +-spec new_account(url(), jose_jwk:key(), proplist(), nonce()) -> + {ok, {url(), proplist()}, nonce()} | {error, _}. new_account(NewAccURl, PrivateKey, Req, Nonce) -> %% Make the request body ReqBody = jiffy:encode({[{ <<"resource">>, <<"new-reg">>}] ++ Req}), - {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, NewAccURl, Nonce), + {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), %% Encode the Signed body with jiffy FinalBody = jiffy:encode(SignedBody), Options = [], HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], - case httpc:request(post, + case httpc:request(post, {NewAccURl, [], "application/jose+json", FinalBody}, HttpOptions, Options) of {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> %% Decode the json string {Return} = jiffy:decode(Body), - TOSUrl = get_TOS(Head), + TOSUrl = get_tos(Head), % Find and save the replay nonce NewNonce = get_nonce(Head), {ok, {TOSUrl, Return}, NewNonce}; - {ok, {{_, 409 = Code, _}, Head, Body}} -> - ?ERROR_MSG("Got status code: ~B from <~s>, Body: ~s", - [NewAccURl, Code, Body]), - NewNonce = get_nonce(Head), - {error, key_in_use, NewNonce}; - {ok, {{_, Code, _}, Head, Body}} -> + {ok, {{_, Code, _}, _Head, Body}} -> ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Body: ~s", [NewAccURl, Code, Body]), - NewNonce = get_nonce(Head), - {error, unexpected_code, NewNonce}; + {error, unexpected_code}; {error, Reason} -> ?ERROR_MSG("Error requesting directory from <~s>: ~p", [NewAccURl, Reason]), - {error, Reason, Nonce} + {error, Reason} end. +-spec update_account(url(), jose_jwk:key(), proplist(), nonce()) -> + {ok, proplist(), nonce()} | {error, _}. update_account(AccURl, PrivateKey, Req, Nonce) -> %% Make the request body ReqBody = jiffy:encode({[{ <<"resource">>, <<"reg">>}] ++ Req}), - {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, AccURl, Nonce), + {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), %% Encode the Signed body with jiffy FinalBody = jiffy:encode(SignedBody), Options = [], HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], - case httpc:request(post, + case httpc:request(post, {AccURl, [], "application/jose+json", FinalBody}, HttpOptions, Options) of {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> %% Decode the json string @@ -94,79 +94,78 @@ update_account(AccURl, PrivateKey, Req, Nonce) -> % Find and save the replay nonce NewNonce = get_nonce(Head), {ok, Return, NewNonce}; - {ok, {{_, Code, _}, Head, Body}} -> + {ok, {{_, Code, _}, _Head, Body}} -> ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Body: ~s", [AccURl, Code, Body]), - NewNonce = get_nonce(Head), - {error, unexpected_code, NewNonce}; + {error, unexpected_code}; {error, Reason} -> ?ERROR_MSG("Error requesting directory from <~s>: ~p", [AccURl, Reason]), - {error, Reason, Nonce} + {error, Reason} end. - +-spec get_account(url(), jose_jwk:key(), nonce()) -> + {ok, {url(), proplist()}, nonce()} | {error, _}. get_account(AccURl, PrivateKey, Nonce) -> %% Make the request body - ReqBody = jiffy:encode({[ - { <<"resource">>, <<"reg">>} - ]}), + ReqBody = jiffy:encode({[{<<"resource">>, <<"reg">>}]}), %% Jose Sign - {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, AccURl, Nonce), + {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), %% Encode the Signed body with jiffy FinalBody = jiffy:encode(SignedBody), Options = [], HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], - case httpc:request(post, + case httpc:request(post, {AccURl, [], "application/jose+json", FinalBody}, HttpOptions, Options) of {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> %% Decode the json string {Return} = jiffy:decode(Body), - TOSUrl = get_TOS(Head), + TOSUrl = get_tos(Head), % Find and save the replay nonce NewNonce = get_nonce(Head), {ok, {TOSUrl, Return}, NewNonce}; - {ok, {{_, Code, _}, Head, Body}} -> + {ok, {{_, Code, _}, _Head, Body}} -> ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Head: ~s", [AccURl, Code, Body]), - NewNonce = get_nonce(Head), - {error, unexpected_code, NewNonce}; + {error, unexpected_code}; {error, Reason} -> ?ERROR_MSG("Error requesting directory from <~s>: ~p", [AccURl, Reason]), - {error, Reason, Nonce} + {error, Reason} end. %% %% Useful funs %% - +-spec get_nonce(proplist()) -> nonce() | 'none'. get_nonce(Head) -> - {"replay-nonce", Nonce} = proplists:lookup("replay-nonce", Head), - Nonce. + case proplists:lookup("replay-nonce", Head) of + {"replay-nonce", Nonce} -> Nonce; + none -> none + end. %% Very bad way to extract this %% TODO: Find a better way -get_TOS(Head) -> +-spec get_tos(proplist()) -> url() | 'none'. +get_tos(Head) -> try - [{_, Link}] = [{K, V} || {K, V} <- Head, + [{_, Link}] = [{K, V} || {K, V} <- Head, K =:= "link" andalso lists:suffix("\"terms-of-service\"", V)], [Link1, _] = string:tokens(Link, ";"), Link2 = string:strip(Link1, left, $<), string:strip(Link2, right, $>) catch _:_ -> - no_tos + none end. - -sign_json_jose(Key, Json, Url, Nonce) -> +-spec sign_json_jose(jose_jwk:key(), string(), nonce()) -> jws(). +sign_json_jose(Key, Json, Nonce) -> % Generate a public key PubKey = jose_jwk:to_public(Key), {_, BinaryPubKey} = jose_jwk:to_binary(PubKey), PubKeyJson = jiffy:decode(BinaryPubKey), - % Jws object containing the algorithm %% TODO: Dont hardcode the alg JwsObj = jose_jws:from( @@ -185,17 +184,16 @@ sign_json_jose(Key, Json, Url, Nonce) -> %% A typical acme workflow scenario(CAUrl, AccId, PrivateKey) -> - - DirURL = CAUrl ++ "/" ++ ?DIRURL, + DirURL = CAUrl ++ "/directory", {ok, Dirs, Nonce0} = directory(DirURL), - AccURL = CAUrl ++ ?REGURL ++ AccId, + AccURL = CAUrl ++ "/acme/reg/" ++ AccId, {ok, {_TOS, Account}, Nonce1} = get_account(AccURL, PrivateKey, Nonce0). new_user_scenario(CAUrl) -> PrivateKey = generate_key(), - DirURL = CAUrl ++ "/" ++ ?DIRURL, + DirURL = CAUrl ++ "/directory", {ok, Dirs, Nonce0} = directory(DirURL), #{"new-reg" := NewAccURL} = Dirs, @@ -203,7 +201,7 @@ new_user_scenario(CAUrl) -> {ok, {TOS, Account}, Nonce1} = new_account(NewAccURL, PrivateKey, Req0, Nonce0), {_, AccId} = proplists:lookup(<<"id">>, Account), - AccURL = CAUrl ++ ?REGURL ++ integer_to_list(AccId), + AccURL = CAUrl ++ "/acme/reg/" ++ integer_to_list(AccId), Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], {ok, Account1, Nonce2} = update_account(AccURL, PrivateKey, Req1, Nonce1), @@ -213,7 +211,7 @@ generate_key() -> jose_jwk:generate_key({ec, secp256r1}). %% Just a test -scenario0() -> - PrivateKey = jose_jwk:from_file(?DEFAULT_KEY_FILE), +scenario0(KeyFile) -> + PrivateKey = jose_jwk:from_file(KeyFile), % scenario("http://localhost:4000", "2", PrivateKey). new_user_scenario("http://localhost:4000"). |