diff options
| author | Oskari Timperi <oskari.timperi@iki.fi> | 2022-09-15 19:35:27 +0300 |
|---|---|---|
| committer | Oskari Timperi <oskari.timperi@iki.fi> | 2022-09-15 19:39:50 +0300 |
| commit | ff34cebaa50ebac63643a5e58989e416e09de4b9 (patch) | |
| tree | 5118bf17a9ca08cad49388b8847c2c0e0f3d035e | |
| download | sqlite-http-c-ff34cebaa50ebac63643a5e58989e416e09de4b9.tar.gz sqlite-http-c-ff34cebaa50ebac63643a5e58989e416e09de4b9.zip | |
Initial commit
| -rw-r--r-- | .clang-format | 166 | ||||
| -rw-r--r-- | CMakeLists.txt | 76 | ||||
| -rw-r--r-- | LICENSE.txt | 21 | ||||
| -rw-r--r-- | http.c | 1850 | ||||
| -rw-r--r-- | src/amalgamate.c | 55 | ||||
| -rw-r--r-- | src/http.c | 791 | ||||
| -rw-r--r-- | src/http.h | 38 | ||||
| -rw-r--r-- | src/http_backend_curl.c | 478 | ||||
| -rw-r--r-- | src/http_backend_dummy.c | 62 | ||||
| -rw-r--r-- | src/http_backend_winhttp.c | 289 | ||||
| -rw-r--r-- | src/http_next_header.c | 174 | ||||
| -rw-r--r-- | tests/CMakeLists.txt | 13 | ||||
| -rw-r--r-- | tests/init.c | 11 | ||||
| -rw-r--r-- | tests/t_http.c | 221 | ||||
| -rw-r--r-- | tests/t_http_next_header.c | 353 | ||||
| -rw-r--r-- | tests/test.h | 77 | ||||
| -rw-r--r-- | tests/tests.py | 72 | ||||
| -rw-r--r-- | tools/format.bat | 1 |
18 files changed, 4748 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..12c3f7d --- /dev/null +++ b/.clang-format @@ -0,0 +1,166 @@ +--- +Language: Cpp +# BasedOnStyle: LLVM +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: None +AlignConsecutiveAssignments: None +AlignConsecutiveBitFields: None +AlignConsecutiveDeclarations: None +AlignEscapedNewlines: Right +AlignOperands: Align +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortEnumsOnASingleLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +AttributeMacros: + - __capability +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeConceptDeclarations: true +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 100 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DeriveLineEnding: true +DerivePointerAlignment: false +DisableFormat: false +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +StatementAttributeLikeMacros: + - Q_EMIT +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 1 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: '(Test)?$' +IncludeIsMainSourceRegex: '' +IndentCaseLabels: false +IndentCaseBlocks: false +IndentGotoLabels: true +IndentPPDirectives: None +IndentExternBlock: AfterExternBlock +IndentRequires: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +InsertTrailingCommas: None +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 2 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PenaltyIndentedWhitespace: 0 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SortJavaStaticImport: Before +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceAroundPointerQualifiers: Default +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +BitFieldColonSpacing: Both +Standard: Latest +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseCRLF: false +UseTab: Never +WhitespaceSensitiveMacros: + - STRINGIZE + - PP_STRINGIZE + - BOOST_PP_STRINGIZE + - NS_SWIFT_NAME + - CF_SWIFT_NAME +... + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..41fa3bc --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,76 @@ +cmake_minimum_required(VERSION 3.21 FATAL_ERROR) + +project(sqlite-http-c VERSION 0.1.0 LANGUAGES C) + +if(UNIX) + set(HTTP_BACKEND_CURL_DEFAULT TRUE) +endif() + +if(WIN32) + set(HTTP_BACKEND_WINHTTP_DEFAULT TRUE) +endif() + +option(HTTP_BACKEND_CURL "Enable curl backend" ${HTTP_BACKEND_CURL_DEFAULT}) +option(HTTP_BACKEND_WINHTTP "Enable WinHTTP backend" ${HTTP_BACKEND_WINHTTP_DEFAULT}) +option(HTTP_BUILD_STATIC "Build a static library" OFF) +option(HTTP_BUILD_SHARED "Build a shared library" ON) + +if(PROJECT_IS_TOP_LEVEL) + add_executable(amalgamate src/amalgamate.c) + + add_custom_command( + OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/http.c" + COMMAND amalgamate + DEPENDS + src/amalgamate.c + src/http.c + src/http.h + src/http_backend_curl.c + src/http_backend_dummy.c + src/http_backend_winhttp.c + src/http_next_header.c + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + ) +endif() + +include(FetchContent) + +FetchContent_Declare( + sqlite + URL https://sqlite.org/2022/sqlite-amalgamation-3390200.zip + URL_HASH SHA3_256=deb2abef617b6305525e3b1a2b39a5dc095ffb62f243b4d1b468ba5f41900ce7 +) + +FetchContent_MakeAvailable(sqlite) + +if(HTTP_BUILD_STATIC) + add_library(sqlite-http-c-static STATIC "http.c") + target_include_directories(sqlite-http-c-static PRIVATE ${sqlite_SOURCE_DIR}) + if(HTTP_BACKEND_CURL) + target_compile_definitions(sqlite-http-c-static PRIVATE HTTP_BACKEND_CURL) + endif() + if(HTTP_BACKEND_WINHTTP) + target_compile_definitions(sqlite-http-c-static PRIVATE HTTP_BACKEND_WINHTTP) + target_link_libraries(sqlite-http-c-static PUBLIC winhttp) + endif() + target_compile_definitions(sqlite-http-c-static PRIVATE SQLITE_CORE) +endif() + +if(HTTP_BUILD_SHARED) + add_library(http SHARED "http.c") + target_include_directories(http PRIVATE ${sqlite_SOURCE_DIR}) + if(HTTP_BACKEND_CURL) + target_compile_definitions(http PRIVATE HTTP_BACKEND_CURL) + endif() + if(HTTP_BACKEND_WINHTTP) + target_compile_definitions(http PRIVATE HTTP_BACKEND_WINHTTP) + target_link_libraries(http PRIVATE winhttp) + endif() +endif() + +if(PROJECT_IS_TOP_LEVEL) + include(CTest) + if(BUILD_TESTING) + add_subdirectory(tests) + endif() +endif() diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..9ba0b79 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Oskari Timperi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. @@ -0,0 +1,1850 @@ + +/********** src/http.h **********/ + +#ifndef HTTP_H +#define HTTP_H + +#include "sqlite3ext.h" + +typedef struct http_request http_request; +struct http_request { + char* zMethod; + char* zUrl; + const void* pBody; + sqlite3_int64 szBody; + const char* zHeaders; +}; + +typedef struct http_response http_response; +struct http_response { + void* pBody; + sqlite3_int64 szBody; + char* zHeaders; + int szHeaders; + int iStatusCode; + char* zStatus; +}; + +int http_do_request(http_request* req, http_response* resp, char** ppErrMsg); + +int http_next_header(const char* headers, + int size, + int* pParsed, + const char** ppName, + int* pNameSize, + const char** ppValue, + int* pValueSize); + +void remove_all_but_last_headers(char* zHeaders); +void separate_status_and_headers(char** ppStatus, char* zHeaders); + +#endif + +/********** src/http.c **********/ + +#include "http.h" + +SQLITE_EXTENSION_INIT1 + +#include <assert.h> +#include <ctype.h> +#include <string.h> + +typedef struct http_vtab http_vtab; +struct http_vtab { + sqlite3_vtab base; + char* zMethod; +}; + +typedef struct http_cursor http_cursor; +struct http_cursor { + sqlite3_vtab_cursor base; + sqlite3_int64 iRowid; + http_request req; + http_response resp; +}; + +// If zHeaders contains headers for multiple responses, then this will strip +// headers from all but the last response. +void remove_all_but_last_headers(char* zHeaders) { + int szHeaders = strlen(zHeaders); + char* pLastTerminator = NULL; + char* pSearch = zHeaders; + + for (;;) { + char* p = strstr(pSearch, "\r\n\r\n"); + // If there isn't more terminators, we are done here + if (!p) { + break; + } + // If it's at the end of zHeaders, we are done here + if (szHeaders - (p - zHeaders) == 4) { + break; + } + pSearch = p + 4; + pLastTerminator = p; + } + + if (pLastTerminator) { + pLastTerminator += 4; + memmove(zHeaders, pLastTerminator, strlen(pLastTerminator) + 1); + } +} + +// Separate the status and header lines +void separate_status_and_headers(char** ppStatus, char* zHeaders) { + char* cr = strchr(zHeaders, '\r'); + *ppStatus = sqlite3_mprintf("%.*s", cr - zHeaders, zHeaders); + memmove(zHeaders, cr + 2, strlen(cr + 2) + 1); +} + +static int httpConnect(sqlite3* db, + void* pAux, + int argc, + const char* const* argv, + sqlite3_vtab** ppVtab, + char** pzErr) { + http_vtab* pNew; + int rc; + + rc = sqlite3_declare_vtab(db, + "CREATE TABLE x(response_status TEXT, " + "response_status_code INT, response_headers TEXT, " + "response_body BLOB, request_method TEXT HIDDEN, " + "request_url TEXT HIDDEN, " + "request_headers TEXT HIDDEN, " + "request_body BLOB HIDDEN)"); + +#define HTTP_COL_RESPONSE_STATUS 0 +#define HTTP_COL_RESPONSE_STATUS_CODE 1 +#define HTTP_COL_RESPONSE_HEADERS 2 +#define HTTP_COL_RESPONSE_BODY 3 +#define HTTP_COL_REQUEST_METHOD 4 +#define HTTP_COL_REQUEST_URL 5 +#define HTTP_COL_REQUEST_HEADERS 6 +#define HTTP_COL_REQUEST_BODY 7 + + if (rc == SQLITE_OK) { + pNew = sqlite3_malloc(sizeof(*pNew)); + *ppVtab = (sqlite3_vtab*)pNew; + if (pNew == 0) + return SQLITE_NOMEM; + memset(pNew, 0, sizeof(*pNew)); + if (sqlite3_stricmp(argv[0], "http_get") == 0) { + pNew->zMethod = sqlite3_mprintf("GET"); + } else if (sqlite3_stricmp(argv[0], "http_post") == 0) { + pNew->zMethod = sqlite3_mprintf("POST"); + } + } + return rc; +} + +static int httpDisconnect(sqlite3_vtab* pVtab) { + http_vtab* p = (http_vtab*)pVtab; + sqlite3_free(p->zMethod); + sqlite3_free(p); + return SQLITE_OK; +} + +static int httpOpen(sqlite3_vtab* p, sqlite3_vtab_cursor** ppCursor) { + http_cursor* pCur; + http_vtab* pVtab = (http_vtab*)p; + pCur = sqlite3_malloc(sizeof(*pCur)); + if (pCur == 0) + return SQLITE_NOMEM; + memset(pCur, 0, sizeof(*pCur)); + if (pVtab->zMethod) { + pCur->req.zMethod = sqlite3_mprintf("%s", pVtab->zMethod); + } + *ppCursor = &pCur->base; + return SQLITE_OK; +} + +static int httpClose(sqlite3_vtab_cursor* cur) { + http_cursor* pCur = (http_cursor*)cur; + sqlite3_free(pCur->resp.pBody); + sqlite3_free(pCur->req.zUrl); + sqlite3_free(pCur->req.zMethod); + sqlite3_free(pCur); + return SQLITE_OK; +} + +static int httpNext(sqlite3_vtab_cursor* cur) { + http_cursor* pCur = (http_cursor*)cur; + pCur->iRowid++; + return SQLITE_OK; +} + +static int httpColumn(sqlite3_vtab_cursor* cur, sqlite3_context* ctx, int i) { + http_cursor* pCur = (http_cursor*)cur; + + switch (i) { + case HTTP_COL_RESPONSE_STATUS: + sqlite3_result_text(ctx, pCur->resp.zStatus, -1, SQLITE_TRANSIENT); + break; + + case HTTP_COL_RESPONSE_STATUS_CODE: + sqlite3_result_int(ctx, pCur->resp.iStatusCode); + break; + + case HTTP_COL_RESPONSE_HEADERS: + sqlite3_result_text(ctx, pCur->resp.zHeaders, -1, SQLITE_TRANSIENT); + break; + + case HTTP_COL_RESPONSE_BODY: + sqlite3_result_blob(ctx, pCur->resp.pBody, pCur->resp.szBody, SQLITE_TRANSIENT); + break; + + case HTTP_COL_REQUEST_METHOD: + sqlite3_result_text(ctx, pCur->req.zMethod, -1, SQLITE_TRANSIENT); + break; + + case HTTP_COL_REQUEST_URL: + sqlite3_result_text(ctx, pCur->req.zUrl, -1, SQLITE_TRANSIENT); + break; + + case HTTP_COL_REQUEST_HEADERS: + if (pCur->req.zHeaders) { + sqlite3_result_text(ctx, pCur->req.zHeaders, -1, SQLITE_TRANSIENT); + } else { + sqlite3_result_null(ctx); + } + break; + + case HTTP_COL_REQUEST_BODY: + if (pCur->req.pBody) { + sqlite3_result_blob(ctx, pCur->req.pBody, pCur->req.szBody, SQLITE_TRANSIENT); + } else { + sqlite3_result_null(ctx); + } + break; + } + + return SQLITE_OK; +} + +static int httpRowid(sqlite3_vtab_cursor* cur, sqlite_int64* pRowid) { + http_cursor* pCur = (http_cursor*)cur; + *pRowid = pCur->iRowid; + return SQLITE_OK; +} + +static int httpEof(sqlite3_vtab_cursor* cur) { + http_cursor* pCur = (http_cursor*)cur; + return pCur->iRowid >= 1; +} + +#define HTTP_FLAG_METHOD 1 +#define HTTP_FLAG_URL 2 +#define HTTP_FLAG_HEADERS 4 +#define HTTP_FLAG_BODY 8 + +static int httpFilter(sqlite3_vtab_cursor* pVtabCursor, + int idxNum, + const char* idxStr, + int argc, + sqlite3_value** argv) { + http_cursor* pCur = (http_cursor*)pVtabCursor; + http_vtab* pVtab = (http_vtab*)pVtabCursor->pVtab; + int bIsDo = pVtab->zMethod == NULL; + char* zErrMsg = NULL; + int rc; + if (bIsDo) { + pCur->req.zMethod = sqlite3_mprintf("%s", sqlite3_value_text(argv[0])); + pCur->req.zUrl = sqlite3_mprintf("%s", sqlite3_value_text(argv[1])); + if (idxNum & HTTP_FLAG_HEADERS) { + pCur->req.zHeaders = sqlite3_mprintf("%s", sqlite3_value_text(argv[2])); + } + if (idxNum & HTTP_FLAG_BODY) { + pCur->req.szBody = sqlite3_value_bytes(argv[3]); + pCur->req.pBody = sqlite3_value_blob(argv[3]); + } + } else { + pCur->req.zUrl = sqlite3_mprintf("%s", sqlite3_value_text(argv[0])); + if (idxNum & HTTP_FLAG_HEADERS) { + pCur->req.zHeaders = sqlite3_mprintf("%s", sqlite3_value_text(argv[1])); + } + if (idxNum & HTTP_FLAG_BODY) { + pCur->req.szBody = sqlite3_value_bytes(argv[2]); + pCur->req.pBody = sqlite3_value_blob(argv[2]); + } + } + rc = http_do_request(&pCur->req, &pCur->resp, &zErrMsg); + if (rc != SQLITE_OK) { + sqlite3_free(pCur->base.pVtab->zErrMsg); + pCur->base.pVtab->zErrMsg = zErrMsg; + } + return rc; +} + +static int httpBestIndex(sqlite3_vtab* tab, sqlite3_index_info* pIdxInfo) { + http_vtab* pTab = (http_vtab*)tab; + int bIsDo = pTab->zMethod == NULL; + int i; + int idxNum = 0; + const struct sqlite3_index_constraint* pConstraint; + + pConstraint = pIdxInfo->aConstraint; + + for (i = 0; i < pIdxInfo->nConstraint; ++i, ++pConstraint) { + if (!pIdxInfo->aConstraint[i].usable) { + return SQLITE_CONSTRAINT; + } + if (pConstraint->op != SQLITE_INDEX_CONSTRAINT_EQ) { + return SQLITE_CONSTRAINT; + }; + switch (pConstraint->iColumn) { + case HTTP_COL_REQUEST_METHOD: + if (bIsDo) { + idxNum |= HTTP_FLAG_METHOD; + } else { + idxNum |= HTTP_FLAG_URL; + } + pIdxInfo->aConstraintUsage[i].argvIndex = 1; + pIdxInfo->aConstraintUsage[i].omit = 1; + break; + + case HTTP_COL_REQUEST_URL: + if (bIsDo) { + idxNum |= HTTP_FLAG_URL; + } else { + idxNum |= HTTP_FLAG_HEADERS; + } + pIdxInfo->aConstraintUsage[i].argvIndex = 2; + pIdxInfo->aConstraintUsage[i].omit = 1; + break; + + case HTTP_COL_REQUEST_HEADERS: + if (bIsDo) { + idxNum |= HTTP_FLAG_HEADERS; + } else { + idxNum |= HTTP_FLAG_BODY; + } + pIdxInfo->aConstraintUsage[i].argvIndex = 3; + pIdxInfo->aConstraintUsage[i].omit = 1; + break; + + case HTTP_COL_REQUEST_BODY: + if (bIsDo) { + idxNum |= HTTP_FLAG_BODY; + } else { + sqlite3_free(tab->zErrMsg); + tab->zErrMsg = sqlite3_mprintf("too many arguments"); + return SQLITE_ERROR; + } + pIdxInfo->aConstraintUsage[i].argvIndex = 4; + pIdxInfo->aConstraintUsage[i].omit = 1; + break; + } + } + + if (bIsDo && !(idxNum & HTTP_FLAG_METHOD)) { + sqlite3_free(tab->zErrMsg); + tab->zErrMsg = sqlite3_mprintf("method missing"); + return SQLITE_ERROR; + } + + if (!(idxNum & HTTP_FLAG_URL)) { + sqlite3_free(tab->zErrMsg); + tab->zErrMsg = sqlite3_mprintf("url missing"); + return SQLITE_ERROR; + } + + pIdxInfo->estimatedCost = (double)1; + pIdxInfo->estimatedRows = 1; + pIdxInfo->idxNum = idxNum; + + return SQLITE_OK; +} + +static sqlite3_module httpModule = { + /* iVersion */ 0, + /* xCreate */ 0, + /* xConnect */ httpConnect, + /* xBestIndex */ httpBestIndex, + /* xDisconnect */ httpDisconnect, + /* xDestroy */ 0, + /* xOpen */ httpOpen, + /* xClose */ httpClose, + /* xFilter */ httpFilter, + /* xNext */ httpNext, + /* xEof */ httpEof, + /* xColumn */ httpColumn, + /* xRowid */ httpRowid, + /* xUpdate */ 0, + /* xBegin */ 0, + /* xSync */ 0, + /* xCommit */ 0, + /* xRollback */ 0, + /* xFindMethod */ 0, + /* xRename */ 0, + /* xSavepoint */ 0, + /* xRelease */ 0, + /* xRollbackTo */ 0, + /* xShadowName */ 0, +}; + +static void httpSimpleFunc(sqlite3_context* ctx, + int argc, + sqlite3_value** argv, + const char* zTable, + const char* zColumn, + int bText) { + char* zSql; + int rc; + sqlite3_stmt* pStmt; + + if (argc == 0) { + sqlite3_result_error(ctx, "not enough arguments", -1); + return; + } + + if (argc == 1) { + zSql = sqlite3_mprintf( + "select \"%w\" from \"%w\"(%Q)", zColumn, zTable, sqlite3_value_text(argv[0])); + } + + if (argc == 2) { + zSql = sqlite3_mprintf("select \"%w\" from \"%w\"(%Q, %Q)", + zColumn, + zTable, + sqlite3_value_text(argv[0]), + sqlite3_value_text(argv[1])); + } + + if (argc == 3) { + zSql = sqlite3_mprintf("select \"%w\" from \"%w\"(%Q, %Q, %Q)", + zColumn, + zTable, + sqlite3_value_text(argv[0]), + sqlite3_value_text(argv[1]), + sqlite3_value_text(argv[2])); + } + + if (argc >= 4) { + zSql = sqlite3_mprintf("select \"%w\" from \"%w\"(%Q, %Q, %Q, %Q)", + zColumn, + zTable, + sqlite3_value_text(argv[0]), + sqlite3_value_text(argv[1]), + sqlite3_value_text(argv[2]), + sqlite3_value_text(argv[2])); + } + + if (zSql == 0) { + sqlite3_result_error_code(ctx, SQLITE_NOMEM); + return; + } + + rc = sqlite3_prepare_v2(sqlite3_context_db_handle(ctx), zSql, -1, &pStmt, 0); + + sqlite3_free(zSql); + + if (rc != SQLITE_OK) { + sqlite3_result_error_code(ctx, rc); + return; + } + + rc = sqlite3_step(pStmt); + + if (rc == SQLITE_ROW) { + int size = sqlite3_column_bytes(pStmt, 0); + if (bText) { + sqlite3_result_text(ctx, sqlite3_column_text(pStmt, 0), size, SQLITE_TRANSIENT); + } else { + sqlite3_result_blob(ctx, sqlite3_column_blob(pStmt, 0), size, SQLITE_TRANSIENT); + } + sqlite3_finalize(pStmt); + return; + } + + rc = sqlite3_finalize(pStmt); + if (rc != SQLITE_OK) { + sqlite3_result_error_code(ctx, rc); + } +} + +static void httpGetBodyFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + httpSimpleFunc(ctx, argc, argv, "http_get", "response_body", 0); +} + +static void httpPostBodyFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + httpSimpleFunc(ctx, argc, argv, "http_post", "response_body", 0); +} + +static void httpDoBodyFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + httpSimpleFunc(ctx, argc, argv, "http_do", "response_body", 0); +} + +static void httpGetHeadersFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + httpSimpleFunc(ctx, argc, argv, "http_get", "response_headers", 1); +} + +static void httpPostHeadersFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + httpSimpleFunc(ctx, argc, argv, "http_post", "response_headers", 1); +} + +static void httpDoHeadersFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + httpSimpleFunc(ctx, argc, argv, "http_do", "response_headers", 1); +} + +static void httpHeadersFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + char* zHeaders = NULL; + int i; + int size = 0; + int offset = 0; + if (argc < 2) { + sqlite3_result_error(ctx, "http_headers: expected at least 2 arguments", -1); + return; + } + if (argc % 2 != 0) { + sqlite3_result_error(ctx, "http_headers: even number of arguments expected", -1); + return; + } + for (i = 0; i < argc; i += 2) { + size += sqlite3_value_bytes(argv[i]) + 2; + size += sqlite3_value_bytes(argv[i + 1]) + 2; + } + zHeaders = sqlite3_malloc(size); + if (!zHeaders) { + sqlite3_result_error_code(ctx, SQLITE_NOMEM); + return; + } + for (i = 0; i < argc; i += 2) { + memcpy(zHeaders + offset, sqlite3_value_text(argv[i]), sqlite3_value_bytes(argv[i])); + offset += sqlite3_value_bytes(argv[i]); + + memcpy(zHeaders + offset, ": ", 2); + offset += 2; + + memcpy( + zHeaders + offset, sqlite3_value_text(argv[i + 1]), sqlite3_value_bytes(argv[i + 1])); + offset += sqlite3_value_bytes(argv[i + 1]); + + memcpy(zHeaders + offset, "\r\n", 2); + offset += 2; + } + assert(offset == size); + sqlite3_result_text(ctx, zHeaders, size, sqlite3_free); +} + +static void httpHeadersHasFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + int zHeadersSize = sqlite3_value_bytes(argv[0]); + const char* zHeaders = sqlite3_value_text(argv[0]); + int zHeaderSize = sqlite3_value_bytes(argv[1]); + const char* zHeader = sqlite3_value_text(argv[1]); + while (zHeadersSize > 0) { + int iParsed = 0; + const char* pName; + int iNameSize; + int rc = http_next_header(zHeaders, zHeadersSize, &iParsed, &pName, &iNameSize, NULL, NULL); + if (rc == SQLITE_ERROR) { + sqlite3_result_error(ctx, "http_headers_has: malformed headers", -1); + return; + } + if (rc == SQLITE_DONE) { + sqlite3_result_int(ctx, 0); + return; + } + zHeaders += iParsed; + zHeadersSize -= iParsed; + if (iNameSize == zHeaderSize && sqlite3_strnicmp(zHeader, pName, zHeaderSize) == 0) { + sqlite3_result_int(ctx, 1); + return; + } + } + sqlite3_result_int(ctx, 0); +} + +static void httpHeadersGetFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + int zHeadersSize = sqlite3_value_bytes(argv[0]); + const char* zHeaders = sqlite3_value_text(argv[0]); + int zHeaderSize = sqlite3_value_bytes(argv[1]); + const char* zHeader = sqlite3_value_text(argv[1]); + while (zHeadersSize > 0) { + int iParsed = 0; + const char* pName; + int iNameSize; + const char* pValue; + int iValueSize; + int rc = http_next_header( + zHeaders, zHeadersSize, &iParsed, &pName, &iNameSize, &pValue, &iValueSize); + if (rc == SQLITE_ERROR) { + sqlite3_result_error(ctx, "http_headers_get: malformed headers", -1); + return; + } + if (rc == SQLITE_DONE) { + sqlite3_result_int(ctx, 0); + return; + } + zHeaders += iParsed; + zHeadersSize -= iParsed; + if (iNameSize == zHeaderSize && sqlite3_strnicmp(zHeader, pName, zHeaderSize) == 0) { + sqlite3_result_text(ctx, pValue, iValueSize, SQLITE_TRANSIENT); + return; + } + } +} + +#define HTTP_HEADERS_EACH_COL_NAME 0 +#define HTTP_HEADERS_EACH_COL_VALUE 1 +#define HTTP_HEADERS_EACH_COL_HEADERS 2 + +typedef struct http_headers_each_vtab http_headers_each_vtab; +struct http_headers_each_vtab { + sqlite3_vtab base; +}; + +typedef struct http_headers_each_cursor http_headers_each_cursor; +struct http_headers_each_cursor { + sqlite3_vtab_cursor base; + sqlite3_int64 iRowid; + const char* zHeaders; + int zHeadersSize; + const char* p; + const char* name; + int nameSize; + const char* value; + int valueSize; + int available; + int done; +}; + +static int httpHeadersEachConnect(sqlite3* db, + void* pAux, + int argc, + const char* const* argv, + sqlite3_vtab** ppVtab, + char** pzErr) { + http_headers_each_vtab* pNew; + int rc; + rc = sqlite3_declare_vtab(db, "CREATE TABLE x(name TEXT, value TEXT, headers TEXT HIDDEN)"); + if (rc == SQLITE_OK) { + pNew = sqlite3_malloc(sizeof(*pNew)); + *ppVtab = (sqlite3_vtab*)pNew; + if (pNew == 0) + return SQLITE_NOMEM; + memset(pNew, 0, sizeof(*pNew)); + } + return rc; +} + +static int httpHeadersEachDisconnect(sqlite3_vtab* pVtab) { + http_headers_each_vtab* p = (http_headers_each_vtab*)pVtab; + sqlite3_free(p); + return SQLITE_OK; +} + +static int httpHeadersEachOpen(sqlite3_vtab* p, sqlite3_vtab_cursor** ppCursor) { + http_headers_each_cursor* pCur; + pCur = sqlite3_malloc(sizeof(*pCur)); + if (pCur == 0) + return SQLITE_NOMEM; + memset(pCur, 0, sizeof(*pCur)); + *ppCursor = &pCur->base; + return SQLITE_OK; +} + +static int httpHeadersEachClose(sqlite3_vtab_cursor* cur) { + http_headers_each_cursor* pCur = (http_headers_each_cursor*)cur; + sqlite3_free(pCur); + return SQLITE_OK; +} + +static int httpHeadersEachNext(sqlite3_vtab_cursor* cur) { + http_headers_each_cursor* pCur = (http_headers_each_cursor*)cur; + pCur->iRowid++; + int nparsed = 0; + int rc = http_next_header(pCur->p, + pCur->available, + &nparsed, + &pCur->name, + &pCur->nameSize, + &pCur->value, + &pCur->valueSize); + pCur->p += nparsed; + pCur->available -= nparsed; + if (rc == SQLITE_ERROR) { + sqlite3_free(cur->pVtab->zErrMsg); + cur->pVtab->zErrMsg = sqlite3_mprintf("http_headers_each: malformed headers"); + return SQLITE_ERROR; + } + pCur->done = rc == SQLITE_DONE; + return SQLITE_OK; +} + +static int httpHeadersEachColumn(sqlite3_vtab_cursor* cur, sqlite3_context* ctx, int i) { + http_headers_each_cursor* pCur = (http_headers_each_cursor*)cur; + switch (i) { + case HTTP_HEADERS_EACH_COL_NAME: + sqlite3_result_text(ctx, pCur->name, pCur->nameSize, SQLITE_TRANSIENT); + break; + + case HTTP_HEADERS_EACH_COL_VALUE: + sqlite3_result_text(ctx, pCur->value, pCur->valueSize, SQLITE_TRANSIENT); + break; + + case HTTP_HEADERS_EACH_COL_HEADERS: + sqlite3_result_text(ctx, pCur->zHeaders, pCur->zHeadersSize, SQLITE_TRANSIENT); + break; + + default: + sqlite3_result_error(ctx, "unknown column", -1); + break; + } + return SQLITE_OK; +} + +static int httpHeadersEachRowid(sqlite3_vtab_cursor* cur, sqlite_int64* pRowid) { + http_headers_each_cursor* pCur = (http_headers_each_cursor*)cur; + *pRowid = pCur->iRowid; + return SQLITE_OK; +} + +static int httpHeadersEachEof(sqlite3_vtab_cursor* cur) { + http_headers_each_cursor* pCur = (http_headers_each_cursor*)cur; + return pCur->done; +} + +static int httpHeadersEachFilter(sqlite3_vtab_cursor* pVtabCursor, + int idxNum, + const char* idxStr, + int argc, + sqlite3_value** argv) { + http_headers_each_cursor* pCur = (http_headers_each_cursor*)pVtabCursor; + pCur->zHeadersSize = sqlite3_value_bytes(argv[0]); + pCur->available = pCur->zHeadersSize; + pCur->zHeaders = sqlite3_value_text(argv[0]); + pCur->p = pCur->zHeaders; + int nparsed = 0; + int rc = http_next_header(pCur->p, + pCur->available, + &nparsed, + &pCur->name, + &pCur->nameSize, + &pCur->value, + &pCur->valueSize); + pCur->p += nparsed; + pCur->available -= nparsed; + pCur->iRowid = 1; + if (rc == SQLITE_ERROR) { + sqlite3_free(pVtabCursor->pVtab->zErrMsg); + pVtabCursor->pVtab->zErrMsg = sqlite3_mprintf("http_headers_each: malformed headers"); + return SQLITE_ERROR; + } + pCur->done = rc == SQLITE_DONE; + return SQLITE_OK; +} + +static int httpHeadersEachBestIndex(sqlite3_vtab* tab, sqlite3_index_info* pIdxInfo) { + http_vtab* pTab = (http_vtab*)tab; + int bHeadersSeen = 0; + + for (int i = 0; i < pIdxInfo->nConstraint; ++i) { + if (pIdxInfo->aConstraint[i].iColumn == HTTP_HEADERS_EACH_COL_HEADERS) { + if (!pIdxInfo->aConstraint[i].usable) { + return SQLITE_CONSTRAINT; + } + pIdxInfo->aConstraintUsage[i].argvIndex = 1; + pIdxInfo->aConstraintUsage[i].omit = 1; + bHeadersSeen = 1; + } + } + + if (!bHeadersSeen) { + sqlite3_free(tab->zErrMsg); + tab->zErrMsg = sqlite3_mprintf("headers missing"); + return SQLITE_ERROR; + } + + pIdxInfo->estimatedCost = (double)1; + // pIdxInfo->estimatedRows = 1; + + return SQLITE_OK; +} + +static sqlite3_module httpHeadersEachModule = { + /* iVersion */ 0, + /* xCreate */ 0, + /* xConnect */ httpHeadersEachConnect, + /* xBestIndex */ httpHeadersEachBestIndex, + /* xDisconnect */ httpHeadersEachDisconnect, + /* xDestroy */ 0, + /* xOpen */ httpHeadersEachOpen, + /* xClose */ httpHeadersEachClose, + /* xFilter */ httpHeadersEachFilter, + /* xNext */ httpHeadersEachNext, + /* xEof */ httpHeadersEachEof, + /* xColumn */ httpHeadersEachColumn, + /* xRowid */ httpHeadersEachRowid, + /* xUpdate */ 0, + /* xBegin */ 0, + /* xSync */ 0, + /* xCommit */ 0, + /* xRollback */ 0, + /* xFindMethod */ 0, + /* xRename */ 0, + /* xSavepoint */ 0, + /* xRelease */ 0, + /* xRollbackTo */ 0, + /* xShadowName */ 0, +}; + +static const struct Func { + const char* name; + void (*xFunc)(sqlite3_context*, int, sqlite3_value**); +} funcs[] = { + {"http_get_body", httpGetBodyFunc}, + {"http_post_body", httpPostBodyFunc}, + {"http_do_body", httpDoBodyFunc}, + {"http_get_headers", httpGetHeadersFunc}, + {"http_post_headers", httpPostHeadersFunc}, + {"http_do_headers", httpDoHeadersFunc}, + {"http_headers", httpHeadersFunc}, + {"http_headers_has", httpHeadersHasFunc}, + {"http_headers_get", httpHeadersGetFunc}, + {NULL, NULL}, +}; + +static const struct Module { + const char* name; + const sqlite3_module* module; +} modules[] = { + {"http_get", &httpModule}, + {"http_post", &httpModule}, + {"http_do", &httpModule}, + {"http_headers_each", &httpHeadersEachModule}, + {NULL, NULL}, +}; + +#ifdef _WIN32 +__declspec(dllexport) +#endif + int sqlite3_http_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines* pApi) { + int rc = SQLITE_OK; + SQLITE_EXTENSION_INIT2(pApi); + int i; + for (i = 0; funcs[i].name && rc == SQLITE_OK; ++i) { + rc = sqlite3_create_function( + db, funcs[i].name, -1, SQLITE_UTF8, NULL, funcs[i].xFunc, NULL, NULL); + } + for (i = 0; modules[i].name && rc == SQLITE_OK; ++i) { + rc = sqlite3_create_module(db, modules[i].name, modules[i].module, 0); + } + return rc; +} + +/********** src/http_backend_curl.c **********/ + +#ifdef HTTP_BACKEND_CURL + +#include "http.h" + +#include <assert.h> +#include <string.h> + +SQLITE_EXTENSION_INIT3 + +#ifdef _WIN32 +#include <windows.h> + +void* http_dlopen(const char* zName) { + return LoadLibraryA(zName); +} + +void http_dlclose(void* library) { + FreeLibrary(library); +} + +void* http_dlsym(void* library, const char* zName) { + return GetProcAddress(library, zName); +} +#else +#include <dlfcn.h> + +void* http_dlopen(const char* zName) { + return dlopen(zName, RTLD_NOW); +} + +void http_dlclose(void* library) { + dlclose(zName); +} + +void* http_dlsym(void* library, const char* zName) { + return dlsym(library, zName); +} +#endif + +#ifndef MIN +#define MIN(A, B) ((A) < (B) ? (A) : (B)) +#endif + +typedef struct CURL CURL; +typedef int CURLcode; +typedef int CURLoption; +typedef int CURLINFO; +typedef int CURLversion; + +#define CURL_ERROR_SIZE 256 + +#define CURLE_OK 0 + +#define CURLOPT_ERRORBUFFER (10000 + 10) +#define CURLOPT_URL (10000 + 2) +#define CURLOPT_FOLLOWLOCATION (52) +#define CURLOPT_HTTPGET (80) +#define CURLOPT_POST (47) +#define CURLOPT_SSL_OPTIONS (216) +#define CURLOPT_WRITEFUNCTION (20000 + 11) +#define CURLOPT_WRITEDATA (10000 + 1) +#define CURLOPT_READFUNCTION (20000 + 12) +#define CURLOPT_READDATA (10000 + 9) +#define CURLOPT_HEADERFUNCTION (20000 + 79) +#define CURLOPT_HEADERDATA (10000 + 29) +#define CURLOPT_POSTFIELDS (10000 + 15) +#define CURLOPT_POSTFIELDSIZE_LARGE (30000 + 120) +#define CURLOPT_HTTPHEADER (10000 + 23) +#define CURLOPT_INFILESIZE_LARGE (30000 + 115) +#define CURLOPT_NOBODY (44) +#define CURLOPT_CUSTOMREQUEST (10000 + 36) +#define CURLOPT_PUT (54) + +#define CURLSSLOPT_NATIVE_CA (1 << 4) + +#define CURLVERSION_NOW 9 + +#define CURLINFO_RESPONSE_CODE (0x200000 + 2) + +// The struct is bigger but I don't need more information for now... +struct curl_version_info_data { + CURLversion age; + const char* version; + unsigned int version_num; +}; +typedef struct curl_version_info_data curl_version_info_data; + +struct curl_slist; + +typedef CURL* (*curl_easy_init_t)(); +typedef void (*curl_easy_cleanup_t)(CURL*); +typedef CURLcode (*curl_easy_setopt_t)(CURL*, CURLoption, ...); +typedef CURLcode (*curl_easy_perform_t)(CURL*); +typedef CURLcode (*curl_easy_getinfo_t)(CURL*, CURLINFO, ...); +typedef char* (*curl_version_t)(); +typedef curl_version_info_data* (*curl_version_info_t)(CURLversion); +typedef struct curl_slist* (*curl_slist_append_t)(struct curl_slist*, const char*); +typedef void (*curl_slist_free_all_t)(struct curl_slist*); +typedef const char* (*curl_easy_strerror_t)(CURLcode); + +struct curl_api_routines { + void* pLibrary; + curl_easy_init_t easy_init; + curl_easy_cleanup_t easy_cleanup; + curl_easy_setopt_t easy_setopt; + curl_easy_perform_t easy_perform; + curl_easy_getinfo_t easy_getinfo; + curl_version_t version; + curl_version_info_t version_info; + curl_slist_append_t slist_append; + curl_slist_free_all_t slist_free_all; + curl_easy_strerror_t easy_strerror; +}; + +static struct curl_api_routines curl_api; + +#define curl_easy_init curl_api.easy_init +#define curl_easy_cleanup curl_api.easy_cleanup +#define curl_easy_setopt curl_api.easy_setopt +#define curl_easy_perform curl_api.easy_perform +#define curl_easy_getinfo curl_api.easy_getinfo +#define curl_version curl_api.version +#define curl_version_info curl_api.version_info +#define curl_slist_append curl_api.slist_append +#define curl_slist_free_all curl_api.slist_free_all +#define curl_easy_strerror curl_api.easy_strerror + +static const char* aCurlLibNames[] = { +#ifdef _WIN32 + "libcurl-x64.dll", + "libcurl.dll", +#else + "libcurl.so", + "libcurl.so.4", +#endif +}; + +static const int szCurlLibNames = sizeof(aCurlLibNames) / sizeof(aCurlLibNames[0]); + +int http_backend_curl_load(char** zErrMsg) { + assert(zErrMsg != NULL); + + *zErrMsg = NULL; + + if (!curl_api.pLibrary) { + for (int i = 0; i < szCurlLibNames && !curl_api.pLibrary; ++i) { + curl_api.pLibrary = http_dlopen(aCurlLibNames[i]); + } + } + + if (!curl_api.pLibrary) { + *zErrMsg = sqlite3_mprintf("failed to load curl"); + goto error; + } + + curl_easy_init = (curl_easy_init_t)http_dlsym(curl_api.pLibrary, "curl_easy_init"); + if (!curl_easy_init) { + *zErrMsg = sqlite3_mprintf("failed to load curl_easy_init"); + goto error; + } + curl_easy_cleanup = (curl_easy_cleanup_t)http_dlsym(curl_api.pLibrary, "curl_easy_cleanup"); + if (!curl_easy_cleanup) { + *zErrMsg = sqlite3_mprintf("failed to load curl_easy_cleanup"); + goto error; + } + curl_easy_setopt = (curl_easy_setopt_t)http_dlsym(curl_api.pLibrary, "curl_easy_setopt"); + if (!curl_easy_setopt) { + *zErrMsg = sqlite3_mprintf("failed to load curl_easy_setopt"); + goto error; + } + curl_easy_perform = (curl_easy_perform_t)http_dlsym(curl_api.pLibrary, "curl_easy_perform"); + if (!curl_easy_perform) { + *zErrMsg = sqlite3_mprintf("failed to load curl_easy_perform"); + goto error; + } + curl_easy_getinfo = (curl_easy_getinfo_t)http_dlsym(curl_api.pLibrary, "curl_easy_getinfo"); + if (!curl_easy_getinfo) { + *zErrMsg = sqlite3_mprintf("failed to load curl_easy_getinfo"); + goto error; + } + curl_version = (curl_version_t)http_dlsym(curl_api.pLibrary, "curl_version"); + if (!curl_version) { + *zErrMsg = sqlite3_mprintf("failed to load curl_version"); + goto error; + } + curl_version_info = (curl_version_info_t)http_dlsym(curl_api.pLibrary, "curl_version_info"); + if (!curl_version_info) { + *zErrMsg = sqlite3_mprintf("failed to load curl_version_info"); + goto error; + } + curl_slist_append = (curl_slist_append_t)http_dlsym(curl_api.pLibrary, "curl_slist_append"); + if (!curl_slist_append) { + *zErrMsg = sqlite3_mprintf("failed to load curl_slist_append"); + goto error; + } + curl_slist_free_all = + (curl_slist_free_all_t)http_dlsym(curl_api.pLibrary, "curl_slist_free_all"); + if (!curl_slist_free_all) { + *zErrMsg = sqlite3_mprintf("failed to load curl_slist_free_all"); + goto error; + } + curl_easy_strerror = (curl_easy_strerror_t)http_dlsym(curl_api.pLibrary, "curl_easy_strerror"); + if (!curl_easy_strerror) { + *zErrMsg = sqlite3_mprintf("failed to load curl_easy_strerror"); + goto error; + } + + return SQLITE_OK; + +error: + + if (curl_api.pLibrary) { + http_dlclose(curl_api.pLibrary); + memset(&curl_api, 0, sizeof(curl_api)); + } + + return SQLITE_ERROR; +} + +static size_t write_callback(char* ptr, size_t size, size_t nmemb, void* userdata) { + http_response* pResp = (http_response*)userdata; + char* p; + p = sqlite3_realloc(pResp->pBody, pResp->szBody + size * nmemb); + if (!p) { + return 0; + } + pResp->pBody = p; + memcpy((char*)pResp->pBody + pResp->szBody, ptr, size * nmemb); + pResp->szBody += size * nmemb; + return size * nmemb; +} + +static size_t header_callback(char* ptr, size_t size, size_t nmemb, void* userdata) { + http_response* pResp = (http_response*)userdata; + char* p; + p = sqlite3_realloc(pResp->zHeaders, pResp->szHeaders + size * nmemb + 1); + if (!p) { + return 0; + } + pResp->zHeaders = p; + memcpy(pResp->zHeaders + pResp->szHeaders, ptr, size * nmemb); + pResp->szHeaders += size * nmemb; + pResp->zHeaders[pResp->szHeaders] = '\0'; + return size * nmemb; +} + +struct readdata { + const char* pBody; + size_t szBody; +}; + +static size_t read_callback(char* ptr, size_t size, size_t nmemb, void* userdata) { + struct readdata* pData = (struct readdata*)userdata; + size_t szToSend = MIN(size * nmemb, pData->szBody); + memcpy(ptr, pData->pBody, szToSend); + pData->pBody += szToSend; + pData->szBody -= szToSend; + return szToSend; +} + +static int +headers_to_curl_headers(struct curl_slist** pHeaders, const char* zHeaders, int szHeaders) { + const char* name; + int szName; + const char* value; + int szValue; + while (szHeaders > 0) { + int nParsed = 0; + int rc = http_next_header(zHeaders, szHeaders, &nParsed, &name, &szName, &value, &szValue); + if (rc == SQLITE_DONE) { + break; + } else if (rc == SQLITE_ROW) { + char* zHeader = sqlite3_mprintf("%.*s: %.*s", szName, name, szValue, value); + if (!zHeader) { + return 0; + } + void* tmp = curl_slist_append(*pHeaders, zHeader); + sqlite3_free(zHeader); + if (!tmp) { + return 0; + } + *pHeaders = tmp; + szHeaders -= nParsed; + zHeaders += nParsed; + } else if (rc == SQLITE_ERROR) { + return 0; + } + } + return 1; +} + +static int set_curl_error_message(char** ppErrMsg, CURLcode rc, const char* message) { + *ppErrMsg = sqlite3_mprintf("%s: %s (curl error code %d)", message, curl_easy_strerror(rc), rc); + return SQLITE_ERROR; +} + +int http_do_request(http_request* req, http_response* resp, char** ppErrMsg) { + int rc; + CURL* curl; + char aErrorBuf[CURL_ERROR_SIZE]; + curl_version_info_data* pCurlVersionInfo; + long responseCode; + struct curl_slist* headers = NULL; + CURLcode curlrc; + struct readdata readdata; + + rc = http_backend_curl_load(ppErrMsg); + if (rc != SQLITE_OK) { + return rc; + } + + pCurlVersionInfo = curl_version_info(CURLVERSION_NOW); + + curl = curl_easy_init(); + if (!curl) { + *ppErrMsg = sqlite3_mprintf("curl_easy_init failed"); + rc = SQLITE_ERROR; + goto error; + } + + memset(aErrorBuf, 0, sizeof(aErrorBuf)); + if ((curlrc = curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, aErrorBuf)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_URL, req->zUrl)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if (sqlite3_stricmp(req->zMethod, "GET") == 0) { + if ((curlrc = curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } else if (sqlite3_stricmp(req->zMethod, "POST") == 0) { + if ((curlrc = curl_easy_setopt(curl, CURLOPT_POST, 1L)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + if ((curlrc = curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req->pBody)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + if ((curlrc = curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, req->szBody)) != + CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } else if (sqlite3_stricmp(req->zMethod, "PUT") == 0) { + if ((curlrc = curl_easy_setopt(curl, CURLOPT_PUT, 1L)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } else if (sqlite3_stricmp(req->zMethod, "HEAD") == 0) { + if ((curlrc = curl_easy_setopt(curl, CURLOPT_NOBODY, 1L)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } else { + if ((curlrc = curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if (req->pBody && req->szBody) { + if ((curlrc = curl_easy_setopt(curl, CURLOPT_PUT, 1L)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, req->zMethod)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } + + if (req->pBody && req->szBody && sqlite3_stricmp(req->zMethod, "POST") != 0) { + readdata.pBody = (const char*)req->pBody; + readdata.szBody = (size_t)req->szBody; + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, req->szBody)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_READDATA, &readdata)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } + + // Since 7.71.0 (Jun 24 2020). Makes life easier on Windows at least. + if (pCurlVersionInfo->version_num >= ((7 << 16) | (71 << 8))) { + if ((curlrc = curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA)) != + CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_WRITEDATA, resp)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_HEADERDATA, resp)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + // Remove content-type header by default... + headers = curl_slist_append(NULL, "content-type;"); + if (!headers) { + *ppErrMsg = sqlite3_mprintf("curl_slist_append failed"); + rc = SQLITE_ERROR; + goto error; + } + + if (req->zHeaders) { + if (!headers_to_curl_headers(&headers, req->zHeaders, strlen(req->zHeaders))) { + *ppErrMsg = sqlite3_mprintf("failed to convert headers for curl"); + rc = SQLITE_ERROR; + goto error; + } + if ((curlrc = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } + + if (curl_easy_perform(curl) != CURLE_OK) { + *ppErrMsg = sqlite3_mprintf("curl_easy_perform failed: %s", aErrorBuf); + rc = SQLITE_ERROR; + goto error; + } + + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &responseCode); + resp->iStatusCode = responseCode; + + remove_all_but_last_headers(resp->zHeaders); + separate_status_and_headers(&resp->zStatus, resp->zHeaders); + + rc = SQLITE_OK; + +error: + +done: + + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + + return rc; +} + +#endif // HTTP_BACKEND_CURL + +/********** src/http_backend_dummy.c **********/ + +#ifdef HTTP_BACKEND_DUMMY + +#include "http.h" + +#include <assert.h> + +SQLITE_EXTENSION_INIT3 + +static http_request sLastRequest; +static http_response* sResponse; +static char* sErrMsg; + +void http_backend_dummy_set_response(http_response* response) { + sResponse = response; +} + +void http_backend_dummy_set_errmsg(const char* zErrMsg) { + sErrMsg = sqlite3_mprintf("%s", zErrMsg); +} + +void http_backend_dummy_reset_request() { + sqlite3_free(sLastRequest.zMethod); + sqlite3_free((void*)sLastRequest.pBody); + sqlite3_free((void*)sLastRequest.zHeaders); + memset(&sLastRequest, 0, sizeof(sLastRequest)); +} + +const http_request* http_backend_dummy_get_last_request() { + return &sLastRequest; +} + +int http_do_request(http_request* req, http_response* resp, char** ppErrMsg) { + int rc = SQLITE_OK; + if (sResponse) { + *resp = *sResponse; + sResponse = NULL; + } else if (sErrMsg) { + *ppErrMsg = sErrMsg; + sErrMsg = NULL; + rc = SQLITE_ERROR; + } else { + assert(0); + } + http_backend_dummy_reset_request(); + if (req->zMethod) { + sLastRequest.zMethod = sqlite3_mprintf("%s", req->zMethod); + } + if (req->zUrl) { + sLastRequest.zUrl = sqlite3_mprintf("%s", req->zUrl); + } + if (req->pBody) { + sLastRequest.pBody = sqlite3_malloc(req->szBody); + memcpy((void*)sLastRequest.pBody, req->pBody, req->szBody); + } + sLastRequest.szBody = req->szBody; + if (req->zHeaders) { + sLastRequest.zHeaders = sqlite3_mprintf("%s", req->zHeaders); + } + return rc; +} + +#endif // HTTP_BACKEND_DUMMY + +/********** src/http_backend_winhttp.c **********/ + +#ifdef HTTP_BACKEND_WINHTTP + +#include "http.h" + +#include <windows.h> +#include <winhttp.h> + +#include <assert.h> + +SQLITE_EXTENSION_INIT3 + +static char* unicode_to_utf8(LPCWSTR zWide) { + DWORD nSize; + char* zUtf8; + nSize = WideCharToMultiByte(CP_UTF8, 0, zWide, -1, NULL, 0, NULL, NULL); + if (nSize == 0) { + return NULL; + } + zUtf8 = sqlite3_malloc(nSize); + if (!zUtf8) { + return NULL; + } + nSize = WideCharToMultiByte(CP_UTF8, 0, zWide, -1, zUtf8, nSize, NULL, NULL); + if (nSize == 0) { + sqlite3_free(zUtf8); + return NULL; + } + return zUtf8; +} + +static LPWSTR utf8_to_unicode(const char* zText) { + DWORD nSize; + LPWSTR zWide; + nSize = MultiByteToWideChar(CP_UTF8, 0, zText, -1, NULL, 0); + if (nSize == 0) { + return NULL; + } + zWide = sqlite3_malloc(sizeof(*zWide) * nSize); + if (!zWide) { + return NULL; + } + nSize = MultiByteToWideChar(CP_UTF8, 0, zText, -1, zWide, nSize); + if (nSize == 0) { + sqlite3_free(zWide); + return NULL; + } + return zWide; +} + +static char* win32_get_last_error(DWORD code) { + LPWSTR zWide = NULL; + FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + code, + 0, + (LPWSTR)&zWide, + 0, + NULL); + char* zUtf8 = unicode_to_utf8(zWide); + LocalFree(zWide); + return zUtf8; +} + +static int query_headers(HINTERNET request, DWORD dwInfoLevel, char** ppResult) { + DWORD szWide = 0; + LPWSTR zWide = NULL; + + WinHttpQueryHeaders( + request, dwInfoLevel, WINHTTP_HEADER_NAME_BY_INDEX, NULL, &szWide, WINHTTP_NO_HEADER_INDEX); + + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + return SQLITE_ERROR; + } + + zWide = sqlite3_malloc(szWide); + if (!zWide) { + return SQLITE_NOMEM; + } + + if (!WinHttpQueryHeaders(request, + dwInfoLevel, + WINHTTP_HEADER_NAME_BY_INDEX, + zWide, + &szWide, + WINHTTP_NO_HEADER_INDEX)) { + sqlite3_free(zWide); + return SQLITE_ERROR; + } + + *ppResult = unicode_to_utf8(zWide); + + sqlite3_free(zWide); + + return SQLITE_OK; +} + +int http_do_request(http_request* req, http_response* resp, char** ppErrMsg) { + LPWSTR zUrlWide = NULL; + LPWSTR zHostWide = NULL; + LPWSTR zMethodWide = NULL; + LPWSTR zHeadersWide = NULL; + HINTERNET session = NULL; + HINTERNET conn = NULL; + HINTERNET request = NULL; + URL_COMPONENTS components; + DWORD lastErr = 0; + const char* errFunc = NULL; + char* zErrMsg = NULL; + int rc = SQLITE_ERROR; + LPWSTR zWideResponseHeaders = NULL; + DWORD szWideResponseHeaders = 0; + DWORD dwSize = 0; + DWORD dwStatusCode; + + zUrlWide = utf8_to_unicode(req->zUrl); + if (!zUrlWide) { + rc = SQLITE_NOMEM; + goto error; + } + + memset(&components, 0, sizeof(components)); + components.dwStructSize = sizeof(components); + components.dwHostNameLength = 1; + components.dwUrlPathLength = 1; + if (!WinHttpCrackUrl(zUrlWide, 0, 0, &components)) { + lastErr = GetLastError(); + errFunc = "WinHttpCrackUrl"; + goto error; + } + + session = WinHttpOpen(L"sqlite3-http", + WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + 0); + + if (!session) { + lastErr = GetLastError(); + errFunc = "WinHttpCrackOpen"; + goto error; + } + + // WinHttpSetStatusCallback(session, + // (WINHTTP_STATUS_CALLBACK)statusCallback, + // WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS, + // 0); + + zHostWide = sqlite3_malloc(sizeof(wchar_t) * (components.dwHostNameLength + 1)); + if (!zHostWide) { + rc = SQLITE_NOMEM; + goto error; + } + memcpy(zHostWide, components.lpszHostName, sizeof(wchar_t) * components.dwHostNameLength); + zHostWide[components.dwHostNameLength] = L'\0'; + + conn = WinHttpConnect(session, zHostWide, components.nPort, 0); + + if (!conn) { + lastErr = GetLastError(); + errFunc = "WinHttpConnect"; + goto error; + } + + zMethodWide = utf8_to_unicode(req->zMethod); + if (!zMethodWide) { + rc = SQLITE_NOMEM; + goto error; + } + + request = WinHttpOpenRequest(conn, + zMethodWide, + components.lpszUrlPath, + NULL, + WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, + components.nPort == 443 ? WINHTTP_FLAG_SECURE : 0); + + if (!request) { + lastErr = GetLastError(); + errFunc = "WinHttpOpenRequest"; + goto error; + } + + if (req->zHeaders) { + zHeadersWide = utf8_to_unicode(req->zHeaders); + } + + if (!WinHttpSendRequest(request, + zHeadersWide, + zHeadersWide ? -1 : 0, + (void*)req->pBody, + req->szBody, + req->szBody, + 0)) { + lastErr = GetLastError(); + errFunc = "WinHttpSendRequest"; + goto error; + } + + if (!WinHttpReceiveResponse(request, NULL)) { + lastErr = GetLastError(); + errFunc = "WinHttpReceiveResponse"; + goto error; + } + + assert(resp->pBody == NULL); + assert(resp->szBody == 0); + for (;;) { + DWORD dwSize = 0; + DWORD nRead = 0; + char* nb; + if (!WinHttpQueryDataAvailable(request, &dwSize)) { + lastErr = GetLastError(); + errFunc = "WinHttpQueryDataAvailable"; + goto error; + } + if (dwSize == 0) { + break; + } + nb = sqlite3_realloc(resp->pBody, resp->szBody + dwSize); + if (!nb) { + rc = SQLITE_NOMEM; + goto error; + } + resp->pBody = nb; + if (!WinHttpReadData(request, (char*)resp->pBody + resp->szBody, dwSize, &nRead)) { + lastErr = GetLastError(); + errFunc = "WinHttpReadData"; + goto error; + } + resp->szBody += dwSize; + if (nRead == 0) { + break; + } + } + + rc = query_headers(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, &resp->zHeaders); + if (rc != SQLITE_OK) { + if (rc == SQLITE_ERROR) { + lastErr = GetLastError(); + errFunc = "WinHttpQueryHeaders(WINHTTP_QUERY_RAW_HEADERS_CRLF)"; + } + goto error; + } + rc = SQLITE_ERROR; // need to reset this back. Otherwise a jump to error below + // could leave rc as SQLITE_OK... + + dwSize = sizeof(dwStatusCode); + if (!WinHttpQueryHeaders(request, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, + &dwStatusCode, + &dwSize, + WINHTTP_NO_HEADER_INDEX)) { + lastErr = GetLastError(); + errFunc = "WinHttpQueryHeaders(WINHTTP_QUERY_STATUS_CODE)"; + goto error; + } + + resp->iStatusCode = dwStatusCode; + + remove_all_but_last_headers(resp->zHeaders); + separate_status_and_headers(&resp->zStatus, resp->zHeaders); + + rc = SQLITE_OK; + + goto done; + +error: + + if (rc == SQLITE_ERROR && errFunc) { + *ppErrMsg = win32_get_last_error(lastErr); + } + +done: + + sqlite3_free(zUrlWide); + sqlite3_free(zHostWide); + sqlite3_free(zMethodWide); + sqlite3_free(zHeadersWide); + WinHttpCloseHandle(request); + WinHttpCloseHandle(conn); + WinHttpCloseHandle(session); + + return rc; +} + +#endif // HTTP_BACKEND_WINHTTP + +/********** src/http_next_header.c **********/ + +#include <ctype.h> +#include <sqlite3.h> + +static int is_token_char(int c) { + return isalnum(c) || (c != 0 && strchr("!#$%&'*+-.^_`|~", c)); +} + +static int is_header_ws(int c) { + return c == ' ' || c == '\t'; +} + +static int is_vchar(int c) { + return c >= 0x21 && c <= 0x7e; +} + +// Return SQLITE_ROW if a header was extracted +// Return SQLITE_DONE if headers has been exhausted +// Return SQLITE_ERROR on malformed input +int http_next_header(const char* headers, + int size, + int* pParsed, + const char** ppName, + int* pNameSize, + const char** ppValue, + int* pValueSize) { + const char* headersEnd = headers + size; + const char* nameStart = NULL; + const char* nameEnd = NULL; + const char* valueStart = NULL; + const char* valueEnd = NULL; + const char* last = NULL; + const char* p = headers; + + enum { + StateInit, + StateName, + StateNameTrailingWs, + StateValueLeadingWs, + StateValue, + StateCR, + StateCheckFold, + StateDone, + StateError, + }; + + int state = StateInit; + + *ppName = NULL; + *pNameSize = 0; + *ppValue = NULL; + *pValueSize = 0; + + if (size == 0) { + *pParsed = 0; + return SQLITE_DONE; + } + + for (; p != headersEnd && state != StateDone && state != StateError; ++p) { + switch (state) { + case StateInit: + // printf("StateInit %02x %c\n", *p, isalnum(*p) ? *p : ' '); + if (*p == '\r') { + state = StateCR; + } else if (is_header_ws(*p)) { + // There shouldn't be whitespace here. Folding is handled in + // StateCR/StateCheckFold. + state = StateError; + } else { + nameStart = p; + state = StateName; + } + break; + + case StateName: + // printf("StateName %02x %c\n", *p, isalnum(*p) ? *p : ' '); + if (is_token_char(*p)) { + // accept + } else if (*p == ':') { + nameEnd = p; + state = StateValueLeadingWs; + } else if (is_header_ws(*p)) { + nameEnd = p; + state = StateNameTrailingWs; + } else { + state = StateError; + } + break; + + case StateNameTrailingWs: + // printf("StateNameTrailingWs %02x %c\n", *p, isalnum(*p) ? *p : ' '); + if (is_header_ws(*p)) { + // accept and skip + } else if (*p == ':') { + state = StateValueLeadingWs; + } else { + state = StateError; + } + break; + + case StateValueLeadingWs: + // printf("StateValueLeadingWs %02x %c\n", *p, isalnum(*p) ? *p : ' '); + if (is_header_ws(*p)) { + // accept and skip + } else if (*p == '\r') { + // empty value + state = StateCR; + } else if (is_vchar(*p)) { + valueStart = p; + last = p; + state = StateValue; + } else { + state = StateError; + } + break; + + case StateValue: + // printf("StateValue %02x %c\n", *p, isalnum(*p) ? *p : ' '); + if (is_header_ws(*p)) { + // accept but keep separate so that we can track trailing ws + } else if (*p == '\r') { + valueEnd = last + 1; + state = StateCR; + } else if (is_vchar(*p)) { + // accept + last = p; + } + break; + + case StateCR: + // printf("StateCr %02x %c\n", *p, isalnum(*p) ? *p : ' '); + if (*p == '\n') { + // If there's more data, check for folding. + if (p + 1 == headers + size) { + state = StateDone; + } else { + state = StateCheckFold; + } + } else { + state = StateError; + } + break; + + case StateCheckFold: + // printf("StateCheckFold %02x %c\n", *p, isalnum(*p) ? *p : ' '); + if (is_header_ws(*p)) { + state = StateValue; + } else { + state = StateDone; + --p; + } + break; + } + } + + *pParsed = p - headers; + + if (state != StateDone || state == StateError) { + return SQLITE_ERROR; + } + + if (!nameStart) { + return SQLITE_DONE; + } + + *ppName = nameStart; + *pNameSize = nameEnd - nameStart; + + if (valueStart) { + *ppValue = valueStart; + *pValueSize = valueEnd - valueStart; + } + + return SQLITE_ROW; +} diff --git a/src/amalgamate.c b/src/amalgamate.c new file mode 100644 index 0000000..f2fdc1e --- /dev/null +++ b/src/amalgamate.c @@ -0,0 +1,55 @@ +#include <stdio.h> +#include <string.h> + +int main(int argc, char const* argv[]) { + static const char* aFilenames[] = { + "src/http.h", + "src/http.c", + "src/http_backend_curl.c", + "src/http_backend_dummy.c", + "src/http_backend_winhttp.c", + "src/http_next_header.c", + }; + + static const int nFilenames = sizeof(aFilenames) / sizeof(aFilenames[0]); + + FILE* fout = fopen("http.c", "wb"); + if (!fout) { + fprintf(stderr, "error opening http.c: %s\n", strerror(errno)); + return 1; + } + + for (int i = 0; i < nFilenames; ++i) { + FILE* fp = fopen(aFilenames[i], "rb"); + if (!fp) { + fprintf(stderr, "error opening %s: %s\n", aFilenames[i], strerror(errno)); + return 1; + } + char buffer[512]; + size_t nread = + snprintf(buffer, sizeof(buffer), "\n/********** %s **********/\n\n", aFilenames[i]); + if (fwrite(buffer, 1, nread, fout) != nread) { + fprintf(stderr, "error writing http.c: %s\n", strerror(errno)); + return 1; + } + while (1) { + nread = fread(buffer, 1, sizeof(buffer), fp); + if (!nread) { + if (ferror(fp)) { + fprintf(stderr, "error reading %s: %s\n", aFilenames[i], strerror(errno)); + return 1; + } + break; + } + if (fwrite(buffer, 1, nread, fout) != nread) { + fprintf(stderr, "error writing http.c: %s\n", strerror(errno)); + return 1; + } + } + fclose(fp); + } + + fclose(fout); + + return 0; +}
\ No newline at end of file diff --git a/src/http.c b/src/http.c new file mode 100644 index 0000000..02dedfb --- /dev/null +++ b/src/http.c @@ -0,0 +1,791 @@ +#include "http.h" + +SQLITE_EXTENSION_INIT1 + +#include <assert.h> +#include <ctype.h> +#include <string.h> + +typedef struct http_vtab http_vtab; +struct http_vtab { + sqlite3_vtab base; + char* zMethod; +}; + +typedef struct http_cursor http_cursor; +struct http_cursor { + sqlite3_vtab_cursor base; + sqlite3_int64 iRowid; + http_request req; + http_response resp; +}; + +// If zHeaders contains headers for multiple responses, then this will strip +// headers from all but the last response. +void remove_all_but_last_headers(char* zHeaders) { + int szHeaders = strlen(zHeaders); + char* pLastTerminator = NULL; + char* pSearch = zHeaders; + + for (;;) { + char* p = strstr(pSearch, "\r\n\r\n"); + // If there isn't more terminators, we are done here + if (!p) { + break; + } + // If it's at the end of zHeaders, we are done here + if (szHeaders - (p - zHeaders) == 4) { + break; + } + pSearch = p + 4; + pLastTerminator = p; + } + + if (pLastTerminator) { + pLastTerminator += 4; + memmove(zHeaders, pLastTerminator, strlen(pLastTerminator) + 1); + } +} + +// Separate the status and header lines +void separate_status_and_headers(char** ppStatus, char* zHeaders) { + char* cr = strchr(zHeaders, '\r'); + *ppStatus = sqlite3_mprintf("%.*s", cr - zHeaders, zHeaders); + memmove(zHeaders, cr + 2, strlen(cr + 2) + 1); +} + +static int httpConnect(sqlite3* db, + void* pAux, + int argc, + const char* const* argv, + sqlite3_vtab** ppVtab, + char** pzErr) { + http_vtab* pNew; + int rc; + + rc = sqlite3_declare_vtab(db, + "CREATE TABLE x(response_status TEXT, " + "response_status_code INT, response_headers TEXT, " + "response_body BLOB, request_method TEXT HIDDEN, " + "request_url TEXT HIDDEN, " + "request_headers TEXT HIDDEN, " + "request_body BLOB HIDDEN)"); + +#define HTTP_COL_RESPONSE_STATUS 0 +#define HTTP_COL_RESPONSE_STATUS_CODE 1 +#define HTTP_COL_RESPONSE_HEADERS 2 +#define HTTP_COL_RESPONSE_BODY 3 +#define HTTP_COL_REQUEST_METHOD 4 +#define HTTP_COL_REQUEST_URL 5 +#define HTTP_COL_REQUEST_HEADERS 6 +#define HTTP_COL_REQUEST_BODY 7 + + if (rc == SQLITE_OK) { + pNew = sqlite3_malloc(sizeof(*pNew)); + *ppVtab = (sqlite3_vtab*)pNew; + if (pNew == 0) + return SQLITE_NOMEM; + memset(pNew, 0, sizeof(*pNew)); + if (sqlite3_stricmp(argv[0], "http_get") == 0) { + pNew->zMethod = sqlite3_mprintf("GET"); + } else if (sqlite3_stricmp(argv[0], "http_post") == 0) { + pNew->zMethod = sqlite3_mprintf("POST"); + } + } + return rc; +} + +static int httpDisconnect(sqlite3_vtab* pVtab) { + http_vtab* p = (http_vtab*)pVtab; + sqlite3_free(p->zMethod); + sqlite3_free(p); + return SQLITE_OK; +} + +static int httpOpen(sqlite3_vtab* p, sqlite3_vtab_cursor** ppCursor) { + http_cursor* pCur; + http_vtab* pVtab = (http_vtab*)p; + pCur = sqlite3_malloc(sizeof(*pCur)); + if (pCur == 0) + return SQLITE_NOMEM; + memset(pCur, 0, sizeof(*pCur)); + if (pVtab->zMethod) { + pCur->req.zMethod = sqlite3_mprintf("%s", pVtab->zMethod); + } + *ppCursor = &pCur->base; + return SQLITE_OK; +} + +static int httpClose(sqlite3_vtab_cursor* cur) { + http_cursor* pCur = (http_cursor*)cur; + sqlite3_free(pCur->resp.pBody); + sqlite3_free(pCur->req.zUrl); + sqlite3_free(pCur->req.zMethod); + sqlite3_free(pCur); + return SQLITE_OK; +} + +static int httpNext(sqlite3_vtab_cursor* cur) { + http_cursor* pCur = (http_cursor*)cur; + pCur->iRowid++; + return SQLITE_OK; +} + +static int httpColumn(sqlite3_vtab_cursor* cur, sqlite3_context* ctx, int i) { + http_cursor* pCur = (http_cursor*)cur; + + switch (i) { + case HTTP_COL_RESPONSE_STATUS: + sqlite3_result_text(ctx, pCur->resp.zStatus, -1, SQLITE_TRANSIENT); + break; + + case HTTP_COL_RESPONSE_STATUS_CODE: + sqlite3_result_int(ctx, pCur->resp.iStatusCode); + break; + + case HTTP_COL_RESPONSE_HEADERS: + sqlite3_result_text(ctx, pCur->resp.zHeaders, -1, SQLITE_TRANSIENT); + break; + + case HTTP_COL_RESPONSE_BODY: + sqlite3_result_blob(ctx, pCur->resp.pBody, pCur->resp.szBody, SQLITE_TRANSIENT); + break; + + case HTTP_COL_REQUEST_METHOD: + sqlite3_result_text(ctx, pCur->req.zMethod, -1, SQLITE_TRANSIENT); + break; + + case HTTP_COL_REQUEST_URL: + sqlite3_result_text(ctx, pCur->req.zUrl, -1, SQLITE_TRANSIENT); + break; + + case HTTP_COL_REQUEST_HEADERS: + if (pCur->req.zHeaders) { + sqlite3_result_text(ctx, pCur->req.zHeaders, -1, SQLITE_TRANSIENT); + } else { + sqlite3_result_null(ctx); + } + break; + + case HTTP_COL_REQUEST_BODY: + if (pCur->req.pBody) { + sqlite3_result_blob(ctx, pCur->req.pBody, pCur->req.szBody, SQLITE_TRANSIENT); + } else { + sqlite3_result_null(ctx); + } + break; + } + + return SQLITE_OK; +} + +static int httpRowid(sqlite3_vtab_cursor* cur, sqlite_int64* pRowid) { + http_cursor* pCur = (http_cursor*)cur; + *pRowid = pCur->iRowid; + return SQLITE_OK; +} + +static int httpEof(sqlite3_vtab_cursor* cur) { + http_cursor* pCur = (http_cursor*)cur; + return pCur->iRowid >= 1; +} + +#define HTTP_FLAG_METHOD 1 +#define HTTP_FLAG_URL 2 +#define HTTP_FLAG_HEADERS 4 +#define HTTP_FLAG_BODY 8 + +static int httpFilter(sqlite3_vtab_cursor* pVtabCursor, + int idxNum, + const char* idxStr, + int argc, + sqlite3_value** argv) { + http_cursor* pCur = (http_cursor*)pVtabCursor; + http_vtab* pVtab = (http_vtab*)pVtabCursor->pVtab; + int bIsDo = pVtab->zMethod == NULL; + char* zErrMsg = NULL; + int rc; + if (bIsDo) { + pCur->req.zMethod = sqlite3_mprintf("%s", sqlite3_value_text(argv[0])); + pCur->req.zUrl = sqlite3_mprintf("%s", sqlite3_value_text(argv[1])); + if (idxNum & HTTP_FLAG_HEADERS) { + pCur->req.zHeaders = sqlite3_mprintf("%s", sqlite3_value_text(argv[2])); + } + if (idxNum & HTTP_FLAG_BODY) { + pCur->req.szBody = sqlite3_value_bytes(argv[3]); + pCur->req.pBody = sqlite3_value_blob(argv[3]); + } + } else { + pCur->req.zUrl = sqlite3_mprintf("%s", sqlite3_value_text(argv[0])); + if (idxNum & HTTP_FLAG_HEADERS) { + pCur->req.zHeaders = sqlite3_mprintf("%s", sqlite3_value_text(argv[1])); + } + if (idxNum & HTTP_FLAG_BODY) { + pCur->req.szBody = sqlite3_value_bytes(argv[2]); + pCur->req.pBody = sqlite3_value_blob(argv[2]); + } + } + rc = http_do_request(&pCur->req, &pCur->resp, &zErrMsg); + if (rc != SQLITE_OK) { + sqlite3_free(pCur->base.pVtab->zErrMsg); + pCur->base.pVtab->zErrMsg = zErrMsg; + } + return rc; +} + +static int httpBestIndex(sqlite3_vtab* tab, sqlite3_index_info* pIdxInfo) { + http_vtab* pTab = (http_vtab*)tab; + int bIsDo = pTab->zMethod == NULL; + int i; + int idxNum = 0; + const struct sqlite3_index_constraint* pConstraint; + + pConstraint = pIdxInfo->aConstraint; + + for (i = 0; i < pIdxInfo->nConstraint; ++i, ++pConstraint) { + if (!pIdxInfo->aConstraint[i].usable) { + return SQLITE_CONSTRAINT; + } + if (pConstraint->op != SQLITE_INDEX_CONSTRAINT_EQ) { + return SQLITE_CONSTRAINT; + }; + switch (pConstraint->iColumn) { + case HTTP_COL_REQUEST_METHOD: + if (bIsDo) { + idxNum |= HTTP_FLAG_METHOD; + } else { + idxNum |= HTTP_FLAG_URL; + } + pIdxInfo->aConstraintUsage[i].argvIndex = 1; + pIdxInfo->aConstraintUsage[i].omit = 1; + break; + + case HTTP_COL_REQUEST_URL: + if (bIsDo) { + idxNum |= HTTP_FLAG_URL; + } else { + idxNum |= HTTP_FLAG_HEADERS; + } + pIdxInfo->aConstraintUsage[i].argvIndex = 2; + pIdxInfo->aConstraintUsage[i].omit = 1; + break; + + case HTTP_COL_REQUEST_HEADERS: + if (bIsDo) { + idxNum |= HTTP_FLAG_HEADERS; + } else { + idxNum |= HTTP_FLAG_BODY; + } + pIdxInfo->aConstraintUsage[i].argvIndex = 3; + pIdxInfo->aConstraintUsage[i].omit = 1; + break; + + case HTTP_COL_REQUEST_BODY: + if (bIsDo) { + idxNum |= HTTP_FLAG_BODY; + } else { + sqlite3_free(tab->zErrMsg); + tab->zErrMsg = sqlite3_mprintf("too many arguments"); + return SQLITE_ERROR; + } + pIdxInfo->aConstraintUsage[i].argvIndex = 4; + pIdxInfo->aConstraintUsage[i].omit = 1; + break; + } + } + + if (bIsDo && !(idxNum & HTTP_FLAG_METHOD)) { + sqlite3_free(tab->zErrMsg); + tab->zErrMsg = sqlite3_mprintf("method missing"); + return SQLITE_ERROR; + } + + if (!(idxNum & HTTP_FLAG_URL)) { + sqlite3_free(tab->zErrMsg); + tab->zErrMsg = sqlite3_mprintf("url missing"); + return SQLITE_ERROR; + } + + pIdxInfo->estimatedCost = (double)1; + pIdxInfo->estimatedRows = 1; + pIdxInfo->idxNum = idxNum; + + return SQLITE_OK; +} + +static sqlite3_module httpModule = { + /* iVersion */ 0, + /* xCreate */ 0, + /* xConnect */ httpConnect, + /* xBestIndex */ httpBestIndex, + /* xDisconnect */ httpDisconnect, + /* xDestroy */ 0, + /* xOpen */ httpOpen, + /* xClose */ httpClose, + /* xFilter */ httpFilter, + /* xNext */ httpNext, + /* xEof */ httpEof, + /* xColumn */ httpColumn, + /* xRowid */ httpRowid, + /* xUpdate */ 0, + /* xBegin */ 0, + /* xSync */ 0, + /* xCommit */ 0, + /* xRollback */ 0, + /* xFindMethod */ 0, + /* xRename */ 0, + /* xSavepoint */ 0, + /* xRelease */ 0, + /* xRollbackTo */ 0, + /* xShadowName */ 0, +}; + +static void httpSimpleFunc(sqlite3_context* ctx, + int argc, + sqlite3_value** argv, + const char* zTable, + const char* zColumn, + int bText) { + char* zSql; + int rc; + sqlite3_stmt* pStmt; + + if (argc == 0) { + sqlite3_result_error(ctx, "not enough arguments", -1); + return; + } + + if (argc == 1) { + zSql = sqlite3_mprintf( + "select \"%w\" from \"%w\"(%Q)", zColumn, zTable, sqlite3_value_text(argv[0])); + } + + if (argc == 2) { + zSql = sqlite3_mprintf("select \"%w\" from \"%w\"(%Q, %Q)", + zColumn, + zTable, + sqlite3_value_text(argv[0]), + sqlite3_value_text(argv[1])); + } + + if (argc == 3) { + zSql = sqlite3_mprintf("select \"%w\" from \"%w\"(%Q, %Q, %Q)", + zColumn, + zTable, + sqlite3_value_text(argv[0]), + sqlite3_value_text(argv[1]), + sqlite3_value_text(argv[2])); + } + + if (argc >= 4) { + zSql = sqlite3_mprintf("select \"%w\" from \"%w\"(%Q, %Q, %Q, %Q)", + zColumn, + zTable, + sqlite3_value_text(argv[0]), + sqlite3_value_text(argv[1]), + sqlite3_value_text(argv[2]), + sqlite3_value_text(argv[2])); + } + + if (zSql == 0) { + sqlite3_result_error_code(ctx, SQLITE_NOMEM); + return; + } + + rc = sqlite3_prepare_v2(sqlite3_context_db_handle(ctx), zSql, -1, &pStmt, 0); + + sqlite3_free(zSql); + + if (rc != SQLITE_OK) { + sqlite3_result_error_code(ctx, rc); + return; + } + + rc = sqlite3_step(pStmt); + + if (rc == SQLITE_ROW) { + int size = sqlite3_column_bytes(pStmt, 0); + if (bText) { + sqlite3_result_text(ctx, sqlite3_column_text(pStmt, 0), size, SQLITE_TRANSIENT); + } else { + sqlite3_result_blob(ctx, sqlite3_column_blob(pStmt, 0), size, SQLITE_TRANSIENT); + } + sqlite3_finalize(pStmt); + return; + } + + rc = sqlite3_finalize(pStmt); + if (rc != SQLITE_OK) { + sqlite3_result_error_code(ctx, rc); + } +} + +static void httpGetBodyFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + httpSimpleFunc(ctx, argc, argv, "http_get", "response_body", 0); +} + +static void httpPostBodyFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + httpSimpleFunc(ctx, argc, argv, "http_post", "response_body", 0); +} + +static void httpDoBodyFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + httpSimpleFunc(ctx, argc, argv, "http_do", "response_body", 0); +} + +static void httpGetHeadersFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + httpSimpleFunc(ctx, argc, argv, "http_get", "response_headers", 1); +} + +static void httpPostHeadersFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + httpSimpleFunc(ctx, argc, argv, "http_post", "response_headers", 1); +} + +static void httpDoHeadersFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + httpSimpleFunc(ctx, argc, argv, "http_do", "response_headers", 1); +} + +static void httpHeadersFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + char* zHeaders = NULL; + int i; + int size = 0; + int offset = 0; + if (argc < 2) { + sqlite3_result_error(ctx, "http_headers: expected at least 2 arguments", -1); + return; + } + if (argc % 2 != 0) { + sqlite3_result_error(ctx, "http_headers: even number of arguments expected", -1); + return; + } + for (i = 0; i < argc; i += 2) { + size += sqlite3_value_bytes(argv[i]) + 2; + size += sqlite3_value_bytes(argv[i + 1]) + 2; + } + zHeaders = sqlite3_malloc(size); + if (!zHeaders) { + sqlite3_result_error_code(ctx, SQLITE_NOMEM); + return; + } + for (i = 0; i < argc; i += 2) { + memcpy(zHeaders + offset, sqlite3_value_text(argv[i]), sqlite3_value_bytes(argv[i])); + offset += sqlite3_value_bytes(argv[i]); + + memcpy(zHeaders + offset, ": ", 2); + offset += 2; + + memcpy( + zHeaders + offset, sqlite3_value_text(argv[i + 1]), sqlite3_value_bytes(argv[i + 1])); + offset += sqlite3_value_bytes(argv[i + 1]); + + memcpy(zHeaders + offset, "\r\n", 2); + offset += 2; + } + assert(offset == size); + sqlite3_result_text(ctx, zHeaders, size, sqlite3_free); +} + +static void httpHeadersHasFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + int zHeadersSize = sqlite3_value_bytes(argv[0]); + const char* zHeaders = sqlite3_value_text(argv[0]); + int zHeaderSize = sqlite3_value_bytes(argv[1]); + const char* zHeader = sqlite3_value_text(argv[1]); + while (zHeadersSize > 0) { + int iParsed = 0; + const char* pName; + int iNameSize; + int rc = http_next_header(zHeaders, zHeadersSize, &iParsed, &pName, &iNameSize, NULL, NULL); + if (rc == SQLITE_ERROR) { + sqlite3_result_error(ctx, "http_headers_has: malformed headers", -1); + return; + } + if (rc == SQLITE_DONE) { + sqlite3_result_int(ctx, 0); + return; + } + zHeaders += iParsed; + zHeadersSize -= iParsed; + if (iNameSize == zHeaderSize && sqlite3_strnicmp(zHeader, pName, zHeaderSize) == 0) { + sqlite3_result_int(ctx, 1); + return; + } + } + sqlite3_result_int(ctx, 0); +} + +static void httpHeadersGetFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { + int zHeadersSize = sqlite3_value_bytes(argv[0]); + const char* zHeaders = sqlite3_value_text(argv[0]); + int zHeaderSize = sqlite3_value_bytes(argv[1]); + const char* zHeader = sqlite3_value_text(argv[1]); + while (zHeadersSize > 0) { + int iParsed = 0; + const char* pName; + int iNameSize; + const char* pValue; + int iValueSize; + int rc = http_next_header( + zHeaders, zHeadersSize, &iParsed, &pName, &iNameSize, &pValue, &iValueSize); + if (rc == SQLITE_ERROR) { + sqlite3_result_error(ctx, "http_headers_get: malformed headers", -1); + return; + } + if (rc == SQLITE_DONE) { + sqlite3_result_int(ctx, 0); + return; + } + zHeaders += iParsed; + zHeadersSize -= iParsed; + if (iNameSize == zHeaderSize && sqlite3_strnicmp(zHeader, pName, zHeaderSize) == 0) { + sqlite3_result_text(ctx, pValue, iValueSize, SQLITE_TRANSIENT); + return; + } + } +} + +#define HTTP_HEADERS_EACH_COL_NAME 0 +#define HTTP_HEADERS_EACH_COL_VALUE 1 +#define HTTP_HEADERS_EACH_COL_HEADERS 2 + +typedef struct http_headers_each_vtab http_headers_each_vtab; +struct http_headers_each_vtab { + sqlite3_vtab base; +}; + +typedef struct http_headers_each_cursor http_headers_each_cursor; +struct http_headers_each_cursor { + sqlite3_vtab_cursor base; + sqlite3_int64 iRowid; + const char* zHeaders; + int zHeadersSize; + const char* p; + const char* name; + int nameSize; + const char* value; + int valueSize; + int available; + int done; +}; + +static int httpHeadersEachConnect(sqlite3* db, + void* pAux, + int argc, + const char* const* argv, + sqlite3_vtab** ppVtab, + char** pzErr) { + http_headers_each_vtab* pNew; + int rc; + rc = sqlite3_declare_vtab(db, "CREATE TABLE x(name TEXT, value TEXT, headers TEXT HIDDEN)"); + if (rc == SQLITE_OK) { + pNew = sqlite3_malloc(sizeof(*pNew)); + *ppVtab = (sqlite3_vtab*)pNew; + if (pNew == 0) + return SQLITE_NOMEM; + memset(pNew, 0, sizeof(*pNew)); + } + return rc; +} + +static int httpHeadersEachDisconnect(sqlite3_vtab* pVtab) { + http_headers_each_vtab* p = (http_headers_each_vtab*)pVtab; + sqlite3_free(p); + return SQLITE_OK; +} + +static int httpHeadersEachOpen(sqlite3_vtab* p, sqlite3_vtab_cursor** ppCursor) { + http_headers_each_cursor* pCur; + pCur = sqlite3_malloc(sizeof(*pCur)); + if (pCur == 0) + return SQLITE_NOMEM; + memset(pCur, 0, sizeof(*pCur)); + *ppCursor = &pCur->base; + return SQLITE_OK; +} + +static int httpHeadersEachClose(sqlite3_vtab_cursor* cur) { + http_headers_each_cursor* pCur = (http_headers_each_cursor*)cur; + sqlite3_free(pCur); + return SQLITE_OK; +} + +static int httpHeadersEachNext(sqlite3_vtab_cursor* cur) { + http_headers_each_cursor* pCur = (http_headers_each_cursor*)cur; + pCur->iRowid++; + int nparsed = 0; + int rc = http_next_header(pCur->p, + pCur->available, + &nparsed, + &pCur->name, + &pCur->nameSize, + &pCur->value, + &pCur->valueSize); + pCur->p += nparsed; + pCur->available -= nparsed; + if (rc == SQLITE_ERROR) { + sqlite3_free(cur->pVtab->zErrMsg); + cur->pVtab->zErrMsg = sqlite3_mprintf("http_headers_each: malformed headers"); + return SQLITE_ERROR; + } + pCur->done = rc == SQLITE_DONE; + return SQLITE_OK; +} + +static int httpHeadersEachColumn(sqlite3_vtab_cursor* cur, sqlite3_context* ctx, int i) { + http_headers_each_cursor* pCur = (http_headers_each_cursor*)cur; + switch (i) { + case HTTP_HEADERS_EACH_COL_NAME: + sqlite3_result_text(ctx, pCur->name, pCur->nameSize, SQLITE_TRANSIENT); + break; + + case HTTP_HEADERS_EACH_COL_VALUE: + sqlite3_result_text(ctx, pCur->value, pCur->valueSize, SQLITE_TRANSIENT); + break; + + case HTTP_HEADERS_EACH_COL_HEADERS: + sqlite3_result_text(ctx, pCur->zHeaders, pCur->zHeadersSize, SQLITE_TRANSIENT); + break; + + default: + sqlite3_result_error(ctx, "unknown column", -1); + break; + } + return SQLITE_OK; +} + +static int httpHeadersEachRowid(sqlite3_vtab_cursor* cur, sqlite_int64* pRowid) { + http_headers_each_cursor* pCur = (http_headers_each_cursor*)cur; + *pRowid = pCur->iRowid; + return SQLITE_OK; +} + +static int httpHeadersEachEof(sqlite3_vtab_cursor* cur) { + http_headers_each_cursor* pCur = (http_headers_each_cursor*)cur; + return pCur->done; +} + +static int httpHeadersEachFilter(sqlite3_vtab_cursor* pVtabCursor, + int idxNum, + const char* idxStr, + int argc, + sqlite3_value** argv) { + http_headers_each_cursor* pCur = (http_headers_each_cursor*)pVtabCursor; + pCur->zHeadersSize = sqlite3_value_bytes(argv[0]); + pCur->available = pCur->zHeadersSize; + pCur->zHeaders = sqlite3_value_text(argv[0]); + pCur->p = pCur->zHeaders; + int nparsed = 0; + int rc = http_next_header(pCur->p, + pCur->available, + &nparsed, + &pCur->name, + &pCur->nameSize, + &pCur->value, + &pCur->valueSize); + pCur->p += nparsed; + pCur->available -= nparsed; + pCur->iRowid = 1; + if (rc == SQLITE_ERROR) { + sqlite3_free(pVtabCursor->pVtab->zErrMsg); + pVtabCursor->pVtab->zErrMsg = sqlite3_mprintf("http_headers_each: malformed headers"); + return SQLITE_ERROR; + } + pCur->done = rc == SQLITE_DONE; + return SQLITE_OK; +} + +static int httpHeadersEachBestIndex(sqlite3_vtab* tab, sqlite3_index_info* pIdxInfo) { + http_vtab* pTab = (http_vtab*)tab; + int bHeadersSeen = 0; + + for (int i = 0; i < pIdxInfo->nConstraint; ++i) { + if (pIdxInfo->aConstraint[i].iColumn == HTTP_HEADERS_EACH_COL_HEADERS) { + if (!pIdxInfo->aConstraint[i].usable) { + return SQLITE_CONSTRAINT; + } + pIdxInfo->aConstraintUsage[i].argvIndex = 1; + pIdxInfo->aConstraintUsage[i].omit = 1; + bHeadersSeen = 1; + } + } + + if (!bHeadersSeen) { + sqlite3_free(tab->zErrMsg); + tab->zErrMsg = sqlite3_mprintf("headers missing"); + return SQLITE_ERROR; + } + + pIdxInfo->estimatedCost = (double)1; + // pIdxInfo->estimatedRows = 1; + + return SQLITE_OK; +} + +static sqlite3_module httpHeadersEachModule = { + /* iVersion */ 0, + /* xCreate */ 0, + /* xConnect */ httpHeadersEachConnect, + /* xBestIndex */ httpHeadersEachBestIndex, + /* xDisconnect */ httpHeadersEachDisconnect, + /* xDestroy */ 0, + /* xOpen */ httpHeadersEachOpen, + /* xClose */ httpHeadersEachClose, + /* xFilter */ httpHeadersEachFilter, + /* xNext */ httpHeadersEachNext, + /* xEof */ httpHeadersEachEof, + /* xColumn */ httpHeadersEachColumn, + /* xRowid */ httpHeadersEachRowid, + /* xUpdate */ 0, + /* xBegin */ 0, + /* xSync */ 0, + /* xCommit */ 0, + /* xRollback */ 0, + /* xFindMethod */ 0, + /* xRename */ 0, + /* xSavepoint */ 0, + /* xRelease */ 0, + /* xRollbackTo */ 0, + /* xShadowName */ 0, +}; + +static const struct Func { + const char* name; + void (*xFunc)(sqlite3_context*, int, sqlite3_value**); +} funcs[] = { + {"http_get_body", httpGetBodyFunc}, + {"http_post_body", httpPostBodyFunc}, + {"http_do_body", httpDoBodyFunc}, + {"http_get_headers", httpGetHeadersFunc}, + {"http_post_headers", httpPostHeadersFunc}, + {"http_do_headers", httpDoHeadersFunc}, + {"http_headers", httpHeadersFunc}, + {"http_headers_has", httpHeadersHasFunc}, + {"http_headers_get", httpHeadersGetFunc}, + {NULL, NULL}, +}; + +static const struct Module { + const char* name; + const sqlite3_module* module; +} modules[] = { + {"http_get", &httpModule}, + {"http_post", &httpModule}, + {"http_do", &httpModule}, + {"http_headers_each", &httpHeadersEachModule}, + {NULL, NULL}, +}; + +#ifdef _WIN32 +__declspec(dllexport) +#endif + int sqlite3_http_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines* pApi) { + int rc = SQLITE_OK; + SQLITE_EXTENSION_INIT2(pApi); + int i; + for (i = 0; funcs[i].name && rc == SQLITE_OK; ++i) { + rc = sqlite3_create_function( + db, funcs[i].name, -1, SQLITE_UTF8, NULL, funcs[i].xFunc, NULL, NULL); + } + for (i = 0; modules[i].name && rc == SQLITE_OK; ++i) { + rc = sqlite3_create_module(db, modules[i].name, modules[i].module, 0); + } + return rc; +} diff --git a/src/http.h b/src/http.h new file mode 100644 index 0000000..f215cf6 --- /dev/null +++ b/src/http.h @@ -0,0 +1,38 @@ +#ifndef HTTP_H +#define HTTP_H + +#include "sqlite3ext.h" + +typedef struct http_request http_request; +struct http_request { + char* zMethod; + char* zUrl; + const void* pBody; + sqlite3_int64 szBody; + const char* zHeaders; +}; + +typedef struct http_response http_response; +struct http_response { + void* pBody; + sqlite3_int64 szBody; + char* zHeaders; + int szHeaders; + int iStatusCode; + char* zStatus; +}; + +int http_do_request(http_request* req, http_response* resp, char** ppErrMsg); + +int http_next_header(const char* headers, + int size, + int* pParsed, + const char** ppName, + int* pNameSize, + const char** ppValue, + int* pValueSize); + +void remove_all_but_last_headers(char* zHeaders); +void separate_status_and_headers(char** ppStatus, char* zHeaders); + +#endif diff --git a/src/http_backend_curl.c b/src/http_backend_curl.c new file mode 100644 index 0000000..5ca2364 --- /dev/null +++ b/src/http_backend_curl.c @@ -0,0 +1,478 @@ +#ifdef HTTP_BACKEND_CURL + +#include "http.h" + +#include <assert.h> +#include <string.h> + +SQLITE_EXTENSION_INIT3 + +#ifdef _WIN32 +#include <windows.h> + +void* http_dlopen(const char* zName) { + return LoadLibraryA(zName); +} + +void http_dlclose(void* library) { + FreeLibrary(library); +} + +void* http_dlsym(void* library, const char* zName) { + return GetProcAddress(library, zName); +} +#else +#include <dlfcn.h> + +void* http_dlopen(const char* zName) { + return dlopen(zName, RTLD_NOW); +} + +void http_dlclose(void* library) { + dlclose(zName); +} + +void* http_dlsym(void* library, const char* zName) { + return dlsym(library, zName); +} +#endif + +#ifndef MIN +#define MIN(A, B) ((A) < (B) ? (A) : (B)) +#endif + +typedef struct CURL CURL; +typedef int CURLcode; +typedef int CURLoption; +typedef int CURLINFO; +typedef int CURLversion; + +#define CURL_ERROR_SIZE 256 + +#define CURLE_OK 0 + +#define CURLOPT_ERRORBUFFER (10000 + 10) +#define CURLOPT_URL (10000 + 2) +#define CURLOPT_FOLLOWLOCATION (52) +#define CURLOPT_HTTPGET (80) +#define CURLOPT_POST (47) +#define CURLOPT_SSL_OPTIONS (216) +#define CURLOPT_WRITEFUNCTION (20000 + 11) +#define CURLOPT_WRITEDATA (10000 + 1) +#define CURLOPT_READFUNCTION (20000 + 12) +#define CURLOPT_READDATA (10000 + 9) +#define CURLOPT_HEADERFUNCTION (20000 + 79) +#define CURLOPT_HEADERDATA (10000 + 29) +#define CURLOPT_POSTFIELDS (10000 + 15) +#define CURLOPT_POSTFIELDSIZE_LARGE (30000 + 120) +#define CURLOPT_HTTPHEADER (10000 + 23) +#define CURLOPT_INFILESIZE_LARGE (30000 + 115) +#define CURLOPT_NOBODY (44) +#define CURLOPT_CUSTOMREQUEST (10000 + 36) +#define CURLOPT_PUT (54) + +#define CURLSSLOPT_NATIVE_CA (1 << 4) + +#define CURLVERSION_NOW 9 + +#define CURLINFO_RESPONSE_CODE (0x200000 + 2) + +// The struct is bigger but I don't need more information for now... +struct curl_version_info_data { + CURLversion age; + const char* version; + unsigned int version_num; +}; +typedef struct curl_version_info_data curl_version_info_data; + +struct curl_slist; + +typedef CURL* (*curl_easy_init_t)(); +typedef void (*curl_easy_cleanup_t)(CURL*); +typedef CURLcode (*curl_easy_setopt_t)(CURL*, CURLoption, ...); +typedef CURLcode (*curl_easy_perform_t)(CURL*); +typedef CURLcode (*curl_easy_getinfo_t)(CURL*, CURLINFO, ...); +typedef char* (*curl_version_t)(); +typedef curl_version_info_data* (*curl_version_info_t)(CURLversion); +typedef struct curl_slist* (*curl_slist_append_t)(struct curl_slist*, const char*); +typedef void (*curl_slist_free_all_t)(struct curl_slist*); +typedef const char* (*curl_easy_strerror_t)(CURLcode); + +struct curl_api_routines { + void* pLibrary; + curl_easy_init_t easy_init; + curl_easy_cleanup_t easy_cleanup; + curl_easy_setopt_t easy_setopt; + curl_easy_perform_t easy_perform; + curl_easy_getinfo_t easy_getinfo; + curl_version_t version; + curl_version_info_t version_info; + curl_slist_append_t slist_append; + curl_slist_free_all_t slist_free_all; + curl_easy_strerror_t easy_strerror; +}; + +static struct curl_api_routines curl_api; + +#define curl_easy_init curl_api.easy_init +#define curl_easy_cleanup curl_api.easy_cleanup +#define curl_easy_setopt curl_api.easy_setopt +#define curl_easy_perform curl_api.easy_perform +#define curl_easy_getinfo curl_api.easy_getinfo +#define curl_version curl_api.version +#define curl_version_info curl_api.version_info +#define curl_slist_append curl_api.slist_append +#define curl_slist_free_all curl_api.slist_free_all +#define curl_easy_strerror curl_api.easy_strerror + +static const char* aCurlLibNames[] = { +#ifdef _WIN32 + "libcurl-x64.dll", + "libcurl.dll", +#else + "libcurl.so", + "libcurl.so.4", +#endif +}; + +static const int szCurlLibNames = sizeof(aCurlLibNames) / sizeof(aCurlLibNames[0]); + +int http_backend_curl_load(char** zErrMsg) { + assert(zErrMsg != NULL); + + *zErrMsg = NULL; + + if (!curl_api.pLibrary) { + for (int i = 0; i < szCurlLibNames && !curl_api.pLibrary; ++i) { + curl_api.pLibrary = http_dlopen(aCurlLibNames[i]); + } + } + + if (!curl_api.pLibrary) { + *zErrMsg = sqlite3_mprintf("failed to load curl"); + goto error; + } + + curl_easy_init = (curl_easy_init_t)http_dlsym(curl_api.pLibrary, "curl_easy_init"); + if (!curl_easy_init) { + *zErrMsg = sqlite3_mprintf("failed to load curl_easy_init"); + goto error; + } + curl_easy_cleanup = (curl_easy_cleanup_t)http_dlsym(curl_api.pLibrary, "curl_easy_cleanup"); + if (!curl_easy_cleanup) { + *zErrMsg = sqlite3_mprintf("failed to load curl_easy_cleanup"); + goto error; + } + curl_easy_setopt = (curl_easy_setopt_t)http_dlsym(curl_api.pLibrary, "curl_easy_setopt"); + if (!curl_easy_setopt) { + *zErrMsg = sqlite3_mprintf("failed to load curl_easy_setopt"); + goto error; + } + curl_easy_perform = (curl_easy_perform_t)http_dlsym(curl_api.pLibrary, "curl_easy_perform"); + if (!curl_easy_perform) { + *zErrMsg = sqlite3_mprintf("failed to load curl_easy_perform"); + goto error; + } + curl_easy_getinfo = (curl_easy_getinfo_t)http_dlsym(curl_api.pLibrary, "curl_easy_getinfo"); + if (!curl_easy_getinfo) { + *zErrMsg = sqlite3_mprintf("failed to load curl_easy_getinfo"); + goto error; + } + curl_version = (curl_version_t)http_dlsym(curl_api.pLibrary, "curl_version"); + if (!curl_version) { + *zErrMsg = sqlite3_mprintf("failed to load curl_version"); + goto error; + } + curl_version_info = (curl_version_info_t)http_dlsym(curl_api.pLibrary, "curl_version_info"); + if (!curl_version_info) { + *zErrMsg = sqlite3_mprintf("failed to load curl_version_info"); + goto error; + } + curl_slist_append = (curl_slist_append_t)http_dlsym(curl_api.pLibrary, "curl_slist_append"); + if (!curl_slist_append) { + *zErrMsg = sqlite3_mprintf("failed to load curl_slist_append"); + goto error; + } + curl_slist_free_all = + (curl_slist_free_all_t)http_dlsym(curl_api.pLibrary, "curl_slist_free_all"); + if (!curl_slist_free_all) { + *zErrMsg = sqlite3_mprintf("failed to load curl_slist_free_all"); + goto error; + } + curl_easy_strerror = (curl_easy_strerror_t)http_dlsym(curl_api.pLibrary, "curl_easy_strerror"); + if (!curl_easy_strerror) { + *zErrMsg = sqlite3_mprintf("failed to load curl_easy_strerror"); + goto error; + } + + return SQLITE_OK; + +error: + + if (curl_api.pLibrary) { + http_dlclose(curl_api.pLibrary); + memset(&curl_api, 0, sizeof(curl_api)); + } + + return SQLITE_ERROR; +} + +static size_t write_callback(char* ptr, size_t size, size_t nmemb, void* userdata) { + http_response* pResp = (http_response*)userdata; + char* p; + p = sqlite3_realloc(pResp->pBody, pResp->szBody + size * nmemb); + if (!p) { + return 0; + } + pResp->pBody = p; + memcpy((char*)pResp->pBody + pResp->szBody, ptr, size * nmemb); + pResp->szBody += size * nmemb; + return size * nmemb; +} + +static size_t header_callback(char* ptr, size_t size, size_t nmemb, void* userdata) { + http_response* pResp = (http_response*)userdata; + char* p; + p = sqlite3_realloc(pResp->zHeaders, pResp->szHeaders + size * nmemb + 1); + if (!p) { + return 0; + } + pResp->zHeaders = p; + memcpy(pResp->zHeaders + pResp->szHeaders, ptr, size * nmemb); + pResp->szHeaders += size * nmemb; + pResp->zHeaders[pResp->szHeaders] = '\0'; + return size * nmemb; +} + +struct readdata { + const char* pBody; + size_t szBody; +}; + +static size_t read_callback(char* ptr, size_t size, size_t nmemb, void* userdata) { + struct readdata* pData = (struct readdata*)userdata; + size_t szToSend = MIN(size * nmemb, pData->szBody); + memcpy(ptr, pData->pBody, szToSend); + pData->pBody += szToSend; + pData->szBody -= szToSend; + return szToSend; +} + +static int +headers_to_curl_headers(struct curl_slist** pHeaders, const char* zHeaders, int szHeaders) { + const char* name; + int szName; + const char* value; + int szValue; + while (szHeaders > 0) { + int nParsed = 0; + int rc = http_next_header(zHeaders, szHeaders, &nParsed, &name, &szName, &value, &szValue); + if (rc == SQLITE_DONE) { + break; + } else if (rc == SQLITE_ROW) { + char* zHeader = sqlite3_mprintf("%.*s: %.*s", szName, name, szValue, value); + if (!zHeader) { + return 0; + } + void* tmp = curl_slist_append(*pHeaders, zHeader); + sqlite3_free(zHeader); + if (!tmp) { + return 0; + } + *pHeaders = tmp; + szHeaders -= nParsed; + zHeaders += nParsed; + } else if (rc == SQLITE_ERROR) { + return 0; + } + } + return 1; +} + +static int set_curl_error_message(char** ppErrMsg, CURLcode rc, const char* message) { + *ppErrMsg = sqlite3_mprintf("%s: %s (curl error code %d)", message, curl_easy_strerror(rc), rc); + return SQLITE_ERROR; +} + +int http_do_request(http_request* req, http_response* resp, char** ppErrMsg) { + int rc; + CURL* curl; + char aErrorBuf[CURL_ERROR_SIZE]; + curl_version_info_data* pCurlVersionInfo; + long responseCode; + struct curl_slist* headers = NULL; + CURLcode curlrc; + struct readdata readdata; + + rc = http_backend_curl_load(ppErrMsg); + if (rc != SQLITE_OK) { + return rc; + } + + pCurlVersionInfo = curl_version_info(CURLVERSION_NOW); + + curl = curl_easy_init(); + if (!curl) { + *ppErrMsg = sqlite3_mprintf("curl_easy_init failed"); + rc = SQLITE_ERROR; + goto error; + } + + memset(aErrorBuf, 0, sizeof(aErrorBuf)); + if ((curlrc = curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, aErrorBuf)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_URL, req->zUrl)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if (sqlite3_stricmp(req->zMethod, "GET") == 0) { + if ((curlrc = curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } else if (sqlite3_stricmp(req->zMethod, "POST") == 0) { + if ((curlrc = curl_easy_setopt(curl, CURLOPT_POST, 1L)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + if ((curlrc = curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req->pBody)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + if ((curlrc = curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, req->szBody)) != + CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } else if (sqlite3_stricmp(req->zMethod, "PUT") == 0) { + if ((curlrc = curl_easy_setopt(curl, CURLOPT_PUT, 1L)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } else if (sqlite3_stricmp(req->zMethod, "HEAD") == 0) { + if ((curlrc = curl_easy_setopt(curl, CURLOPT_NOBODY, 1L)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } else { + if ((curlrc = curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if (req->pBody && req->szBody) { + if ((curlrc = curl_easy_setopt(curl, CURLOPT_PUT, 1L)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, req->zMethod)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } + + if (req->pBody && req->szBody && sqlite3_stricmp(req->zMethod, "POST") != 0) { + readdata.pBody = (const char*)req->pBody; + readdata.szBody = (size_t)req->szBody; + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, req->szBody)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_READDATA, &readdata)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } + + // Since 7.71.0 (Jun 24 2020). Makes life easier on Windows at least. + if (pCurlVersionInfo->version_num >= ((7 << 16) | (71 << 8))) { + if ((curlrc = curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA)) != + CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_WRITEDATA, resp)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + if ((curlrc = curl_easy_setopt(curl, CURLOPT_HEADERDATA, resp)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + + // Remove content-type header by default... + headers = curl_slist_append(NULL, "content-type;"); + if (!headers) { + *ppErrMsg = sqlite3_mprintf("curl_slist_append failed"); + rc = SQLITE_ERROR; + goto error; + } + + if (req->zHeaders) { + if (!headers_to_curl_headers(&headers, req->zHeaders, strlen(req->zHeaders))) { + *ppErrMsg = sqlite3_mprintf("failed to convert headers for curl"); + rc = SQLITE_ERROR; + goto error; + } + if ((curlrc = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers)) != CURLE_OK) { + rc = set_curl_error_message(ppErrMsg, curlrc, "curl_easy_setopt"); + goto error; + } + } + + if (curl_easy_perform(curl) != CURLE_OK) { + *ppErrMsg = sqlite3_mprintf("curl_easy_perform failed: %s", aErrorBuf); + rc = SQLITE_ERROR; + goto error; + } + + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &responseCode); + resp->iStatusCode = responseCode; + + remove_all_but_last_headers(resp->zHeaders); + separate_status_and_headers(&resp->zStatus, resp->zHeaders); + + rc = SQLITE_OK; + +error: + +done: + + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + + return rc; +} + +#endif // HTTP_BACKEND_CURL diff --git a/src/http_backend_dummy.c b/src/http_backend_dummy.c new file mode 100644 index 0000000..27dc2f1 --- /dev/null +++ b/src/http_backend_dummy.c @@ -0,0 +1,62 @@ +#ifdef HTTP_BACKEND_DUMMY + +#include "http.h" + +#include <assert.h> + +SQLITE_EXTENSION_INIT3 + +static http_request sLastRequest; +static http_response* sResponse; +static char* sErrMsg; + +void http_backend_dummy_set_response(http_response* response) { + sResponse = response; +} + +void http_backend_dummy_set_errmsg(const char* zErrMsg) { + sErrMsg = sqlite3_mprintf("%s", zErrMsg); +} + +void http_backend_dummy_reset_request() { + sqlite3_free(sLastRequest.zMethod); + sqlite3_free((void*)sLastRequest.pBody); + sqlite3_free((void*)sLastRequest.zHeaders); + memset(&sLastRequest, 0, sizeof(sLastRequest)); +} + +const http_request* http_backend_dummy_get_last_request() { + return &sLastRequest; +} + +int http_do_request(http_request* req, http_response* resp, char** ppErrMsg) { + int rc = SQLITE_OK; + if (sResponse) { + *resp = *sResponse; + sResponse = NULL; + } else if (sErrMsg) { + *ppErrMsg = sErrMsg; + sErrMsg = NULL; + rc = SQLITE_ERROR; + } else { + assert(0); + } + http_backend_dummy_reset_request(); + if (req->zMethod) { + sLastRequest.zMethod = sqlite3_mprintf("%s", req->zMethod); + } + if (req->zUrl) { + sLastRequest.zUrl = sqlite3_mprintf("%s", req->zUrl); + } + if (req->pBody) { + sLastRequest.pBody = sqlite3_malloc(req->szBody); + memcpy((void*)sLastRequest.pBody, req->pBody, req->szBody); + } + sLastRequest.szBody = req->szBody; + if (req->zHeaders) { + sLastRequest.zHeaders = sqlite3_mprintf("%s", req->zHeaders); + } + return rc; +} + +#endif // HTTP_BACKEND_DUMMY diff --git a/src/http_backend_winhttp.c b/src/http_backend_winhttp.c new file mode 100644 index 0000000..936e335 --- /dev/null +++ b/src/http_backend_winhttp.c @@ -0,0 +1,289 @@ +#ifdef HTTP_BACKEND_WINHTTP + +#include "http.h" + +#include <windows.h> +#include <winhttp.h> + +#include <assert.h> + +SQLITE_EXTENSION_INIT3 + +static char* unicode_to_utf8(LPCWSTR zWide) { + DWORD nSize; + char* zUtf8; + nSize = WideCharToMultiByte(CP_UTF8, 0, zWide, -1, NULL, 0, NULL, NULL); + if (nSize == 0) { + return NULL; + } + zUtf8 = sqlite3_malloc(nSize); + if (!zUtf8) { + return NULL; + } + nSize = WideCharToMultiByte(CP_UTF8, 0, zWide, -1, zUtf8, nSize, NULL, NULL); + if (nSize == 0) { + sqlite3_free(zUtf8); + return NULL; + } + return zUtf8; +} + +static LPWSTR utf8_to_unicode(const char* zText) { + DWORD nSize; + LPWSTR zWide; + nSize = MultiByteToWideChar(CP_UTF8, 0, zText, -1, NULL, 0); + if (nSize == 0) { + return NULL; + } + zWide = sqlite3_malloc(sizeof(*zWide) * nSize); + if (!zWide) { + return NULL; + } + nSize = MultiByteToWideChar(CP_UTF8, 0, zText, -1, zWide, nSize); + if (nSize == 0) { + sqlite3_free(zWide); + return NULL; + } + return zWide; +} + +static char* win32_get_last_error(DWORD code) { + LPWSTR zWide = NULL; + FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + code, + 0, + (LPWSTR)&zWide, + 0, + NULL); + char* zUtf8 = unicode_to_utf8(zWide); + LocalFree(zWide); + return zUtf8; +} + +static int query_headers(HINTERNET request, DWORD dwInfoLevel, char** ppResult) { + DWORD szWide = 0; + LPWSTR zWide = NULL; + + WinHttpQueryHeaders( + request, dwInfoLevel, WINHTTP_HEADER_NAME_BY_INDEX, NULL, &szWide, WINHTTP_NO_HEADER_INDEX); + + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + return SQLITE_ERROR; + } + + zWide = sqlite3_malloc(szWide); + if (!zWide) { + return SQLITE_NOMEM; + } + + if (!WinHttpQueryHeaders(request, + dwInfoLevel, + WINHTTP_HEADER_NAME_BY_INDEX, + zWide, + &szWide, + WINHTTP_NO_HEADER_INDEX)) { + sqlite3_free(zWide); + return SQLITE_ERROR; + } + + *ppResult = unicode_to_utf8(zWide); + + sqlite3_free(zWide); + + return SQLITE_OK; +} + +int http_do_request(http_request* req, http_response* resp, char** ppErrMsg) { + LPWSTR zUrlWide = NULL; + LPWSTR zHostWide = NULL; + LPWSTR zMethodWide = NULL; + LPWSTR zHeadersWide = NULL; + HINTERNET session = NULL; + HINTERNET conn = NULL; + HINTERNET request = NULL; + URL_COMPONENTS components; + DWORD lastErr = 0; + const char* errFunc = NULL; + char* zErrMsg = NULL; + int rc = SQLITE_ERROR; + LPWSTR zWideResponseHeaders = NULL; + DWORD szWideResponseHeaders = 0; + DWORD dwSize = 0; + DWORD dwStatusCode; + + zUrlWide = utf8_to_unicode(req->zUrl); + if (!zUrlWide) { + rc = SQLITE_NOMEM; + goto error; + } + + memset(&components, 0, sizeof(components)); + components.dwStructSize = sizeof(components); + components.dwHostNameLength = 1; + components.dwUrlPathLength = 1; + if (!WinHttpCrackUrl(zUrlWide, 0, 0, &components)) { + lastErr = GetLastError(); + errFunc = "WinHttpCrackUrl"; + goto error; + } + + session = WinHttpOpen(L"sqlite3-http", + WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + 0); + + if (!session) { + lastErr = GetLastError(); + errFunc = "WinHttpCrackOpen"; + goto error; + } + + // WinHttpSetStatusCallback(session, + // (WINHTTP_STATUS_CALLBACK)statusCallback, + // WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS, + // 0); + + zHostWide = sqlite3_malloc(sizeof(wchar_t) * (components.dwHostNameLength + 1)); + if (!zHostWide) { + rc = SQLITE_NOMEM; + goto error; + } + memcpy(zHostWide, components.lpszHostName, sizeof(wchar_t) * components.dwHostNameLength); + zHostWide[components.dwHostNameLength] = L'\0'; + + conn = WinHttpConnect(session, zHostWide, components.nPort, 0); + + if (!conn) { + lastErr = GetLastError(); + errFunc = "WinHttpConnect"; + goto error; + } + + zMethodWide = utf8_to_unicode(req->zMethod); + if (!zMethodWide) { + rc = SQLITE_NOMEM; + goto error; + } + + request = WinHttpOpenRequest(conn, + zMethodWide, + components.lpszUrlPath, + NULL, + WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, + components.nPort == 443 ? WINHTTP_FLAG_SECURE : 0); + + if (!request) { + lastErr = GetLastError(); + errFunc = "WinHttpOpenRequest"; + goto error; + } + + if (req->zHeaders) { + zHeadersWide = utf8_to_unicode(req->zHeaders); + } + + if (!WinHttpSendRequest(request, + zHeadersWide, + zHeadersWide ? -1 : 0, + (void*)req->pBody, + req->szBody, + req->szBody, + 0)) { + lastErr = GetLastError(); + errFunc = "WinHttpSendRequest"; + goto error; + } + + if (!WinHttpReceiveResponse(request, NULL)) { + lastErr = GetLastError(); + errFunc = "WinHttpReceiveResponse"; + goto error; + } + + assert(resp->pBody == NULL); + assert(resp->szBody == 0); + for (;;) { + DWORD dwSize = 0; + DWORD nRead = 0; + char* nb; + if (!WinHttpQueryDataAvailable(request, &dwSize)) { + lastErr = GetLastError(); + errFunc = "WinHttpQueryDataAvailable"; + goto error; + } + if (dwSize == 0) { + break; + } + nb = sqlite3_realloc(resp->pBody, resp->szBody + dwSize); + if (!nb) { + rc = SQLITE_NOMEM; + goto error; + } + resp->pBody = nb; + if (!WinHttpReadData(request, (char*)resp->pBody + resp->szBody, dwSize, &nRead)) { + lastErr = GetLastError(); + errFunc = "WinHttpReadData"; + goto error; + } + resp->szBody += dwSize; + if (nRead == 0) { + break; + } + } + + rc = query_headers(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, &resp->zHeaders); + if (rc != SQLITE_OK) { + if (rc == SQLITE_ERROR) { + lastErr = GetLastError(); + errFunc = "WinHttpQueryHeaders(WINHTTP_QUERY_RAW_HEADERS_CRLF)"; + } + goto error; + } + rc = SQLITE_ERROR; // need to reset this back. Otherwise a jump to error below + // could leave rc as SQLITE_OK... + + dwSize = sizeof(dwStatusCode); + if (!WinHttpQueryHeaders(request, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, + &dwStatusCode, + &dwSize, + WINHTTP_NO_HEADER_INDEX)) { + lastErr = GetLastError(); + errFunc = "WinHttpQueryHeaders(WINHTTP_QUERY_STATUS_CODE)"; + goto error; + } + + resp->iStatusCode = dwStatusCode; + + remove_all_but_last_headers(resp->zHeaders); + separate_status_and_headers(&resp->zStatus, resp->zHeaders); + + rc = SQLITE_OK; + + goto done; + +error: + + if (rc == SQLITE_ERROR && errFunc) { + *ppErrMsg = win32_get_last_error(lastErr); + } + +done: + + sqlite3_free(zUrlWide); + sqlite3_free(zHostWide); + sqlite3_free(zMethodWide); + sqlite3_free(zHeadersWide); + WinHttpCloseHandle(request); + WinHttpCloseHandle(conn); + WinHttpCloseHandle(session); + + return rc; +} + +#endif // HTTP_BACKEND_WINHTTP diff --git a/src/http_next_header.c b/src/http_next_header.c new file mode 100644 index 0000000..e2d2052 --- /dev/null +++ b/src/http_next_header.c @@ -0,0 +1,174 @@ +#include <ctype.h> +#include <sqlite3.h> + +static int is_token_char(int c) { + return isalnum(c) || (c != 0 && strchr("!#$%&'*+-.^_`|~", c)); +} + +static int is_header_ws(int c) { + return c == ' ' || c == '\t'; +} + +static int is_vchar(int c) { + return c >= 0x21 && c <= 0x7e; +} + +// Return SQLITE_ROW if a header was extracted +// Return SQLITE_DONE if headers has been exhausted +// Return SQLITE_ERROR on malformed input +int http_next_header(const char* headers, + int size, + int* pParsed, + const char** ppName, + int* pNameSize, + const char** ppValue, + int* pValueSize) { + const char* headersEnd = headers + size; + const char* nameStart = NULL; + const char* nameEnd = NULL; + const char* valueStart = NULL; + const char* valueEnd = NULL; + const char* last = NULL; + const char* p = headers; + + enum { + StateInit, + StateName, + StateNameTrailingWs, + StateValueLeadingWs, + StateValue, + StateCR, + StateCheckFold, + StateDone, + StateError, + }; + + int state = StateInit; + + *ppName = NULL; + *pNameSize = 0; + *ppValue = NULL; + *pValueSize = 0; + + if (size == 0) { + *pParsed = 0; + return SQLITE_DONE; + } + + for (; p != headersEnd && state != StateDone && state != StateError; ++p) { + switch (state) { + case StateInit: + // printf("StateInit %02x %c\n", *p, isalnum(*p) ? *p : ' '); + if (*p == '\r') { + state = StateCR; + } else if (is_header_ws(*p)) { + // There shouldn't be whitespace here. Folding is handled in + // StateCR/StateCheckFold. + state = StateError; + } else { + nameStart = p; + state = StateName; + } + break; + + case StateName: + // printf("StateName %02x %c\n", *p, isalnum(*p) ? *p : ' '); + if (is_token_char(*p)) { + // accept + } else if (*p == ':') { + nameEnd = p; + state = StateValueLeadingWs; + } else if (is_header_ws(*p)) { + nameEnd = p; + state = StateNameTrailingWs; + } else { + state = StateError; + } + break; + + case StateNameTrailingWs: + // printf("StateNameTrailingWs %02x %c\n", *p, isalnum(*p) ? *p : ' '); + if (is_header_ws(*p)) { + // accept and skip + } else if (*p == ':') { + state = StateValueLeadingWs; + } else { + state = StateError; + } + break; + + case StateValueLeadingWs: + // printf("StateValueLeadingWs %02x %c\n", *p, isalnum(*p) ? *p : ' '); + if (is_header_ws(*p)) { + // accept and skip + } else if (*p == '\r') { + // empty value + state = StateCR; + } else if (is_vchar(*p)) { + valueStart = p; + last = p; + state = StateValue; + } else { + state = StateError; + } + break; + + case StateValue: + // printf("StateValue %02x %c\n", *p, isalnum(*p) ? *p : ' '); + if (is_header_ws(*p)) { + // accept but keep separate so that we can track trailing ws + } else if (*p == '\r') { + valueEnd = last + 1; + state = StateCR; + } else if (is_vchar(*p)) { + // accept + last = p; + } + break; + + case StateCR: + // printf("StateCr %02x %c\n", *p, isalnum(*p) ? *p : ' '); + if (*p == '\n') { + // If there's more data, check for folding. + if (p + 1 == headers + size) { + state = StateDone; + } else { + state = StateCheckFold; + } + } else { + state = StateError; + } + break; + + case StateCheckFold: + // printf("StateCheckFold %02x %c\n", *p, isalnum(*p) ? *p : ' '); + if (is_header_ws(*p)) { + state = StateValue; + } else { + state = StateDone; + --p; + } + break; + } + } + + *pParsed = p - headers; + + if (state != StateDone || state == StateError) { + return SQLITE_ERROR; + } + + if (!nameStart) { + return SQLITE_DONE; + } + + *ppName = nameStart; + *pNameSize = nameEnd - nameStart; + + if (valueStart) { + *ppValue = valueStart; + *pValueSize = valueEnd - valueStart; + } + + return SQLITE_ROW; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..8ae9d98 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,13 @@ +add_library(sqlite3 STATIC "${sqlite_SOURCE_DIR}/sqlite3.c") +target_include_directories(sqlite3 PUBLIC "${sqlite_SOURCE_DIR}") + +add_executable(t_http_next_header t_http_next_header.c ../http.c) +target_link_libraries(t_http_next_header PRIVATE sqlite3) +target_compile_definitions(t_http_next_header PRIVATE HTTP_BACKEND_DUMMY) +add_test(NAME http_next_header COMMAND t_http_next_header) + +add_executable(t_http t_http.c ../http.c) +target_link_libraries(t_http PRIVATE sqlite3) +target_compile_definitions(t_http PRIVATE HTTP_BACKEND_DUMMY SQLITE_CORE) +target_include_directories(t_http PRIVATE ../src) +add_test(NAME http COMMAND t_http) diff --git a/tests/init.c b/tests/init.c new file mode 100644 index 0000000..7fec8f1 --- /dev/null +++ b/tests/init.c @@ -0,0 +1,11 @@ +#include <sqlite3.h> +#include <stdio.h> + +typedef void (*entrypoint)(void); + +int sqlite3_http_init(sqlite3*, char**, const sqlite3_api_routines*); + +void test_init() { + sqlite3_initialize(); + sqlite3_auto_extension((entrypoint)sqlite3_http_init); +} diff --git a/tests/t_http.c b/tests/t_http.c new file mode 100644 index 0000000..5251dc7 --- /dev/null +++ b/tests/t_http.c @@ -0,0 +1,221 @@ +#include "http.h" + +#include <sqlite3.h> + +#include "test.h" + +SQLITE_EXTENSION_INIT3 + +void http_backend_dummy_set_errmsg(const char* zErrMsg); +void http_backend_dummy_set_response(http_response* response); +void http_backend_dummy_reset_request(); +const http_request* http_backend_dummy_get_last_request(); + +int sqlite3_http_init(sqlite3*, char**, const sqlite3_api_routines*); + +void new_text_response(http_response* response, + const char* zBody, + const char* zHeaders, + int iStatusCode, + const char* zStatus) { + memset(response, 0, sizeof(*response)); + assert(zStatus != NULL); + if (zBody) { + response->pBody = sqlite3_mprintf("%s", zBody); + response->szBody = strlen(zBody); + } + if (zHeaders) { + response->zHeaders = sqlite3_mprintf("%s", zHeaders); + response->szHeaders = strlen(zHeaders); + } + response->iStatusCode = iStatusCode; + response->zStatus = sqlite3_mprintf("%s", zStatus); +} + +static sqlite3* db; + +void test_http_get() { + sqlite3_stmt* stmt; + http_response response; + new_text_response(&response, "hello, world!", "Foo: Bar\r\n\r\n", 200, "HTTP/1.0 200 OK"); + http_backend_dummy_set_response(&response); + ASSERT_INT_EQ( + sqlite3_prepare_v2(db, "select * from http_get('http://example.com')", -1, &stmt, NULL), + SQLITE_OK); + ASSERT_INT_EQ(sqlite3_step(stmt), SQLITE_ROW); + ASSERT_INT_EQ(sqlite3_column_count(stmt), 4); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 0), "HTTP/1.0 200 OK"); + ASSERT_INT_EQ(sqlite3_column_int(stmt, 1), 200); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 2), "Foo: Bar\r\n\r\n"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 3), "hello, world!"); + ASSERT_INT_EQ(sqlite3_finalize(stmt), SQLITE_OK); +} + +void test_http_get_hidden_columns() { + sqlite3_stmt* stmt; + http_response response; + new_text_response(&response, "hello, world!", "Foo: Bar\r\n\r\n", 200, "HTTP/1.0 200 OK"); + http_backend_dummy_set_response(&response); + ASSERT_INT_EQ( + sqlite3_prepare_v2(db, + "select response_status, response_status_code, response_headers, " + "response_body, request_method, request_url, request_headers, " + "request_body from http_get('http://example.com/foo')", + -1, + &stmt, + NULL), + SQLITE_OK); + ASSERT_INT_EQ(sqlite3_step(stmt), SQLITE_ROW); + ASSERT_INT_EQ(sqlite3_column_count(stmt), 8); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 0), "HTTP/1.0 200 OK"); + ASSERT_INT_EQ(sqlite3_column_int(stmt, 1), 200); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 2), "Foo: Bar\r\n\r\n"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 3), "hello, world!"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 4), "GET"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 5), "http://example.com/foo"); + ASSERT_NULL(sqlite3_column_text(stmt, 6)); + ASSERT_NULL(sqlite3_column_text(stmt, 7)); + ASSERT_INT_EQ(sqlite3_finalize(stmt), SQLITE_OK); +} + +void test_http_get_request_headers() { + sqlite3_stmt* stmt; + http_response response; + new_text_response(&response, "hello, world!", "Foo: Bar\r\n\r\n", 200, "HTTP/1.0 200 OK"); + http_backend_dummy_set_response(&response); + ASSERT_INT_EQ( + sqlite3_prepare_v2(db, + "select response_status, response_status_code, response_headers, " + "response_body, request_method, request_url, request_headers, " + "request_body from http_get('http://example.com/foo', " + "http_headers('Req1', 'Val1', 'Req2', 'Val2'))", + -1, + &stmt, + NULL), + SQLITE_OK); + ASSERT_INT_EQ(sqlite3_step(stmt), SQLITE_ROW); + ASSERT_INT_EQ(sqlite3_column_count(stmt), 8); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 0), "HTTP/1.0 200 OK"); + ASSERT_INT_EQ(sqlite3_column_int(stmt, 1), 200); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 2), "Foo: Bar\r\n\r\n"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 3), "hello, world!"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 4), "GET"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 5), "http://example.com/foo"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 6), "Req1: Val1\r\nReq2: Val2\r\n"); + ASSERT_NULL(sqlite3_column_text(stmt, 7)); + ASSERT_INT_EQ(sqlite3_finalize(stmt), SQLITE_OK); +} + +void test_http_post() { + sqlite3_stmt* stmt; + http_response response; + new_text_response(&response, "hello, world!", "Foo: Bar\r\n\r\n", 200, "HTTP/1.0 200 OK"); + http_backend_dummy_set_response(&response); + ASSERT_INT_EQ( + sqlite3_prepare_v2(db, "select * from http_post('http://example.com')", -1, &stmt, NULL), + SQLITE_OK); + ASSERT_INT_EQ(sqlite3_step(stmt), SQLITE_ROW); + ASSERT_INT_EQ(sqlite3_column_count(stmt), 4); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 0), "HTTP/1.0 200 OK"); + ASSERT_INT_EQ(sqlite3_column_int(stmt, 1), 200); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 2), "Foo: Bar\r\n\r\n"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 3), "hello, world!"); + ASSERT_INT_EQ(sqlite3_finalize(stmt), SQLITE_OK); +} + +void test_http_post_hidden_columns() { + sqlite3_stmt* stmt; + http_response response; + new_text_response(&response, "hello, world!", "Foo: Bar\r\n\r\n", 200, "HTTP/1.0 200 OK"); + http_backend_dummy_set_response(&response); + ASSERT_INT_EQ( + sqlite3_prepare_v2(db, + "select response_status, response_status_code, response_headers, " + "response_body, request_method, request_url, request_headers, " + "request_body from http_post('http://example.com/foo')", + -1, + &stmt, + NULL), + SQLITE_OK); + ASSERT_INT_EQ(sqlite3_step(stmt), SQLITE_ROW); + ASSERT_INT_EQ(sqlite3_column_count(stmt), 8); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 0), "HTTP/1.0 200 OK"); + ASSERT_INT_EQ(sqlite3_column_int(stmt, 1), 200); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 2), "Foo: Bar\r\n\r\n"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 3), "hello, world!"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 4), "POST"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 5), "http://example.com/foo"); + ASSERT_NULL(sqlite3_column_text(stmt, 6)); + ASSERT_NULL(sqlite3_column_text(stmt, 7)); + ASSERT_INT_EQ(sqlite3_finalize(stmt), SQLITE_OK); +} + +void test_http_post_request_headers() { + sqlite3_stmt* stmt; + http_response response; + new_text_response(&response, "hello, world!", "Foo: Bar\r\n\r\n", 200, "HTTP/1.0 200 OK"); + http_backend_dummy_set_response(&response); + ASSERT_INT_EQ( + sqlite3_prepare_v2(db, + "select response_status, response_status_code, response_headers, " + "response_body, request_method, request_url, request_headers, " + "request_body from http_post('http://example.com/foo', " + "http_headers('Req1', 'Val1', 'Req2', 'Val2'))", + -1, + &stmt, + NULL), + SQLITE_OK); + ASSERT_INT_EQ(sqlite3_step(stmt), SQLITE_ROW); + ASSERT_INT_EQ(sqlite3_column_count(stmt), 8); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 0), "HTTP/1.0 200 OK"); + ASSERT_INT_EQ(sqlite3_column_int(stmt, 1), 200); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 2), "Foo: Bar\r\n\r\n"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 3), "hello, world!"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 4), "POST"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 5), "http://example.com/foo"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 6), "Req1: Val1\r\nReq2: Val2\r\n"); + ASSERT_NULL(sqlite3_column_text(stmt, 7)); + ASSERT_INT_EQ(sqlite3_finalize(stmt), SQLITE_OK); +} + +void test_http_post_request_body() { + sqlite3_stmt* stmt; + http_response response; + new_text_response(&response, "hello, world!", "Foo: Bar\r\n\r\n", 200, "HTTP/1.0 200 OK"); + http_backend_dummy_set_response(&response); + ASSERT_INT_EQ( + sqlite3_prepare_v2(db, + "select response_status, response_status_code, response_headers, " + "response_body, request_method, request_url, request_headers, " + "request_body from http_post('http://example.com/foo', " + "http_headers('Req1', 'Val1', 'Req2', 'Val2'), 'hello')", + -1, + &stmt, + NULL), + SQLITE_OK); + ASSERT_INT_EQ(sqlite3_step(stmt), SQLITE_ROW); + ASSERT_INT_EQ(sqlite3_column_count(stmt), 8); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 0), "HTTP/1.0 200 OK"); + ASSERT_INT_EQ(sqlite3_column_int(stmt, 1), 200); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 2), "Foo: Bar\r\n\r\n"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 3), "hello, world!"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 4), "POST"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 5), "http://example.com/foo"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 6), "Req1: Val1\r\nReq2: Val2\r\n"); + ASSERT_STR_EQ(sqlite3_column_text(stmt, 7), "hello"); + ASSERT_INT_EQ(sqlite3_finalize(stmt), SQLITE_OK); +} + +int main(int argc, char const* argv[]) { + sqlite3_initialize(); + sqlite3_auto_extension((void (*)(void))sqlite3_http_init); + ASSERT_INT_EQ(sqlite3_open(":memory:", &db), SQLITE_OK); + test_http_get(); + test_http_get_hidden_columns(); + test_http_get_request_headers(); + test_http_post(); + test_http_post_hidden_columns(); + test_http_post_request_headers(); + test_http_post_request_body(); + return 0; +} diff --git a/tests/t_http_next_header.c b/tests/t_http_next_header.c new file mode 100644 index 0000000..2f101af --- /dev/null +++ b/tests/t_http_next_header.c @@ -0,0 +1,353 @@ +#include <sqlite3.h> + +#include "test.h" + +int http_next_header(const char* headers, + int size, + int* pParsed, + const char** ppName, + int* pNameSize, + const char** ppValue, + int* pValueSize); + +void single_header() { + const char* headers = "Foo: Bar\r\n"; + int nParsed; + const char* name; + int nameSize; + const char* value; + int valueSize; + int rc = + http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_ROW); + ASSERT_INT_EQ(nameSize, 3); + ASSERT_MEM_EQ(name, "Foo", 3); + ASSERT_INT_EQ(valueSize, 3); + ASSERT_MEM_EQ(value, "Bar", 3); + ASSERT_INT_EQ(nParsed, 10); +} + +void empty_string() { + const char* headers = ""; + int nParsed; + const char* name; + int nameSize; + const char* value; + int valueSize; + int rc = + http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_DONE); + ASSERT_INT_EQ(nameSize, 0); + ASSERT_NULL(name); + ASSERT_INT_EQ(valueSize, 0); + ASSERT_NULL(value); + ASSERT_INT_EQ(nParsed, 0); +} + +void terminating_crnl() { + const char* headers = "\r\n"; + int nParsed; + const char* name; + int nameSize; + const char* value; + int valueSize; + int rc = + http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_DONE); + ASSERT_INT_EQ(nameSize, 0); + ASSERT_NULL(name); + ASSERT_INT_EQ(valueSize, 0); + ASSERT_NULL(value); + ASSERT_INT_EQ(nParsed, 2); +} + +void multiple_headers() { + const char* headers = "Content-Type: application/json\r\nContent-Length: " + "343\r\nAccess-Control-Allow-Origin: *\r\n\r\n"; + int nParsed; + const char* name; + int nameSize; + const char* value; + int valueSize; + int rc; + + rc = http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_ROW); + ASSERT_INT_EQ(nameSize, 12); + ASSERT_MEM_EQ(name, "Content-Type", 12); + ASSERT_INT_EQ(valueSize, 16); + ASSERT_MEM_EQ(value, "application/json", 16); + ASSERT_INT_EQ(nParsed, 32); + + headers += nParsed; + + rc = http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_ROW); + ASSERT_INT_EQ(nameSize, 14); + ASSERT_MEM_EQ(name, "Content-Length", 14); + ASSERT_INT_EQ(valueSize, 3); + ASSERT_MEM_EQ(value, "343", 3); + ASSERT_INT_EQ(nParsed, 21); + + headers += nParsed; + + rc = http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_ROW); + ASSERT_INT_EQ(nameSize, 27); + ASSERT_MEM_EQ(name, "Access-Control-Allow-Origin", 27); + ASSERT_INT_EQ(valueSize, 1); + ASSERT_MEM_EQ(value, "*", 1); + ASSERT_INT_EQ(nParsed, 32); + + headers += nParsed; + + rc = http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_DONE); + ASSERT_INT_EQ(nameSize, 0); + ASSERT_NULL(name); + ASSERT_INT_EQ(valueSize, 0); + ASSERT_NULL(value); + ASSERT_INT_EQ(nParsed, 2); +} + +void empty_value() { + const char* headers = "Foo:\r\n"; + int nParsed; + const char* name; + int nameSize; + const char* value; + int valueSize; + int rc = + http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_ROW); + ASSERT_INT_EQ(nameSize, 3); + ASSERT_MEM_EQ(name, "Foo", 3); + ASSERT_INT_EQ(valueSize, 0); + ASSERT_NULL(value); + ASSERT_INT_EQ(nParsed, 6); +} + +void empty_value_leading_ws() { + const char* headers = "Foo: \r\n"; + int nParsed; + const char* name; + int nameSize; + const char* value; + int valueSize; + int rc = + http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_ROW); + ASSERT_INT_EQ(nameSize, 3); + ASSERT_MEM_EQ(name, "Foo", 3); + ASSERT_INT_EQ(valueSize, 0); + ASSERT_NULL(value); + ASSERT_INT_EQ(nParsed, 11); +} + +void value_with_leading_ws() { + const char* headers = "Foo: \t Bar\r\n"; + int nParsed; + const char* name; + int nameSize; + const char* value; + int valueSize; + int rc = + http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_ROW); + ASSERT_INT_EQ(nameSize, 3); + ASSERT_MEM_EQ(name, "Foo", 3); + ASSERT_INT_EQ(valueSize, 3); + ASSERT_MEM_EQ(value, "Bar", 3); + ASSERT_INT_EQ(nParsed, 16); +} + +void value_with_trailing_ws() { + const char* headers = "Foo: Bar \t \r\n"; + int nParsed; + const char* name; + int nameSize; + const char* value; + int valueSize; + int rc = + http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_ROW); + ASSERT_INT_EQ(nameSize, 3); + ASSERT_MEM_EQ(name, "Foo", 3); + ASSERT_INT_EQ(valueSize, 3); + ASSERT_MEM_EQ(value, "Bar", 3); + ASSERT_INT_EQ(nParsed, strlen(headers)); +} + +void value_with_leading_and_trailing_ws() { + const char* headers = "Foo: Bar \t \r\n"; + int nParsed; + const char* name; + int nameSize; + const char* value; + int valueSize; + int rc = + http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_ROW); + ASSERT_INT_EQ(nameSize, 3); + ASSERT_MEM_EQ(name, "Foo", 3); + ASSERT_INT_EQ(valueSize, 3); + ASSERT_MEM_EQ(value, "Bar", 3); + ASSERT_INT_EQ(nParsed, strlen(headers)); +} + +void value_with_no_leading_ws() { + const char* headers = "Foo:Bar\r\n"; + int nParsed; + const char* name; + int nameSize; + const char* value; + int valueSize; + int rc = + http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_ROW); + ASSERT_INT_EQ(nameSize, 3); + ASSERT_MEM_EQ(name, "Foo", 3); + ASSERT_INT_EQ(valueSize, 3); + ASSERT_MEM_EQ(value, "Bar", 3); + ASSERT_INT_EQ(nParsed, strlen(headers)); +} + +void value_with_spaces_inside() { + const char* headers = "Foo: This is a value\r\n"; + int nParsed; + const char* name; + int nameSize; + const char* value; + int valueSize; + int rc = + http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_ROW); + ASSERT_INT_EQ(nameSize, 3); + ASSERT_MEM_EQ(name, "Foo", 3); + ASSERT_INT_EQ(valueSize, 20); + ASSERT_MEM_EQ(value, "This is a value", 20); + ASSERT_INT_EQ(nParsed, strlen(headers)); +} + +void value_with_spaces_inside_trailing_ws() { + const char* headers = "Foo: This is a value \r\n"; + int nParsed; + const char* name; + int nameSize; + const char* value; + int valueSize; + int rc = + http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_ROW); + ASSERT_INT_EQ(nameSize, 3); + ASSERT_MEM_EQ(name, "Foo", 3); + ASSERT_INT_EQ(valueSize, 20); + ASSERT_MEM_EQ(value, "This is a value", 20); + ASSERT_INT_EQ(nParsed, strlen(headers)); +} + +void value_with_spaces_inside_leading_ws() { + const char* headers = "Foo: This is a value\r\n"; + int nParsed; + const char* name; + int nameSize; + const char* value; + int valueSize; + int rc = + http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_ROW); + ASSERT_INT_EQ(nameSize, 3); + ASSERT_MEM_EQ(name, "Foo", 3); + ASSERT_INT_EQ(valueSize, 20); + ASSERT_MEM_EQ(value, "This is a value", 20); + ASSERT_INT_EQ(nParsed, strlen(headers)); +} + +void value_with_spaces_inside_leading_and_trailing_ws() { + const char* headers = "Foo: This is a value \r\n"; + int nParsed; + const char* name; + int nameSize; + const char* value; + int valueSize; + int rc = + http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_ROW); + ASSERT_INT_EQ(nameSize, 3); + ASSERT_MEM_EQ(name, "Foo", 3); + ASSERT_INT_EQ(valueSize, 20); + ASSERT_MEM_EQ(value, "This is a value", 20); + ASSERT_INT_EQ(nParsed, strlen(headers)); +} + +void value_with_spaces_inside_no_leading_ws() { + const char* headers = "Foo:This is a value\r\n"; + int nParsed; + const char* name; + int nameSize; + const char* value; + int valueSize; + int rc = + http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_ROW); + ASSERT_INT_EQ(nameSize, 3); + ASSERT_MEM_EQ(name, "Foo", 3); + ASSERT_INT_EQ(valueSize, 20); + ASSERT_MEM_EQ(value, "This is a value", 20); + ASSERT_INT_EQ(nParsed, strlen(headers)); +} + +void trailing_ws_after_name_is_ignored() { + const char* headers = "Foo\t : Bar\r\n"; + int nParsed; + const char* name; + int nameSize; + const char* value; + int valueSize; + int rc = + http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_ROW); + ASSERT_INT_EQ(nameSize, 3); + ASSERT_MEM_EQ(name, "Foo", 3); + ASSERT_INT_EQ(valueSize, 3); + ASSERT_MEM_EQ(value, "Bar", 3); + ASSERT_INT_EQ(nParsed, strlen(headers)); +} + +void folding() { + const char* headers = "Foo: Bar\r\n Baz\r\n\tAnd more\r\n"; + int nParsed; + const char* name; + int nameSize; + const char* value; + int valueSize; + int rc = + http_next_header(headers, strlen(headers), &nParsed, &name, &nameSize, &value, &valueSize); + ASSERT_INT_EQ(rc, SQLITE_ROW); + ASSERT_INT_EQ(nameSize, 3); + ASSERT_MEM_EQ(name, "Foo", 3); + ASSERT_INT_EQ(valueSize, 21); + ASSERT_MEM_EQ(value, "Bar\r\n Baz\r\n\tAnd more", 21); + ASSERT_INT_EQ(nParsed, strlen(headers)); +} + +int main(int argc, char const* argv[]) { + single_header(); + empty_string(); + terminating_crnl(); + multiple_headers(); + empty_value(); + empty_value_leading_ws(); + value_with_leading_ws(); + value_with_trailing_ws(); + value_with_leading_and_trailing_ws(); + value_with_no_leading_ws(); + value_with_spaces_inside(); + value_with_spaces_inside_trailing_ws(); + value_with_spaces_inside_leading_ws(); + value_with_spaces_inside_leading_and_trailing_ws(); + value_with_spaces_inside_no_leading_ws(); + trailing_ws_after_name_is_ignored(); + folding(); + return 0; +} diff --git a/tests/test.h b/tests/test.h new file mode 100644 index 0000000..38f5d6e --- /dev/null +++ b/tests/test.h @@ -0,0 +1,77 @@ +#ifndef TEST_H +#define TEST_H + +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#define ASSERT_INT_EQ(X, Y) \ + do { \ + int xxx = (X); \ + int yyy = (Y); \ + if (xxx != yyy) { \ + fprintf(stderr, \ + "*** %s ***\n%s is not equal to %s\nwhere\n%s is %d\nand\n%s " \ + "is %d\n", \ + __FUNCTION__, \ + #X, \ + #Y, \ + #X, \ + xxx, \ + #Y, \ + yyy); \ + exit(1); \ + } \ + } while (0) + +#define ASSERT_STR_EQ(X, Y) \ + do { \ + const char* xxx = (X); \ + const char* yyy = (Y); \ + if (strcmp(xxx, yyy) != 0) { \ + fprintf(stderr, \ + "*** %s ***\n%s is not equal to %s\nwhere\n%s is %s\nand\n%s " \ + "is %s\n", \ + __FUNCTION__, \ + #X, \ + #Y, \ + #X, \ + xxx, \ + #Y, \ + yyy); \ + exit(1); \ + } \ + } while (0) + +#define ASSERT_MEM_EQ(X, Y, SIZE) \ + do { \ + const char* xxx = (X); \ + const char* yyy = (Y); \ + if (memcmp(xxx, yyy, SIZE) != 0) { \ + fprintf(stderr, \ + "*** %s ***\n%s is not equal to %s\nwhere\n%s is %.*s\nand\n%s " \ + "is %.*s\n", \ + __FUNCTION__, \ + #X, \ + #Y, \ + #X, \ + SIZE, \ + xxx, \ + #Y, \ + SIZE, \ + yyy); \ + exit(1); \ + } \ + } while (0) + +#define ASSERT_NULL(X) \ + do { \ + const void* xxx = (X); \ + if (xxx != NULL) { \ + fprintf(stderr, "*** %s ***\n%s is not NULL (was %p)\n", __FUNCTION__, #X, xxx); \ + exit(1); \ + } \ + } while (0) + +#endif diff --git a/tests/tests.py b/tests/tests.py new file mode 100644 index 0000000..34d4f13 --- /dev/null +++ b/tests/tests.py @@ -0,0 +1,72 @@ +import http.server +import subprocess +import threading +import sqlite3 +import json + +tests = ( + { + "request_url": "http://localhost:8080/foobar", + "request_method": "GET", + "response_body": "foobar", + "response_headers": "Content-Size: 6\r\n", + "response_status_code": 200, + }, +) + +test_idx = 0 + +class Handler(http.server.BaseHTTPRequestHandler): + def version_string(self): + return "testclient/1.0" + + def date_time_string(self, timestamp=None): + return "Mon, 05 Sep 2022 19:26:53 GMT" + + def do_GET(self): + self._handle_test("GET") + + def do_POST(self): + self._handle_test("POST") + + def _handle_test(self, method): + global test_idx + data = tests[test_idx] + test_idx += 1 + + try: + if method != data["request_method"]: + self.send_response(400) + self.end_headers() + else: + self.send_response(data["response_status_code"]) + self.flush_headers() + self.wfile.write(data["response_headers"].encode("latin1", "strict")) + self.end_headers() + self.wfile.write(data["response_body"].encode("utf-8")) + except: + self.send_response(500) + self.end_headers() + + threading.Thread(target=server.shutdown).start() + + +server = http.server.HTTPServer(('', 8080), Handler) +server_thread = threading.Thread(target=server.serve_forever) +server_thread.start() + +for test in tests: + if test["request_method"] == "GET": + table = "http_get" + elif test["request_method"] == "POST": + table = "http_post" + request_url = test["request_url"] + sql = f".mode json\nselect request_method, request_url, request_headers, request_body, * from {table}('{request_url}')" + cp = subprocess.run(["./build/tests/Debug/sqlite3.exe", ":memory:"], input=sql, text=True, capture_output=True) + j = json.loads(cp.stdout) + print(j) + assert j[0]["response_status_code"] == test["response_status_code"] + assert j[0]["response_body"] == test["response_body"] + assert j[0]["response_headers"] == "Date: Mon, 05 Sep 2022 19:26:53 GMT\r\nServer: testclient/1.0\r\n" + test["response_headers"] + "\r\n" + +server_thread.join() diff --git a/tools/format.bat b/tools/format.bat new file mode 100644 index 0000000..8b3c1e8 --- /dev/null +++ b/tools/format.bat @@ -0,0 +1 @@ +@clang-format -i src\*.c src\*.h tests\*.c tests\*.h |
