From 100ca8fd74ce3e5b971d4254264d7df4248a7a7d Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Sat, 21 Sep 2013 17:19:13 +0000 Subject: [PATCH] Added SSL, CORS support for the API. --- src/ts_api.erl | 146 ++++++++++++++++++++++++++++++++----------------- yaws.prod.conf | 14 +++++ 2 files changed, 109 insertions(+), 51 deletions(-) diff --git a/src/ts_api.erl b/src/ts_api.erl index 2e9e675..8b0b38f 100644 --- a/src/ts_api.erl +++ b/src/ts_api.erl @@ -40,8 +40,8 @@ dispatch_request(YArg, Session, [H|T]) -> {_, "login"} -> do_login(YArg); {_, "logout"} -> do_logout(YArg); - {"not_logged_in", _} -> make_json_401(YArg); - {"session_expired", _} -> make_json_401(YArg, [{error, "session expired"}]); + {not_logged_in, _} -> make_json_401(YArg); + {session_expired, _} -> make_json_401(YArg, [{error, "session expired"}]); {_S, "app"} -> dispatch_app(YArg, Session, T); {_S, "users"} -> dispatch_user(YArg, Session, T); @@ -57,6 +57,8 @@ dispatch_app(YArg, Session, Params) -> case {HTTPMethod, Params} of + {'OPTIONS', ["user_summary", _]} -> make_CORS_options(YArg, "GET"); + {'GET', ["user_summary", UsernameStr]} -> case {Session#ts_api_session.username, UsernameStr} of @@ -83,8 +85,9 @@ dispatch_user(YArg, Session, [Username]) -> % compare to the logged-in user case {HTTPMethod, Session#ts_api_session.username} of - {'GET', Username} -> get_user(YArg, Username); - {'PUT', Username} -> put_user(YArg, Username); + {'OPTIONS', Username} -> make_CORS_options(YArg, "GET, PUT"); + {'GET', Username} -> get_user(YArg, Username); + {'PUT', Username} -> put_user(YArg, Username); {_BadMethod, Username} -> make_json_405(YArg, [{see_docs, "/ts_api_doc/users.html"}]); @@ -109,14 +112,16 @@ dispatch_timeline(YArg, [Username]) -> HTTPMethod = (YArg#arg.req)#http_request.method, case HTTPMethod of - 'GET' -> list_timelines(YArg, Username); - _Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/timelines.html"}]) + 'OPTIONS' -> make_CORS_options(YArg, "GET"); + 'GET' -> list_timelines(YArg, Username); + _Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/timelines.html"}]) end; dispatch_timeline(YArg, [Username, TimelineId]) -> HTTPMethod = (YArg#arg.req)#http_request.method, case HTTPMethod of + 'OPTIONS'-> make_CORS_options(YArg, "GET, PUT, DELETE"); 'GET' -> get_timeline(YArg, Username, TimelineId); 'PUT' -> put_timeline(YArg, Username, TimelineId); 'DELETE' -> delete_timeline(YArg, Username, TimelineId); @@ -142,9 +147,10 @@ dispatch_entry(YArg, [Username, TimelineId]) -> HTTPMethod = (YArg#arg.req)#http_request.method, case HTTPMethod of - 'GET' -> list_entries(YArg, Username, TimelineId); - 'POST' -> post_entry(YArg, Username, TimelineId); - _Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/entries.html"}]) + 'OPTIONS' -> make_CORS_options(YArg, "GET, POST"); + 'GET' -> list_entries(YArg, Username, TimelineId); + 'POST' -> post_entry(YArg, Username, TimelineId); + _Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/entries.html"}]) end; dispatch_entry(YArg, [Username, TimelineId, UrlEntryId]) -> @@ -152,6 +158,7 @@ dispatch_entry(YArg, [Username, TimelineId, UrlEntryId]) -> HTTPMethod = (YArg#arg.req)#http_request.method, case HTTPMethod of + 'OPTIONS'-> make_CORS_options(YArg, "GET, PUT, DELETE"); 'GET' -> get_entry(YArg, Username, TimelineId, EntryId); 'PUT' -> put_entry(YArg, Username, TimelineId, EntryId); 'DELETE' -> delete_entry(YArg, Username, TimelineId, EntryId); @@ -182,11 +189,13 @@ do_login(YArg) -> true -> {CookieVal, _Session} = ts_api_session:new(Username), - [{content, "application/json", - json:encode({struct, [{status, "ok"}]})}, - {header, {set_cookie, io_lib:format( - "ts_api_session=~s; Path=/ts_api; httponly", - [CookieVal])}}]; + [{header, {set_cookie, io_lib:format( + "ts_api_session=~s; Path=/ts_api", + [CookieVal])}}, + {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]}, + {header, ["Access-Control-Allow-Credentials: ", "true"]}, + {content, "application/json", + json:encode({struct, [{status, "ok"}]})}]; % they are not good false -> make_json_401(YArg, [{error, @@ -200,7 +209,9 @@ do_logout(YArg) -> Cookie = (YArg#arg.headers)#headers.cookie, CookieVal = yaws_api:find_cookie_val("ts_api_session", Cookie), ts_api_session:logout(CookieVal), - {status, 200}. + [{status, 200}, + {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]}, + {header, ["Access-Control-Allow-Credentials: ", "true"]}]. get_user_summary(YArg, Username) -> % find user record @@ -229,14 +240,11 @@ get_user_summary(YArg, Username) -> end, Timelines)}, - % convert to JSON - JSONResp = json:encode({struct, + % write response out + make_json_200(YArg, {struct, [{user, EJSONUser}, {timelines, EJSONTimelines} - ]}), - - % write response out - {content, "application/json", JSONResp} + ]}) end. get_user(YArg, Username) -> @@ -245,7 +253,7 @@ get_user(YArg, Username) -> % no such user, barf no_record -> make_json_404(YArg); % found, return a 200 with the record - User -> make_json_200(YArg, User) + User -> make_json_200_record(YArg, User) end. put_user(YArg, Username) -> @@ -265,7 +273,7 @@ put_user(YArg, Username) -> {ok, UpdatedRec} = ts_user:update(UR, ExtData), % return a 200 - make_json_200(YArg, UpdatedRec). + make_json_200_record(YArg, UpdatedRec). list_timelines(YArg, Username) -> % pull out the POST data @@ -295,11 +303,8 @@ list_timelines(YArg, Username) -> end, Timelines)}, - % convert to JSON and create resposne - JSONResponse = json:encode(EJSONTimelines), - % return response - {content, "application/json", JSONResponse}. + make_json_200(YArg, EJSONTimelines). get_timeline(YArg, Username, TimelineId) -> % look for timeline @@ -307,7 +312,7 @@ get_timeline(YArg, Username, TimelineId) -> % no such timeline, return 404 no_record -> make_json_404(YArg, [{error, "no such timeline"}]); % return the timeline data - Timeline -> make_json_200(YArg, Timeline) + Timeline -> make_json_200_record(YArg, Timeline) end. put_timeline(YArg, Username, TimelineId) -> @@ -329,7 +334,7 @@ put_timeline(YArg, Username, TimelineId) -> ts_timeline:write(TR, ExtData), % return a 200 - make_json_200(YArg, TR). + make_json_200_record(YArg, TR). delete_timeline(_YArg, _Username, _TimelineId) -> {status, 405}. @@ -380,9 +385,7 @@ list_entries(YArg, Username, TimelineId) -> end, Entries)}, - JSONResponse = json:encode(EJSONEntries), - - {content, "application/json", JSONResponse}; + make_json_200(YArg, EJSONEntries); % listing by table position _Other -> @@ -416,9 +419,7 @@ list_entries(YArg, Username, TimelineId) -> end, Entries)}, - JSONResponse = json:encode(EJSONEntries), - - {content, "application/json", JSONResponse} + make_json_200(YArg, EJSONEntries) end. get_entry(YArg, Username, TimelineId, EntryId) -> @@ -426,7 +427,7 @@ get_entry(YArg, Username, TimelineId, EntryId) -> % no such entry no_record -> make_json_404(YArg, [{error, "no such entry"}]); % return the entry data - Entry -> make_json_200(YArg, Entry) + Entry -> make_json_200_record(YArg, Entry) end. post_entry(YArg, Username, TimelineId) -> @@ -446,7 +447,7 @@ post_entry(YArg, Username, TimelineId) -> % record created {ok, CreatedRecord} -> - [{status, 201}, make_json_200(YArg, CreatedRecord)]; + [{status, 201}, make_json_200_record(YArg, CreatedRecord)]; OtherError -> error_logger:error_report("Could not create entry: ~p", [OtherError]), @@ -467,7 +468,7 @@ put_entry(YArg, Username, TimelineId, EntryId) -> end, ts_entry:write(ER, ExtData), - make_json_200(YArg, ER). + make_json_200_record(YArg, ER). delete_entry(YArg, Username, TimelineId, EntryId) -> @@ -499,31 +500,65 @@ parse_json_body(YArg) -> throw(make_json_400(YArg)) end. +get_origin_header(YArg) -> + Headers = (YArg#arg.headers)#headers.other, + case lists:keyfind("Origin", 3, Headers) of + false -> "*"; + {http_header, 0, "Origin", _, Origin} -> Origin; + _ -> make_json_500(YArg, "Unrecognized Origin header.") + end. + +make_CORS_options(YArg, AllowedMethods) -> + [{status, 200}, + {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]}, + {header, ["Access-Control-Allow-Methods: ", AllowedMethods]}, + {header, ["Access-Control-Allow-Credentials: ", "true"]}]. + +make_CORS_options(_YArg, AllowedOrigins, AllowedMethods) -> + [{status, 200}, + {header, ["Access-Control-Allow-Origin: ", AllowedOrigins]}, + {header, ["Access-Control-Allow-Methods: ", AllowedMethods]}, + {header, ["Access-Control-Allow-Credentials: ", "true"]}]. + %% Create a JSON 200 response. -make_json_200(_YArg, Record) -> +make_json_200(YArg, EJSONResponse) -> + JSONResponse = json:encode(EJSONResponse), + [{content, "application/json", JSONResponse}, + {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]}, + {header, ["Access-Control-Allow-Credentials: ", "true"]}]. + + +make_json_200_record(YArg, Record) -> RecordExtData = ts_ext_data:get_properties(Record), EJSON = ts_json:record_to_ejson(Record, RecordExtData), JSONResponse = json:encode(EJSON), - {content, "application/json", JSONResponse}. + [{content, "application/json", JSONResponse}, + {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]}, + {header, ["Access-Control-Allow-Credentials: ", "true"]}]. make_json_400(YArg) -> make_json_400(YArg, []). -make_json_400(_YArg, Fields) -> +make_json_400(YArg, Fields) -> F1 = case lists:keyfind(status, 1, Fields) of false -> Fields ++ [{status, "bad request"}]; _Else -> Fields end, - [{status, 400}, {content, "application/json", json:encode({struct, F1})}]. + [{status, 400}, {content, "application/json", json:encode({struct, F1})}, + {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]}, + {header, ["Access-Control-Allow-Credentials: ", "true"]}]. make_json_401(YArg) -> make_json_401(YArg, []). -make_json_401(_YArg, Fields) -> +make_json_401(YArg, Fields) -> % add default status if not provided F1 = case lists:keyfind(status, 1, Fields) of false -> Fields ++ [{status, "unauthorized"}]; _Else -> Fields end, - [{status, 401}, {content, "application/json", json:encode({struct, F1})}]. + [{status, 401}, + {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]}, + {header, ["Access-Control-Allow-Credentials: ", "true"]}, + {content, "application/json", json:encode({struct, F1})}]. %% Create a JSON 404 response. make_json_404(YArg) -> make_json_404(YArg, []). @@ -537,10 +572,12 @@ make_json_404(YArg, Fields) -> % add the path they requested F2 = F1 ++ [{path, element(2, (YArg#arg.req)#http_request.path)}], - [{status, 404}, {content, "application/json", json:encode({struct, F2})}]. + [{status, 404}, {content, "application/json", json:encode({struct, F2})}, + {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]}, + {header, ["Access-Control-Allow-Credentials: ", "true"]}]. make_json_405(YArg) -> make_json_405(YArg, []). -make_json_405(_YArg, Fields) -> +make_json_405(YArg, Fields) -> % add default status if not provided F1 = case lists:keyfind(status, 1, Fields) of false -> Fields ++ [{status, "method not allowed"}]; @@ -550,15 +587,22 @@ make_json_405(_YArg, Fields) -> % add the path they requested % F2 = F1 ++ [{path, io_lib:format("~p", [(YArg#arg.req)#http_request.path])}], - [{status, 405}, {content, "application/json", json:encode({struct, F1})}]. + [{status, 405}, {content, "application/json", json:encode({struct, F1})}, + {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]}, + {header, ["Access-Control-Allow-Credentials: ", "true"]}]. -make_json_500(_YArg, Error) -> +make_json_500(YArg, Error) -> + io:format("Error: ~n~p", [Error]), EJSON = {struct, [ {status, "internal server error"}, {error, lists:flatten(io_lib:format("~p", [Error]))}]}, - [{status, 500}, {content, "application/json", json:encode(EJSON)}]. + [{status, 500}, {content, "application/json", json:encode(EJSON)}, + {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]}, + {header, ["Access-Control-Allow-Credentials: ", "true"]}]. -make_json_500(_YArg) -> +make_json_500(YArg) -> EJSON = {struct, [ {status, "internal server error"}]}, - [{status, 500}, {content, "application/json", json:encode(EJSON)}]. + [{status, 500}, {content, "application/json", json:encode(EJSON)}, + {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]}, + {header, ["Access-Control-Allow-Credentials: ", "true"]}]. diff --git a/yaws.prod.conf b/yaws.prod.conf index aea2bf0..f8e8084 100644 --- a/yaws.prod.conf +++ b/yaws.prod.conf @@ -9,3 +9,17 @@ include_dir = /usr/local/var/yaws/jdb-labs.com/timestamper/include docroot = /usr/local/var/yaws/jdb-labs.com/timestamper/www appmods = ts_api + + + port = 443 + listen = 0.0.0.0 + docroot = /usr/local/var/yaws/jdb-labs.com/timestamper/www + appmods = ts_api + dir_listings = false + + keyfile = /usr/local/var/yaws/keys/jdb-labs.com.key.pem + certfile = /usr/local/var/yaws/keys/jdb-labs.com.cert.pem + depth = 0 + + +