IRCWebHooks
Barry Adding files 3f04e22 (3 years, 3 months ago)diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..699ecea --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.swp +*.o +*.pem +bot diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2dc44ad --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +PRODUCT=bot + +CC=gcc +CFLAGS=-lssl -lcrypto -lpthread +LFLAGS=-lssl -lcrypto -lpthread + +SOURCES := $(shell find src/ -name '*.c') +OBJS = $(sort $(subst src/,build/,$(subst .c,.o,$(SOURCES)))) + +install: $(PRODUCT) + @echo "DONE!" + +$(PRODUCT): $(OBJS) + @echo "LINKING $(PRODUCT)..." + @$(CC) -o $@ $^ $(LFLAGS) + +clean: + @echo "REMOVING OBJECT FILES..." + @touch $(OBJS) + @rm $(OBJS) + +build/%.o: src/%.c + @echo "COMPILING $<..." + @$(CC) -c $< -o $@ $(CFLAGS) diff --git a/README b/README new file mode 100644 index 0000000..4c1b0b4 --- /dev/null +++ b/README @@ -0,0 +1,37 @@ +# IRC Web Hook Bot +This is a simple IRC bot that posts the content of POST requests it receives. + +It runs a small webserver, and connects to an IRC server. + +## How to install +Download the code, then run `make`, `sudo ./webircbot`. + +Root access is required to make use of low port numbers, but you can run the webserver on any port. + +If you want to change any settings, edit `src/config.h`, then run `make clean install` + +## How to use +By default the bot will join the channels specified in the config. +To get it to join more channels, private message the bot in the server and send `JOIN #channel`. + +You can also use `LEAVE #channel` to get it to leave. + +It will by default send notifications to all channels it is joined to. + +Only allowed users can control the bot from IRC. +You can specify users by an ident or a vhost in the config. +You may specify as many as you like by separating them with a comma + + + +Only want it to message one channel, or a specific user? + +Send the webhook to the channel name, e.g. `https://webhook.barryserver.net/channel`, or `https://webhook.barryserver.net/@Barry` + +`@` specified that you want to message a user. + +## How to call a webhook +The bot reads from POST data, specifically the data sent in `content`. +This content is plaintext, not URL encoded. + +So `curl -k -X POST -d 'content=Hello, World!' https://webhook.barryserver.net/@Barry` will send `Hello, World!` directly to the user with the nick `Barry`. diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..fb8f9ac --- /dev/null +++ b/src/config.h @@ -0,0 +1,21 @@ +#ifndef CONFIG_H +#define CONFIG_H + +//#define VERBOSE 1 + +/* IRC settings */ +#define NICK "webhook" +#define PASS "password" // Leave undefined if no password +#define CHAN "#webhooks" // Comma separated list +#define HOST "irc.rizon.net" +#define PORT "6697" + +/* Admin settings */ +#define ADMIN_IDENT "~Barry" // Comma separated list +#define ADMIN_VHOST "barryserver.net" // Comma separated list + +/* Web server settings */ +#define WEB_PORT 8080 +#define SSL_SERVER 1 // Leave undefined for HTTP + +#endif diff --git a/src/irc.c b/src/irc.c new file mode 100644 index 0000000..5ab201b --- /dev/null +++ b/src/irc.c @@ -0,0 +1,240 @@ +#include <stdio.h> +#include <stdarg.h> +#include <stdlib.h> +#include <unistd.h> +#include <netdb.h> +#include <string.h> + +#include <openssl/bio.h> +#include <openssl/ssl.h> +#include <openssl/err.h> +#include <openssl/pem.h> +#include <openssl/x509.h> +#include <openssl/x509_vfy.h> + +#include "config.h" +#include "list.h" +#include "irc.h" + +SSL_CTX *ctx; +SSL *ssl; +int conn; +char sbuf[512]; +struct Node *head = NULL; +struct Node *current = NULL; + +/* Auth a user */ +int +auth(char *ident, char *vhost) +{ + char *pt; + char *identlist = strdup(ADMIN_IDENT); + char *vhostlist = strdup(ADMIN_VHOST); + + pt = strtok(identlist,","); + while (pt != NULL) { + if (!strncmp(ident, pt, strlen(pt))) return 1; + pt = strtok(NULL, ","); + } + + pt = strtok(vhostlist,","); + while (pt != NULL) { + if (!strncmp(vhost, pt, strlen(pt))) return 1; + pt = strtok(NULL, ","); + } + + return 0; +} + +/* Send data to server */ +void +raw(char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vsnprintf(sbuf, 512, fmt, ap); + va_end(ap); +#ifdef VERBOSE + printf("<< %s", sbuf); +#endif + SSL_write(ssl, sbuf, strlen(sbuf)); +} + +/* Bot code */ +void +bot(void) +{ + BIO *certbio = NULL, *outbio = NULL; + X509 *cert = NULL; + X509_NAME *certname = NULL; + const SSL_METHOD *method; + + char *user, *ident, *vhost, *command, *where, *message, *sep, *target; + int i, j, l, sl, o = -1, start, wordcount; + char buf[513]; + struct addrinfo hints, *res; + + /* Connect to IRC */ + printf("Connecting to IRC...\n"); + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + getaddrinfo(HOST, PORT, &hints, &res); + conn = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + connect(conn, res->ai_addr, res->ai_addrlen); + + /* SSL connection */ + { + OpenSSL_add_all_algorithms(); + ERR_load_BIO_strings(); + ERR_load_crypto_strings(); + SSL_load_error_strings(); + certbio = BIO_new(BIO_s_file()); + outbio = BIO_new_fp(stdout, BIO_NOCLOSE); + if (SSL_library_init() < 0) { + BIO_printf(outbio, "Could not initialise the OpenSSL library!\n"); + exit(1); + } + method = SSLv23_client_method(); + if ((ctx = SSL_CTX_new(method)) == NULL) { + BIO_printf(outbio, "Unable to create a new SSL context!\n"); + exit(1); + } + SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2); + ssl = SSL_new(ctx); + SSL_set_fd(ssl, conn); + if (SSL_connect(ssl) != 1) { + BIO_printf(outbio, "Error: Could not SSL connect!\n"); + exit(1); + } + cert = SSL_get_peer_certificate(ssl); + if (cert == NULL) { + BIO_printf(outbio, "Error: Could not get certificate!\n"); + exit(1); + } + certname = X509_NAME_new(); + certname = X509_get_subject_name(cert); + } + + /* IRC user */ + raw("USER %s 0 0 :%s\r\n", NICK, NICK); + raw("NICK %s\r\n", NICK); + printf("Joined IRC!\n"); + + /* Add initial channel to list */ + char *chanlist = strdup(CHAN); + char *pt; + pt = strtok(chanlist,","); + while (pt != NULL) { + insert_first(pt); + pt = strtok(NULL, ","); + } + + /* IRC messages */ + while (sl = SSL_read(ssl, sbuf, 512)) + for (i = 0; i < sl; i++) { + o++; + buf[o] = sbuf[i]; + if ((i > 0 && sbuf[i] == '\n' && sbuf[i-1] == '\r') || o == 512) { + buf[o + 1] = '\0'; + l = o; + o = -1; + +#ifdef VERBOSE + printf(">> %s", buf); +#endif + + /* Deal with PINGs */ + if (!strncmp(buf, "PING", 4)) { + buf[1] = 'O'; + raw(buf); + /* Messages */ + } else if (buf[0] == ':') { + wordcount = 0; + user = command = where = message = NULL; + /* Separate words */ + for (j = 1; j < l; j++) { + if (buf[j] == ' ') { + buf[j] = '\0'; + wordcount++; + switch (wordcount) { + case 1: + user = buf + 1; + break; + case 2: + command = buf + start; + break; + case 3: + where = buf + start; + break; + } + if (j == l - 1) + continue; + start = j + 1; + } else if (buf[j] == ':' && wordcount == 3) { + if (j < l - 1) + message = buf + j + 1; + break; + } + } + + if (wordcount < 2) + + /* Join a channel */ + sleep(1); + if (!strncmp(command, "001", 3)) { +#ifdef PASS + raw("PRIVMSG NickServ :IDENTIFY %s\r\n", PASS); + sleep(1); + raw("PRIVMSG HostServ :ON\r\n", PASS); + sleep(1); +#endif + raw("JOIN %s\r\n", CHAN); + printf("Identified for nick, and joined channel!\n"); + /* Incoming messages */ + } else if (!strncmp(command, "PRIVMSG", 7) + || !strncmp(command, "NOTICE", 6)) { + if (where == NULL || message == NULL) + continue; + if ((sep = strchr(user, '!')) != NULL) { + user[sep - user] = '\0'; + ident = sep + 1; + if ((sep = strchr(ident, '@')) != NULL) { + ident[sep - ident] = '\0'; + vhost = sep + 1; + } + } + if (where[0] == '#' || where[0] == '&' || where[0] == '+' || where[0] == '!') + target = where; + else + target = user; + + if (target == user && !strncmp(message, "JOIN ", 5) && auth(ident, vhost)) { + raw("%s\r\n", message); + char *p = message+5; + while (*p++) if (*p == '\n' || *p == '\r' || *p == ' ') *p = '\0'; + insert_first(message+5); + } else + if (target == user && !strncmp(message, "LEAVE ", 6) && auth(ident, vhost)) { + raw("PART %s\r\n", message+6); + char *p = message+5; + while (*p++) if (*p == '\n' || *p == '\r' || *p == ' ') *p = '\0'; + delete(message+6); + } + +#ifdef VERBOSE + printf( + "[from: %s] [reply-with: %s] [where: %s] [reply-to: %s] %s", + user, command, where, target, message + ); +#endif + } + } + } + } + + SSL_free(ssl); + close(conn); + X509_free(cert); + SSL_CTX_free(ctx); +} diff --git a/src/irc.h b/src/irc.h new file mode 100644 index 0000000..2e81da0 --- /dev/null +++ b/src/irc.h @@ -0,0 +1,9 @@ +#ifndef IRC_H +#define IRC_H + +extern struct Node *head, *current; + +void raw(char *fmt, ...); +void bot(void); + +#endif diff --git a/src/list.c b/src/list.c new file mode 100644 index 0000000..54e1cff --- /dev/null +++ b/src/list.c @@ -0,0 +1,89 @@ +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <stdbool.h> + +#include "list.h" +#include "irc.h" + +/* Insert first item */ +void +insert_first(char *data) +{ + struct Node *link = (struct Node*) malloc(sizeof(struct Node)); + strcpy(link->data, data); + link->next = head; + head = link; +} + +/* Delete the first item */ +struct Node * +delete_first(void) +{ + struct Node *tempLink = head; + head = head->next; + free(tempLink); + return tempLink; +} + +/* Check if list is empty */ +bool +is_empty(void) +{ + return head == NULL; +} + +int +length(void) +{ + int length = 0; + struct Node *current; + for(current = head; current != NULL; current = current->next) { + length++; + } + return length; +} + +/* Find Node with data */ +struct Node * +find(char *data) +{ + struct Node* current = head; + if (head == NULL) { + return NULL; + } + while (strncmp(current->data, data, strlen(data))) { + if (current->next == NULL) { + return NULL; + } else { + current = current->next; + } + } + return current; +} + +/* Delete a Node */ +struct Node * +delete(char *data) +{ + struct Node* current = head; + struct Node* previous = NULL; + if (head == NULL) { + return NULL; + } + while (strncmp(current->data, data, strlen(data))) { + if(current->next == NULL) { + return NULL; + } else { + previous = current; + current = current->next; + } + } + if (current == head) { + head = head->next; + } else { + previous->next = current->next; + } + free(current); + return current; +} diff --git a/src/list.h b/src/list.h new file mode 100644 index 0000000..413235d --- /dev/null +++ b/src/list.h @@ -0,0 +1,18 @@ +#ifndef LIST_H +#define LIST_H + +#include <stdbool.h> + +struct Node { + char data[32]; + struct Node *next; +}; + +void insert_first(char *data); +struct Node *delete_first(void); +bool is_empty(void); +int length(void); +struct Node *find(char *data); +struct Node *delete(char *data); + +#endif diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..877fb67 --- /dev/null +++ b/src/main.c @@ -0,0 +1,22 @@ +#include <stdio.h> +#include <stdlib.h> +#include <sys/wait.h> +#include <unistd.h> +#include <netdb.h> +#include <pthread.h> + +#include "config.h" +#include "irc.h" +#include "webserver.h" + +int +main(int argc, char *argv[]) +{ + pthread_t srvThread, ircThread; + pthread_create(&srvThread, NULL, (void *) srv, NULL); + pthread_create(&ircThread, NULL, (void *) bot, NULL); + pthread_join(srvThread, NULL); + pthread_join(ircThread, NULL); + + return 0; +} diff --git a/src/webserver.c b/src/webserver.c new file mode 100644 index 0000000..60ad2e5 --- /dev/null +++ b/src/webserver.c @@ -0,0 +1,178 @@ +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <sys/socket.h> +#include <arpa/inet.h> +#include <openssl/ssl.h> +#include <openssl/err.h> + +#include "config.h" +#include "list.h" +#include "irc.h" + +int +create_socket(int port) +{ + int s; + struct sockaddr_in addr; + + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = htonl(INADDR_ANY); + + s = socket(AF_INET, SOCK_STREAM, 0); + if (s < 0) { + perror("Unable to create socket"); + exit(EXIT_FAILURE); + } + + if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("Unable to bind"); + exit(EXIT_FAILURE); + } + + if (listen(s, 1) < 0) { + perror("Unable to listen"); + exit(EXIT_FAILURE); + } + + return s; +} + +void +init_openssl(void) +{ + SSL_load_error_strings(); + OpenSSL_add_ssl_algorithms(); +} + +void +cleanup_openssl(void) +{ + EVP_cleanup(); +} + +SSL_CTX * +create_context(void) +{ + const SSL_METHOD *method; + SSL_CTX *ctx; + + method = SSLv23_server_method(); + + ctx = SSL_CTX_new(method); + if (!ctx) { + perror("Unable to create SSL context"); + ERR_print_errors_fp(stderr); + exit(EXIT_FAILURE); + } + + return ctx; +} + +void +configure_context(SSL_CTX *ctx) +{ + SSL_CTX_set_ecdh_auto(ctx, 1); + + /* Set the key and cert */ + if (SSL_CTX_use_certificate_file(ctx, "cert.pem", SSL_FILETYPE_PEM) <= 0) { + ERR_print_errors_fp(stderr); + exit(EXIT_FAILURE); + } + + if (SSL_CTX_use_PrivateKey_file(ctx, "cert.pem", SSL_FILETYPE_PEM) <= 0 ) { + ERR_print_errors_fp(stderr); + exit(EXIT_FAILURE); + } +} + +int +srv(int argc, char **argv) +{ + printf("Spinning up webserver...\n"); + + int sock; +#ifdef SSL_SERVER + SSL_CTX *ctx; + init_openssl(); + ctx = create_context(); + configure_context(ctx); +#endif + + sock = create_socket(WEB_PORT); + + /* Handle connections */ + while(1) { + struct sockaddr_in addr; + uint len = sizeof(addr); +#ifdef SSL_SERVER + SSL *ssl; +#endif + const char reply[] = "test\r\n"; + + int client = accept(sock, (struct sockaddr*)&addr, &len); + if (client < 0) { + perror("Unable to accept"); + exit(EXIT_FAILURE); + } + +#ifdef SSL_SERVER + ssl = SSL_new(ctx); + SSL_set_fd(ssl, client); + SSL_accept(ssl); +#endif + + char buf[1025]; + char *p, *uri, *cont; + int conlen; + char key[64]; + +#ifdef SSL_SERVER + SSL_read(ssl, &buf, 1024); +#else + read(client, &buf, 1024); +#endif + + if (!strncmp(buf, "POST /", 6)) { + p = buf + 5; + while (*p++) if (*p == '\n' || *p == '\r' || *p == ' ') *p = '\0'; + uri = buf + 6; + cont = uri + strlen(uri) + 1; + if ((p = strstr(cont, "Content-Length: ")) != NULL) { + sscanf(p+16, "%i", &conlen); + } + if ((p = strstr(cont, "content=")) != NULL) { + p[conlen] = '\0'; + p += 8; + + if (*(uri) == '\0') { + for (current = head; current != NULL; current = current->next) { + raw("PRIVMSG %s :%s\r\n", current->data, p); + } + } else + if (*(uri) == '@') { + raw("PRIVMSG %s :%s\r\n", buf+7, p); + } else { + raw("PRIVMSG #%s :%s\r\n", buf+6, p); + } + } + memset(&buf, 0, 1024); + } + +#ifdef SSL_SERVER + SSL_write(ssl, "HTTP/1.1 200 OK\r\n", 17); + SSL_shutdown(ssl); + SSL_free(ssl); +#else + write(client, "HTTP/1.1 200 OK\r\n", 17); +#endif + close(client); + } + + close(sock); +#ifdef SSL_SERVER + SSL_CTX_free(ctx); + cleanup_openssl(); +#endif +} diff --git a/src/webserver.h b/src/webserver.h new file mode 100644 index 0000000..05c8d59 --- /dev/null +++ b/src/webserver.h @@ -0,0 +1,6 @@ +#ifndef WEBSERVER_H +#define WEBSERVER_H + +void srv(void); + +#endif