; ============================================================================ ; FCGI Module - FastCGI Request Handler ; ============================================================================ Module FCGI EnableExplicit ; Session UUID regenerates on each suspicious request to prevent brute-force Global SessionUUID.s = DataModel::MakeUUID() ; -- Private Procedure Declarations -- Declare Handler_Contact(JSONString.s) Declare Handler_AdminRequest(Path.s, BufferSize) Declare GetLanguage() Declare SendTextResponse(Content.s) Declare SendBinaryResponse(*Data, Size, Mime.s) Declare Send404() ; ======================================================================== ; Public Procedures ; ======================================================================== Procedure Init() PrintN("[FCGI] Initializing network...") If Not InitCGI() ProcedureReturn #False EndIf If Not InitFastCGI(General::#FCGIPort) ProcedureReturn #False EndIf ProcedureReturn #True EndProcedure Procedure MainLoop() Protected BufferSize, Language Protected Path.s, Field.s While WaitFastCGIRequest() BufferSize = ReadCGI() Path = CGIVariable(#PB_CGI_ScriptName) Field = LCase(StringField(Path, 2, "/")) ; --- Route Request --- Select Field Case "admin" Handler_AdminRequest(Path, BufferSize) Continue Case "preview" If CGIParameterValue("UUID") = DataModel::PreviewUUID SendBinaryResponse(DataModel::*PreviewData, DataModel::PreviewSize, "text/html") Continue EndIf Case "article" If FindMapElement(DataModel::Articles(), CGIParameterName(0)) If Not DataModel::Articles()\Draft Language = GetLanguage() SendBinaryResponse(DataModel::Articles()\language[Language]\data, DataModel::Articles()\language[Language]\size, "text/html") Continue EndIf EndIf Case "tag" If FindMapElement(DataModel::StructurePages(), "tag?"+CGIParameterName(0)) Language = GetLanguage() SendBinaryResponse(DataModel::StructurePages()\language[Language]\data, DataModel::StructurePages()\language[Language]\size, "text/html") Continue EndIf Case "rss", "feed" ; UNTESTED! I only read some RSS specification... If FindMapElement(DataModel::StructurePages(), "rss") If CGIParameterName(0) = "fr" Language = 1 Else Language = 0 EndIf SendBinaryResponse(DataModel::StructurePages()\language[Language]\data, DataModel::StructurePages()\language[Language]\size, "application/rss+xml") Continue EndIf Case "browse" ; TODO: Implement history browsing Case "" ; Index page If FindMapElement(DataModel::StructurePages(), "index") Language = GetLanguage() SendBinaryResponse(DataModel::StructurePages()\language[Language]\data, DataModel::StructurePages()\language[Language]\size, "text/html") Continue EndIf Case "setlanguage" Protected NewLang = Val(CGIParameterName(0)) If NewLang >= 1 And NewLang <= General::#LanguageCount WriteCGIHeader(#PB_CGI_HeaderSetCookie, "LNG=" + Str(NewLang)) WriteCGIHeader(#PB_CGI_HeaderContentType, "text/plain", #PB_CGI_LastHeader) WriteCGIString("OK") Continue EndIf Case "contact" If Handler_Contact(PeekS(CGIBuffer(), BufferSize, #PB_UTF8)) WriteCGIHeader(#PB_CGI_HeaderContentType, "application/json", #PB_CGI_LastHeader) WriteCGIString("{" + Chr(34) + "success" + Chr(34) + ": true}") Else WriteCGIHeader(#PB_CGI_HeaderContentType, "application/json") WriteCGIHeader(#PB_CGI_HeaderStatus, "400 Bad Request", #PB_CGI_LastHeader) WriteCGIString("{" + Chr(34) + "success" + Chr(34) + ": false, " + Chr(34) + "error" + Chr(34) + ": " + Chr(34) + "Invalid request" + Chr(34) + "}") EndIf Continue Default ; Serve public binaries (images, etc.) If FindString(Path, ".") If FindMapElement(DataModel::Binaries(), Path) SendBinaryResponse(DataModel::Binaries()\data, DataModel::Binaries()\size, DataModel::Binaries()\mime) Continue EndIf EndIf EndSelect Send404() Wend EndProcedure ; ======================================================================== ; Admin Request Handler ; ======================================================================== Procedure Handler_AdminRequest(Path.s, BufferSize) ; Verify authentication If CGICookieValue("ADMIN") <> General::#SecretKey Debug "Invalid admin cookie: " + CGICookieValue("ADMIN") Delay(500) ; Rate limit failed auth attempts Send404() ProcedureReturn EndIf ; Session validation (prevents brute force) If CGICookieValue("SESSION") <> SessionUUID Delay(500) SessionUUID = DataModel::MakeUUID() WriteCGIHeader(#PB_CGI_HeaderSetCookie, "SESSION=" + SessionUUID) EndIf ; Route admin API requests Select CGIParameterName(0) Case "newarticle" SendTextResponse("NA:" + DataModel::CreateArticle()) ProcedureReturn Case "listarticles" SendTextResponse("LA:" + DataModel::ListArticles()) ProcedureReturn Case "getarticle" SendTextResponse("AC:" + DataModel::GetArticle(Val(CGIParameterValue("getarticle")))) ProcedureReturn Case "updatearticle" ; Can't use CGIParameterValue() - it gets tricked by '=' in base64 Protected RawData.s = PeekS(CGIBuffer(), BufferSize, #PB_Ascii) DataModel::UpdateArticle(RemoveString(RawData, "updatearticle=", #PB_String_CaseSensitive, 0, 1)) SendTextResponse("UA:OK") ProcedureReturn Case "previewarticle" Protected PreviewData.s = StringField(PeekS(CGIBuffer(), BufferSize, #PB_Ascii), 2, "previewarticle=") SendTextResponse("PA:" + DataModel::PreviewArticle(PreviewData)) ProcedureReturn Case "deletearticle" SendTextResponse("DA:" + DataModel::DeleteArticle(Val(CGIParameterValue("deletearticle")))) ProcedureReturn Case "listfiles" SendTextResponse("LF:" + DataModel::ListFiles()) ProcedureReturn Case "newfile" Protected FileData.s = StringField(PeekS(CGIBuffer(), BufferSize, #PB_Ascii), 2, "filedata=") SendTextResponse("NF:" + DataModel::NewFile(CGIParameterValue("newfile"), FileData)) ProcedureReturn Case "deletefile" SendTextResponse("DF:" + DataModel::DeleteAFile(Val(CGIParameterValue("deletefile")))) ProcedureReturn Case "listtags" SendTextResponse("LT:" + DataModel::ListTags()) ProcedureReturn Case "newtag" SendTextResponse("NT:" + DataModel::CreateTag()) ProcedureReturn Case "gettag" SendTextResponse("TC:" + DataModel::GetTag(Val(CGIParameterValue("gettag")))) ProcedureReturn Case "updatetag" Protected TagData.s = PeekS(CGIBuffer(), BufferSize, #PB_Ascii) DataModel::UpdateTag(RemoveString(TagData, "updatetag=", #PB_String_CaseSensitive, 0, 1)) SendTextResponse("UT:OK") ProcedureReturn Case "listlanguages" SendTextResponse("LL:" + DataModel::ListLanguages()) ProcedureReturn Default ; Add index.html if path has no file If StringField(Path, 3, "/") = "" Path + "index.html" EndIf EndSelect ; Serve static admin files If FindMapElement(DataModel::AdminBinaries(), Path) SendBinaryResponse(DataModel::AdminBinaries()\data, DataModel::AdminBinaries()\size, DataModel::AdminBinaries()\mime) Else Send404() EndIf EndProcedure Procedure Handler_Contact(JSONString.s) Protected JSON = ParseJSON(#PB_Any, JSONString), JSONValue Protected Email.s, Message.s, Name.s If JSON JSONValue = JSONValue(JSON) If ExamineJSONMembers(JSONValue) While NextJSONMember(JSONValue) Select JSONMemberKey(JSONValue) Case "email" Email = GetJSONString(JSONMemberValue(JSONValue)) Case "message" Message = GetJSONString(JSONMemberValue(JSONValue)) Case "name" Name = GetJSONString(JSONMemberValue(JSONValue)) EndSelect Wend EndIf If Email And Message And Name If CreateMail(0, General::#Email, "Message from the blog") AddMailRecipient(0, General::#Email, #PB_Mail_To) SetMailBody(0, "Author : " + Name + "( " + email + " )" + #CRLF$ + "Message" + #CRLF$ + Message) SendMail(0, "smtp.gmail.com", 465, #PB_Mail_UseSSL | #PB_Mail_UseSMTPS, General::#Email, General::#PassWord) EndIf ProcedureReturn #True EndIf EndIf ProcedureReturn #False EndProcedure ; ======================================================================== ; Helper Functions ; ======================================================================== Procedure GetLanguageFromBrowser(HTTP_ACCEPT_LANGUAGE.s) Protected Loop, Entry.s, Result = 1 Protected Count = CountString(HTTP_ACCEPT_LANGUAGE, ",") + 1 For Loop = 1 To Count ; Parse "en-US,fr;q=0.9" format Entry = LCase(StringField(StringField(StringField(HTTP_ACCEPT_LANGUAGE, Loop, ","), 1, ";"), 1, "-")) If FindMapElement(DataModel::Language(), Entry) Result = DataModel::Language() Break EndIf Next ProcedureReturn Result EndProcedure Procedure GetLanguage() Protected Language Language = Val(CGICookieValue("LNG")) If Language = 0 Or Language > General::#LanguageCount Language = GetLanguageFromBrowser(CGIVariable(#PB_CGI_HttpAcceptLanguage)) WriteCGIHeader(#PB_CGI_HeaderSetCookie, "LNG=" + Str(Language)) EndIf Language - 1 ; Convert to 0-index ProcedureReturn Language EndProcedure Procedure SendTextResponse(Content.s) WriteCGIHeader(#PB_CGI_HeaderContentType, "text/html", #PB_CGI_LastHeader) WriteCGIString(Content) EndProcedure Procedure SendBinaryResponse(*Data, Size, Mime.s) WriteCGIHeader(#PB_CGI_HeaderContentType, Mime, #PB_CGI_LastHeader) WriteCGIData(*Data, Size) EndProcedure Procedure Send404() WriteCGIHeader(#PB_CGI_HeaderContentType, "text/html", #PB_CGI_LastHeader) WriteCGIString("