Schema validation

main
Uriel Fanelli 2025-05-11 11:39:45 +02:00
parent c6c547df51
commit b6735ee54d
12 changed files with 154 additions and 52 deletions

3
.gitignore vendored
View File

@ -3,13 +3,14 @@
*.beam *.beam
*.o *.o
*.plt *.plt
*.lock
# Dipendenze scaricate # Dipendenze scaricate
/deps/ /deps/
# File di test e log # File di test e log
.eunit/ .eunit/
logs/ log/
erl_crash.dump erl_crash.dump
# File generati da strumenti # File generati da strumenti

18
priv/note.json Normal file
View File

@ -0,0 +1,18 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "ActivityPub Note",
"type": "object",
"required": ["type", "id", "content"],
"properties": {
"@context": { "type": "string" },
"type": { "type": "string", "enum": ["Note"] },
"id": { "type": "string" },
"attributedTo": { "type": "string" },
"content": { "type": "string" },
"to": {
"type": "array",
"items": { "type": "string" }
}
},
"additionalProperties": true
}

View File

@ -1,4 +1,6 @@
{deps, [ {deps, [
jsonlog,
jesse,
{cowboy, "2.10.0"}, {cowboy, "2.10.0"},
{jsx, "3.1.0"} {jsx, "3.1.0"}
]}. ]}.

View File

@ -1,17 +0,0 @@
{"1.2.0",
[{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.10.0">>},0},
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.12.1">>},1},
{<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},0},
{<<"ranch">>,{pkg,<<"ranch">>,<<"1.8.0">>},1}]}.
[
{pkg_hash,[
{<<"cowboy">>, <<"FF9FFEFF91DAE4AE270DD975642997AFE2A1179D94B1887863E43F681A203E26">>},
{<<"cowlib">>, <<"A9FA9A625F1D2025FE6B462CB865881329B5CAFF8F1854D1CBC9F9533F00E1E1">>},
{<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>},
{<<"ranch">>, <<"8C7A100A139FD57F17327B6413E4167AC559FBC04CA7448E9BE9057311597A1D">>}]},
{pkg_hash_ext,[
{<<"cowboy">>, <<"3AFDCCB7183CC6F143CB14D3CF51FA00E53DB9EC80CDCD525482F5E99BC41D6B">>},
{<<"cowlib">>, <<"163B73F6367A7341B33C794C4E88E7DBFE6498AC42DCD69EF44C5BC5507C8DB0">>},
{<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>},
{<<"ranch">>, <<"49FBCFD3682FAB1F5D109351B61257676DA1A2FDBE295904176D5E521A2DDFE5">>}]}
].

View File

@ -10,6 +10,7 @@
start(_Type, _Args) -> start(_Type, _Args) ->
%% creiamo un utente di test, solo per vedere se mnesia va %% creiamo un utente di test, solo per vedere se mnesia va
log_handler:init(), %% va per primo, se no, non logga l'avvio
db_organization:setup(), db_organization:setup(),
db_organization:init(), db_organization:init(),
users_local_check:check_ENV(), %% controlla le variabili d'ambiente users_local_check:check_ENV(), %% controlla le variabili d'ambiente

View File

@ -26,7 +26,7 @@ remove_oldest(Tab, Fraction) ->
lists:foreach(fun(Rec) -> mnesia:dirty_delete_object(Rec) end, ToDelete). lists:foreach(fun(Rec) -> mnesia:dirty_delete_object(Rec) end, ToDelete).
make_pattern(global_message) -> #global_message{id='_', activity='_', timestamp='_'}; make_pattern(global_message) -> #global_message{id='_', activity='_', timestamp='_'};
make_pattern(ap_users) -> #ap_users{id='_', name='_', email='_', created_at='_'}. %% <-- PUNTO QUI DIOCANE! make_pattern(ap_users) -> #ap_users{id='_', username='_', email='_', created_at='_'}. %% <-- PUNTO QUI DIOCANE!
get_timestamp(#global_message{timestamp=T}) -> T; get_timestamp(#global_message{timestamp=T}) -> T;
get_timestamp(#ap_users{created_at=T}) -> T. %% <-- PUNTO SOLO ALLA FINE, DIOCANE! get_timestamp(#ap_users{created_at=T}) -> T. %% <-- PUNTO SOLO ALLA FINE, DIOCANE!

View File

@ -1,40 +1,66 @@
%% index_handler.erl %% index_handler.erl
-module(index_handler). -module(index_handler).
-behaviour(cowboy_handler). -behaviour(cowboy_handler).
-include("db_safe_insert.hrl").
-export([init/2]). -export([init/2]).
init(Req, State) -> init(Req, State) ->
{ok, Body, Req2} = cowboy_req:read_body(Req), {Body, Req2} = read_full_body(Req),
Activity = jsx:decode(Body, [return_maps]), case json_validate:validate_activity(Body) of
ok ->
%% Validazione dei campi obbligatori %% Decodifica il JSON già validato
case validate_activity(Activity) of Activity = jsx:decode(Body, [return_maps]),
false ->
{ok, Resp} = cowboy_req:reply(400, #{}, <<"Invalid ActivityPub message">>, Req2),
{ok, Resp, State};
true ->
To = maps:get(<<"to">>, Activity, []), To = maps:get(<<"to">>, Activity, []),
%% Qui uso la funzione del nuovo modulo!
case users_local_check:has_local_recipient(To) of case users_local_check:has_local_recipient(To) of
true -> true ->
{ok, Resp} = cowboy_req:reply(200, #{}, <<"Delivered to local user">>, Req2), cowboy_req:reply(200, #{}, <<"Delivered to local user">>, Req2),
{ok, Resp, State}; {ok, Req2, State};
false -> false ->
%% Salva nell'inbox globale %% SAFE INSERT NELLA GLOBAL INBOX
timeline_db:add_message(Activity), InsertResult = db_safe_insert:safe_insert(global_message, #global_message{
{ok, Resp} = cowboy_req:reply(202, #{}, <<"Saved to global inbox">>, Req2), id = make_ref(),
{ok, Resp, State} activity = Activity,
end timestamp = erlang:system_time(second)
}),
case InsertResult of
ok ->
cowboy_req:reply(202, #{}, <<"Saved to global inbox">>, Req2),
{ok, Req2, State};
{aborted, Reason} ->
logger:error("DB aborted: ~p", [Reason]),
cowboy_req:reply(500, #{}, <<"Database error">>, Req2),
{ok, Req2, State};
Other ->
logger:error("DB unknown error: ~p", [Other]),
cowboy_req:reply(500, #{}, <<"Unknown DB error">>, Req2),
{ok, Req2, State}
end
end;
{error, malformed_json} ->
cowboy_req:reply(400, #{}, <<"Malformed JSON">>, Req2),
{ok, Req2, State};
{error, missing_type} ->
cowboy_req:reply(400, #{}, <<"Missing 'type' field">>, Req2),
{ok, Req2, State};
{error, {unsupported_type, TypeBin}} ->
Msg = <<"Unsupported type: ", TypeBin/binary>>,
cowboy_req:reply(400, #{}, Msg, Req2),
{ok, Req2, State};
{error, invalid_type_field} ->
cowboy_req:reply(400, #{}, <<"Invalid 'type' field">>, Req2),
{ok, Req2, State};
{error, Errors} ->
ErrorMsg = io_lib:format("Invalid ActivityPub message: ~p", [Errors]),
cowboy_req:reply(400, #{}, list_to_binary(ErrorMsg), Req2),
{ok, Req2, State}
end. end.
%% Funzione di utilità per leggere tutto il body
%% vediamo di controllare se ci sono i campi obbligatori minimi di ActivityPub: read_full_body(Req) ->
case cowboy_req:read_body(Req) of
validate_activity(Activity) when is_map(Activity) -> {ok, Body, Req2} ->
maps:is_key(<<"type">>, Activity) andalso {Body, Req2};
maps:is_key(<<"to">>, Activity) andalso {more, Data, Req2} ->
maps:is_key(<<"actor">>, Activity) andalso {Rest, FinalReq} = read_full_body(Req2),
maps:is_key(<<"object">>, Activity) andalso {<<Data/binary, Rest/binary>>, FinalReq}
maps:is_key(<<"content">>, Activity). end.

32
src/json_validate.erl Normal file
View File

@ -0,0 +1,32 @@
-module(json_validate).
-export([validate_activity/1, load_schema/1]).
load_schema(SchemaName) ->
Filename = "priv/" ++ SchemaName ++ ".json",
{ok, Bin} = file:read_file(Filename),
jsx:decode(Bin, [return_maps]).
validate_activity(JsonBin) ->
case catch jsx:decode(JsonBin, [return_maps]) of
{'EXIT', _} ->
{error, malformed_json};
JsonMap when is_map(JsonMap) ->
case maps:get(<<"type">>, JsonMap, undefined) of
undefined ->
{error, missing_type};
TypeBin when is_binary(TypeBin) ->
TypeLower = string:lowercase(binary_to_list(TypeBin)),
case TypeLower of
"note" ->
Schema = load_schema("note"),
case jesse:validate_with_schema(Schema, JsonMap) of
{ok, _} -> ok;
{error, Errors} -> {error, Errors}
end;
OtherType ->
{error, {unsupported_type, TypeBin}}
end;
_ ->
{error, invalid_type_field}
end
end.

36
src/log_handler.erl Normal file
View File

@ -0,0 +1,36 @@
-module(log_handler).
-export([init/0, info/2, error/2, warning/2, debug/2, log_map/2]).
init() ->
ok = ensure_log_dir(),
lists:foreach(fun(H) -> logger:remove_handler(H) end, logger:get_handler_ids()),
logger:add_handler(file, logger_std_h, #{
level => debug,
config => #{file => "log/erlang.json", sync_mode_qlen => 0},
formatter => {logger_formatter, #{}}
}),
ok.
info(Format, Args) ->
logger:info(Format, Args).
error(Format, Args) ->
logger:error(Format, Args).
warning(Format, Args) ->
logger:warning(Format, Args).
debug(Format, Args) ->
logger:debug(Format, Args).
%% Logga direttamente una mappa come evento JSON
log_map(Level, Map) when is_map(Map) ->
logger:log(Level, Map).
%% Utility per assicurarsi che la directory log/ esista
ensure_log_dir() ->
case file:read_file_info("log") of
{ok, _} -> ok;
_ -> file:make_dir("log")
end.

View File

@ -5,7 +5,7 @@
add_user(Name, Email) -> add_user(Name, Email) ->
Id = erlang:unique_integer([monotonic, positive]), Id = erlang:unique_integer([monotonic, positive]),
Timestamp = erlang:system_time(microsecond), Timestamp = erlang:system_time(microsecond),
Record = #ap_users{id=Id, name=Name, email=Email, created_at=Timestamp}, Record = #ap_users{id=Id, username=Name, email=Email, created_at=Timestamp},
db_safe_insert:safe_insert(ap_users, Record). db_safe_insert:safe_insert(ap_users, Record).
get_user(Id) -> get_user(Id) ->
@ -20,7 +20,7 @@ get_user(Id) ->
all_users() -> all_users() ->
F = fun() -> F = fun() ->
mnesia:match_object(#ap_users{id='_', name='_', email='_', created_at='_'}) mnesia:match_object(#ap_users{id='_', username='_', email='_', created_at='_'})
end, end,
{atomic, Users} = mnesia:transaction(F), {atomic, Users} = mnesia:transaction(F),
Users. Users.

View File

@ -12,9 +12,10 @@
check_ENV() -> check_ENV() ->
case os:getenv("AP_FQDN") of case os:getenv("AP_FQDN") of
false -> false ->
io:format("Errore: la variabile di ambiente AP_FQDN non è settata!~n"), log_handler:error("Errore: la variabile di ambiente AP_FQDN non è settata!~n"),
erlang:halt(1); erlang:halt(1);
_ -> _ ->
log_handler:debug("La variabile AP_FQDN esiste, bene", []),
ok ok
end. end.
@ -24,10 +25,10 @@ get_fqdn() ->
logger:notice("Chiamata get_fqdn()~n"), logger:notice("Chiamata get_fqdn()~n"),
case os:getenv("AP_FQDN") of case os:getenv("AP_FQDN") of
false -> false ->
logger:notice("Errore: la variabile di ambiente AP_FQDN deve essere settata!~n"), log_handler:error("Errore: la variabile di ambiente AP_FQDN deve essere settata!~n"),
exit({error, ap_fqdn_not_set}); exit({error, ap_fqdn_not_set});
FQDN -> FQDN ->
logger:notice("FQDN settata correttamente: ~p", [FQDN]), log_handler:debug("FQDN settata correttamente: ~p", [FQDN]),
list_to_binary(FQDN) list_to_binary(FQDN)
end. end.

View File

@ -3,6 +3,8 @@
export AP_FQDN="localhost" export AP_FQDN="localhost"
export AP_DB_DIR="./mnesia" export AP_DB_DIR="./mnesia"
rm -rf mnesia
rebar3 get-deps
rebar3 compile rebar3 compile
rebar3 shell --apps activitypub_server rebar3 shell --apps activitypub_server