HTTP now automatically encodes jsonvalue content

This commit is contained in:
2022-06-07 01:54:08 -04:00
parent 779d9e20b8
commit 79b84a588a
9 changed files with 191 additions and 61 deletions

View File

@@ -9,6 +9,7 @@
#include "wrap-string.hpp"
#include "util.hpp"
#include "luastack.hpp"
#include "json.hpp"
#include <cstdint>
@@ -319,6 +320,38 @@ static void send_error_response(bool http11, int code, eng::string extrainfo, St
}
}
// HTTP 1.0: An entity body is included with a request message only when the request method calls for one.
// The presence of an entity body in a request is signaled by the inclusion of a Content-Length header field
// in the request message headers. HTTP/1.0 requests containing an entity body must include a valid
// Content-Length header field.
//
// HTTP 1.1: The presence of a message-body in a request is signaled by the inclusion of a Content-Length or
// Transfer-Encoding header field in the request's message-headers
//
static bool request_contains_content(int content_length, std::string_view transfer_encoding) {
return (content_length >= 0) || (!transfer_encoding.empty());
}
// HTTP 1.0: For response messages, whether or not an entity body is included with a message is dependent on
// both the request method and the response code. All responses to the HEAD request method must not include a
// body, even though the presence of entity header fields may lead one to believe they do. All 1xx (informational),
// 204 (no content), and 304 (not modified) responses must not include a body. All other responses must include an
// entity body or a Content-Length header field defined with a value of zero (0).
//
// HTTP 1.1: For response messages, whether or not a message-body is included with a message is dependent on both
// the request method and the response status code (section 6.1.1). All responses to the HEAD request method MUST
// NOT include a message-body, even though the presence of entity- header fields might lead one to believe they
// do. All 1xx (informational), 204 (no content), and 304 (not modified) responses MUST NOT include a message-body.
// All other responses do include a message-body, although it MAY be of zero length.
//
static bool response_contains_content(std::string_view method, int status) {
if (method == "HEAD") return false;
if ((status >= 100) && (status <= 199)) return false;
if (status == 204) return false;
if (status == 304) return false;
return true;
}
// In a properly-formed url, the hostname and path are url encoded.
// This parser expects an encoded URL.
struct ParsedURL {
@@ -451,8 +484,8 @@ void HttpClientRequest::send_internal(StreamBuffer *sb, bool debug_string) const
sb->write_bytes("\r\n");
}
// If it's a post request, send the content length and the content type.
if (method_ == "POST") {
// Send the content length and the content type.
if (content_assigned_) {
send_content_length_header(true, content_.size(), sb);
send_content_type_header(true, mime_type_, sb);
}
@@ -460,8 +493,8 @@ void HttpClientRequest::send_internal(StreamBuffer *sb, bool debug_string) const
// Send the extra linebreak.
sb->write_bytes("\r\n");
// If it's a post request, send the content.
if (method_ == "POST") {
// Send the content.
if (content_assigned_) {
sb->write_bytes(content_);
}
}
@@ -681,6 +714,17 @@ void HttpClientRequest::set_content(LuaStack &LS, LuaSlot val) {
set_content(LS.ckstring(val));
}
void HttpClientRequest::set_jsonvalue(LuaStack &LS, LuaSlot val) {
eng::string out;
eng::string err = json::encode(LS, val, out, false, HttpParser::MAX_CONTENT_LENGTH);
if (!err.empty()) {
check_fail(util::ss("json encode failure: ", err));
return;
}
set_content(out);
set_mime_type("application/json");
}
void HttpClientRequest::set_defaults() {
if (method_.empty()) {
method_ = "GET";
@@ -715,6 +759,20 @@ void HttpClientRequest::set_config(LuaStack &LS0, LuaSlot tab) {
set_mime_type(LS, val);
} else if (kstr == "content") {
set_content(LS, val);
} else if (kstr == "html") {
set_content(LS, val);
set_mime_type("text/html");
} else if (kstr == "text") {
set_content(LS, val);
set_mime_type("text/plain");
} else if (kstr == "json") {
set_content(LS, val);
set_mime_type("application/json");
} else if (kstr == "bytes") {
set_content(LS, val);
set_mime_type("application/octet-stream");
} else if (kstr == "jsonvalue") {
set_jsonvalue(LS, val);
} else if (kstr == "") {
check_fail(util::ss("configuration parameter names must be strings."));
} else {
@@ -755,13 +813,6 @@ eng::string HttpClientRequest::check() const {
if (mime_type_.empty()) {
return "mime type not set for POST request";
}
} else {
if (content_assigned_) {
return "content can only be specified for POST requests";
}
if (!mime_type_.empty()) {
return "mime type can only be specified for POST requests";
}
}
return "";
}
@@ -822,20 +873,23 @@ void HttpServerResponse::send_internal(StreamBuffer *sb, bool debug_string) cons
return;
}
// Determine if we're going to be sending content.
bool contains_content = response_contains_content("GET", status_);
// The status message is the human-readable status code.
eng::string statusmsg = status_code_to_string(status_);
// Annotate error messages.
const eng::string *content = &content_;
if (errstatus() && (mime_type_ == "text/plain") &&
!content_.empty() && !contains_newline(content_)) {
if (errstatus() && contains_content &&
(mime_type_ == "text/plain") && !contains_newline(content_)) {
if (sv::has_prefix(content_, statusmsg)) {
statusmsg = content_;
} else {
} else if (!content_.empty()) {
statusmsg += ": ";
statusmsg += content_;
content = &statusmsg;
}
content = &statusmsg;
}
// Send the status line.
@@ -848,7 +902,7 @@ void HttpServerResponse::send_internal(StreamBuffer *sb, bool debug_string) cons
}
// Send the headers that make sense if we're sending content.
if (content_assigned_) {
if (contains_content) {
if (!errstatus()) {
send_cache_control_header(http11_, max_age_, sb);
}
@@ -860,7 +914,7 @@ void HttpServerResponse::send_internal(StreamBuffer *sb, bool debug_string) cons
sb->write_bytes("\r\n");
// Send the content.
if (content_assigned_) {
if (contains_content) {
sb->write_bytes(*content);
}
}
@@ -960,6 +1014,17 @@ void HttpServerResponse::set_content(LuaStack &LS, LuaSlot val) {
set_content(LS.ckstring(val));
}
void HttpServerResponse::set_jsonvalue(LuaStack &LS, LuaSlot val) {
eng::string out;
eng::string err = json::encode(LS, val, out, false, HttpParser::MAX_CONTENT_LENGTH);
if (!err.empty()) {
check_fail(util::ss("json encode failure: ", err));
return;
}
set_content(out);
set_mime_type("application/json");
}
void HttpServerResponse::set_config(LuaStack &LS0, LuaSlot tab) {
LuaVar key, val;
LuaStack LS(LS0.state(), key, val);
@@ -987,6 +1052,8 @@ void HttpServerResponse::set_config(LuaStack &LS0, LuaSlot tab) {
} else if (kstr == "bytes") {
set_content(LS, val);
set_mime_type("application/octet-stream");
} else if (kstr == "jsonvalue") {
set_jsonvalue(LS, val);
} else if (kstr == "") {
check_fail(util::ss("response configuration parameters must be strings."));
} else {
@@ -1010,13 +1077,13 @@ void HttpServerResponse::set_defaults() {
status_ = 200;
}
// If you're sending an error message along with
// an error status code, then assume the error
// message is text/plain.
if ((status_ >= 400) && (status_ <= 599)) {
if (content_assigned_ && (mime_type_.empty())) {
mime_type_ = "text/plain";
}
// If the response status indicates that we're sending
// content, and there's no content, generate blank content.
if (response_contains_content("GET", status_) &&
(!content_assigned_) && (mime_type_.empty())) {
content_assigned_ = true;
content_ = "";
mime_type_ = "text/plain";
}
}
@@ -1030,15 +1097,16 @@ eng::string HttpServerResponse::check() const {
return "status code not specified";
}
// If you specify content, you have to specify mime
// type, and vice versa. Also, we need both for a valid response.
if ((status_ == 200) || (content_assigned_) || (!mime_type_.empty())) {
if (!content_assigned_) {
return "content not specified";
}
if (mime_type_.empty()) {
return "mime type not specified";
}
// If you assigned a mime type and didn't specify
// content, that's bad.
if ((!content_assigned_) && (!mime_type_.empty())) {
return "mime type specified without content";
}
// If you assigned content, but didn't assign
// a mime type, that's bad.
if (content_assigned_ && (mime_type_.empty())) {
return "content specified without mime type";
}
return "";
}
@@ -1337,6 +1405,17 @@ bool HttpParser::parse_content_chunked(std::string_view &view, bool closed) {
}
bool HttpParser::parse_content(std::string_view &view, bool closed) {
// If there's no body, just return true.
if (is_request_) {
if (!request_contains_content(content_length_, transfer_encoding_)) {
return true;
}
} else {
if (!response_contains_content(method_, status_)) {
return true;
}
}
// Parse the content.
if (transfer_encoding_ == "") {
if (!parse_content_basic(view, closed)) {
@@ -1386,8 +1465,8 @@ bool HttpParser::parse_content(std::string_view &view, bool closed) {
}
void HttpParser::store(LuaStack &LS0, LuaSlot tab) const {
LuaVar ptab;
LuaStack LS(LS0.state(), ptab);
LuaVar ptab, djson;
LuaStack LS(LS0.state(), ptab, djson);
LS.newtable(tab);
if (!is_request_) {
@@ -1402,6 +1481,10 @@ void HttpParser::store(LuaStack &LS0, LuaSlot tab) const {
if (!mime_type_.empty() || !content_.empty()) {
LS.rawset(tab, "mimetype", mime_type_);
LS.rawset(tab, "content", content_);
if (mime_type_ == "application/json") {
json::decode(LS, djson, content_);
LS.rawset(tab, "jsonvalue", djson);
}
}
// We currently don't store the method, because
// our server only supports GET. Even if we
@@ -1475,9 +1558,10 @@ eng::string HttpParser::debug_string() const {
return oss.str();
}
void HttpParser::parse_response(std::string_view view, bool closed) {
void HttpParser::parse_response(std::string_view view, bool closed, std::string_view method) {
std::string_view original_view = view;
is_request_ = false;
method_ = method;
// Parse the status line.
if (!parse_status_line(view, closed)) {
@@ -1524,10 +1608,8 @@ void HttpParser::parse_request(std::string_view view, bool closed) {
}
// Process the content, if any.
if (method_ == "POST") {
if (!parse_content(view, closed)) {
return;
}
if (!parse_content(view, closed)) {
return;
}
// Calculate the comm length.
@@ -1594,8 +1676,22 @@ LuaDefine(http_clientrequest, "request",
"| path (ie, '/index.html')"
"| params (a table of url parameters)"
"| verifycertificate (default: true)"
"| content (the body to send to the server, if any)"
"| mimetype (mime type of the body, if any)"
"|"
"|The table can also contain this field, which sets"
"|host, port, path, and params in a single directive."
"|"
"| url (ie, 'https://host:port/path.html?a=b&c=d')"
"|"
"|The table can also contain these fields, which set"
"|both the content and the mimetype in a single directive:"
"|"
"| html - content as a string (text/html)"
"| text - content as a string (text/plain)"
"| json - content as a string (application/json)"
"| bytes - content as a string (application/octet-stream)"
"|"
"|You can specify url components separately (host, port, path,"
"|and params), or you can specify the entire url as a unit. "
"|If you specify components, they must not be url-encoded. "
@@ -1674,7 +1770,7 @@ LuaDefine(http_clientresponse, "response",
LuaRet tab;
LuaStack LS(L, text, tab);
HttpParser parser;
parser.parse_response(LS.ckstring(text), true);
parser.parse_response(LS.ckstring(text), true, "GET");
parser.store(LS, tab);
return LS.result();
}