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.
This commit is contained in:
William Brawner 2019-12-30 14:18:57 -06:00
commit b88fba9644
15 changed files with 589 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
build/
*.o

6
.gitmodules vendored Normal file
View file

@ -0,0 +1,6 @@
[submodule "cJSON"]
path = cJSON
url = https://github.com/DaveGamble/cJSON
[submodule "curl"]
path = curl
url = https://github.com/curl/curl

10
.vimrc Normal file
View file

@ -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 <F4> :!mkdir -p build <bar> cmake -B build <bar> make -C build<cr>
nnoremap <F4> :!build/PiHelper/pihelper localhost<cr>
nnoremap <F9> :!rm -rf build<cr>
let &path.="/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include,"

14
CMakeLists.txt Normal file
View file

@ -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)

59
PiHelper/CMakeLists.txt Normal file
View file

@ -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})

119
PiHelper/cli.c Normal file
View file

@ -0,0 +1,119 @@
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 <duration> Disable the Pi-hole for a given duration, or permanently if empty\n");
printf(" -e, --enable Enable the Pi-hole\n");
printf(" -f, --file <config-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");
}

17
PiHelper/cli.h Normal file
View file

@ -0,0 +1,17 @@
#include <getopt.h>
#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);

110
PiHelper/config.c Normal file
View file

@ -0,0 +1,110 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <limits.h>
#include <pwd.h>
#include <unistd.h>
#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;
}

19
PiHelper/config.h Normal file
View file

@ -0,0 +1,19 @@
#ifndef PIHELPER_CONFIG
#define PIHELPER_CONFIG
#include <openssl/sha.h>
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

20
PiHelper/log.c Normal file
View file

@ -0,0 +1,20 @@
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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);
}

10
PiHelper/log.h Normal file
View file

@ -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

134
PiHelper/network.c Normal file
View file

@ -0,0 +1,134 @@
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#include <cjson/cJSON.h>
#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';
}

42
PiHelper/network.h Normal file
View file

@ -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

17
PiHelper/pihelper.c Normal file
View file

@ -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
*
* =====================================================================================
*/

10
PiHelper/pihelper.h Normal file
View file

@ -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