HTTP now automatically encodes jsonvalue content
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user