commit b88fba9644574b95ddd5afac22e0ab0e105a2666 Author: William Brawner Date: Mon Dec 30 14:18:57 2019 -0600 Initial commit This code is absolutely horrendous in its current state, with poor error handling (where it's even present) and memory leaks galore. USE AT YOUR OWN RISK. I certainly won't be publishing it anywhere or speaking of it until it's in a much better state. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bde8c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +*.o diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e07a852 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "cJSON"] + path = cJSON + url = https://github.com/DaveGamble/cJSON +[submodule "curl"] + path = curl + url = https://github.com/curl/curl diff --git a/.vimrc b/.vimrc new file mode 100644 index 0000000..371c485 --- /dev/null +++ b/.vimrc @@ -0,0 +1,10 @@ +set colorcolumn=110 +highlight ColorColumn ctermbg=darkgray +augroup project + autocmd! + autocmd BufRead,BufNewFile *.h,*.c set filetype=c.doxygen +augroup END +nnoremap :!mkdir -p build cmake -B build make -C build +nnoremap :!build/PiHelper/pihelper localhost +nnoremap :!rm -rf build +let &path.="/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include," diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9955d80 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required (VERSION 3.15.5) + +set(PIHELPER_VERSION 0.1.0) + +project( + pihelper + VERSION ${PIHELPER_VERSION} + ) + +add_subdirectory(PiHelper) + +install(TARGETS pihelper libpihelper) + +#target_link_libraries(PiHelper SHARED cJSON) diff --git a/PiHelper/CMakeLists.txt b/PiHelper/CMakeLists.txt new file mode 100644 index 0000000..7128ec4 --- /dev/null +++ b/PiHelper/CMakeLists.txt @@ -0,0 +1,59 @@ +set(PIHELPER_SOURCES + pihelper.c + log.c + network.c + config.c + ) + +add_library(libpihelper + ${PIHELPER_SOURCES} + ) + +set_target_properties(libpihelper PROPERTIES OUTPUT_NAME "libpihelper") + +add_executable(pihelper + ${PIHELPER_SOURCES} + cli.c + ) + +include_directories(/usr/local/include) +find_library ( + CJSON + NAMES cjson libcjson + HINTS /usr/local/lib /usr/lib + ) +find_library ( + CRYPTO + NAMES crypto libcrypto + HINTS /usr/local/lib /usr/lib + ) +find_library ( + OPENSSL + NAMES ssl libssl + HINTS /usr/local/lib /usr/lib + ) + +if (NOT CJSON) + message(SEND_ERROR "Did not find cJSON") +endif() + +if (NOT CRYPTO) + message(SEND_ERROR "Did not find OpenSSL") +endif() + +if (NOT OPENSSL) + message(SEND_ERROR "Did not find OpenSSL") +endif() + +target_link_libraries(libpihelper curl) +target_link_libraries(pihelper curl) + +target_link_libraries(libpihelper ${CJSON}) +target_link_libraries(pihelper ${CJSON}) + +target_link_libraries(libpihelper ${CRYPTO}) +target_link_libraries(pihelper ${CRYPTO}) + +target_link_libraries(libpihelper ${OPENSSL}) +target_link_libraries(pihelper ${OPENSSL}) + diff --git a/PiHelper/cli.c b/PiHelper/cli.c new file mode 100644 index 0000000..ea25cb8 --- /dev/null +++ b/PiHelper/cli.c @@ -0,0 +1,119 @@ +#include +#include +#include +#include +#include "cli.h" +#include "log.h" +#include "network.h" +#include "pihelper.h" + +int main(int argc, char ** argv) { + + bool configure, enable; + char * disable; + char * config_path; + char ch; + while ((ch = getopt_long(argc, argv, "cd:ef:hv", longopts, NULL)) != -1) { + switch(ch) { + case 'c': + configure = true; + break; + case 'd': + if (optarg != NULL) { + disable = malloc(strlen(optarg) + 1); + strncpy(disable, optarg, strlen(optarg)); + disable[strlen(optarg)] = '\0'; + } else { + disable = ""; + } + write_log(LOG_DEBUG, "Disabling pi-hole for %s seconds", disable); + break; + case 'e': + enable = true; + break; + case 'f': + if (optarg == NULL) break; + if (strstr(optarg, "/") != optarg) { + // This is a relative path, prepend the current working directory + char * cwd = getcwd(NULL, 0); + int full_path_len = strlen(cwd) + 1 + strlen(optarg) + 1; + config_path = malloc(full_path_len); + strncpy(config_path, cwd, strlen(cwd)); + config_path[strlen(cwd)] = '/'; + strncpy(&(config_path[strlen(cwd) + 1]), optarg, strlen(optarg)); + config_path[full_path_len] = '\0'; + write_log(LOG_DEBUG, "Fixed config_path: %s", config_path); + } else { + // This is an absolute path, copy as-is + config_path = malloc(strlen(optarg) + 1); + strncpy(config_path, optarg, strlen(optarg)); + config_path[strlen(optarg)] = '\0'; + } + break; + case 'v': + // TODO: Add log level and set here + //PIHELPER_DEBUG = true; + break; + case 'h': + default: + print_usage(); + return PIHELPER_HELP; + } + } + if (config_path == NULL) { + char * home_dir = getenv("HOME"); + int path_len = strlen(home_dir) + strlen(DEFAULT_CONFIG_PATH); + config_path = malloc(path_len + 1); + sprintf(config_path, "%s%s", home_dir, DEFAULT_CONFIG_PATH); + config_path[path_len + 1] = '\0'; + } + FILE * config_file = fopen(config_path, "r+"); + if (config_file == NULL) { + char * user_input = malloc(2); + printf("No Pi-Helper configuration found. Would you like to create it now? [Y/n] "); + fgets(user_input, 2, stdin); + if (strstr(user_input, "\n") == user_input + || strstr(user_input, "Y") == user_input + || strstr(user_input, "y") == user_input + ) { + configure = true; + } else { + return 1; + } + } + + pihole_config * config; + if (configure) { + config = configure_pihole(config_path); + } else { + config = read_config(config_path); + } + + if (config == NULL) { + printf("Failed to parse Pi-Helper config\n"); + exit(1); + } + + if (enable && disable != 0) { + print_usage(); + return PIHELPER_INVALID_COMMANDS; + } else if (enable) { + return enable_pihole(config); + } else if (disable != 0) { + return disable_pihole(config, disable); + } else { + return get_status(config); + } +} + +void print_usage() { + printf("Usage: pihelper [options]\n"); + printf(" -c, --configure Configure Pi-Helper\n"); + printf(" -d, --disable Disable the Pi-hole for a given duration, or permanently if empty\n"); + printf(" -e, --enable Enable the Pi-hole\n"); + printf(" -f, --file Use the given config file instead of the default\n"); + printf(" -h, --help Display this message\n"); + printf(" -q, --quiet Don't print anything\n"); + printf(" -v, --verbose Print debug logs\n"); +} + diff --git a/PiHelper/cli.h b/PiHelper/cli.h new file mode 100644 index 0000000..85f39e6 --- /dev/null +++ b/PiHelper/cli.h @@ -0,0 +1,17 @@ +#include +#include "config.h" + +static struct option longopts[] = { + { "configure", no_argument, NULL, 'c' }, + { "disable", optional_argument, NULL, 'd' }, + { "enable", no_argument, NULL, 'e' }, + { "file", required_argument, NULL, 'f' }, + { "help", no_argument, NULL, 'h' }, + { "quiet", no_argument, NULL, 'q' }, + { "verbose", no_argument, NULL, 'v' } +}; + +void print_usage(); + +pihole_config * configure_pihole(char * config_path); + diff --git a/PiHelper/config.c b/PiHelper/config.c new file mode 100644 index 0000000..29c2fa7 --- /dev/null +++ b/PiHelper/config.c @@ -0,0 +1,110 @@ +#include +#include +#include +#include +#include +#include +#include +#include "config.h" + +int mkdirs(char * path) { + char * curPos = strstr(path, "/") + 1; + char parents[strlen(path)]; + int retval = 0; + while (curPos != NULL) { + snprintf(parents, strlen(path) - strlen(curPos) + 1, "%s", path); + curPos = strstr(curPos + 1, "/"); + if (access(parents, F_OK)) { + if ((retval = mkdir(parents, 0755)) != 0) { + return retval; + } + } + } + return retval; +} + +void save_config(pihole_config * config, char * config_path) { + if (mkdirs(config_path)) { + perror(config_path); + exit(1); + } + FILE * config_file = fopen(config_path, "w"); + int config_len = strlen(config->host) + strlen(config->api_key) + 16; + char config_string[config_len + 1]; + snprintf(config_string, config_len, "host=%s\napi-key=%s\n", config->host, config->api_key); + config_string[config_len + 1] = '\0'; + fputs(config_string, config_file); + fclose(config_file); +} + +/* + * Calculate the hash of the password + */ +static char * hash_string (char * raw_string) { + unsigned char bytes[SHA256_DIGEST_LENGTH]; + SHA256((unsigned char *) raw_string, strlen(raw_string), bytes); + char * hash = malloc(65); + int i; + for(i = 0; i < SHA256_DIGEST_LENGTH; i++) { + sprintf(hash + (i * 2), "%02x", bytes[i]); + } + hash[64] = '\0'; + return hash; +} + +pihole_config * read_config(char * config_path) { + if (access(config_path, F_OK)) { + printf("ERROR: The specified config file doesn't exist\n"); + return NULL; + } + + pihole_config * config = calloc(1, sizeof(pihole_config)); + FILE * config_file = fopen(config_path, "r"); + char host[_POSIX_HOST_NAME_MAX + 7]; + fgets(host, _POSIX_HOST_NAME_MAX + 7, config_file); + if (strstr(host, "host=") == NULL) { + printf("Invalid config file\n"); + exit(1); + } + config->host = calloc(1, strlen(host) - 7); + strncpy(config->host, host + 5, strlen(host) - 6); + char api_key[74]; + fgets(api_key, 74, config_file); + if (strstr(api_key, "api-key=") == NULL) { + printf("Invalid config file\n"); + exit(1); + } + config->api_key = calloc(1, strlen(api_key) - 8); + strncpy(config->api_key, api_key + 8, strlen(api_key) - 9); + fclose(config_file); + return config; +} + +pihole_config * configure_pihole(char * config_path) { + if (access(config_path, F_OK) == 0) { + // TODO: Check if file is accessible for read/write (not just if it exists) + printf("WARNING: The config file already exists. Continuing will overwrite any existing configuration.\n"); + } + pihole_config * config = malloc(sizeof(pihole_config)); + config->host = calloc(1, _POSIX_HOST_NAME_MAX); + config->host[_POSIX_HOST_NAME_MAX] = '\0'; + printf("Enter the hostname or ip address for your pi-hole: "); + fgets(config->host, _POSIX_HOST_NAME_MAX, stdin); + char * newline = strstr(config->host, "\n"); + if (newline != NULL) { + config->host[strlen(config->host) - strlen(newline)] = '\0'; + } + config->api_key = getpass("Enter the api key or web password for your pi-hole: "); + if (strlen(config->api_key) < 64) { + // This is definitely not an API key, so hash it + // The Pi-hole hashes twice so we do the same here + char * first = hash_string(config->api_key); + char * hash = hash_string(first); + free(first); + config->api_key = hash; + } + // TODO: Make an authenticated request to verify that the credentials are valid and save the config + save_config(config, config_path); + return config; +} + diff --git a/PiHelper/config.h b/PiHelper/config.h new file mode 100644 index 0000000..4a1889d --- /dev/null +++ b/PiHelper/config.h @@ -0,0 +1,19 @@ +#ifndef PIHELPER_CONFIG +#define PIHELPER_CONFIG +#include + +static char * DEFAULT_CONFIG_PATH = "/.config/pihelper.conf"; + +typedef struct { + char * host; + char * api_key; +} pihole_config; + + +void save_config(pihole_config * config, char * config_path); + +pihole_config * read_config(char * config_path); + +pihole_config * configure_pihole(char * config_path); +#endif + diff --git a/PiHelper/log.c b/PiHelper/log.c new file mode 100644 index 0000000..6776e4e --- /dev/null +++ b/PiHelper/log.c @@ -0,0 +1,20 @@ +#include +#include +#include +#include +#include +#include "log.h" + +void write_log(int level, char * format, ...) { + if (level == LOG_DEBUG) return; + va_list args; + va_start(args, format); + int format_len = strlen(format); + char * new_format = malloc(format_len + 1); + memcpy(new_format, format, format_len); + new_format[format_len] = '\n'; + new_format[format_len + 1] = '\0'; + vprintf(new_format, args); + va_end(args); +} + diff --git a/PiHelper/log.h b/PiHelper/log.h new file mode 100644 index 0000000..3bf12ac --- /dev/null +++ b/PiHelper/log.h @@ -0,0 +1,10 @@ +#ifndef PIHELPER_LOG +#define PIHELPER_LOG + +static int LOG_ERROR = 0; +static int LOG_WARN = 1; +static int LOG_INFO = 2; +static int LOG_DEBUG = 3; + +void write_log(int level, char * format, ...); +#endif diff --git a/PiHelper/network.c b/PiHelper/network.c new file mode 100644 index 0000000..95b2d61 --- /dev/null +++ b/PiHelper/network.c @@ -0,0 +1,134 @@ +#include +#include +#include +#include +#include "config.h" +#include "log.h" +#include "network.h" + +static char * URL_FORMAT = "http://%s/admin/api.php"; +static int URL_FORMAT_LEN = 22; +static char * AUTH_QUERY = "auth"; +static char * ENABLE_QUERY = "enable"; +static char * DISABLE_QUERY = "disable"; +static char * HTTP_SCHEME = "http://"; +static int HTTP_SCHEME_LEN = 8; +static char * HTTPS_SCHEME = "https://"; +static int HTTPS_SCHEME_LEN = 9; + +int get_status(pihole_config * config) { + char * formatted_host = prepend_scheme(config->host); + char * response = get(formatted_host); + if (response == NULL) { + return 1; + } else { + parse_status(response); + free(response); + return 0; + } +} + +int enable_pihole(pihole_config * config) { + char * formatted_host = prepend_scheme(config->host); + append_query_parameter(&formatted_host, AUTH_QUERY, config->api_key); + append_query_parameter(&formatted_host, ENABLE_QUERY, NULL); + char * response = get(formatted_host); + if (response == NULL) { + return 1; + } else { + parse_status(response); + free(response); + return 0; + } +} + +int disable_pihole(pihole_config * config, char * duration) { + char * formatted_host = prepend_scheme(config->host); + append_query_parameter(&formatted_host, AUTH_QUERY, config->api_key); + append_query_parameter(&formatted_host, DISABLE_QUERY, duration); + char * response = get(formatted_host); + if (response == NULL) { + return 1; + } else { + parse_status(response); + free(response); + return 0; + } +} + + +/* + * Used to handle curl data callbacks + */ +static size_t receive_data(char *ptr, size_t size, size_t nmemb, void *userdata) { + size_t realsize = size * nmemb; + http_response * response = (http_response *) userdata; + char * next = realloc(response->body, response->size + realsize + 1); + response->body = next; + memcpy(&(response->body[response->size]), ptr, realsize); + response->size += realsize; + response->body[response->size] = '\0'; + return realsize; +} + +static char * get(char endpoint[]) { + http_response response; + response.body = malloc(1); + response.size = 0; + curl_global_init(CURL_GLOBAL_ALL); + CURL * curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, endpoint); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receive_data); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + int res = curl_easy_perform(curl); + curl_easy_cleanup(curl); + if (res == CURLE_OK) { + return response.body; + } else { + return NULL; + } +} + +/* + * Given a potentially unformatted host (missing scheme), prepends the scheme (http://) to the host. Note + * that the caller is responsible for freeing the memory allocated by this method. + * @return a pointer to the host with the scheme prepended + */ +static char * prepend_scheme(char * raw_host) { + char * formatted_host; + if (strnstr(raw_host, HTTP_SCHEME, HTTP_SCHEME_LEN) == NULL + && strnstr(raw_host, HTTPS_SCHEME, HTTPS_SCHEME_LEN) == NULL) { + formatted_host = malloc(URL_FORMAT_LEN + strlen(raw_host)); + sprintf(formatted_host, URL_FORMAT, raw_host); + } else { + formatted_host = malloc(strlen(raw_host)); + strcpy(formatted_host, raw_host); + } + return formatted_host; +} + +static void parse_status(char * raw_json) { + cJSON *json = cJSON_Parse(raw_json); + cJSON *status = cJSON_GetObjectItemCaseSensitive(json, "status"); + if (cJSON_IsString(status) && (status->valuestring != NULL)) { + printf("Pi-hole status: %s\n", status->valuestring); + } else { + write_log(LOG_DEBUG, "Unable to parse response: %s", raw_json); + } + cJSON_Delete(json); +} + +static void append_query_parameter(char ** host, char * key, char * value) { + char separator = strstr(*host, "?") ? '&' : '?'; + int host_len = strlen(*host); + int new_len = host_len + 1 + strlen(key) + 1; + if (value) { + // Add another byte for the '=' + new_len += strlen(value) + 1; + } + *host = realloc(*host, new_len); + char * format = value ? "%c%s=%s" : "%c%s"; + sprintf(&((*host)[host_len]), format, separator, key, value); + (*host)[strlen(*host)] = '\0'; +} + diff --git a/PiHelper/network.h b/PiHelper/network.h new file mode 100644 index 0000000..375156c --- /dev/null +++ b/PiHelper/network.h @@ -0,0 +1,42 @@ +#ifndef PIHELPER_NETWORK +#define PIHELPER_NETWORK + +typedef struct { + size_t size; + char * body; +} http_response; + +typedef struct { + size_t domains_being_blocked; + size_t dns_queries_today; + size_t ads_blocked_today; + double ads_percentage_today; + size_t unique_domains; + size_t queries_forwarded; + size_t queries_cached; + size_t clients_ever_seen; + size_t unique_clients; + size_t dns_queries_all_types; + size_t reply_NODATA; + size_t reply_NXDOMAIN; + size_t reply_CNAME; + size_t reply_IP; + size_t privacy_level; + char * status; +} pihole_status; + +int get_status(pihole_config * config); + +int enable_pihole(pihole_config * config); + +int disable_pihole(pihole_config * config, char * duration); + +static char * get(char endpoint[]); + +static void parse_status(char * raw_json); + +static void append_query_parameter(char ** host, char * key, char * value); + +static char * prepend_scheme(char * raw_host); +#endif + diff --git a/PiHelper/pihelper.c b/PiHelper/pihelper.c new file mode 100644 index 0000000..67233b7 --- /dev/null +++ b/PiHelper/pihelper.c @@ -0,0 +1,17 @@ +/* + * ===================================================================================== + * + * Filename: pihelper.c + * + * Description: The main PiHelper class + * + * Version: 1.0 + * Created: 12/20/2019 18:24:51 + * Revision: none + * Compiler: gcc + * + * Author: William Brawner (Billy), billy@wbrawner.com + * + * ===================================================================================== + */ + diff --git a/PiHelper/pihelper.h b/PiHelper/pihelper.h new file mode 100644 index 0000000..ffceb67 --- /dev/null +++ b/PiHelper/pihelper.h @@ -0,0 +1,10 @@ +#ifndef PIHELPER +#define PIHELPER +#include "config.h" +#include "log.h" +#include "network.h" +const int PIHELPER_OK = 0; +const int PIHELPER_HELP = 1; +const int PIHELPER_INVALID_COMMANDS = 2; +#endif +