/* * http.c - API for HTTP exchanges * * Copyright (C) 2022, IOPSYS Software Solutions AB. * * Author Omar Kallel * Author Mohamed Kallel * Author Ahmed Zribi * * See LICENSE file for license related information. * */ #include #include #include #include #include "http.h" #include "log.h" #include "event.h" #include "ubus_utils.h" #include "config.h" #include "digauth.h" #include "session.h" #include "uci_utils.h" #define REALM "authenticate@cwmp" #define OPAQUE "11733b200778ce33060f31c9af70a870ba96ddd4" #define HTTP_GET_HDR_LEN 512 #define HTTP_FD_FEEDS_COUNT 10 /* Maximum number of lines to be read from HTTP header */ extern pthread_mutex_t mutex_config_load; static struct curl_slist *header_list = NULL; static CURL *curl = NULL; static bool curl_glob_init = false; void http_set_timeout(void) { if (curl) curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 1); } int icwmp_http_client_init() { if (CWMP_STRLEN(cwmp_main->conf.acs_url) == 0) return -1; CWMP_LOG(INFO, "ACS url: %s", cwmp_main->conf.acs_url); curl_global_init(CURL_GLOBAL_SSL); curl_glob_init = true; curl = curl_easy_init(); if (!curl) return -1; curl_easy_setopt(curl, CURLOPT_COOKIEFILE, ""); return 0; } void icwmp_http_client_exit(void) { if (header_list) { curl_slist_free_all(header_list); header_list = NULL; } if (curl) { /* erasing all session cookies from memory */ curl_easy_setopt(curl, CURLOPT_COOKIELIST, "SESS"); curl_easy_cleanup(curl); curl = NULL; } if (curl_glob_init) { curl_global_cleanup(); curl_glob_init = false; } } static size_t http_get_response(void *buffer, size_t size, size_t rxed, void *userp) { char *c; char **msg_in; if (userp == NULL) return 0; msg_in = (char **) userp; if (buffer == NULL) return 0; if (asprintf(&c, "%s%.*s", *msg_in, (int)(size * rxed), (char *)buffer) == -1) { FREE(*msg_in); return -1; } FREE(*msg_in); *msg_in = c; return size * rxed; } static void http_set_security_options() { curl_easy_setopt(curl, CURLOPT_USERNAME, cwmp_main->conf.acs_userid); curl_easy_setopt(curl, CURLOPT_PASSWORD, cwmp_main->conf.acs_passwd); curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC | CURLAUTH_DIGEST); if (CWMP_STRLEN(cwmp_main->conf.acs_ssl_capath) !=0 ) { curl_easy_setopt(curl, CURLOPT_CAPATH, cwmp_main->conf.acs_ssl_capath); } else if (CWMP_STRLEN(cwmp_main->conf.acs_ssl_cabundle) != 0) { curl_easy_setopt(curl, CURLOPT_CAINFO, cwmp_main->conf.acs_ssl_cabundle); } if (cwmp_main->conf.insecure_enable) { curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); } } static void http_set_connection_options() { curl_easy_setopt(curl, CURLOPT_URL, cwmp_main->conf.acs_url); curl_easy_setopt(curl, CURLOPT_TIMEOUT, HTTP_TIMEOUT); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, HTTP_TIMEOUT); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_UNRESTRICTED_AUTH, 1L); curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); curl_easy_setopt(curl, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); curl_easy_setopt(curl, CURLOPT_NOBODY, 0); curl_easy_setopt(curl, CURLOPT_IPRESOLVE, cwmp_main->net.ip_resolve); if (cwmp_main->net.use_curl_ifname && CWMP_STRLEN(cwmp_main->net.interface)) curl_easy_setopt(curl, CURLOPT_INTERFACE, cwmp_main->net.interface); } static bool valid_cookie_path(const char *cookie) { char *ptr = NULL; int count; char value[5120] = {0}; if (cookie == NULL) return false; snprintf(value, sizeof(value), "%s", cookie); /* path should be the third field */ ptr = strtok(value, "\t"); count = 1; while (ptr && count < 3) { ptr = strtok(NULL, "\t"); count = count + 1; } if (ptr == NULL) return true; /* allowed path field to remain not filled in */ if (strcmp(ptr, "TRUE") && strcmp(ptr, "FALSE")) { int i; int n = strlen(ptr); for (i = 0; i < n; i++) { switch (ptr[i]) { // ? " \ < > * | : // these characters can not be used in file or folder names // case '?': case '\\': case '<': case '>': case '*': case '|': case ':': return false; // some stupid site sends path value within ", so ignore if // first and last char of path // case '\"': if ((i != 0) && (i + 1 != n)) return false; break; // Space and point can not be the last character of a file or folder names // case ' ': case '.': if ((i + 1 == n) || (ptr[i+1] == '/')) return false; break; // two slashes can not go straight // case '/': if (i > 0 && ptr[i - 1] == '/') return false; break; } } } return true; } static void http_filter_valid_cookie() { struct curl_slist *cookies, *nc; /* get the known list of cookies */ if (CURLE_OK != curl_easy_getinfo(curl, CURLINFO_COOKIELIST, &cookies)) return; /* erasing curl's knowledge of cookies */ curl_easy_setopt(curl, CURLOPT_COOKIELIST, "ALL"); /* add the cookies having valid path */ nc = cookies; while (nc) { if (valid_cookie_path(nc->data)) curl_easy_setopt(curl, CURLOPT_COOKIELIST, nc->data); else CWMP_LOG(DEBUG, "Reject cookie (%s)", nc->data); nc = nc->next; } curl_slist_free_all(cookies); } static void http_set_header_list_options() { switch (cwmp_main->conf.compression) { case COMP_NONE: break; case COMP_GZIP: curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip"); header_list = curl_slist_append(header_list, "Content-Encoding: gzip"); break; case COMP_DEFLATE: curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "deflate"); header_list = curl_slist_append(header_list, "Content-Encoding: deflate"); break; } curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list); } static void http_set_inout_options(char *msg_out, int msg_out_len, char **msg_in) { curl_easy_setopt(curl, CURLOPT_POSTFIELDS, msg_out); if (msg_out) curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)msg_out_len); else curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, http_get_response); curl_easy_setopt(curl, CURLOPT_WRITEDATA, msg_in); } int icwmp_http_send_message(char *msg_out, int msg_out_len, char **msg_in) { unsigned char buf[sizeof(struct in6_addr)]; int tmp = 0; CURLcode res; long http_code = 0; static char ip_acs[128] = { 0 }; char *ip = NULL; char errbuf[CURL_ERROR_SIZE]; header_list = NULL; header_list = curl_slist_append(header_list, "User-Agent: iopsys-cwmp"); if (!header_list) return -1; header_list = curl_slist_append(header_list, "Content-Type: text/xml"); if (!header_list) return -1; if (cwmp_main->conf.http_disable_100continue) { header_list = curl_slist_append(header_list, "Expect:"); if (!header_list) return -1; } http_set_connection_options(); http_filter_valid_cookie(); http_set_security_options(); http_set_header_list_options(); http_set_inout_options(msg_out, msg_out_len, msg_in); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf); *msg_in = (char *)calloc(1, sizeof(char)); res = curl_easy_perform(curl); if (res != CURLE_OK) { size_t len = strlen(errbuf); if (len) { if (errbuf[len - 1] == '\n') errbuf[len - 1] = '\0'; CWMP_LOG(ERROR, "libcurl: (%d) %s", res, errbuf); } else { CWMP_LOG(ERROR, "libcurl: [%d] %s", res, curl_easy_strerror(res)); } } if (*msg_in && !strlen(*msg_in)) FREE(*msg_in); curl_easy_getinfo(curl, CURLINFO_PRIMARY_IP, &ip); if (CWMP_STRLEN(ip)) { if (ip_acs[0] == '\0' || strcmp(ip_acs, ip) != 0) { CWMP_STRNCPY(ip_acs, ip, sizeof(ip_acs)); tmp = inet_pton(AF_INET, ip, buf); if (tmp == 1) { tmp = 0; } else { tmp = inet_pton(AF_INET6, ip, buf); } if (tmp) { set_uci_path_value(VARSTATE_CONFIG, "icwmp.acs.ip6", ip_acs); } else { set_uci_path_value(VARSTATE_CONFIG, "icwmp.acs.ip", ip_acs); } // Trigger firewall to reload firewall.cwmp if (cwmp_main->cr_policy != CR_POLICY_Port_Only) { /* Flawfinder: ignore */ FILE *pp = popen(FIREWALL_CWMP, "r"); if (pp) { pclose(pp); } } } } curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); if (http_code == 204) { CWMP_LOG(INFO, "Receive HTTP 204 No Content"); } if (http_code == 415) { cwmp_main->conf.compression = COMP_NONE; goto error; } if (http_code != 200 && http_code != 204) goto error; if (header_list) { curl_slist_free_all(header_list); header_list = NULL; } if (res) goto error; return 0; error: FREE(*msg_in); if (header_list) { curl_slist_free_all(header_list); header_list = NULL; } return -1; } static void http_success_cr(void) { CWMP_LOG(INFO, "Connection Request triggering ..."); int retry = 0, rc = -1; while (rc != 0 && retry < 5) { /* Flawfinder: ignore */ FILE *pp = popen("ubus call tr069 inform", "r"); if (pp) { int status = pclose(pp); rc = WEXITSTATUS(status); } retry = retry + 1; } if (rc != 0) CWMP_LOG(ERROR, "Failed to send Inform message after 5 retry"); } static void http_cr_new_client(int client, bool service_available) { FILE *fp = NULL; char data[BUFSIZ] = {0}; char buffer[BUFSIZ] = {0}; char auth_digest_buffer[BUFSIZ] = {0}; int8_t auth_status = 0; bool auth_digest_checked = false; bool method_is_get = false; bool internal_error = false; char request_host[2049] = {0}; char cr_http_get_head[HTTP_GET_HDR_LEN] = {0}; fd_set rfds; struct timeval tv; int fd_feed = 0; int status = 0; pthread_mutex_lock(&mutex_config_load); char *username = (strlen(cwmp_main->conf.cpe_userid) != 0) ? strdup(cwmp_main->conf.cpe_userid) : NULL; char *password = (strlen(cwmp_main->conf.cpe_passwd) != 0) ? strdup(cwmp_main->conf.cpe_passwd) : NULL; char *cr_path = (strlen(cwmp_main->conf.connection_request_path) != 0) ? strdup(cwmp_main->conf.connection_request_path) : NULL; int cr_timeout = cwmp_main->conf.cr_timeout; pthread_mutex_unlock(&mutex_config_load); if (!username || !password) { // if we dont have username or password configured proceed with connecting to ACS CWMP_LOG(WARNING, "CPE username/password not defined - 503"); service_available = false; goto http_end; } snprintf(cr_http_get_head, sizeof(cr_http_get_head), "GET %s HTTP/1.1", cr_path); CWMP_MEMSET(auth_digest_buffer, 0, BUFSIZ); /* Initialize timeout of select, so that it will wait for specific time * period before timed out to receive data from client. Otherwise if client * will not send any data after a successful connection then server will * wait forever and not entertain any other connection requests */ tv.tv_sec = cr_timeout; tv.tv_usec = 0; FD_ZERO(&rfds); FD_SET(client, &rfds); /* Make FD non blocking, so that no operation on FD will block and make the * server halt forever. */ if (fcntl(client, F_SETFL, O_NONBLOCK) < 0) { CWMP_LOG(ERROR, "Failed to set NONBLOCK"); goto http_end; } fp = fdopen(client, "r+"); if (fp == NULL) { CWMP_LOG(ERROR, "Failed to open client socket"); goto http_end; } CWMP_LOG(DEBUG, "Processing CR service_available %d, client %d", service_available, client); bool read_done = false; /* Perform read from FD until all required data are collected or * HTTP_FD_FEEDS_COUNT number of read operation has been performed. * So that flooding of data not blocks the server. */ while (!read_done && fd_feed < HTTP_FD_FEEDS_COUNT) { status = select(client+1, &rfds, NULL, NULL, &tv); if (status <= 0) { CWMP_LOG(INFO, "TIMEOUT occurred or select failed"); break; } /* Check how many bytes available in the FD */ int read_bytes = 0; if (ioctl(client, FIONREAD, &read_bytes) == -1) { CWMP_LOG(INFO, "ioctl failed"); break; } if (read_bytes < 1) { /* It means the client has been disconnected */ CWMP_LOG(INFO, "client disconnected"); break; } /* Read upto the number of bytes or HTTP_FD_FEEDS_COUNT number of * read operation whichever is earlier, to avoid halt on data flooding */ while (read_bytes > 0 && fd_feed < HTTP_FD_FEEDS_COUNT) { if (fgets(buffer, sizeof(buffer), fp) == NULL) { CWMP_LOG(INFO, "No more data from FD"); break; } size_t buf_len = strlen(buffer); read_bytes = read_bytes - buf_len; fd_feed = fd_feed + 1; /* Check if a whole line has been read, since a non blocking FD so * possible to have fewer bytes than its in whole line based on the * availability of data in the FD */ if (buffer[buf_len - 1] != '\n') { /* there should be more data in current line, store the current * data and wait for next read if max data length not exceeded */ size_t avail_space = (size_t)(sizeof(data) - strlen(data)); if (buf_len < avail_space) { CWMP_LOG(DEBUG, "Continue buffer overrun %d=>%d", buf_len, avail_space); snprintf(data, BUFSIZ, "%s", buffer); continue; } } else { /* A whole line has been read, so store it if max data length is * not exceeded and process the data */ size_t avail_space = (size_t)(sizeof(data) - strlen(data)); if (buf_len < avail_space) { snprintf(data, BUFSIZ, "%s", buffer); } } strip_lead_trail_char(data, '\n'); strip_lead_trail_char(data, '\r'); if (strlen(data) == 0) { /* empty line reached */ CWMP_LOG(DEBUG, "Empty line found in packet"); read_done = true; break; } if (fd_feed == 1 && (strstr(data, "GET ") == NULL || strstr(data, "HTTP/1.1") == NULL)) { CWMP_LOG(INFO, "GET not found at initial:: %s", data); read_done = true; break; } CWMP_LOG(DEBUG, "Data:: (%s)", data); if (strstr(data, "GET ") != NULL && strstr(data, "HTTP/1.1") != NULL) { // check if extra url parameter then ignore extra params int j = 0; bool ignore = false; char rec_http_get_head[HTTP_GET_HDR_LEN] = {0}; CWMP_MEMSET(rec_http_get_head, 0, HTTP_GET_HDR_LEN); for (size_t i = 0; i < strlen(data) && j < (HTTP_GET_HDR_LEN - 1); i++) { if (data[i] == '?') ignore = true; if (data[i] == ' ') ignore = false; if (ignore == false) { rec_http_get_head[j] = data[i]; j++; } } if (!strncasecmp(rec_http_get_head, cr_http_get_head, strlen(cr_http_get_head))) { method_is_get = true; } else { CWMP_LOG(WARNING, "Get header [%s] mismatch[%s]", rec_http_get_head, cr_http_get_head); } } if (!strncasecmp(data, "Authorization: Digest ", strlen("Authorization: Digest "))) { auth_digest_checked = true; CWMP_STRNCPY(auth_digest_buffer, data, BUFSIZ); } if (strncasecmp(data, "Host: ", strlen("Host: ")) == 0 && strlen(data) > strlen("Host: ")) { snprintf(request_host, sizeof(request_host), "http://%s", data + strlen("Host: ")); } CWMP_MEMSET(data, 0, sizeof(data)); } } if (!service_available || !method_is_get) { goto http_end; } CWMP_LOG(DEBUG, "Received host: (%s)", request_host); int auth_check = validate_http_digest_auth("GET", cr_path, auth_digest_buffer + strlen("Authorization: Digest "), REALM, username, password, cwmp_main->conf.session_timeout, request_host); if (auth_check == -1) { /* invalid nonce */ CWMP_LOG(INFO, "Auth check failed for incoming CR"); internal_error = true; goto http_end; } if (auth_digest_checked && auth_check == 1) auth_status = 1; else auth_status = 0; http_end: if (fp) { fflush(fp); } if (!service_available || !method_is_get) { CWMP_LOG(INFO, "Receive Connection Request: Return 503 Service Unavailable"); if (fp) { fputs("HTTP/1.1 503 Service Unavailable\r\n", fp); fputs("Connection: close\r\n", fp); fputs("Content-Length: 0\r\n", fp); fputs("\r\n", fp); fclose(fp); } close(client); } else if (auth_status) { CWMP_LOG(INFO, "Receive Connection Request: success authentication"); if (fp) { fputs("HTTP/1.1 200 OK\r\n", fp); fputs("Connection: close\r\n", fp); fputs("Content-Length: 0\r\n", fp); fputs("\r\n", fp); fclose(fp); } close(client); http_success_cr(); } else if (internal_error) { CWMP_LOG(INFO, "Receive Connection Request: Return 500 Internal Error"); if (fp) { fputs("HTTP/1.1 500 Internal Server Error\r\n", fp); fputs("Connection: close\r\n", fp); fputs("Content-Length: 0\r\n", fp); fputs("\r\n", fp); fclose(fp); } close(client); } else { CWMP_LOG(INFO, "Receive Connection Request: Return 401 Unauthorized"); if (fp) { fputs("HTTP/1.1 401 Unauthorized\r\n", fp); fputs("Connection: close\r\n", fp); http_authentication_failure_resp(fp, "GET", cr_path, REALM, OPAQUE); fputs("\r\n", fp); fputs("\r\n", fp); fclose(fp); } close(client); } FREE(username); FREE(password); FREE(cr_path); } void icwmp_http_server_init(void) { struct sockaddr_in6 server = { 0 }; unsigned short cr_port; unsigned short prev_cr_port = (unsigned short)(cwmp_main->conf.connection_request_port); for (;;) { cr_port = (unsigned short)(cwmp_main->conf.connection_request_port); unsigned short i = (DEFAULT_CONNECTION_REQUEST_PORT == cr_port) ? 1 : 0; //Create socket if (cwmp_stop) return; cwmp_main->cr_socket_desc = socket(AF_INET6, SOCK_STREAM, 0); if (cwmp_main->cr_socket_desc == -1) { CWMP_LOG(ERROR, "Could not open server socket for Connection Requests, Error no is : %d, Error description is : %s", errno, strerror(errno)); sleep(1); continue; } fcntl(cwmp_main->cr_socket_desc, F_SETFD, fcntl(cwmp_main->cr_socket_desc, F_GETFD) | FD_CLOEXEC); int reusaddr = 1; if (setsockopt(cwmp_main->cr_socket_desc, SOL_SOCKET, SO_REUSEADDR, &reusaddr, sizeof(int)) < 0) { CWMP_LOG(WARNING, "setsockopt(SO_REUSEADDR) failed"); } //Prepare the sockaddr_in structure server.sin6_family = AF_INET6; server.sin6_addr = in6addr_any; for (;; i++) { if (cwmp_stop) return; server.sin6_port = htons(cr_port); //Bind if (bind(cwmp_main->cr_socket_desc, (struct sockaddr *)&server, sizeof(server)) < 0) { //print the error message CWMP_LOG(ERROR, "Could not bind server socket on the port %d, Error no is : %d, Error description is : %s", cr_port, errno, strerror(errno)); cr_port = DEFAULT_CONNECTION_REQUEST_PORT + i; CWMP_LOG(INFO, "Trying to use another connection request port: %d", cr_port); continue; } break; } break; } if (cr_port != prev_cr_port) { char cr_port_str[6]; snprintf(cr_port_str, 6, "%hu", cr_port); cr_port_str[5] = '\0'; set_uci_path_value(NULL, "cwmp.cpe.port", cr_port_str); /* Flawfinder: ignore */ FILE *pp = popen(FIREWALL_CWMP, "r"); if (pp) { pclose(pp); } connection_request_port_value_change(cr_port); } CWMP_LOG(INFO, "Connection Request server initiated with the port: %d", cr_port); } void icwmp_http_server_listen(void) { int c; int cr_request = 0; time_t restrict_start_time = 0; struct sockaddr_in6 client; //Listen listen(cwmp_main->cr_socket_desc, 5); //Accept and incoming connection c = sizeof(struct sockaddr_in); do { if (cwmp_stop) return; int client_sock = accept(cwmp_main->cr_socket_desc, (struct sockaddr *)&client, (socklen_t *)&c); if (client_sock < 0) { CWMP_LOG(ERROR, "Could not accept connections for Connection Request!"); shutdown(cwmp_main->cr_socket_desc, SHUT_RDWR); icwmp_http_server_init(); listen(cwmp_main->cr_socket_desc, 5); cr_request = 0; restrict_start_time = 0; continue; } bool service_available; time_t current_time; current_time = time(NULL); service_available = true; if ((restrict_start_time == 0) || ((current_time - restrict_start_time) > CONNECTION_REQUEST_RESTRICT_PERIOD)) { restrict_start_time = current_time; cr_request = 1; } else { cr_request++; if (cr_request > CONNECTION_REQUEST_RESTRICT_REQUEST) { CWMP_LOG(WARNING, "Too many connection requests %d, block CR for %d Sec", cr_request, CONNECTION_REQUEST_RESTRICT_PERIOD) restrict_start_time = current_time; service_available = false; } } http_cr_new_client(client_sock, service_available); } while (1); }