Created
July 3, 2017 17:29
-
-
Save king1600/56535855cfd0bb09904b025b4c20c9a5 to your computer and use it in GitHub Desktop.
Mini GET only HTTP Server in C
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "http.h" | |
#include <netdb.h> // addrinfo, getaddrinfo, freeaddrinfo, AI_PASSIVE, gai_strerror | |
#include <fcntl.h> // fcntl | |
#include <stdio.h> // printf, fprintf, fileno | |
#include <stdlib.h> // calloc, free | |
#include <string.h> // memcpy, memset | |
#include <unistd.h> // write, close | |
#include <netinet/tcp.h> // TCP_NODELAY, TCP_CORK/TCP_NOPUSH | |
// use BSD style flag | |
#ifndef TCP_NOPUSH | |
#define TCP_NOPUSH TCP_CORK | |
#endif | |
/** | |
* Print error and close program | |
* @param {char*} err the error to print | |
*/ | |
void CError(const char *err) { | |
fprintf(stderr, "[x] %s", err); | |
abort(); | |
} | |
/** | |
* Parse path of http request | |
* @param {char*} data the http request data | |
* @param {HttpReq*} req the request object to save data to | |
*/ | |
void HttpParse(const char* data, HttpReq* req) { | |
// check if request is complete | |
req->complete = strstr(data, "\r\n\r\n") == NULL ? 0 : 1; | |
// check if connection should keep alive | |
req->keep_alive = strstr( | |
data, "Connection: keep-alive\r\n") == NULL ? 0 : 1; | |
// extract file path from request | |
if (req->path == NULL || strlen(req->path) < 1) { | |
char *p_start = strstr(data, " "); | |
char *p_end = strstr(data + (p_start - data) + 1, " "); | |
char *q_end = strstr(data + (p_start - data) + 1, "?"); | |
// display HTTP Method | |
char *m_end = strstr(data, "\r\n"); | |
write(STDOUT_FILENO, "[*] ", 4); | |
write(STDOUT_FILENO, data, m_end - data); | |
write(STDOUT_FILENO, "\r\n", 2); | |
// get path | |
if (q_end != NULL) p_end = q_end - 1; | |
int size = (p_end - data) - (p_start - data) - 1; | |
req->path = (char*)calloc(size, sizeof(char*)); | |
memcpy(req->path, data + (p_start - data) + 1, size); | |
} | |
} | |
/** | |
* Set a client socket blocking mode | |
* @param {Client*} client the client socket | |
* @param {int} blocking state (1 = on 0 = off) | |
*/ | |
int CBlocking(Client* client, int on) { | |
int flags = fcntl(client->fd, F_GETFL, 0); | |
if (flags < 0) return -1; | |
flags = !on ? (flags | O_NONBLOCK) : (flags & ~O_NONBLOCK); | |
flags = fcntl(client->fd, F_SETFL, flags); | |
return flags != 0 ? -1 : 0; | |
} | |
/** | |
* Set TCP_NODELAY flag on client to reduce latency | |
* @param {Client*} client the client socket | |
* @param {int} flag state (1 = on 0 = off) | |
*/ | |
int CNoDelay(Client* client, int on) { | |
int ret = setsockopt(client->fd, | |
IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)); | |
return ret < 0 ? -1 : 0; | |
} | |
/** | |
* Set TCP_NO flag on client to buildup data before sending | |
* @param {Client*} client the client socket | |
* @param {int} flag state (1 = on 0 = off) | |
*/ | |
int CNoPush(Client* client, int on) { | |
int ret = setsockopt(client->fd, | |
IPPROTO_TCP, TCP_NOPUSH, &on, sizeof(on)); | |
return ret < 0 ? -1 : 0; | |
} | |
/** | |
* Create a server socket using a client object | |
* @param {Client*} client the client to hose the server socket | |
* @param {char*} the port number as c-string to host server on | |
*/ | |
int openServer(Client *c, const char *port) { | |
// find address to bind to | |
int ret, flag = 1; | |
struct addrinfo hints, *res, *r; | |
memset(&hints, 0, sizeof(hints)); | |
hints.ai_family = AF_UNSPEC; | |
hints.ai_socktype = SOCK_STREAM; | |
hints.ai_flags = AI_PASSIVE; | |
ret = getaddrinfo(NULL, port, &hints, &res); | |
if (ret != 0) { | |
fprintf(stderr, "Failed to get address info: %s\n", | |
gai_strerror(ret)); | |
return -1; | |
} | |
// look through address results and pick one that works | |
for (r = res; r != NULL; r = r->ai_next) { | |
c->fd = socket(r->ai_family, r->ai_socktype, r->ai_protocol); | |
if (c->fd == -1) continue; | |
ret = setsockopt(c->fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)); | |
if (ret < 0) { close(c->fd); continue; } | |
#ifdef SO_REUSEPORT | |
ret = setsockopt(c->fd, SOL_SOCKET, SO_REUSEPORT, &flag, sizeof(flag)); | |
#endif | |
if (ret < 0) { close(c->fd); continue; } | |
ret = bind(c->fd, r->ai_addr, r->ai_addrlen); | |
if (ret == 0) break; | |
close(c->fd); | |
} | |
// check if creating socket was successful | |
if (r == NULL || c->fd < 0) { | |
CError("Could not bind socket\n"); | |
return -1; | |
} | |
freeaddrinfo(res); | |
return c->fd; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef _HTTP_H | |
#define _HTTP_H | |
/** Http Request object */ | |
typedef struct HttpReq { | |
char* path; // the path url to fetch | |
unsigned char complete; // if ready to respond to | |
unsigned char keep_alive; // if connection should be kept-alive | |
} HttpReq; | |
/** | |
* Parse path of http request | |
* @param {char*} data the http request data | |
* @param {HttpReq*} req the request object to save data to | |
*/ | |
void HttpParse(const char* data, HttpReq* req); | |
/** A socket client */ | |
typedef struct Client { | |
int fd; // the socket file descriptor | |
HttpReq req; // the http object associated with it | |
} Client; | |
/** | |
* Print error and close program | |
* @param {char*} err the error to print | |
*/ | |
void CError(const char *err); | |
/** | |
* Set a client socket blocking mode | |
* @param {Client*} client the client socket | |
* @param {int} blocking state (1 = on 0 = off) | |
*/ | |
int CBlocking(Client* client, int on); | |
/** | |
* Set TCP_NODELAY flag on client to reduce latency | |
* @param {Client*} client the client socket | |
* @param {int} flag state (1 = on 0 = off) | |
*/ | |
int CNoDelay(Client* client, int on); | |
/** | |
* Set TCP_NO flag on client to buildup data before sending | |
* @param {Client*} client the client socket | |
* @param {int} flag state (1 = on 0 = off) | |
*/ | |
int CNoPush(Client* client, int on); | |
/** | |
* Create a server socket using a client object | |
* @param {Client*} client the client to hose the server socket | |
* @param {char*} the port number as c-string to host server on | |
*/ | |
int openServer(Client *client, const char *port); | |
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "http.h" | |
#include <stdio.h> // fopen, fclose, ftell, fseek, printf, sprintf, fileno | |
#include <errno.h> // errno | |
#include <string.h> // memset | |
#include <stdlib.h> // free, malloc, calloc | |
#include <unistd.h> // write, read, close | |
#include <sys/epoll.h> // epoll_create1, epoll_ctl, epoll_wait | |
#include <netinet/in.h> // sockaddr, accept | |
#include <sys/sendfile.h> // sendfile | |
#define READ_BUFFER 4096 // amount of data to read per session | |
#define MAX_EVENTS 64 // max simultanious epoll events in one tick | |
#define MAX_CONNS 1024 // max amount of backlog connections | |
// norrmal ile http response | |
static const char* NORMAL_RESP = | |
"HTTP/1.1 200 Ok\r\n" | |
"Content-Length: %d\r\n" | |
"Server: epoll-server\r\n" | |
"Content-Type: application/octet-stream\r\n" | |
"\r\n"; | |
// file not found response | |
static const char* FILE_ERR_RESP = | |
"HTTP/1.1 400 Not Found\r\n" | |
"Content-Length: 9\r\n" | |
"Server: epoll-server\r\n" | |
"Content-Type: text/plain\r\n" | |
"\r\nNot Found"; | |
// server error responses | |
static const char* SERVER_ERR_RESP = | |
"HTTP/1.1 500 Interal Server Error\r\n" | |
"Content-Type: text/plain\r\n" | |
"Content-Length: 20\r\n" | |
"\r\nFailed to open file!"; | |
// run the server | |
int run(const char* port); | |
int main(int argc, char* argv[]) { | |
// get server port number | |
if (argc < 2) | |
CError("Provide port number\n"); | |
// change directory if provided | |
if (argc > 2) { | |
printf("[^] Moving to: %s\n", argv[2]); | |
if (chdir(argv[2]) < 0) | |
CError("Failed to change directory\n"); | |
} | |
// start event loop | |
return run(argv[1]); | |
} | |
/** | |
* Destroy a client object (close socket, free data) | |
* @param {Client*} sock the client object to destroy | |
*/ | |
static inline void CFree(Client *sock) { | |
if (sock != NULL) { | |
close(sock->fd); | |
if (sock->req.path != NULL) | |
if (strlen(sock->req.path) > 0) | |
free(sock->req.path); | |
free(sock); | |
} | |
} | |
/** | |
* Respond to a client request | |
* @param {Client*} sock the request to respond to | |
*/ | |
static inline void Respond(Client *sock) { | |
// setup socket for writing | |
CBlocking(sock, 1); | |
CNoDelay(sock, 1); | |
// check if file exists | |
if (access(sock->req.path + 1, F_OK) != -1) { | |
FILE *f = fopen(sock->req.path + 1, "rb"); | |
// could not open file | |
if (f == NULL) { | |
write(sock->fd, SERVER_ERR_RESP, strlen(SERVER_ERR_RESP)); | |
// use sendfile to send file contents | |
} else { | |
// get file size | |
off_t offset = 0; | |
fseek(f, 0, SEEK_END); | |
ssize_t sent = 0, size = ftell(f); | |
fseek(f, 0, SEEK_SET); | |
// send header | |
CNoDelay(sock, 0); | |
CNoPush(sock, 1); | |
char header[512]; | |
sprintf(header, NORMAL_RESP, size); | |
write(sock->fd, header, strlen(header)); | |
// send data | |
size -= 1; | |
int filefd = fileno(f); | |
while (sent < size) { | |
sent = sendfile(sock->fd, filefd, &offset, size - sent); | |
if (sent < 0) { | |
CFree(sock); | |
return; | |
} | |
} | |
// finish sending | |
CNoPush(sock, 0); | |
CNoDelay(sock, 1); | |
sent = sendfile(sock->fd, filefd, &offset, size); | |
fclose(f); | |
} | |
// file doesn't exit, 404 | |
} else { | |
write(sock->fd, FILE_ERR_RESP, strlen(FILE_ERR_RESP)); | |
} | |
// Close socket if not keep alive | |
if (sock->req.keep_alive != 1) { | |
CFree(sock); | |
return; | |
} | |
// Return socket back to normal | |
CBlocking(sock, 0); | |
sock->req.complete = 0; | |
if (strlen(sock->req.path) > 0) | |
free(sock->req.path); | |
} | |
/** | |
* Start running the http server | |
* @param {char*} port the port to start it on | |
*/ | |
int run(const char* port) { | |
Client server; // the server object | |
ssize_t nread; // amount read from client | |
char buffer[READ_BUFFER]; // data read from client | |
struct epoll_event event; // creating epoll events | |
struct epoll_event *events; // events polled | |
int i, ret, epoll_fd, polled; // epoll and other vars | |
// create server socket | |
if (openServer(&server, port) < 0) | |
return -1; | |
if (CBlocking(&server, 0) < 0) | |
CError("Failed to set server non-blocking\n"); | |
if (listen(server.fd, MAX_CONNS) < 0) | |
CError("Failed to set listen backlog\n"); | |
// create epoll context and register server | |
epoll_fd = epoll_create1(0); | |
if (epoll_fd < 0) | |
CError("Failed to create epoll fd\n"); | |
event.data.fd = server.fd; | |
event.events = EPOLLIN | EPOLLET; | |
ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server.fd, &event); | |
if (ret < 0) CError("Server epoll_ctl error\n"); | |
events = (struct epoll_event*)calloc(MAX_EVENTS, sizeof(events)); | |
// start the event loop | |
printf("[*] Server started on localhost:%s\n", port); | |
while (1) { | |
// iterate through event | |
polled = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); | |
for (i = 0; i < polled; i++) { | |
// check for errors | |
if ((events[i].events & EPOLLERR) || | |
(events[i].events & EPOLLHUP) || | |
(!(events[i].events & EPOLLIN))) | |
{ | |
CFree((Client*)events[i].data.ptr); | |
continue; | |
} | |
// handle incoming connections | |
else if (events[i].data.fd == server.fd) { | |
struct sockaddr addr; | |
socklen_t len = sizeof(addr); | |
// accept all connections | |
while (1) { | |
Client *sock = (Client*)malloc(sizeof(sock)); | |
sock->req.path = NULL; | |
sock->fd = accept(server.fd, &addr, &len); | |
if (sock->fd < 0) { | |
if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) | |
break; | |
free(sock); | |
CError("Socket accept failed!\n"); | |
} | |
// set client socket to non-blocking | |
if (CBlocking(sock, 0) < 0) { | |
CFree(sock); | |
continue; | |
} | |
// register client socket to epoll | |
event.data.fd = sock->fd; | |
event.data.ptr = (void*)sock; | |
event.events = EPOLLIN | EPOLLET; | |
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock->fd, &event) < 0) { | |
CFree(sock); | |
continue; | |
} | |
} | |
} | |
// handle reading data from client | |
else { | |
Client *sock = (Client*)events[i].data.ptr; | |
// start reading data from client | |
while (1) { | |
memset(buffer, 0, sizeof(buffer)); | |
nread = read(sock->fd, buffer, sizeof(buffer)); | |
// handle EOF | |
if (nread == -1) { | |
if (errno != EAGAIN) | |
CFree(sock); | |
break; | |
// handle empty data | |
} else if (nread == 0) { | |
break; | |
// handle data | |
} else { | |
HttpParse(buffer, &(sock->req)); | |
if (sock->req.complete == 1) | |
Respond(sock); | |
} | |
} | |
} | |
} | |
} | |
// free all resources before exiting | |
free(events); | |
close(server.fd); | |
return 0; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
all: | |
gcc -g -Wall -fPIC http.c main.c -o app |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment