Rework RTSP parser to fix many spec compliance issues

This commit is contained in:
Cameron Gutman
2026-10-14 17:11:45 -05:00
parent 5ec839727a
commit aeba988e0a
2 changed files with 203 additions and 101 deletions
-2
View File
@@ -6,8 +6,6 @@
#define TYPE_REQUEST 0
#define TYPE_RESPONSE 1
#define TOKEN_OPTION 0
#define RTSP_ERROR_SUCCESS 0
#define RTSP_ERROR_NO_MEMORY -1
#define RTSP_ERROR_MALFORMED -2
+203 -99
View File
@@ -1,11 +1,35 @@
#include "Platform.h"
#include "Rtsp.h"
#include <limits.h>
#include <errno.h>
// Case-insensitive ASCII string comparison for RTSP header names
static int rtspStrCaseCmp(const char* s1, const char* s2) {
while (*s1 && *s2) {
char c1 = (*s1 >= 'A' && *s1 <= 'Z') ? (*s1 + ('a' - 'A')) : *s1;
char c2 = (*s2 >= 'A' && *s2 <= 'Z') ? (*s2 + ('a' - 'A')) : *s2;
if (c1 != c2) {
return (unsigned char)c1 - (unsigned char)c2;
}
s1++;
s2++;
}
return (unsigned char)*s1 - (unsigned char)*s2;
}
// Check if String s begins with the given prefix
static bool startsWith(const char* s, const char* prefix) {
return strncmp(s, prefix, strlen(prefix)) == 0;
}
// Trims trailing spaces and tabs from s in place
static void trimTrailingWhitespace(char* s) {
size_t len = strlen(s);
while (len > 0 && (s[len - 1] == ' ' || s[len - 1] == '\t')) {
s[--len] = '\0';
}
}
// Gets the length of the message
static int getMessageLength(PRTSP_MESSAGE msg) {
POPTION_ITEM current;
@@ -51,11 +75,91 @@ static int getMessageLength(PRTSP_MESSAGE msg) {
return (int)count;
}
// Check if we're at the end of the message portion marked by \r\n\r\n
static bool checkMessageEnded(char* endCheck, char* messageBuffer, int length, char** payload) {
if (endCheck <= messageBuffer + length) {
int remaining = (int)((messageBuffer + length) - endCheck);
// RTSP over ENet doesn't always have the second CRLF for some reason
if (remaining == 0) {
return true;
}
// Previous `if` statement already handle situation when two bytes are missing.
// This is the workaround for the same problem, but for only one byte missing.
// Sometimes on android emulators last byte or two bytes are missing.
// This is link to this bug in Goggle Issue Tracker:
// https://issuetracker.google.com/issues/150758736?pli=1
else if (remaining == 1 && endCheck[0] == '\r') {
return true;
}
else if (remaining >= 2 && memcmp(endCheck, "\r\n", 2) == 0) {
// We've encountered the end of the message - mark it thus
// The payload is the remainder of messageBuffer. If none, then payload = null
if (remaining > 2) {
*payload = &endCheck[2];
}
return true;
}
else if (remaining >= 1 && endCheck[0] == '\n') {
// End of message marked by bare LF
if (remaining > 1) {
*payload = &endCheck[1];
}
return true;
}
}
return false;
}
// Terminates current line at lineEnd and returns a pointer to the start of the next line
static char* getNextLine(char* lineEnd, char* bufferEnd) {
char origEnd = *lineEnd;
*lineEnd = '\0';
char* nextLine = lineEnd + 1;
if (origEnd == '\r' && nextLine < bufferEnd && *nextLine == '\n') {
nextLine++;
}
return nextLine;
}
// Skips spaces and tabs, returning a pointer to the first non-whitespace character
static char* skipSpaces(char* s) {
while (*s == ' ' || *s == '\t') {
s++;
}
return s;
}
// Extracts the next whitespace-delimited token from *ptr, null-terminates it, and advances *ptr
static char* getNextToken(char** ptr) {
char* token = skipSpaces(*ptr);
if (*token == '\0') {
*ptr = token;
return NULL;
}
char* end = token;
while (*end != '\0' && *end != ' ' && *end != '\t') {
end++;
}
if (*end != '\0') {
*end = '\0';
*ptr = skipSpaces(end + 1);
}
else {
*ptr = end;
}
return token;
}
// Given an RTSP message string rtspMessage, parse it into an RTSP_MESSAGE struct msg
int parseRtspMessage(PRTSP_MESSAGE msg, char* rtspMessage, int length) {
char* token;
char* protocol;
char* endCheck;
char* target;
char* statusStr;
char* command;
@@ -63,7 +167,6 @@ int parseRtspMessage(PRTSP_MESSAGE msg, char* rtspMessage, int length) {
char flag;
bool messageEnded = false;
char* strtokCtx = NULL;
char* payload = NULL;
char* opt = NULL;
int statusCode = 0;
@@ -72,11 +175,9 @@ int parseRtspMessage(PRTSP_MESSAGE msg, char* rtspMessage, int length) {
POPTION_ITEM options = NULL;
POPTION_ITEM newOpt;
// Delimeter sets for strtok()
char* delim = " \r\n";
char* end = "\r\n";
char* optDelim = " :\r\n";
char typeFlag = TOKEN_OPTION;
if (length <= 0) {
return RTSP_ERROR_MALFORMED;
}
// Put the raw message into a string we can use
char* messageBuffer = malloc(length + 1);
@@ -87,119 +188,115 @@ int parseRtspMessage(PRTSP_MESSAGE msg, char* rtspMessage, int length) {
memcpy(messageBuffer, rtspMessage, length);
// The payload logic depends on a null-terminator at the end
messageBuffer[length] = 0;
messageBuffer[length] = '\0';
// Get the first token of the message
token = strtok_r(messageBuffer, delim, &strtokCtx);
if (token == NULL) {
char* ptr = messageBuffer;
char* bufferEnd = messageBuffer + length;
// Find the end of the start line
char* lineEnd = strpbrk(ptr, "\r\n");
if (lineEnd == NULL) {
exitCode = RTSP_ERROR_MALFORMED;
goto ExitFailure;
}
char* nextLine = getNextLine(lineEnd, bufferEnd);
ptr = skipSpaces(ptr);
// The message is a response
if (startsWith(token, "RTSP")) {
if (startsWith(ptr, "RTSP/")) {
flag = TYPE_RESPONSE;
// The current token is the protocol
protocol = token;
// Get the status code
token = strtok_r(NULL, delim, &strtokCtx);
if (token == NULL) {
exitCode = RTSP_ERROR_MALFORMED;
goto ExitFailure;
}
statusCode = atoi(token);
// Get the status string
statusStr = strtok_r(NULL, end, &strtokCtx);
if (statusStr == NULL) {
protocol = getNextToken(&ptr);
char* codeStr = getNextToken(&ptr);
if (codeStr == NULL) {
exitCode = RTSP_ERROR_MALFORMED;
goto ExitFailure;
}
// Request fields - we don't care about them here
char* endPtr;
errno = 0;
long val = strtol(codeStr, &endPtr, 10);
if (endPtr == codeStr || *endPtr != '\0' || errno == ERANGE || val <= 0 || val > INT_MAX) {
exitCode = RTSP_ERROR_MALFORMED;
goto ExitFailure;
}
statusCode = (int)val;
statusStr = ptr;
trimTrailingWhitespace(statusStr);
target = NULL;
command = NULL;
}
// The message is a request
else {
flag = TYPE_REQUEST;
command = token;
target = strtok_r(NULL, delim, &strtokCtx);
if (target == NULL) {
command = getNextToken(&ptr);
target = getNextToken(&ptr);
protocol = getNextToken(&ptr);
if (target == NULL || protocol == NULL) {
exitCode = RTSP_ERROR_MALFORMED;
goto ExitFailure;
}
protocol = strtok_r(NULL, delim, &strtokCtx);
if (protocol == NULL) {
exitCode = RTSP_ERROR_MALFORMED;
goto ExitFailure;
}
// Response field - we don't care about it here
statusStr = NULL;
}
if (strcmp(protocol, "RTSP/1.0")) {
exitCode = RTSP_ERROR_MALFORMED;
goto ExitFailure;
}
ptr = nextLine;
// Parse remaining options
while (token != NULL)
{
token = strtok_r(NULL, typeFlag == TOKEN_OPTION ? optDelim : end, &strtokCtx);
if (token != NULL) {
if (typeFlag == TOKEN_OPTION) {
opt = token;
}
// The token is content
else {
// Create a new node containing the option and content
newOpt = (POPTION_ITEM)malloc(sizeof(OPTION_ITEM));
if (newOpt == NULL) {
exitCode = RTSP_ERROR_NO_MEMORY;
goto ExitFailure;
}
newOpt->flags = 0;
newOpt->option = opt;
newOpt->content = token + 1; // Skip the protocol defined blank space
newOpt->next = NULL;
insertOption(&options, newOpt);
// Check if we're at the end of the message portion marked by \r\n\r\n
// endCheck points to the remainder of messageBuffer after the token
endCheck = &token[0] + strlen(token) + 1;
// See if we've hit the end of the message. The first \r is missing because it's been tokenized
if (startsWith(endCheck, "\n") && endCheck[1] == '\0') {
// RTSP over ENet doesn't always have the second CRLF for some reason
messageEnded = true;
break;
}
else if (startsWith(endCheck, "\n\r") && endCheck[2] == '\0') {
// Previous `if` statement already handle situation when two bytes are missing.
// This is the workaround for the same problem, but for only one byte missing.
// Sometimes on android emulators last byte or two bytes are missing.
// This is link to this bug in Goggle Issue Tracker:
// https://issuetracker.google.com/issues/150758736?pli=1
messageEnded = true;
break;
}
else if (startsWith(endCheck, "\n\r\n")) {
// We've encountered the end of the message - mark it thus
messageEnded = true;
// The payload is the remainder of messageBuffer. If none, then payload = null
if (endCheck[3] != '\0')
payload = &endCheck[3];
break;
}
}
while (ptr <= bufferEnd) {
if (checkMessageEnded(ptr, messageBuffer, length, &payload)) {
messageEnded = true;
break;
}
typeFlag ^= 1; // flip the flag
lineEnd = strpbrk(ptr, "\r\n");
if (lineEnd == NULL) {
break;
}
nextLine = getNextLine(lineEnd, bufferEnd);
char* colon = strchr(ptr, ':');
if (colon == NULL) {
exitCode = RTSP_ERROR_MALFORMED;
goto ExitFailure;
}
*colon = '\0';
opt = skipSpaces(ptr);
trimTrailingWhitespace(opt);
if (*opt == '\0') {
exitCode = RTSP_ERROR_MALFORMED;
goto ExitFailure;
}
char* val = skipSpaces(colon + 1);
trimTrailingWhitespace(val);
// Create a new node containing the option and content
newOpt = (POPTION_ITEM)malloc(sizeof(OPTION_ITEM));
if (newOpt == NULL) {
exitCode = RTSP_ERROR_NO_MEMORY;
goto ExitFailure;
}
newOpt->flags = 0;
newOpt->option = opt;
newOpt->content = val;
newOpt->next = NULL;
insertOption(&options, newOpt);
ptr = nextLine;
}
// If we never encountered the double CRLF, then the message is malformed!
if (!messageEnded) {
exitCode = RTSP_ERROR_MALFORMED;
@@ -208,11 +305,14 @@ int parseRtspMessage(PRTSP_MESSAGE msg, char* rtspMessage, int length) {
// Get sequence number as an integer
sequence = getOptionContent(options, "CSeq");
sequenceNum = SEQ_INVALID;
if (sequence != NULL) {
sequenceNum = atoi(sequence);
}
else {
sequenceNum = SEQ_INVALID;
char* endPtr;
errno = 0;
long val = strtol(sequence, &endPtr, 10);
if (endPtr != sequence && *endPtr == '\0' && errno != ERANGE && val >= 0 && val <= INT_MAX) {
sequenceNum = (int)val;
}
}
// Package the new parsed message into the struct
if (flag == TYPE_REQUEST) {
@@ -270,7 +370,7 @@ char* getOptionContent(POPTION_ITEM optionsHead, char* option) {
POPTION_ITEM current = optionsHead;
while (current != NULL) {
// Check if current node is what we're looking for
if (!strcmp(current->option, option)) {
if (!rtspStrCaseCmp(current->option, option)) {
return current->content;
}
current = current->next;
@@ -282,10 +382,10 @@ char* getOptionContent(POPTION_ITEM optionsHead, char* option) {
// Adds new option opt to the struct's option list
void insertOption(POPTION_ITEM* optionsHead, POPTION_ITEM opt) {
POPTION_ITEM current = *optionsHead;
opt->next = NULL;
// Empty options list
if (*optionsHead == NULL) {
opt->next = NULL;
*optionsHead = opt;
return;
}
@@ -299,7 +399,7 @@ void insertOption(POPTION_ITEM* optionsHead, POPTION_ITEM opt) {
}
// Check for duplicate option; if so, replace the option currently there
if (!strcmp(current->option, opt->option)) {
if (!rtspStrCaseCmp(current->option, opt->option)) {
if (current->flags & FLAG_ALLOCATED_OPTION_FIELDS) {
free(current->option);
free(current->content);
@@ -311,6 +411,7 @@ void insertOption(POPTION_ITEM* optionsHead, POPTION_ITEM opt) {
return;
}
if (current->next == NULL) {
opt->next = NULL;
current->next = opt;
return;
}
@@ -333,7 +434,7 @@ void freeOptionList(POPTION_ITEM optionsHead) {
}
}
bool appendString(char* dest, int* destOffset, int* destRemainingLength, char* source) {
bool appendString(char* dest, int* destOffset, int* destRemainingLength, const char* source) {
int ret = snprintf(&dest[*destOffset], *destRemainingLength, "%s", source);
if (ret < 0 || ret >= *destRemainingLength) {
LC_ASSERT(false);
@@ -417,6 +518,9 @@ char* serializeRtspMessage(PRTSP_MESSAGE msg, int* serializedLength) {
*serializedLength = offset;
}
// Ensure null-termination as reserved by getMessageLength
serializedMessage[*serializedLength] = '\0';
return serializedMessage;
fail: