// Buffered CGI-worker request/response API; HTTP framing belongs to the gateway. static void webValidate(const CodeLine& c) { count(c,2,4); const auto& a=c.arg; const auto& s=c.source; if(a[1].text=="request") { count(c,3,3); destination(a[2],s); } else if(a[1].text=="status") count(c,3,3); else if(a[1].text=="header") count(c,4,4); else if(a[1].text=="escape") { count(c,4,4); destination(a[2],s); } else fail(s,"unknown web operation"); } static bool webHeaderName(const std::string& name) { if(name.empty() || name.size()>64) return false; for(unsigned char ch:name) if(!((ch>='a' && ch<='z') || (ch>='A' && ch<='Z') || (ch>='0' && ch<='9') || ch=='-')) return false; return true; } bool vScript::webCommand(Frame& frame,const CodeLine& c) { const auto& a=c.arg; if(a[0].text!="web") return false; const auto& s=c.source; const auto& op=a[1].text; auto val=[&](std::size_t i){return value(frame,a[i],s);}; if(op=="escape") { lookup(frame,a[2],s); std::string out; for(char ch:val(3)) { if(ch=='&') append(out,"&",s); else if(ch=='<') append(out,"<",s); else if(ch=='>') append(out,">",s); else if(ch=='\"') append(out,""",s); else if(ch=='\'') append(out,"'",s); else append(out,std::string(1,ch),s); } assign(frame,a[2],out,s); return true; } if(!options.webWorker) fail(s,"web request/response operations require the CGI worker"); if(op=="request") { Json request; try { request=Json::parse(options.webRequest); } catch(...) { fail(s,"invalid CGI worker request JSON"); } if(!request.is_object()) fail(s,"CGI worker request must be an object"); putJson(frame,a[2],std::move(request),s); } else if(op=="status") webStatus=static_cast(integer(val(2),200,599,s)); else { auto name=val(2), text=val(3), lower=name; if(!webHeaderName(name) || text.size()>4096) fail(s,"invalid web response header"); for(unsigned char ch:text) if(ch<32 || ch>126) fail(s,"web response header requires printable ASCII (no CR/LF)"); std::transform(lower.begin(),lower.end(),lower.begin(),[](unsigned char ch){return static_cast(std::tolower(ch));}); static const std::set reserved={"status","content-length","connection","transfer-encoding","keep-alive","trailer","upgrade","proxy-authenticate","proxy-authorization","x-vanilla-request-id","x-content-type-options"}; if(reserved.count(lower)) fail(s,"reserved web response header"); auto next=webHeaders; if(lower!="set-cookie") next.erase(std::remove_if(next.begin(),next.end(),[&](const auto& h){return h.first==lower;}),next.end()); next.emplace_back(lower,text); std::size_t bytes=0; for(const auto& h:next) bytes+=h.first.size()+h.second.size()+4; if(next.size()>32 || bytes>8192) failLimit(s,"web response header limit (32 headers, 8 KiB)"); webHeaders=std::move(next); } return true; } void vScript::emit(const std::string& text,const Source& s) { auto limit=options.webWorker ? valueLimit : 16*valueLimit; if(text.size()>limit-outputBytes) failLimit(s,options.webWorker ? "CGI response byte limit (1 MiB)" : "output byte limit (16 MiB)"); if(!text.empty()) outputSource=s; outputBytes+=text.size(); if(options.webWorker) webBody+=text; else { std::cout << text; if(!std::cout) fail(s,"output write failed"); } } std::string vScript::WebResponse() const { Json headers=Json::array(); for(const auto& h:webHeaders) headers.push_back(Json::array({h.first,h.second})); return Json{{"status",webStatus},{"headers",std::move(headers)},{"body",webBody}}.dump(); }