uxplayer/lib/httpd.c
2025-05-03 17:09:23 +08:00

664 lines
21 KiB
C

/**
* Copyright (C) 2011-2012 Juho Vähä-Herttua
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
*===================================================================
* modified by fduncanh 2022
*/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <assert.h>
#include <stdbool.h>
#include <errno.h>
#include "httpd.h"
#include "netutils.h"
#include "http_request.h"
#include "compat.h"
#include "logger.h"
#include "utils.h"
static const char *typename[] = {
[CONNECTION_TYPE_UNKNOWN] = "Unknown",
[CONNECTION_TYPE_RAOP] = "RAOP",
[CONNECTION_TYPE_AIRPLAY] = "AirPlay",
[CONNECTION_TYPE_PTTH] = "AirPlay (reversed)",
[CONNECTION_TYPE_HLS] = "HLS"
};
struct http_connection_s {
int connected;
int socket_fd;
void *user_data;
connection_type_t type;
http_request_t *request;
};
typedef struct http_connection_s http_connection_t;
struct httpd_s {
logger_t *logger;
httpd_callbacks_t callbacks;
int max_connections;
int open_connections;
http_connection_t *connections;
char nohold;
/* These variables only edited mutex locked */
int running;
int joined;
thread_handle_t thread;
mutex_handle_t run_mutex;
/* Server fds for accepting connections */
int server_fd4;
int server_fd6;
};
const char *
httpd_get_connection_typename (connection_type_t type) {
return typename[type];
}
int
httpd_get_connection_socket (httpd_t *httpd, void *user_data) {
for (int i = 0; i < httpd->max_connections; i++) {
http_connection_t *connection = &httpd->connections[i];
if (!connection->connected) {
continue;
}
if (connection->user_data == user_data) {
return connection->socket_fd;
}
}
return -1;
}
int
httpd_set_connection_type (httpd_t *httpd, void *user_data, connection_type_t type) {
for (int i = 0; i < httpd->max_connections; i++) {
http_connection_t *connection = &httpd->connections[i];
if (!connection->connected) {
continue;
}
if (connection->user_data == user_data) {
connection->type = type;
return i;
}
}
return -1;
}
int
httpd_count_connection_type (httpd_t *httpd, connection_type_t type) {
int count = 0;
for (int i = 0; i < httpd->max_connections; i++) {
http_connection_t *connection = &httpd->connections[i];
if (!connection->connected) {
continue;
}
if (connection->type == type) {
count++;
}
}
return count;
}
int
httpd_get_connection_socket_by_type (httpd_t *httpd, connection_type_t type, int instance){
int count = 0;
for (int i = 0; i < httpd->max_connections; i++) {
http_connection_t *connection = &httpd->connections[i];
if (!connection->connected) {
continue;
}
if (connection->type == type) {
count++;
if (count == instance) {
return connection->socket_fd;
}
}
}
return 0;
}
void *
httpd_get_connection_by_type (httpd_t *httpd, connection_type_t type, int instance){
int count = 0;
for (int i = 0; i < httpd->max_connections; i++) {
http_connection_t *connection = &httpd->connections[i];
if (!connection->connected) {
continue;
}
if (connection->type == type) {
count++;
if (count == instance) {
return connection->user_data;
}
}
}
return NULL;
}
#define MAX_CONNECTIONS 12 /* value used in AppleTV 3*/
httpd_t *
httpd_init(logger_t *logger, httpd_callbacks_t *callbacks, int nohold)
{
httpd_t *httpd;
assert(logger);
assert(callbacks);
/* Allocate the httpd_t structure */
httpd = calloc(1, sizeof(httpd_t));
if (!httpd) {
return NULL;
}
httpd->nohold = (nohold ? 1 : 0);
httpd->max_connections = MAX_CONNECTIONS;
httpd->connections = calloc(httpd->max_connections, sizeof(http_connection_t));
if (!httpd->connections) {
free(httpd);
return NULL;
}
/* Use the logger provided */
httpd->logger = logger;
/* Save callback pointers */
memcpy(&httpd->callbacks, callbacks, sizeof(httpd_callbacks_t));
/* Initial status joined */
httpd->running = 0;
httpd->joined = 1;
return httpd;
}
void
httpd_destroy(httpd_t *httpd)
{
if (httpd) {
httpd_stop(httpd);
free(httpd->connections);
free(httpd);
}
}
static void
httpd_remove_connection(httpd_t *httpd, http_connection_t *connection)
{
if (connection->request) {
http_request_destroy(connection->request);
connection->request = NULL;
}
httpd->callbacks.conn_destroy(connection->user_data);
shutdown(connection->socket_fd, SHUT_WR);
closesocket(connection->socket_fd);
connection->connected = 0;
connection->user_data = NULL;
connection->type = CONNECTION_TYPE_UNKNOWN;
httpd->open_connections--;
}
static int
httpd_add_connection(httpd_t *httpd, int fd, unsigned char *local, int local_len, unsigned char *remote,
int remote_len, unsigned int zone_id)
{
void *user_data;
int i;
for (i=0; i<httpd->max_connections; i++) {
if (!httpd->connections[i].connected) {
break;
}
}
if (i == httpd->max_connections) {
/* This code should never be reached, we do not select server_fds when full */
logger_log(httpd->logger, LOGGER_INFO, "Max connections reached");
return -1;
}
user_data = httpd->callbacks.conn_init(httpd->callbacks.opaque, local, local_len, remote, remote_len, zone_id);
if (!user_data) {
logger_log(httpd->logger, LOGGER_ERR, "Error initializing HTTP request handler");
return -1;
}
httpd->open_connections++;
httpd->connections[i].socket_fd = fd;
httpd->connections[i].connected = 1;
httpd->connections[i].user_data = user_data;
httpd->connections[i].type = CONNECTION_TYPE_UNKNOWN; //should not be necessary ...
return 0;
}
static int
httpd_accept_connection(httpd_t *httpd, int server_fd, int is_ipv6)
{
struct sockaddr_storage remote_saddr;
socklen_t remote_saddrlen;
struct sockaddr_storage local_saddr;
socklen_t local_saddrlen;
unsigned char *local, *remote;
unsigned int local_zone_id, remote_zone_id;
int local_len, remote_len;
int ret, fd;
remote_saddrlen = sizeof(remote_saddr);
fd = accept(server_fd, (struct sockaddr *)&remote_saddr, &remote_saddrlen);
if (fd == -1) {
/* FIXME: Error happened */
return -1;
}
local_saddrlen = sizeof(local_saddr);
ret = getsockname(fd, (struct sockaddr *)&local_saddr, &local_saddrlen);
if (ret == -1) {
shutdown(fd, SHUT_RDWR);
closesocket(fd);
return 0;
}
logger_log(httpd->logger, LOGGER_INFO, "Accepted %s client on socket %d",
(is_ipv6 ? "IPv6" : "IPv4"), fd);
local = netutils_get_address(&local_saddr, &local_len, &local_zone_id);
remote = netutils_get_address(&remote_saddr, &remote_len, &remote_zone_id);
assert (local_zone_id == remote_zone_id);
ret = httpd_add_connection(httpd, fd, local, local_len, remote, remote_len, local_zone_id);
if (ret == -1) {
shutdown(fd, SHUT_RDWR);
closesocket(fd);
return 0;
}
return 1;
}
bool
httpd_nohold(httpd_t *httpd) {
return (httpd->nohold ? true: false);
}
void
httpd_remove_known_connections(httpd_t *httpd) {
for (int i = 0; i < httpd->max_connections; i++) {
http_connection_t *connection = &httpd->connections[i];
if (!connection->connected || connection->type == CONNECTION_TYPE_UNKNOWN) {
continue;
}
httpd_remove_connection(httpd, connection);
}
}
static THREAD_RETVAL
httpd_thread(void *arg)
{
httpd_t *httpd = arg;
char http[] = "HTTP/1.1";
char buffer[1024];
int i;
bool logger_debug = (logger_get_level(httpd->logger) >= LOGGER_DEBUG);
assert(httpd);
while (1) {
fd_set rfds;
struct timeval tv;
int nfds=0;
int ret;
int new_request;
MUTEX_LOCK(httpd->run_mutex);
if (!httpd->running) {
MUTEX_UNLOCK(httpd->run_mutex);
break;
}
MUTEX_UNLOCK(httpd->run_mutex);
/* Set timeout value to 5ms */
tv.tv_sec = 1;
tv.tv_usec = 5000;
/* Get the correct nfds value and set rfds */
FD_ZERO(&rfds);
if (httpd->open_connections < httpd->max_connections) {
if (httpd->server_fd4 != -1) {
FD_SET(httpd->server_fd4, &rfds);
if (nfds <= httpd->server_fd4) {
nfds = httpd->server_fd4+1;
}
}
if (httpd->server_fd6 != -1) {
FD_SET(httpd->server_fd6, &rfds);
if (nfds <= httpd->server_fd6) {
nfds = httpd->server_fd6+1;
}
}
}
for (i=0; i<httpd->max_connections; i++) {
int socket_fd;
if (!httpd->connections[i].connected) {
continue;
}
socket_fd = httpd->connections[i].socket_fd;
FD_SET(socket_fd, &rfds);
if (nfds <= socket_fd) {
nfds = socket_fd+1;
}
}
ret = select(nfds, &rfds, NULL, NULL, &tv);
if (ret == 0) {
/* Timeout happened */
continue;
} else if (ret == -1) {
logger_log(httpd->logger, LOGGER_ERR, "httpd error in select: %d %s", errno, strerror(errno));
break;
}
if (httpd->open_connections < httpd->max_connections &&
httpd->server_fd4 != -1 && FD_ISSET(httpd->server_fd4, &rfds)) {
ret = httpd_accept_connection(httpd, httpd->server_fd4, 0);
if (ret == -1) {
logger_log(httpd->logger, LOGGER_ERR, "httpd error in accept ipv4");
break;
} else if (ret == 0) {
continue;
}
}
if (httpd->open_connections < httpd->max_connections &&
httpd->server_fd6 != -1 && FD_ISSET(httpd->server_fd6, &rfds)) {
ret = httpd_accept_connection(httpd, httpd->server_fd6, 1);
if (ret == -1) {
logger_log(httpd->logger, LOGGER_ERR, "httpd error in accept ipv6");
break;
} else if (ret == 0) {
continue;
}
}
for (i=0; i<httpd->max_connections; i++) {
http_connection_t *connection = &httpd->connections[i];
if (!connection->connected) {
continue;
}
if (!FD_ISSET(connection->socket_fd, &rfds)) {
continue;
}
/* If not in the middle of request, allocate one */
if (!connection->request) {
connection->request = http_request_init();
assert(connection->request);
new_request = 1;
if (connection->type == CONNECTION_TYPE_PTTH) {
http_request_is_reverse(connection->request);
}
logger_log(httpd->logger, LOGGER_DEBUG, "new request, connection %d, socket %d type %s",
i, connection->socket_fd, typename [connection->type]);
} else {
new_request = 0;
}
logger_log(httpd->logger, LOGGER_DEBUG, "httpd receiving on socket %d, connection %d",
connection->socket_fd, i);
if (logger_debug) {
logger_log(httpd->logger, LOGGER_DEBUG,"\nhttpd: current connections:");
for (int i = 0; i < httpd->max_connections; i++) {
http_connection_t *connection = &httpd->connections[i];
if(!connection->connected) {
continue;
}
if (!FD_ISSET(connection->socket_fd, &rfds)) {
logger_log(httpd->logger, LOGGER_DEBUG, "connection %d type %d socket %d conn %p %s", i,
connection->type, connection->socket_fd,
connection->user_data, typename [connection->type]);
} else {
logger_log(httpd->logger, LOGGER_DEBUG, "connection %d type %d socket %d conn %p %s ACTIVE CONNECTION",
i, connection->type, connection->socket_fd, connection->user_data, typename [connection->type]);
}
}
logger_log(httpd->logger, LOGGER_DEBUG, " ");
}
/* reverse-http responses from the client must not be sent to the llhttp parser:
* such messages start with "HTTP/1.1" */
if (new_request) {
int readstart = 0;
new_request = 0;
while (readstart < 8) {
ret = recv(connection->socket_fd, buffer + readstart, sizeof(buffer) - 1 - readstart, 0);
if (ret == 0) {
logger_log(httpd->logger, LOGGER_INFO, "Connection closed for socket %d",
connection->socket_fd);
break;
} else if (ret == -1) {
if (errno == EAGAIN) {
continue;
} else {
int sock_err = SOCKET_GET_ERROR();
logger_log(httpd->logger, LOGGER_ERR, "httpd: recv socket error %d:%s",
sock_err, strerror(sock_err));
break;
}
} else {
readstart += ret;
ret = readstart;
}
}
if (!memcmp(buffer, http, 8)) {
http_request_set_reverse(connection->request);
}
} else {
ret = recv(connection->socket_fd, buffer, sizeof(buffer) - 1, 0);
if (ret == 0) {
logger_log(httpd->logger, LOGGER_INFO, "Connection closed for socket %d",
connection->socket_fd);
httpd_remove_connection(httpd, connection);
continue;
}
}
if (http_request_is_reverse(connection->request)) {
/* this is a response from the client to a
* GET /event reverse HTTP request from the server */
if (ret && logger_debug) {
buffer[ret] = '\0';
logger_log(httpd->logger, LOGGER_INFO, "<<<< received response from client"
" (reversed HTTP = \"PTTH/1.0\") connection"
" on socket %d:\n%s\n", connection->socket_fd, buffer);
}
if (ret == 0) {
httpd_remove_connection(httpd, connection);
}
continue;
}
/* Parse HTTP request from data read from connection */
http_request_add_data(connection->request, buffer, ret);
if (http_request_has_error(connection->request)) {
logger_log(httpd->logger, LOGGER_ERR, "httpd error in parsing: %s",
http_request_get_error_name(connection->request));
httpd_remove_connection(httpd, connection);
continue;
}
/* If request is finished, process and deallocate */
if (http_request_is_complete(connection->request)) {
http_response_t *response = NULL;
// Callback the received data to raop
if (logger_debug) {
const char *method = http_request_get_method(connection->request);
const char *url = http_request_get_url(connection->request);
const char *protocol = http_request_get_protocol(connection->request);
logger_log(httpd->logger, LOGGER_INFO, "httpd request received on socket %d, "
"connection %d, method = %s, url = %s, protocol = %s",
connection->socket_fd, i, method, url, protocol);
}
httpd->callbacks.conn_request(connection->user_data, connection->request, &response);
http_request_destroy(connection->request);
connection->request = NULL;
if (response) {
const char *data;
int datalen;
int written;
int ret;
/* Get response data and datalen */
data = http_response_get_data(response, &datalen);
written = 0;
while (written < datalen) {
ret = send(connection->socket_fd, data+written, datalen-written, 0);
if (ret == -1) {
logger_log(httpd->logger, LOGGER_ERR, "httpd error in sending data");
break;
}
written += ret;
}
if (http_response_get_disconnect(response)) {
logger_log(httpd->logger, LOGGER_INFO, "Disconnecting on software request");
httpd_remove_connection(httpd, connection);
}
} else {
logger_log(httpd->logger, LOGGER_WARNING, "httpd didn't get response");
}
http_response_destroy(response);
} else {
logger_log(httpd->logger, LOGGER_DEBUG, "Request not complete, waiting for more data...");
}
}
}
/* Remove all connections that are still connected */
for (i=0; i<httpd->max_connections; i++) {
http_connection_t *connection = &httpd->connections[i];
if (!connection->connected) {
continue;
}
logger_log(httpd->logger, LOGGER_INFO, "Removing connection for socket %d", connection->socket_fd);
httpd_remove_connection(httpd, connection);
}
/* Close server sockets since they are not used any more */
if (httpd->server_fd4 != -1) {
shutdown(httpd->server_fd4, SHUT_RDWR);
closesocket(httpd->server_fd4);
httpd->server_fd4 = -1;
}
if (httpd->server_fd6 != -1) {
shutdown(httpd->server_fd6, SHUT_RDWR);
closesocket(httpd->server_fd6);
httpd->server_fd6 = -1;
}
// Ensure running reflects the actual state
MUTEX_LOCK(httpd->run_mutex);
httpd->running = 0;
MUTEX_UNLOCK(httpd->run_mutex);
logger_log(httpd->logger, LOGGER_DEBUG, "Exiting HTTP thread");
return 0;
}
int
httpd_start(httpd_t *httpd, unsigned short *port)
{
/* How many connection attempts are kept in queue */
int backlog = 5;
assert(httpd);
assert(port);
MUTEX_LOCK(httpd->run_mutex);
if (httpd->running || !httpd->joined) {
MUTEX_UNLOCK(httpd->run_mutex);
return 0;
}
httpd->server_fd4 = netutils_init_socket(port, 0, 0);
if (httpd->server_fd4 == -1) {
logger_log(httpd->logger, LOGGER_ERR, "Error initialising socket %d", SOCKET_GET_ERROR());
MUTEX_UNLOCK(httpd->run_mutex);
return -1;
}
httpd->server_fd6 = netutils_init_socket(port, 1, 0);
if (httpd->server_fd6 == -1) {
logger_log(httpd->logger, LOGGER_WARNING, "Error initialising IPv6 socket %d", SOCKET_GET_ERROR());
logger_log(httpd->logger, LOGGER_WARNING, "Continuing without IPv6 support");
}
if (httpd->server_fd4 != -1 && listen(httpd->server_fd4, backlog) == -1) {
logger_log(httpd->logger, LOGGER_ERR, "Error listening to IPv4 socket");
closesocket(httpd->server_fd4);
closesocket(httpd->server_fd6);
MUTEX_UNLOCK(httpd->run_mutex);
return -2;
}
if (httpd->server_fd6 != -1 && listen(httpd->server_fd6, backlog) == -1) {
logger_log(httpd->logger, LOGGER_ERR, "Error listening to IPv6 socket");
closesocket(httpd->server_fd4);
closesocket(httpd->server_fd6);
MUTEX_UNLOCK(httpd->run_mutex);
return -2;
}
logger_log(httpd->logger, LOGGER_INFO, "Initialized server socket(s)");
/* Set values correctly and create new thread */
httpd->running = 1;
httpd->joined = 0;
THREAD_CREATE(httpd->thread, httpd_thread, httpd);
MUTEX_UNLOCK(httpd->run_mutex);
return 1;
}
int
httpd_is_running(httpd_t *httpd)
{
int running;
assert(httpd);
MUTEX_LOCK(httpd->run_mutex);
running = httpd->running || !httpd->joined;
MUTEX_UNLOCK(httpd->run_mutex);
return running;
}
void
httpd_stop(httpd_t *httpd)
{
assert(httpd);
MUTEX_LOCK(httpd->run_mutex);
if (!httpd->running || httpd->joined) {
MUTEX_UNLOCK(httpd->run_mutex);
return;
}
httpd->running = 0;
MUTEX_UNLOCK(httpd->run_mutex);
THREAD_JOIN(httpd->thread);
MUTEX_LOCK(httpd->run_mutex);
httpd->joined = 1;
MUTEX_UNLOCK(httpd->run_mutex);
}