diff --git a/src/api/docs/content/specs/lists.yaml b/src/api/docs/content/specs/lists.yaml index 90df09acd..9226e6624 100644 --- a/src/api/docs/content/specs/lists.yaml +++ b/src/api/docs/content/specs/lists.yaml @@ -5,6 +5,7 @@ components: summary: Modify list parameters: - $ref: 'lists.yaml#/components/parameters/list' + - $ref: 'lists.yaml#/components/parameters/listtype' get: summary: Get lists tags: @@ -449,3 +450,14 @@ components: required: true description: Address of the list example: https://hosts-file.net/ad_servers.txt + listtype: + in: query + name: type + schema: + type: string + enum: + - "allow" + - "block" + required: false + description: Type of list, optional + example: block diff --git a/src/api/list.c b/src/api/list.c index 467ea03b8..56d4e009a 100644 --- a/src/api/list.c +++ b/src/api/list.c @@ -36,7 +36,7 @@ static int api_list_read(struct ftl_conn *api, sql_msg); } - tablerow table; + tablerow table = { 0 }; cJSON *rows = JSON_NEW_ARRAY(); while(gravityDB_readTableGetRow(listtype, &table, &sql_msg)) { @@ -48,7 +48,9 @@ static int api_list_read(struct ftl_conn *api, JSON_COPY_STR_TO_OBJECT(row, "name", table.name); JSON_COPY_STR_TO_OBJECT(row, "comment", table.comment); } - else if(listtype == GRAVITY_ADLISTS) + else if(listtype == GRAVITY_ADLISTS || + listtype == GRAVITY_ADLISTS_BLOCK || + listtype == GRAVITY_ADLISTS_ALLOW) { JSON_COPY_STR_TO_OBJECT(row, "address", table.address); JSON_COPY_STR_TO_OBJECT(row, "comment", table.comment); @@ -126,7 +128,9 @@ static int api_list_read(struct ftl_conn *api, JSON_ADD_NUMBER_TO_OBJECT(row, "date_modified", table.date_modified); // Properties added in https://github.com/pi-hole/pi-hole/pull/3951 - if(listtype == GRAVITY_ADLISTS) + if(listtype == GRAVITY_ADLISTS || + listtype == GRAVITY_ADLISTS_BLOCK || + listtype == GRAVITY_ADLISTS_ALLOW) { JSON_REF_STR_IN_OBJECT(row, "type", table.type); JSON_ADD_NUMBER_TO_OBJECT(row, "date_updated", table.date_updated); @@ -147,7 +151,9 @@ static int api_list_read(struct ftl_conn *api, cJSON *json = JSON_NEW_OBJECT(); if(listtype == GRAVITY_GROUPS) objname = "groups"; - else if(listtype == GRAVITY_ADLISTS) + else if(listtype == GRAVITY_ADLISTS || + listtype == GRAVITY_ADLISTS_BLOCK || + listtype == GRAVITY_ADLISTS_ALLOW) objname = "lists"; else if(listtype == GRAVITY_CLIENTS) objname = "clients"; @@ -268,6 +274,8 @@ static int api_list_write(struct ftl_conn *api, } case GRAVITY_ADLISTS: + case GRAVITY_ADLISTS_BLOCK: + case GRAVITY_ADLISTS_ALLOW: { cJSON *json_address = cJSON_GetObjectItemCaseSensitive(api->payload.json, "address"); if(cJSON_IsString(json_address) && strlen(json_address->valuestring) > 0) @@ -331,6 +339,10 @@ static int api_list_write(struct ftl_conn *api, NULL); } } + else if(listtype == GRAVITY_ADLISTS_BLOCK) + row.type_int = ADLIST_BLOCK; + else if(listtype == GRAVITY_ADLISTS_ALLOW) + row.type_int = ADLIST_ALLOW; else { cJSON *json_type = cJSON_GetObjectItemCaseSensitive(api->payload.json, "type"); @@ -552,7 +564,9 @@ static int api_list_remove(struct ftl_conn *api, if(listtype == GRAVITY_DOMAINLIST_ALLOW_EXACT || listtype == GRAVITY_DOMAINLIST_DENY_EXACT || listtype == GRAVITY_DOMAINLIST_ALLOW_REGEX || - listtype == GRAVITY_DOMAINLIST_DENY_REGEX) + listtype == GRAVITY_DOMAINLIST_DENY_REGEX || + listtype == GRAVITY_ADLISTS_BLOCK || + listtype == GRAVITY_ADLISTS_ALLOW) { int type = -1; switch (listtype) @@ -568,12 +582,17 @@ static int api_list_remove(struct ftl_conn *api, break; case GRAVITY_DOMAINLIST_DENY_REGEX: type = 3; + break; + case GRAVITY_ADLISTS_BLOCK: + type = ADLIST_BLOCK; + break; + case GRAVITY_ADLISTS_ALLOW: + type = ADLIST_ALLOW; + break; + // Not handled herein case GRAVITY_GROUPS: case GRAVITY_ADLISTS: case GRAVITY_CLIENTS: - // No type required for these tables - break; - // Aggregate types cannot be handled by this routine case GRAVITY_GRAVITY: case GRAVITY_ANTIGRAVITY: case GRAVITY_DOMAINLIST_ALLOW_ALL: @@ -582,7 +601,7 @@ static int api_list_remove(struct ftl_conn *api, case GRAVITY_DOMAINLIST_ALL_REGEX: case GRAVITY_DOMAINLIST_ALL_ALL: default: - return false; + break; } // Create new JSON array with the item and type: @@ -827,6 +846,30 @@ int api_list(struct ftl_conn *api) api->request->local_uri_raw); } + // If this is a request for a list, we check if there is a request + // parameter narrowing down which kind of list. If so, we modify the + // list type accordingly + if(listtype == GRAVITY_ADLISTS && api->request->query_string != NULL) + { + // Check if there is a type parameter + char typestr[16] = { 0 }; + if(get_string_var(api->request->query_string, "type", typestr, sizeof(typestr)) > 0) + { + if(strcasecmp(typestr, "allow") == 0) + listtype = GRAVITY_ADLISTS_ALLOW; + else if(strcasecmp(typestr, "block") == 0) + listtype = GRAVITY_ADLISTS_BLOCK; + else + { + // Invalid type parameter + return send_json_error(api, 400, + "bad_request", + "Invalid request: Invalid type parameter (should be either \"allow\" or \"block\")", + api->request->query_string); + } + } + } + if(api->method == HTTP_GET) { // Read list item identified by URI (or read them all) diff --git a/src/database/gravity-db.c b/src/database/gravity-db.c index 9cae78410..fef07869a 100644 --- a/src/database/gravity-db.c +++ b/src/database/gravity-db.c @@ -1575,6 +1575,8 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row, // Nothing to be done for these tables case GRAVITY_GROUPS: case GRAVITY_ADLISTS: + case GRAVITY_ADLISTS_BLOCK: + case GRAVITY_ADLISTS_ALLOW: case GRAVITY_CLIENTS: break; @@ -1597,7 +1599,9 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row, { querystr = "INSERT INTO \"group\" (name,enabled,description) VALUES (:item,:enabled,:comment);"; } - else if(listtype == GRAVITY_ADLISTS) + else if(listtype == GRAVITY_ADLISTS || + listtype == GRAVITY_ADLISTS_BLOCK || + listtype == GRAVITY_ADLISTS_ALLOW) { querystr = "INSERT INTO adlist (address,enabled,comment,type) VALUES (:item,:enabled,:comment,:type);"; } @@ -1605,7 +1609,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row, { querystr = "INSERT INTO client (ip,comment) VALUES (:item,:comment);"; } - else // domainlis + else // domainlist { querystr = "INSERT INTO domainlist (domain,type,enabled,comment) VALUES (:item,:type,:enabled,:comment);"; } @@ -1625,9 +1629,11 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row, querystr = "UPDATE \"group\" SET name = :name, enabled = :enabled, description = :comment " "WHERE name = :item"; } - else if(listtype == GRAVITY_ADLISTS) + else if(listtype == GRAVITY_ADLISTS || + listtype == GRAVITY_ADLISTS_BLOCK || + listtype == GRAVITY_ADLISTS_ALLOW) querystr = "INSERT INTO adlist (address,enabled,comment,type) VALUES (:item,:enabled,:comment,:type) "\ - "ON CONFLICT(address) DO UPDATE SET enabled = :enabled, comment = :comment, type = :type;"; + "ON CONFLICT(address,type) DO UPDATE SET enabled = :enabled, comment = :comment, type = :type;"; else if(listtype == GRAVITY_CLIENTS) querystr = "INSERT INTO client (ip,comment) VALUES (:item,:comment) "\ "ON CONFLICT(ip) DO UPDATE SET comment = :comment;"; @@ -1825,11 +1831,14 @@ bool gravityDB_delFromTable(const enum gravity_list_type listtype, const cJSON* return false; } - const bool isDomain = listtype == GRAVITY_DOMAINLIST_ALLOW_EXACT || - listtype == GRAVITY_DOMAINLIST_DENY_EXACT || - listtype == GRAVITY_DOMAINLIST_ALLOW_REGEX || - listtype == GRAVITY_DOMAINLIST_DENY_REGEX || - listtype == GRAVITY_DOMAINLIST_ALL_ALL; // batch delete + const bool hasType = listtype == GRAVITY_DOMAINLIST_ALLOW_EXACT || + listtype == GRAVITY_DOMAINLIST_DENY_EXACT || + listtype == GRAVITY_DOMAINLIST_ALLOW_REGEX || + listtype == GRAVITY_DOMAINLIST_DENY_REGEX || + listtype == GRAVITY_DOMAINLIST_ALL_ALL || + listtype == GRAVITY_ADLISTS || + listtype == GRAVITY_ADLISTS_BLOCK || + listtype == GRAVITY_ADLISTS_ALLOW; // Begin transaction const char *querystr = "BEGIN TRANSACTION;"; @@ -1843,7 +1852,7 @@ bool gravityDB_delFromTable(const enum gravity_list_type listtype, const cJSON* } // Create temporary table for JSON argument - if(isDomain) + if(hasType) // Create temporary table for domains to be deleted querystr = "CREATE TEMPORARY TABLE deltable (type INT, item TEXT);"; else @@ -1885,7 +1894,7 @@ bool gravityDB_delFromTable(const enum gravity_list_type listtype, const cJSON* sqlite3_finalize(stmt); // Prepare statement for inserting items into virtual table - if(isDomain) + if(hasType) querystr = "INSERT INTO deltable (type, item) VALUES (:type, :item);"; else querystr = "INSERT INTO deltable (item) VALUES (:item);"; @@ -1910,12 +1919,24 @@ bool gravityDB_delFromTable(const enum gravity_list_type listtype, const cJSON* { // Bind type to prepared statement cJSON *type = cJSON_GetObjectItemCaseSensitive(it, "type"); + int type_int = cJSON_IsNumber(type) ? type->valueint : -1; + if(listtype == GRAVITY_ADLISTS_BLOCK) + type_int = ADLIST_BLOCK; + else if(listtype == GRAVITY_ADLISTS_ALLOW) + type_int = ADLIST_ALLOW; + else if(listtype == GRAVITY_ADLISTS && cJSON_IsString(type)) + { + if(strcasecmp(type->valuestring, "block") == 0) + type_int = ADLIST_BLOCK; + else if(strcasecmp(type->valuestring, "allow") == 0) + type_int = ADLIST_ALLOW; + } const int type_idx = sqlite3_bind_parameter_index(stmt, ":type"); - if(type_idx > 0 && (!cJSON_IsNumber(type) || (rc = sqlite3_bind_int(stmt, type_idx, type->valueint)) != SQLITE_OK)) + if(type_idx > 0 && (rc = sqlite3_bind_int(stmt, type_idx, type_int)) != SQLITE_OK) { *message = sqlite3_errmsg(gravity_db); log_err("gravityDB_delFromTable(%d): Failed to bind type (error %d) - %s", - type->valueint, rc, *message); + type_int, rc, *message); sqlite3_reset(stmt); sqlite3_finalize(stmt); @@ -1981,12 +2002,15 @@ bool gravityDB_delFromTable(const enum gravity_list_type listtype, const cJSON* const char *querystrs[4] = {NULL, NULL, NULL, NULL}; if(listtype == GRAVITY_GROUPS) querystrs[0] = "DELETE FROM \"group\" WHERE name IN (SELECT item FROM deltable);"; - else if(listtype == GRAVITY_ADLISTS) + else if(listtype == GRAVITY_ADLISTS || + listtype == GRAVITY_ADLISTS_BLOCK || + listtype == GRAVITY_ADLISTS_ALLOW) { - // This is actually a three-step deletion to satisfy foreign-key constraints - querystrs[0] = "DELETE FROM gravity WHERE adlist_id IN (SELECT id FROM adlist WHERE address IN (SELECT item FROM deltable));"; - querystrs[1] = "DELETE FROM antigravity WHERE adlist_id IN (SELECT id FROM adlist WHERE address IN (SELECT item FROM deltable));"; - querystrs[2] = "DELETE FROM adlist WHERE address IN (SELECT item FROM deltable);"; + // This is actually a four-step deletion to satisfy foreign-key constraints + querystrs[0] = "DELETE FROM gravity WHERE adlist_id IN (SELECT id FROM adlist WHERE address IN (SELECT item FROM deltable WHERE type = 0));"; + querystrs[1] = "DELETE FROM antigravity WHERE adlist_id IN (SELECT id FROM adlist WHERE address IN (SELECT item FROM deltable WHERE type = 1));"; + querystrs[2] = "DELETE FROM adlist WHERE address IN (SELECT item FROM deltable WHERE type = 0) AND type = 0;"; + querystrs[3] = "DELETE FROM adlist WHERE address IN (SELECT item FROM deltable WHERE type = 1) AND type = 1;"; } else if(listtype == GRAVITY_CLIENTS) querystrs[0] = "DELETE FROM client WHERE ip IN (SELECT item FROM deltable);"; @@ -2096,12 +2120,16 @@ bool gravityDB_readTable(const enum gravity_list_type listtype, case GRAVITY_DOMAINLIST_ALL_ALL: type = "0,1,2,3"; break; + + // No type required for these tables case GRAVITY_GRAVITY: case GRAVITY_ANTIGRAVITY: case GRAVITY_GROUPS: - case GRAVITY_ADLISTS: case GRAVITY_CLIENTS: - // No type required for these tables + case GRAVITY_ADLISTS: + // Type is set in the SQL query directly + case GRAVITY_ADLISTS_BLOCK: + case GRAVITY_ADLISTS_ALLOW: break; } @@ -2133,19 +2161,29 @@ bool gravityDB_readTable(const enum gravity_list_type listtype, } snprintf(querystr, buflen, "SELECT id,name,enabled,date_added,date_modified,description AS comment FROM \"group\"%s;", filter); } - else if(listtype == GRAVITY_ADLISTS) + else if(listtype == GRAVITY_ADLISTS || + listtype == GRAVITY_ADLISTS_BLOCK || + listtype == GRAVITY_ADLISTS_ALLOW) { + if(listtype == GRAVITY_ADLISTS_BLOCK) + filter = "type = 0"; + else if(listtype == GRAVITY_ADLISTS_ALLOW) + filter = "type = 1"; + else + filter = "TRUE"; + + const char *filter2 = ""; if(item != NULL && item[0] != '\0') { if(exact) - filter = " WHERE address = :item"; + filter2 = " AND address = :item"; else - filter = " WHERE address LIKE :item"; + filter2 = " AND address LIKE :item"; } snprintf(querystr, buflen, "SELECT id,type,address,enabled,date_added,date_modified,comment," "(SELECT GROUP_CONCAT(group_id) FROM adlist_by_group g WHERE g.adlist_id = a.id) AS group_ids," "date_updated,number,invalid_domains,status,abp_entries " - "FROM adlist a%s;", filter); + "FROM adlist a WHERE %s%s;", filter, filter2); } else if(listtype == GRAVITY_CLIENTS) { @@ -2312,6 +2350,8 @@ bool gravityDB_readTableGetRow(const enum gravity_list_type listtype, tablerow * } } else if(listtype == GRAVITY_ADLISTS || + listtype == GRAVITY_ADLISTS_ALLOW || + listtype == GRAVITY_ADLISTS_BLOCK || listtype == GRAVITY_GRAVITY || listtype == GRAVITY_ANTIGRAVITY) { @@ -2328,6 +2368,10 @@ bool gravityDB_readTableGetRow(const enum gravity_list_type listtype, tablerow * break; } } + else + { + row->type = "unknown"; + } } else if(strcasecmp(cname, "domain") == 0) @@ -2425,9 +2469,14 @@ bool gravityDB_edit_groups(const enum gravity_list_type listtype, cJSON *groups, del_querystr = "DELETE FROM client_by_group WHERE client_id = :id;"; add_querystr = "INSERT INTO client_by_group (client_id,group_id) VALUES (:id,:gid);"; } - else if(listtype == GRAVITY_ADLISTS) + else if(listtype == GRAVITY_ADLISTS || + listtype == GRAVITY_ADLISTS_BLOCK || + listtype == GRAVITY_ADLISTS_ALLOW) { - get_querystr = "SELECT id FROM adlist WHERE address = :item"; + if(listtype == GRAVITY_ADLISTS) + get_querystr = "SELECT id FROM adlist WHERE address = :item"; + else + get_querystr = "SELECT id FROM adlist WHERE address = :item AND type = :type"; del_querystr = "DELETE FROM adlist_by_group WHERE adlist_id = :id;"; add_querystr = "INSERT INTO adlist_by_group (adlist_id,group_id) VALUES (:id,:gid);"; } diff --git a/src/enums.h b/src/enums.h index 84b35bedb..bc80472bd 100644 --- a/src/enums.h +++ b/src/enums.h @@ -189,7 +189,9 @@ enum gravity_list_type { GRAVITY_ADLISTS, GRAVITY_CLIENTS, GRAVITY_GRAVITY, - GRAVITY_ANTIGRAVITY + GRAVITY_ANTIGRAVITY, + GRAVITY_ADLISTS_BLOCK, + GRAVITY_ADLISTS_ALLOW } __attribute__ ((packed)); enum gravity_tables { diff --git a/test/test_suite.bats b/test/test_suite.bats index 5a8fd9a09..121a2308d 100644 --- a/test/test_suite.bats +++ b/test/test_suite.bats @@ -1420,6 +1420,28 @@ [[ ${lines[0]} == "145" ]] } +@test "Check /api/lists?type=block returning only blocking lists" { + run bash -c 'curl -s 127.0.0.1/api/lists?type=block | jq ".lists[].type"' + printf "%s\n" "${lines[@]}" + # Check no allow entries are present + [[ ${lines[@]} != *"allow"* ]] +} + +@test "Check /api/lists?type=allow returning only allowing lists" { + run bash -c 'curl -s 127.0.0.1/api/lists?type=allow | jq ".lists[].type"' + printf "%s\n" "${lines[@]}" + # Check no block entries are present + [[ ${lines[@]} != *"block"* ]] +} + +@test "Check /api/lists without type parameter returning all lists" { + run bash -c 'curl -s 127.0.0.1/api/lists | jq ".lists[].type"' + printf "%s\n" "${lines[@]}" + # Check both block and allow entries are present + [[ ${lines[@]} == *"allow"* ]] + [[ ${lines[@]} == *"block"* ]] +} + @test "API authorization (without password): No login required" { run bash -c 'curl -s 127.0.0.1/api/auth' printf "%s\n" "${lines[@]}"