947 constexpr std::array root_fields {
948 std::string_view { "format_version" },
949 std::string_view { "defaults" },
950 std::string_view { "doors" },
951 };
953 if (configuration.value("format_version", 0) != 1) {
954 throw std::invalid_argument("transport.format_version must be 1");
955 }
956 policy_settings global_policy;
958 global_policy,
960 "transport.defaults"
961 );
962 const auto door_documents = configuration.find("doors");
963 if (door_documents == configuration.end() || !door_documents->is_array()
964 || door_documents->empty()) {
965 throw std::invalid_argument(
966 "transport.doors must be a non-empty array"
967 );
968 }
969 for (std::size_t door_index = 0; door_index < door_documents->size();
970 ++door_index) {
971 const json& document = door_documents->at(door_index);
972 const std::string location
973 = "transport.doors[" + std::to_string(door_index) + "]";
974 constexpr std::array door_fields {
975 std::string_view { "door_id" },
976 std::string_view { "defaults" },
977 std::string_view { "endpoints" },
978 };
980 door_configuration door;
983 throw std::invalid_argument(
984 location + ".door_id is invalid or duplicated"
985 );
986 }
987 door.policy = global_policy;
988 if (const auto defaults = document.find("defaults");
989 defaults != document.end()) {
991 }
992 door.admission
993 = std::make_shared<admission_gate>(door.policy.admission);
994 const auto endpoint_documents = document.find("endpoints");
995 if (endpoint_documents == document.end()
996 || !endpoint_documents->is_array()
997 || endpoint_documents->empty()) {
998 throw std::invalid_argument(
999 location + ".endpoints must be a non-empty array"
1000 );
1001 }
1002 for (std::size_t endpoint_index = 0;
1003 endpoint_index < endpoint_documents->size();
1004 ++endpoint_index) {
1005 const json& endpoint_document
1006 = endpoint_documents->at(endpoint_index);
1007 const std::string endpoint_location = location + ".endpoints["
1008 + std::to_string(endpoint_index) + "]";
1009 constexpr std::array endpoint_fields {
1010 std::string_view { "endpoint_id" },
1011 std::string_view { "protocol" },
1012 std::string_view { "base_url" },
1013 std::string_view { "allowed_methods" },
1014 std::string_view { "authentication" },
1015 std::string_view { "bulk_capable" },
1016 std::string_view { "resumable_download" },
1017 std::string_view { "write_enabled" },
1018 std::string_view { "idempotency_header" },
1019 std::string_view { "allow_insecure_http" },
1020 std::string_view { "policy" },
1021 };
1023 endpoint_document, endpoint_fields, endpoint_location
1024 );
1025 endpoint_configuration endpoint;
1027 endpoint_document, "endpoint_id", endpoint_location
1028 );
1030 || door.endpoints.contains(endpoint.endpoint_id)) {
1031 throw std::invalid_argument(
1032 endpoint_location
1033 + ".endpoint_id is invalid or duplicated"
1034 );
1035 }
1037 endpoint_document, "protocol", endpoint_location
1038 );
1039 constexpr std::array protocols {
1040 std::string_view { "http_file" },
1041 std::string_view { "rest" },
1042 std::string_view { "sparql" },
1043 std::string_view { "oai_pmh" },
1044 std::string_view { "ldes" },
1045 std::string_view { "iiif" },
1046 };
1047 if (std::ranges::find(protocols, endpoint.protocol)
1048 == protocols.end()) {
1049 throw std::invalid_argument(
1050 endpoint_location + ".protocol is unsupported"
1051 );
1052 }
1055 endpoint_document, "base_url", endpoint_location
1056 ),
1057 endpoint_location + ".base_url"
1058 );
1060 endpoint_document, "allow_insecure_http", false,
1061 endpoint_location
1062 );
1063 if (endpoint.base_url.scheme != "https"
1064 && !endpoint.allow_insecure_http) {
1065 throw std::invalid_argument(
1066 endpoint_location
1067 + " uses HTTP without explicit test/development "
1068 "permission"
1069 );
1070 }
1071 const auto methods = endpoint_document.find("allowed_methods");
1072 if (methods == endpoint_document.end() || !methods->is_array()
1073 || methods->empty()) {
1074 throw std::invalid_argument(
1075 endpoint_location
1076 + ".allowed_methods must be a non-empty array"
1077 );
1078 }
1079 for (const auto& method : *methods) {
1080 if (!method.is_string()) {
1081 throw std::invalid_argument(
1082 endpoint_location
1083 + ".allowed_methods contains a non-string"
1084 );
1085 }
1086 const std::string value = method.get<std::string>();
1087 if (value == "GET") {
1089 } else if (value == "POST") {
1091 } else {
1092 throw std::invalid_argument(
1093 endpoint_location
1094 + ".allowed_methods contains an unsupported method"
1095 );
1096 }
1097 }
1100 endpoint_document, "authentication", endpoint_location
1101 ),
1102 endpoint_location + ".authentication"
1103 );
1105 endpoint_document, "bulk_capable", false, endpoint_location
1106 );
1108 endpoint_document, "resumable_download", false,
1109 endpoint_location
1110 );
1112 endpoint_document, "write_enabled", false, endpoint_location
1113 );
1114 if (endpoint_document.contains("idempotency_header")) {
1116 endpoint_document, "idempotency_header",
1117 endpoint_location
1118 );
1119 const std::string lowered
1121 if (!endpoint.write_enabled || lowered == "host"
1124 endpoint.idempotency_header
1125 )) {
1126 throw std::invalid_argument(
1127 endpoint_location
1128 + ".idempotency_header is unsafe or declared for a "
1129 "read-only endpoint"
1130 );
1131 }
1132 }
1133 endpoint.policy = door.policy;
1134 if (const auto overrides = endpoint_document.find("policy");
1135 overrides != endpoint_document.end()) {
1137 endpoint.policy, *overrides,
1138 endpoint_location + ".policy"
1139 );
1140 }
1141 if (endpoint.policy.redirects.allowed_hosts.empty()) {
1142 endpoint.policy.redirects.allowed_hosts
1144 }
1145 endpoint.admission = std::make_shared<admission_gate>(
1146 endpoint.policy.admission
1147 );
1148 door.endpoints.emplace(
1149 endpoint.endpoint_id, std::move(endpoint)
1150 );
1151 }
1152 doors.emplace(door.door_id, std::move(door));
1153 }
1154
1155 std::error_code error;
1156 const auto root_state = fs::symlink_status(this->
artifact_root, error);
1157 if (error == std::errc::no_such_file_or_directory) {
1158 error.clear();
1160 } else if (
1161 !error
1162 && (fs::is_symlink(root_state) || !fs::is_directory(root_state))
1163 ) {
1164 throw std::invalid_argument(
1165 "artifact root must be a real directory"
1166 );
1167 }
1168 if (error) {
1169 throw std::invalid_argument(
1170 "cannot create artifact root: " + error.message()
1171 );
1172 }
1174 const auto cache_state = fs::symlink_status(
cache_root, error);
1175 if (error == std::errc::no_such_file_or_directory) {
1176 error.clear();
1178 } else if (
1179 !error
1180 && (fs::is_symlink(cache_state) || !fs::is_directory(cache_state))
1181 ) {
1182 throw std::invalid_argument(
1183 "transport cache root must be a real directory"
1184 );
1185 }
1186 if (error) {
1187 throw std::invalid_argument(
1188 "cannot create transport cache root: " + error.message()
1189 );
1190 }
1191 }