ares-openbsd/nall/http/response.hpp

293 行
9.8 KiB
C++

#pragma once
#include <nall/chrono.hpp>
#include <nall/file-map.hpp>
#include <nall/http/message.hpp>
namespace nall::HTTP {
struct Response : Message {
using type = Response;
Response() = default;
Response(const Request& request) { setRequest(request); }
explicit operator bool() const { return responseType() != 0; }
auto operator()(u32 responseType) -> type& { return setResponseType(responseType); }
auto head(const function<bool (const u8* data, u32 size)>& callback) const -> bool override;
auto setHead() -> bool override;
auto body(const function<bool (const u8* data, u32 size)>& callback) const -> bool override;
auto setBody() -> bool override;
auto request() const -> const Request* { return _request; }
auto setRequest(const Request& value) -> type& { _request = &value; return *this; }
auto responseType() const -> u32 { return _responseType; }
auto setResponseType(u32 value) -> type& { _responseType = value; return *this; }
auto hasData() const -> bool { return (bool)_data; }
auto data() const -> const vector<u8>& { return _data; }
auto setData(const vector<u8>& value) -> type&;
auto hasFile() const -> bool { return (bool)_file; }
auto file() const -> const string& { return _file; }
auto setFile(const string& value) -> type&;
auto hasText() const -> bool { return (bool)_text; }
auto text() const -> const string& { return _text; }
auto setText(const string& value) -> type&;
auto hasBody() const -> bool;
auto findContentLength() const -> u32;
auto findContentType() const -> string;
auto findContentType(const string& suffix) const -> string;
auto findResponseType() const -> string;
auto findResponseTypeVerbose() const -> string;
auto setFileETag() -> void;
const Request* _request = nullptr;
u32 _responseType = 0;
vector<u8> _data;
string _file;
string _text;
};
inline auto Response::head(const function<bool (const u8*, u32)>& callback) const -> bool {
if(!callback) return false;
string output;
if(auto request = this->request()) {
if(auto eTag = header["ETag"]) {
if(eTag.value() == request->header["If-None-Match"].value()) {
output.append("HTTP/1.1 304 Not Modified\r\n");
output.append("Connection: close\r\n");
output.append("\r\n");
return callback(output.data<u8>(), output.size());
}
}
}
output.append("HTTP/1.1 ", findResponseType(), "\r\n");
for(auto& variable : header) {
output.append(variable.name(), ": ", variable.value(), "\r\n");
}
if(hasBody()) {
if(!header["Content-Length"] && !header["Transfer-Encoding"].value().iequals("chunked")) {
output.append("Content-Length: ", findContentLength(), "\r\n");
}
if(!header["Content-Type"]) {
output.append("Content-Type: ", findContentType(), "\r\n");
}
}
if(!header["Connection"]) {
output.append("Connection: close\r\n");
}
output.append("\r\n");
return callback(output.data<u8>(), output.size());
}
inline auto Response::setHead() -> bool {
auto headers = _head.split("\n");
string response = headers.takeLeft().trimRight("\r");
if(response.ibeginsWith("HTTP/1.0 ")) response.itrimLeft("HTTP/1.0 ", 1L);
else if(response.ibeginsWith("HTTP/1.1 ")) response.itrimLeft("HTTP/1.1 ", 1L);
else return false;
setResponseType(response.natural());
for(auto& header : headers) {
if(header.beginsWith(" ") || header.beginsWith("\t")) continue;
auto variable = header.split(":", 1L).strip();
if(variable.size() != 2) continue;
this->header.append(variable[0], variable[1]);
}
return true;
}
inline auto Response::body(const function<bool (const u8*, u32)>& callback) const -> bool {
if(!callback) return false;
if(!hasBody()) return true;
bool chunked = header["Transfer-Encoding"].value() == "chunked";
if(chunked) {
string prefix = {hex(findContentLength()), "\r\n"};
if(!callback(prefix.data<u8>(), prefix.size())) return false;
}
if(_body) {
if(!callback(_body.data<u8>(), _body.size())) return false;
} else if(hasData()) {
if(!callback(data().data(), data().size())) return false;
} else if(hasFile()) {
file_map map(file(), file_map::mode::read);
if(!callback(map.data(), map.size())) return false;
} else if(hasText()) {
if(!callback(text().data<u8>(), text().size())) return false;
} else {
string response = findResponseType();
if(!callback(response.data<u8>(), response.size())) return false;
}
if(chunked) {
string suffix = {"\r\n0\r\n\r\n"};
if(!callback(suffix.data<u8>(), suffix.size())) return false;
}
return true;
}
inline auto Response::setBody() -> bool {
return true;
}
inline auto Response::hasBody() const -> bool {
if(auto request = this->request()) {
if(request->requestType() == Request::RequestType::Head) return false;
}
if(responseType() == 301) return false;
if(responseType() == 302) return false;
if(responseType() == 303) return false;
if(responseType() == 304) return false;
if(responseType() == 307) return false;
return true;
}
inline auto Response::findContentLength() const -> u32 {
if(auto contentLength = header["Content-Length"]) return contentLength.value().natural();
if(_body) return _body.size();
if(hasData()) return data().size();
if(hasFile()) return file::size(file());
if(hasText()) return text().size();
return findResponseType().size();
}
inline auto Response::findContentType() const -> string {
if(auto contentType = header["Content-Type"]) return contentType.value();
if(hasData()) return "application/octet-stream";
if(hasFile()) return findContentType(Location::suffix(file()));
return "text/html; charset=utf-8";
}
inline auto Response::findContentType(const string& s) const -> string {
if(s == ".7z" ) return "application/x-7z-compressed";
if(s == ".avi" ) return "video/avi";
if(s == ".bml" ) return "text/plain; charset=utf-8";
if(s == ".bz2" ) return "application/x-bzip2";
if(s == ".c" ) return "text/plain; charset=utf-8";
if(s == ".cpp" ) return "text/plain; charset=utf-8";
if(s == ".css" ) return "text/css; charset=utf-8";
if(s == ".gif" ) return "image/gif";
if(s == ".gz" ) return "application/gzip";
if(s == ".h" ) return "text/plain; charset=utf-8";
if(s == ".hpp" ) return "text/plain; charset=utf-8";
if(s == ".htm" ) return "text/html; charset=utf-8";
if(s == ".html") return "text/html; charset=utf-8";
if(s == ".ico" ) return "image/x-icon";
if(s == ".jpg" ) return "image/jpeg";
if(s == ".jpeg") return "image/jpeg";
if(s == ".js" ) return "application/javascript";
if(s == ".mka" ) return "audio/x-matroska";
if(s == ".mkv" ) return "video/x-matroska";
if(s == ".mp3" ) return "audio/mpeg";
if(s == ".mp4" ) return "video/mp4";
if(s == ".mpeg") return "video/mpeg";
if(s == ".mpg" ) return "video/mpeg";
if(s == ".ogg" ) return "audio/ogg";
if(s == ".pdf" ) return "application/pdf";
if(s == ".png" ) return "image/png";
if(s == ".rar" ) return "application/x-rar-compressed";
if(s == ".svg" ) return "image/svg+xml";
if(s == ".tar" ) return "application/x-tar";
if(s == ".txt" ) return "text/plain; charset=utf-8";
if(s == ".wav" ) return "audio/vnd.wave";
if(s == ".webm") return "video/webm";
if(s == ".xml" ) return "text/xml; charset=utf-8";
if(s == ".xz" ) return "application/x-xz";
if(s == ".zip" ) return "application/zip";
return "application/octet-stream"; //binary
}
inline auto Response::findResponseType() const -> string {
switch(responseType()) {
case 200: return "200 OK";
case 301: return "301 Moved Permanently";
case 302: return "302 Found";
case 303: return "303 See Other";
case 304: return "304 Not Modified";
case 307: return "307 Temporary Redirect";
case 400: return "400 Bad Request";
case 403: return "403 Forbidden";
case 404: return "404 Not Found";
case 500: return "500 Internal Server Error";
case 501: return "501 Not Implemented";
case 503: return "503 Service Unavailable";
}
return "501 Not Implemented";
}
inline auto Response::findResponseTypeVerbose() const -> string {
switch(responseType()) {
case 400: return "The server was unable to understand your request.";
case 403: return "You are not authorized to access this resource.";
case 404: return "The requested content could not be found.";
case 500: return "The server has encountered an error.";
case 501: return "The requested feature has not been implemented.";
case 503: return "The requested service is not currently available.";
}
return findResponseType(); //fallback for uncommon responses
}
inline auto Response::setData(const vector<u8>& value) -> type& {
_data = value;
header.assign("Content-Length", value.size());
return *this;
}
inline auto Response::setFile(const string& value) -> type& {
//block path escalation exploits ("../" and "..\" in the file location)
bool valid = true;
for(u32 n : range(value.size())) {
if(value(n + 0, '\0') != '.') continue;
if(value(n + 1, '\0') != '.') continue;
if(value(n + 2, '\0') != '/' && value(n + 2, '\0') != '\\') continue;
valid = false;
break;
}
if(!valid) return *this;
//cache images for seven days
auto suffix = Location::suffix(value);
u32 maxAge = 0;
if(suffix == ".svg"
|| suffix == ".ico"
|| suffix == ".png"
|| suffix == ".gif"
|| suffix == ".jpg"
|| suffix == ".jpeg") {
maxAge = 7 * 24 * 60 * 60;
}
_file = value;
header.assign("Content-Length", file::size(value));
header.assign("ETag", {"\"", chrono::utc::datetime(file::timestamp(value, file::time::modify)), "\""});
if(maxAge == 0) {
header.assign("Cache-Control", {"public"});
} else {
header.assign("Cache-Control", {"public, max-age=", maxAge});
}
return *this;
}
inline auto Response::setText(const string& value) -> type& {
_text = value;
header.assign("Content-Length", value.size());
return *this;
}
}