494 lines
16 KiB
C++
494 lines
16 KiB
C++
/**
|
|
* @file kHttpServerResponse.cpp
|
|
*
|
|
* @internal
|
|
* Copyright (C) 2013-2021 by LMI Technologies Inc.
|
|
* Licensed under the MIT License.
|
|
* Redistributed files must retain the above copyright notice.
|
|
*/
|
|
#include <kApi/Io/kHttpServerResponse.h>
|
|
#include <kApi/Data/kMap.h>
|
|
#include <kApi/Data/kString.h>
|
|
#include <kApi/Io/kHttpServer.h>
|
|
#include <kApi/Io/kHttpServerChannel.h>
|
|
#include <kApi/Io/kTcpClient.h>
|
|
|
|
kBeginEnumEx(k, kHttpStatus)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_CONTINUE)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_SWITCHING_PROTOCOLS)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_OK)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_CREATED)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_ACCEPTED)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_NON_AUTHORITATIVE)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_NO_CONTENT)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_RESET_CONTENT)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_PARTIAL_CONTENT)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_MULTIPLE_CHOICES)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_MOVED_PERMANENTLY)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_FOUND)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_SEE_OTHER)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_NOT_MODIFIED)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_USE_PROXY)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_TEMPORARY_REDIRECT)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_BAD_REQUEST)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_UNAUTHORIZED)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_PAYMENT_REQUIRED)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_FORBIDDEN)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_NOT_FOUND)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_METHOD_NOT_ALLOWED)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_NOT_ACCEPTABLE)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_PROXY_AUTH_REQUIRED)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_REQUEST_TIMEOUT)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_CONFLICT)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_GONE)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_LENGTH_REQUIRED)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_PRECONDITION_FAILED)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_REQUEST_ENTITY_SIZE)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_REQUEST_URI_SIZE)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_UNSUPPORTED_MEDIA_TYPE)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_INVALID_RANGE)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_EXPECTATION_FAILED)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_INTERNAL_SERVER_ERROR)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_NOT_IMPLEMENTED)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_BAD_GATEWAY)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_SERVICE_UNAVAILABLE)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_GATEWAY_TIMEOUT)
|
|
kAddEnumerator(kHttpStatus, kHTTP_STATUS_UNSUPPORTED_VERSION)
|
|
kEndEnumEx()
|
|
|
|
kBeginClassEx(k, kHttpServerResponse)
|
|
kAddPrivateVMethod(kHttpServerResponse, kObject, VRelease)
|
|
kEndClassEx()
|
|
|
|
kFx(kStatus) xkHttpServerResponse_Construct(kHttpServerResponse* response, kHttpServerChannel channel, kAlloc allocator)
|
|
{
|
|
kAlloc alloc = kAlloc_Fallback(allocator);
|
|
kType type = kTypeOf(kHttpServerResponse);
|
|
kStatus status;
|
|
|
|
kCheck(kAlloc_GetObject(alloc, type, response));
|
|
|
|
if (!kSuccess(status = xkHttpServerResponse_Init(*response, type, channel, alloc)))
|
|
{
|
|
kAlloc_FreeRef(alloc, response);
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
kFx(kStatus) xkHttpServerResponse_Init(kHttpServerResponse response, kType type, kHttpServerChannel channel, kAlloc alloc)
|
|
{
|
|
kObjR(kHttpServerResponse, response);
|
|
kStatus status;
|
|
|
|
kCheck(kObject_Init(response, type, alloc));
|
|
|
|
obj->channel = channel;
|
|
obj->client = xkHttpServerChannel_Client(channel);
|
|
obj->headers = kNULL;
|
|
obj->version = 0;
|
|
obj->status = 0;
|
|
obj->reason = kNULL;
|
|
obj->shouldClose = kFALSE;
|
|
obj->isChunkCoded = kFALSE;
|
|
obj->contentLength = 0;
|
|
obj->messageStarted = kFALSE;
|
|
obj->chunkIndex = 0;
|
|
obj->line = kNULL;
|
|
|
|
kTry
|
|
{
|
|
kTest(kMap_Construct(&obj->headers, kTypeOf(kString), kTypeOf(kString), 0, alloc));
|
|
|
|
kTest(kString_Construct(&obj->reason, kNULL, alloc));
|
|
kTest(kString_Construct(&obj->line, kNULL, alloc));
|
|
}
|
|
kCatch(&status)
|
|
{
|
|
xkHttpServerResponse_VRelease(response);
|
|
kEndCatch(status);
|
|
}
|
|
|
|
return kOK;
|
|
}
|
|
|
|
kFx(kStatus) xkHttpServerResponse_VRelease(kHttpServerResponse response)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
|
|
kCheck(kObject_Dispose(obj->headers));
|
|
|
|
kCheck(kObject_Destroy(obj->reason));
|
|
kCheck(kObject_Destroy(obj->line));
|
|
|
|
kCheck(kObject_VRelease(response));
|
|
|
|
return kOK;
|
|
}
|
|
|
|
kFx(kStatus) xkHttpServerResponse_Begin(kHttpServerResponse response)
|
|
{
|
|
return xkHttpServerResponse_Clear(response);
|
|
}
|
|
|
|
kFx(kStatus) xkHttpServerResponse_Clear(kHttpServerResponse response)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
|
|
kCheck(kMap_Purge(obj->headers));
|
|
|
|
kCheck(kString_Clear(obj->reason));
|
|
kCheck(kString_Clear(obj->line));
|
|
|
|
obj->version = kVersion_Create(1, 1, 0, 0);
|
|
obj->status = kHTTP_STATUS_OK;
|
|
obj->contentLength = -1;
|
|
obj->isChunkCoded = kFALSE;
|
|
obj->shouldClose = kFALSE;
|
|
obj->messageStarted = kFALSE;
|
|
obj->chunkIndex = 0;
|
|
|
|
return kOK;
|
|
}
|
|
|
|
kFx(kStatus) kHttpServerResponse_SetVersion(kHttpServerResponse response, kVersion version)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
|
|
obj->version = version;
|
|
|
|
return kOK;
|
|
}
|
|
|
|
kFx(kStatus) kHttpServerResponse_SetStatus(kHttpServerResponse response, kHttpStatus status)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
|
|
obj->status = status;
|
|
|
|
return kOK;
|
|
}
|
|
|
|
kFx(kStatus) kHttpServerResponse_SetReason(kHttpServerResponse response, const kChar* reason)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
|
|
return kString_Set(obj->reason, reason);
|
|
}
|
|
|
|
kFx(kStatus) xkHttpServerResponse_SetContentLength(kHttpServerResponse response, k64u length)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
|
|
obj->contentLength = (k64s)length;
|
|
|
|
return kOK;
|
|
}
|
|
|
|
kFx(kStatus) xkHttpServerResponse_EnableChunkCoding(kHttpServerResponse response, kBool enabled)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
|
|
obj->isChunkCoded = enabled;
|
|
|
|
return kOK;
|
|
}
|
|
|
|
kFx(kStatus) kHttpServerResponse_AddHeader(kHttpServerResponse response, const kChar* name, const kChar* value)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
kMap headerMap = obj->headers;
|
|
kMapItem headerItem = kNULL;
|
|
kString nameStr = kNULL;
|
|
kString valueStr = kNULL;
|
|
|
|
kTry
|
|
{
|
|
kTest(kString_Construct(&nameStr, name, kObject_Alloc(response)));
|
|
kTest(xkHttpServerChannel_NormalizeHeaderCaps(kString_Chars(nameStr)));
|
|
|
|
if (kSuccess(kMap_FindItemT(headerMap, &nameStr, &headerItem)))
|
|
{
|
|
kString existingValue = kMap_ValueAsT(headerMap, headerItem, kString);
|
|
|
|
kTest(kString_Addf(existingValue, ",%s", value));
|
|
}
|
|
else
|
|
{
|
|
kTest(kString_Construct(&valueStr, value, kObject_Alloc(response)));
|
|
|
|
kTest(kMap_AddT(headerMap, &nameStr, &valueStr));
|
|
nameStr = valueStr = kNULL;
|
|
}
|
|
}
|
|
kFinally
|
|
{
|
|
kCheck(kObject_Destroy(nameStr));
|
|
kCheck(kObject_Destroy(valueStr));
|
|
|
|
kEndFinally();
|
|
}
|
|
|
|
return kOK;
|
|
}
|
|
|
|
kFx(kStatus) kHttpServerResponse_SetHeader(kHttpServerResponse response, const kChar* name, const kChar* value)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
kMap headerMap = obj->headers;
|
|
kMapItem headerItem = kNULL;
|
|
kString nameStr = kNULL;
|
|
kString valueStr = kNULL;
|
|
|
|
kTry
|
|
{
|
|
kTest(kString_Construct(&nameStr, name, kObject_Alloc(response)));
|
|
kTest(xkHttpServerChannel_NormalizeHeaderCaps(kString_Chars(nameStr)));
|
|
|
|
if (kSuccess(kMap_FindItemT(headerMap, &nameStr, &headerItem)))
|
|
{
|
|
kString existingValue = kMap_ValueAsT(headerMap, headerItem, kString);
|
|
|
|
kTest(kString_Set(existingValue, value));
|
|
}
|
|
else
|
|
{
|
|
kTest(kString_Construct(&valueStr, value, kObject_Alloc(response)));
|
|
|
|
kTest(kMap_AddT(headerMap, &nameStr, &valueStr));
|
|
nameStr = valueStr = kNULL;
|
|
}
|
|
}
|
|
kFinally
|
|
{
|
|
kCheck(kObject_Destroy(nameStr));
|
|
kCheck(kObject_Destroy(valueStr));
|
|
|
|
kEndFinally();
|
|
}
|
|
|
|
return kOK;
|
|
}
|
|
|
|
kFx(kStatus) xkHttpServerResponse_BeginWriteMessage(kHttpServerResponse response)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
|
|
obj->messageStarted = kTRUE;
|
|
|
|
kCheck(xkHttpServerResponse_FormatStatusLine(response));
|
|
|
|
kCheck(xkHttpServerResponse_AddKnownHeaders(response));
|
|
kCheck(kHttpServerResponse_FormatHeaders(response, obj->headers));
|
|
|
|
return kOK;
|
|
}
|
|
|
|
kFx(kStatus) xkHttpServerResponse_AddKnownHeaders(kHttpServerResponse response)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
kText128 buffer;
|
|
|
|
if (obj->contentLength >= 0)
|
|
{
|
|
kCheck(kStrPrintf(buffer, kCountOf(buffer), "%llu", obj->contentLength));
|
|
kCheck(kHttpServerResponse_SetHeader(response, "Content-Length", buffer));
|
|
}
|
|
|
|
if (obj->isChunkCoded)
|
|
{
|
|
kCheck(kHttpServerResponse_AddHeader(response, "Transfer-Encoding", "chunked"));
|
|
}
|
|
|
|
if (obj->shouldClose)
|
|
{
|
|
kCheck(kHttpServerResponse_AddHeader(response, "Connection", "close"));
|
|
}
|
|
|
|
return kOK;
|
|
}
|
|
|
|
const kChar* xkHttpServerResponse_DefaultReason(kHttpServerResponse response, kHttpStatus status)
|
|
{
|
|
switch (status)
|
|
{
|
|
case kHTTP_STATUS_CONTINUE: return "Continue";
|
|
case kHTTP_STATUS_SWITCHING_PROTOCOLS: return "Switching protocols";
|
|
case kHTTP_STATUS_OK: return "OK";
|
|
case kHTTP_STATUS_CREATED: return "Created";
|
|
case kHTTP_STATUS_ACCEPTED: return "Accepted";
|
|
case kHTTP_STATUS_NON_AUTHORITATIVE: return "Non-authoritative information";
|
|
case kHTTP_STATUS_NO_CONTENT: return "No content";
|
|
case kHTTP_STATUS_RESET_CONTENT: return "Reset content";
|
|
case kHTTP_STATUS_PARTIAL_CONTENT: return "Partial content";
|
|
case kHTTP_STATUS_MULTIPLE_CHOICES: return "Multiple choices";
|
|
case kHTTP_STATUS_MOVED_PERMANENTLY: return "Moved permanently";
|
|
case kHTTP_STATUS_FOUND: return "Found";
|
|
case kHTTP_STATUS_SEE_OTHER: return "See other";
|
|
case kHTTP_STATUS_NOT_MODIFIED: return "Not modified";
|
|
case kHTTP_STATUS_USE_PROXY: return "Use proxy";
|
|
case kHTTP_STATUS_TEMPORARY_REDIRECT: return "Temporary redirect";
|
|
case kHTTP_STATUS_BAD_REQUEST: return "Bad request";
|
|
case kHTTP_STATUS_UNAUTHORIZED: return "Unauthorized";
|
|
case kHTTP_STATUS_PAYMENT_REQUIRED: return "Payment required";
|
|
case kHTTP_STATUS_FORBIDDEN: return "Forbidden";
|
|
case kHTTP_STATUS_NOT_FOUND: return "Not found";
|
|
case kHTTP_STATUS_METHOD_NOT_ALLOWED: return "Method not allowed";
|
|
case kHTTP_STATUS_NOT_ACCEPTABLE: return "Not acceptable";
|
|
case kHTTP_STATUS_PROXY_AUTH_REQUIRED: return "Proxy authentication required";
|
|
case kHTTP_STATUS_REQUEST_TIMEOUT: return "Request timeout";
|
|
case kHTTP_STATUS_CONFLICT: return "Conflict";
|
|
case kHTTP_STATUS_GONE: return "Gone";
|
|
case kHTTP_STATUS_LENGTH_REQUIRED: return "Length required";
|
|
case kHTTP_STATUS_PRECONDITION_FAILED: return "Precondition failed";
|
|
case kHTTP_STATUS_REQUEST_ENTITY_SIZE: return "Request entity too large";
|
|
case kHTTP_STATUS_REQUEST_URI_SIZE: return "Request URI size too large";
|
|
case kHTTP_STATUS_UNSUPPORTED_MEDIA_TYPE: return "Unsupported media type";
|
|
case kHTTP_STATUS_INVALID_RANGE: return "Requested range not satisfiable";
|
|
case kHTTP_STATUS_EXPECTATION_FAILED: return "Expectation failed";
|
|
case kHTTP_STATUS_INTERNAL_SERVER_ERROR: return "Internal server error";
|
|
case kHTTP_STATUS_NOT_IMPLEMENTED: return "Not implemented";
|
|
case kHTTP_STATUS_BAD_GATEWAY: return "Bad gateway";
|
|
case kHTTP_STATUS_SERVICE_UNAVAILABLE: return "Service unavailable";
|
|
case kHTTP_STATUS_GATEWAY_TIMEOUT: return "Gateway timeout";
|
|
case kHTTP_STATUS_UNSUPPORTED_VERSION: return "HTTP version not supported";
|
|
default: return "Unknown";
|
|
}
|
|
}
|
|
|
|
kFx(kStatus) xkHttpServerResponse_FormatStatusLine(kHttpServerResponse response)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
|
|
if (kString_Length(obj->reason) == 0)
|
|
{
|
|
kCheck(kString_Set(obj->reason, xkHttpServerResponse_DefaultReason(response, obj->status)));
|
|
}
|
|
|
|
kCheck(kString_Setf(obj->line, "HTTP/%u.%u %d %s\r\n", kVersion_Major(obj->version), kVersion_Minor(obj->version), obj->status, kString_Chars(obj->reason)));
|
|
|
|
kCheck(kStream_Write(obj->client, kString_Chars(obj->line), kString_Length(obj->line)));
|
|
|
|
return kOK;
|
|
}
|
|
|
|
kFx(kStatus) kHttpServerResponse_FormatHeaders(kHttpServerResponse response, kMap headerMap)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
kMapItem header = kMap_First(headerMap);
|
|
|
|
//write all headers to the stream
|
|
while (!kIsNull(header))
|
|
{
|
|
kString name = kMap_KeyAsT(headerMap, header, kString);
|
|
kString value = kMap_ValueAsT(headerMap, header, kString);
|
|
|
|
kCheck(kString_Setf(obj->line, "%s: %s\r\n", kString_Chars(name), kString_Chars(value)));
|
|
kCheck(kStream_Write(obj->client, kString_Chars(obj->line), kString_Length(obj->line)));
|
|
|
|
header = kMap_Next(headerMap, header);
|
|
}
|
|
|
|
//add trailing crlf
|
|
kCheck(kString_Set(obj->line, "\r\n"));
|
|
kCheck(kStream_Write(obj->client, kString_Chars(obj->line), kString_Length(obj->line)));
|
|
|
|
//clear out headers
|
|
kCheck(kMap_Purge(obj->headers));
|
|
|
|
return kOK;
|
|
}
|
|
|
|
kFx(kStatus) kHttpServerResponse_SetClosed(kHttpServerResponse response, kBool closed)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
|
|
obj->shouldClose = closed;
|
|
|
|
return kOK;
|
|
}
|
|
|
|
kFx(kBool) xkHttpServerResponse_Closed(kHttpServerResponse response)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
|
|
return obj->shouldClose;
|
|
}
|
|
|
|
kFx(kBool) xkHttpServerResponse_MessageStarted(kHttpServerResponse response)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
|
|
return obj->messageStarted;
|
|
}
|
|
|
|
kFx(kStatus) kHttpServerResponse_BeginWriteContent(kHttpServerResponse response, k64u length, kStream* stream)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
|
|
if (!kIsNull(stream))
|
|
{
|
|
*stream = obj->client;
|
|
}
|
|
|
|
kCheckState(!obj->messageStarted);
|
|
|
|
kCheck(xkHttpServerResponse_SetContentLength(response, length));
|
|
kCheck(xkHttpServerResponse_BeginWriteMessage(response));
|
|
|
|
return kOK;
|
|
}
|
|
|
|
kFx(kStatus) kHttpServerResponse_BeginWriteChunk(kHttpServerResponse response, k64u length, kStream* stream)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
|
|
if (!kIsNull(stream))
|
|
{
|
|
*stream = obj->client;
|
|
}
|
|
|
|
if (!obj->messageStarted)
|
|
{
|
|
kCheck(xkHttpServerResponse_EnableChunkCoding(response, kTRUE));
|
|
kCheck(xkHttpServerResponse_BeginWriteMessage(response));
|
|
}
|
|
|
|
if (obj->chunkIndex != 0)
|
|
{
|
|
//write terminating CRLF from previous chunk
|
|
kCheck(kString_Set(obj->line, "\r\n"));
|
|
kCheck(kStream_Write(obj->client, kString_Chars(obj->line), kString_Length(obj->line)));
|
|
}
|
|
|
|
//write chunk length
|
|
kCheck(kString_Setf(obj->line, "%llX\r\n", length));
|
|
kCheck(kStream_Write(obj->client, kString_Chars(obj->line), kString_Length(obj->line)));
|
|
|
|
obj->chunkIndex++;
|
|
|
|
return kOK;
|
|
}
|
|
|
|
kFx(kStatus) xkHttpServerResponse_End(kHttpServerResponse response)
|
|
{
|
|
kObj(kHttpServerResponse, response);
|
|
|
|
//if the response has no body, the headers may not have been written at this point
|
|
if (!obj->messageStarted)
|
|
{
|
|
kCheck(xkHttpServerResponse_BeginWriteMessage(response));
|
|
}
|
|
|
|
//if the response has a chunk-encoded body, write any final trailing headers here
|
|
if (obj->isChunkCoded)
|
|
{
|
|
kCheck(kHttpServerResponse_FormatHeaders(response, obj->headers));
|
|
}
|
|
|
|
//flush any outgoing buffered bytes to the stream
|
|
kCheck(kStream_Flush(obj->client));
|
|
|
|
return kOK;
|
|
}
|