diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000..c5233c0 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,7 @@ +# See: https://github.com/codespell-project/codespell#using-a-config-file +[codespell] +# In the event of a false positive, add the problematic word, in all lowercase, to a comma-separated list here: +ignore-words-list = , +check-filenames = +check-hidden = +skip = ./.git,./src/utility/URLParser diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..fa738ec --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# See: https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#about-the-dependabotyml-file +version: 2 + +updates: + # Configure check for outdated GitHub Actions actions in workflows. + # See: https://docs.github.com/en/github/administering-a-repository/keeping-your-actions-up-to-date-with-dependabot + - package-ecosystem: github-actions + directory: / # Check the repository's workflows under /.github/workflows/ + schedule: + interval: daily + labels: + - "topic: infrastructure" diff --git a/.github/workflows/check-arduino.yml b/.github/workflows/check-arduino.yml new file mode 100644 index 0000000..3cda2c8 --- /dev/null +++ b/.github/workflows/check-arduino.yml @@ -0,0 +1,28 @@ +name: Check Arduino + +# See: https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows +on: + push: + pull_request: + # schedule: + # # Run every Tuesday at 8 AM UTC to catch breakage caused by new rules added to Arduino Lint. + # - cron: "0 8 * * TUE" + workflow_dispatch: + repository_dispatch: + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Arduino Lint + uses: arduino/arduino-lint-action@v2 + with: + compliance: specification + library-manager: update + # Always use this setting for official repositories. Remove for 3rd party projects. + official: true + project-type: library diff --git a/.github/workflows/compile-examples.yml.disabled b/.github/workflows/compile-examples.yml.disabled new file mode 100644 index 0000000..cee34bb --- /dev/null +++ b/.github/workflows/compile-examples.yml.disabled @@ -0,0 +1,63 @@ +name: Compile Examples + +# See: https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows +on: + push: + paths: + - ".github/workflows/compile-examples.yml" + - "examples/**" + - "src/**" + pull_request: + paths: + - ".github/workflows/compile-examples.yml" + - "examples/**" + - "src/**" + # schedule: + # # Run every Tuesday at 8 AM UTC to catch breakage caused by changes to external resources (libraries, platforms). + # - cron: "0 8 * * TUE" + workflow_dispatch: + repository_dispatch: + +jobs: + build: + name: ${{ matrix.board.fqbn }} + runs-on: ubuntu-latest + + env: + SKETCHES_REPORTS_PATH: sketches-reports + + strategy: + fail-fast: false + + matrix: + board: + - fqbn: arduino:samd:mkr1000 + platforms: | + - name: arduino:samd + artifact-name-suffix: arduino-samd-mkr1000 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Compile examples + uses: arduino/compile-sketches@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fqbn: ${{ matrix.board.fqbn }} + platforms: ${{ matrix.board.platforms }} + libraries: | + # Install the library from the local path. + - source-path: ./ + - name: WiFi101 + sketch-paths: | + - examples + enable-deltas-report: true + sketches-report-path: ${{ env.SKETCHES_REPORTS_PATH }} + + - name: Save sketches report as workflow artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + path: ${{ env.SKETCHES_REPORTS_PATH }} + name: sketches-report-${{ matrix.board.artifact-name-suffix }} diff --git a/.github/workflows/report-size-deltas.yml.disabled b/.github/workflows/report-size-deltas.yml.disabled new file mode 100644 index 0000000..42bd95c --- /dev/null +++ b/.github/workflows/report-size-deltas.yml.disabled @@ -0,0 +1,24 @@ +name: Report Size Deltas + +# See: https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows +on: + push: + paths: + - ".github/workflows/report-size-deltas.yml" + # schedule: + # # Run at the minimum interval allowed by GitHub Actions. + # # Note: GitHub Actions periodically has outages which result in workflow failures. + # # In this event, the workflows will start passing again once the service recovers. + # - cron: "*/5 * * * *" + workflow_dispatch: + repository_dispatch: + +jobs: + report: + runs-on: ubuntu-latest + steps: + - name: Comment size deltas reports to PRs + uses: arduino/report-size-deltas@v1 + with: + # Regex matching the names of the workflow artifacts created by the "Compile Examples" workflow + sketches-reports-source: ^sketches-report-.+ diff --git a/.github/workflows/spell-check.yml.disabled b/.github/workflows/spell-check.yml.disabled new file mode 100644 index 0000000..45a2641 --- /dev/null +++ b/.github/workflows/spell-check.yml.disabled @@ -0,0 +1,22 @@ +name: Spell Check + +# See: https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows +on: + push: + pull_request: + # schedule: + # # Run every Tuesday at 8 AM UTC to catch new misspelling detections resulting from dictionary updates. + # - cron: "0 8 * * TUE" + workflow_dispatch: + repository_dispatch: + +jobs: + spellcheck: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Spell check + uses: codespell-project/actions-codespell@master diff --git a/.github/workflows/sync-labels.yml.disabled b/.github/workflows/sync-labels.yml.disabled new file mode 100644 index 0000000..f9f4af3 --- /dev/null +++ b/.github/workflows/sync-labels.yml.disabled @@ -0,0 +1,138 @@ +# Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/sync-labels.md +name: Sync Labels + +# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows +on: + push: + paths: + - ".github/workflows/sync-labels.ya?ml" + - ".github/label-configuration-files/*.ya?ml" + pull_request: + paths: + - ".github/workflows/sync-labels.ya?ml" + - ".github/label-configuration-files/*.ya?ml" + # schedule: + # # Run daily at 8 AM UTC to sync with changes to shared label configurations. + # - cron: "0 8 * * *" + workflow_dispatch: + repository_dispatch: + +env: + CONFIGURATIONS_FOLDER: .github/label-configuration-files + CONFIGURATIONS_ARTIFACT: label-configuration-files + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download JSON schema for labels configuration file + id: download-schema + uses: carlosperate/download-file-action@v2 + with: + file-url: https://raw.githubusercontent.com/arduino/tooling-project-assets/main/workflow-templates/assets/sync-labels/arduino-tooling-gh-label-configuration-schema.json + location: ${{ runner.temp }}/label-configuration-schema + + - name: Install JSON schema validator + run: | + sudo npm install \ + --global \ + ajv-cli \ + ajv-formats + + - name: Validate local labels configuration + run: | + # See: https://github.com/ajv-validator/ajv-cli#readme + ajv validate \ + --all-errors \ + -c ajv-formats \ + -s "${{ steps.download-schema.outputs.file-path }}" \ + -d "${{ env.CONFIGURATIONS_FOLDER }}/*.{yml,yaml}" + + download: + needs: check + runs-on: ubuntu-latest + + strategy: + matrix: + filename: + # Filenames of the shared configurations to apply to the repository in addition to the local configuration. + # https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/sync-labels + - universal.yml + + steps: + - name: Download + uses: carlosperate/download-file-action@v2 + with: + file-url: https://raw.githubusercontent.com/arduino/tooling-project-assets/main/workflow-templates/assets/sync-labels/${{ matrix.filename }} + + - name: Pass configuration files to next job via workflow artifact + uses: actions/upload-artifact@v4 + with: + path: | + *.yaml + *.yml + if-no-files-found: error + name: ${{ env.CONFIGURATIONS_ARTIFACT }} + + sync: + needs: download + runs-on: ubuntu-latest + + steps: + - name: Set environment variables + run: | + # See: https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable + echo "MERGED_CONFIGURATION_PATH=${{ runner.temp }}/labels.yml" >> "$GITHUB_ENV" + + - name: Determine whether to dry run + id: dry-run + if: > + github.event_name == 'pull_request' || + ( + ( + github.event_name == 'push' || + github.event_name == 'workflow_dispatch' + ) && + github.ref != format('refs/heads/{0}', github.event.repository.default_branch) + ) + run: | + # Use of this flag in the github-label-sync command will cause it to only check the validity of the + # configuration. + echo "::set-output name=flag::--dry-run" + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download configuration files artifact + uses: actions/download-artifact@v4 + with: + name: ${{ env.CONFIGURATIONS_ARTIFACT }} + path: ${{ env.CONFIGURATIONS_FOLDER }} + + - name: Remove unneeded artifact + uses: geekyeggo/delete-artifact@v5 + with: + name: ${{ env.CONFIGURATIONS_ARTIFACT }} + + - name: Merge label configuration files + run: | + # Merge all configuration files + shopt -s extglob + cat "${{ env.CONFIGURATIONS_FOLDER }}"/*.@(yml|yaml) > "${{ env.MERGED_CONFIGURATION_PATH }}" + + - name: Install github-label-sync + run: sudo npm install --global github-label-sync + + - name: Sync labels + env: + GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # See: https://github.com/Financial-Times/github-label-sync + github-label-sync \ + --labels "${{ env.MERGED_CONFIGURATION_PATH }}" \ + ${{ steps.dry-run.outputs.flag }} \ + ${{ github.repository }} diff --git a/.gitignore b/.gitignore index 653acab..24a0007 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ examples/node_test_server/node_modules/ *.DS_Store */.DS_Store examples/.DS_Store +.idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md index ac78d18..fc0e879 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ ## ArduinoHttpClient 0.3.1 - 2017.09.25 * Changed examples to support Arduino Create secret tabs -* Increase WebSocket secrect-key length to 24 characters +* Increase WebSocket secret-key length to 24 characters ## ArduinoHttpClient 0.3.0 - 2017.04.20 diff --git a/README.md b/README.md index 1b95559..e9c163b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # ArduinoHttpClient +[![Check Arduino status](https://github.com/arduino-libraries/ArduinoHttpClient/actions/workflows/check-arduino.yml/badge.svg)](https://github.com/arduino-libraries/ArduinoHttpClient/actions/workflows/check-arduino.yml) +[![Compile Examples status](https://github.com/arduino-libraries/ArduinoHttpClient/actions/workflows/compile-examples.yml/badge.svg)](https://github.com/arduino-libraries/ArduinoHttpClient/actions/workflows/compile-examples.yml) +[![Spell Check status](https://github.com/arduino-libraries/ArduinoHttpClient/actions/workflows/spell-check.yml/badge.svg)](https://github.com/arduino-libraries/ArduinoHttpClient/actions/workflows/spell-check.yml) + ArduinoHttpClient is a library to make it easier to interact with web servers from Arduino. Derived from [Adrian McEwen's HttpClient library](https://github.com/amcewen/HttpClient) diff --git a/examples/BasicAuthGet/BasicAuthGet.ino b/examples/BasicAuthGet/BasicAuthGet.ino index ca801b7..861acc1 100644 --- a/examples/BasicAuthGet/BasicAuthGet.ino +++ b/examples/BasicAuthGet/BasicAuthGet.ino @@ -15,7 +15,7 @@ #include #include "arduino_secrets.h" ///////please enter your sensitive data in the Secret tab/arduino_secrets.h -/////// Wifi Settings /////// +/////// WiFi Settings /////// char ssid[] = SECRET_SSID; char pass[] = SECRET_PASS; diff --git a/examples/CustomHeader/CustomHeader.ino b/examples/CustomHeader/CustomHeader.ino index e2131eb..4b8abbb 100644 --- a/examples/CustomHeader/CustomHeader.ino +++ b/examples/CustomHeader/CustomHeader.ino @@ -15,7 +15,7 @@ #include "arduino_secrets.h" ///////please enter your sensitive data in the Secret tab/arduino_secrets.h -/////// Wifi Settings /////// +/////// WiFi Settings /////// char ssid[] = SECRET_SSID; char pass[] = SECRET_PASS; diff --git a/examples/DweetGet/DweetGet.ino b/examples/DweetGet/DweetGet.ino index d191285..1c26c35 100644 --- a/examples/DweetGet/DweetGet.ino +++ b/examples/DweetGet/DweetGet.ino @@ -20,7 +20,7 @@ #include "arduino_secrets.h" ///////please enter your sensitive data in the Secret tab/arduino_secrets.h -/////// Wifi Settings /////// +/////// WiFi Settings /////// char ssid[] = SECRET_SSID; char pass[] = SECRET_PASS; diff --git a/examples/DweetPost/DweetPost.ino b/examples/DweetPost/DweetPost.ino index f1da2b9..17e76f5 100644 --- a/examples/DweetPost/DweetPost.ino +++ b/examples/DweetPost/DweetPost.ino @@ -16,7 +16,7 @@ #include "arduino_secrets.h" ///////please enter your sensitive data in the Secret tab/arduino_secrets.h -/////// Wifi Settings /////// +/////// WiFi Settings /////// char ssid[] = SECRET_SSID; char pass[] = SECRET_PASS; diff --git a/examples/HueBlink/HueBlink.ino b/examples/HueBlink/HueBlink.ino index eab74fe..0583fe9 100644 --- a/examples/HueBlink/HueBlink.ino +++ b/examples/HueBlink/HueBlink.ino @@ -10,7 +10,7 @@ The body of the PUT request looks like this: {"on": true} or {"on":false} - This example shows how to concatenate Strings to assemble the + This example shows how to concatenate Strings to assemble the PUT request and the body of the request. modified 15 Feb 2016 @@ -23,16 +23,16 @@ #include "arduino_secrets.h" ///////please enter your sensitive data in the Secret tab/arduino_secrets.h -/////// Wifi Settings /////// +/////// WiFi Settings /////// char ssid[] = SECRET_SSID; char pass[] = SECRET_PASS; -int status = WL_IDLE_STATUS; // the Wifi radio's status +int status = WL_IDLE_STATUS; // the WiFi radio's status char hueHubIP[] = "192.168.0.3"; // IP address of the HUE bridge String hueUserName = "huebridgeusername"; // hue bridge username -// make a wifi instance and a HttpClient instance: +// make a WiFiClient instance and a HttpClient instance: WiFiClient wifi; HttpClient httpClient = HttpClient(wifi, hueHubIP); @@ -42,7 +42,7 @@ void setup() { Serial.begin(9600); while (!Serial); // wait for serial port to connect. - // attempt to connect to Wifi network: + // attempt to connect to WiFi network: while ( status != WL_CONNECTED) { Serial.print("Attempting to connect to WPA SSID: "); Serial.println(ssid); @@ -95,4 +95,4 @@ void sendRequest(int light, String cmd, String value) { Serial.print("Server response: "); Serial.println(response); Serial.println(); -} \ No newline at end of file +} diff --git a/examples/ParseURL/ParseURL.ino b/examples/ParseURL/ParseURL.ino new file mode 100644 index 0000000..410ac85 --- /dev/null +++ b/examples/ParseURL/ParseURL.ino @@ -0,0 +1,29 @@ +#include "URLParser.h" + +void setup() { + + Serial.begin(9600); + + while(!Serial); + + Serial.println("starting"); + + ParsedUrl url( + "https://www.google.com/search?q=arduino" + ); + + Serial.print("parsed URL schema: \""); + Serial.print(url.schema()); + Serial.print("\"\nparsed URL host: \""); + Serial.print(url.host()); + Serial.print("\"\nparsed URL path: \""); + Serial.print(url.path()); + Serial.print("\"\nparsed URL query: \""); + Serial.print(url.query()); + Serial.print("\"\nparsed URL userinfo: \""); + Serial.print(url.userinfo()); + Serial.println("\""); + +} + +void loop() { } \ No newline at end of file diff --git a/examples/PostWithHeaders/PostWithHeaders.ino b/examples/PostWithHeaders/PostWithHeaders.ino index c2b1f0a..f97439f 100644 --- a/examples/PostWithHeaders/PostWithHeaders.ino +++ b/examples/PostWithHeaders/PostWithHeaders.ino @@ -1,7 +1,7 @@ /* POST with headers client for ArduinoHttpClient library Connects to server once every five seconds, sends a POST request - with custome headers and a request body + with custom headers and a request body created 14 Feb 2016 by Tom Igoe @@ -18,7 +18,7 @@ #include "arduino_secrets.h" ///////please enter your sensitive data in the Secret tab/arduino_secrets.h -/////// Wifi Settings /////// +/////// WiFi Settings /////// char ssid[] = SECRET_SSID; char pass[] = SECRET_PASS; diff --git a/examples/SimpleDelete/SimpleDelete.ino b/examples/SimpleDelete/SimpleDelete.ino index 120f8d4..336cb7a 100644 --- a/examples/SimpleDelete/SimpleDelete.ino +++ b/examples/SimpleDelete/SimpleDelete.ino @@ -15,7 +15,7 @@ #include "arduino_secrets.h" ///////please enter your sensitive data in the Secret tab/arduino_secrets.h -/////// Wifi Settings /////// +/////// WiFi Settings /////// char ssid[] = SECRET_SSID; char pass[] = SECRET_PASS; diff --git a/examples/SimpleGet/SimpleGet.ino b/examples/SimpleGet/SimpleGet.ino index 90d1f68..759b13f 100644 --- a/examples/SimpleGet/SimpleGet.ino +++ b/examples/SimpleGet/SimpleGet.ino @@ -14,7 +14,7 @@ #include "arduino_secrets.h" ///////please enter your sensitive data in the Secret tab/arduino_secrets.h -/////// Wifi Settings /////// +/////// WiFi Settings /////// char ssid[] = SECRET_SSID; char pass[] = SECRET_PASS; diff --git a/examples/SimpleHttpExample/SimpleHttpExample.ino b/examples/SimpleHttpExample/SimpleHttpExample.ino index f64b9ba..449d6af 100644 --- a/examples/SimpleHttpExample/SimpleHttpExample.ino +++ b/examples/SimpleHttpExample/SimpleHttpExample.ino @@ -2,7 +2,7 @@ // Released under Apache License, version 2.0 // // Simple example to show how to use the HttpClient library -// Get's the web page given at http:// and +// Gets the web page given at http:// and // outputs the content to the serial port #include @@ -14,7 +14,7 @@ #include "arduino_secrets.h" ///////please enter your sensitive data in the Secret tab/arduino_secrets.h -/////// Wifi Settings /////// +/////// WiFi Settings /////// char ssid[] = SECRET_SSID; char pass[] = SECRET_PASS; @@ -42,7 +42,7 @@ void setup() ; // wait for serial port to connect. Needed for native USB port only } - // attempt to connect to Wifi network: + // attempt to connect to WiFi network: Serial.print("Attempting to connect to WPA SSID: "); Serial.println(ssid); while (WiFi.begin(ssid, pass) != WL_CONNECTED) { @@ -129,5 +129,3 @@ void loop() // And just stop, now that we've tried a download while(1); } - - diff --git a/examples/SimplePost/SimplePost.ino b/examples/SimplePost/SimplePost.ino index a4704e5..8717438 100644 --- a/examples/SimplePost/SimplePost.ino +++ b/examples/SimplePost/SimplePost.ino @@ -14,7 +14,7 @@ #include "arduino_secrets.h" ///////please enter your sensitive data in the Secret tab/arduino_secrets.h -/////// Wifi Settings /////// +/////// WiFi Settings /////// char ssid[] = SECRET_SSID; char pass[] = SECRET_PASS; diff --git a/examples/SimplePut/SimplePut.ino b/examples/SimplePut/SimplePut.ino index 99af49f..06dcd15 100644 --- a/examples/SimplePut/SimplePut.ino +++ b/examples/SimplePut/SimplePut.ino @@ -14,7 +14,7 @@ #include "arduino_secrets.h" ///////please enter your sensitive data in the Secret tab/arduino_secrets.h -/////// Wifi Settings /////// +/////// WiFi Settings /////// char ssid[] = SECRET_SSID; char pass[] = SECRET_PASS; diff --git a/examples/SimpleWebSocket/SimpleWebSocket.ino b/examples/SimpleWebSocket/SimpleWebSocket.ino index b20d74b..32f74e1 100644 --- a/examples/SimpleWebSocket/SimpleWebSocket.ino +++ b/examples/SimpleWebSocket/SimpleWebSocket.ino @@ -15,7 +15,7 @@ #include "arduino_secrets.h" ///////please enter your sensitive data in the Secret tab/arduino_secrets.h -/////// Wifi Settings /////// +/////// WiFi Settings /////// char ssid[] = SECRET_SSID; char pass[] = SECRET_PASS; diff --git a/keywords.txt b/keywords.txt index 1b4bd2c..209c917 100644 --- a/keywords.txt +++ b/keywords.txt @@ -1,5 +1,5 @@ ####################################### -# Syntax Coloring Map For HttpClient +# Syntax Coloring Map For ArduinoHttpClient ####################################### ####################################### diff --git a/library.json b/library.json deleted file mode 100644 index 6bbda24..0000000 --- a/library.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "ArduinoHttpClient", - "keywords": "http, web, client, ethernet, wifi, GSM", - "description": "Easily interact with web servers from Arduino, using HTTP and WebSocket's.", - "repository": - { - "type": "git", - "url": "https://github.com/arduino-libraries/ArduinoHttpClient.git" - }, - "frameworks": "arduino", - "platforms": "*" -} diff --git a/library.properties b/library.properties index 632910e..c93d0de 100644 --- a/library.properties +++ b/library.properties @@ -1,8 +1,8 @@ name=ArduinoHttpClient -version=0.4.0 +version=0.6.1 author=Arduino maintainer=Arduino -sentence=[EXPERIMENTAL] Easily interact with web servers from Arduino, using HTTP and WebSocket's. +sentence=[EXPERIMENTAL] Easily interact with web servers from Arduino, using HTTP and WebSockets. paragraph=This library can be used for HTTP (GET, POST, PUT, DELETE) requests to a web server. It also supports exchanging messages with WebSocket servers. Based on Adrian McEwen's HttpClient library. category=Communication url=https://github.com/arduino-libraries/ArduinoHttpClient diff --git a/src/ArdHttpClient.cpp b/src/ArdHttpClient.cpp new file mode 100644 index 0000000..0c2d31b --- /dev/null +++ b/src/ArdHttpClient.cpp @@ -0,0 +1,928 @@ +// Class to simplify HTTP fetching on Arduino +// (c) Copyright 2010-2011 MCQN Ltd +// Released under Apache License, version 2.0 + +#include "ArdHttpClient.h" +#include "b64.h" +#include + +// Initialize constants +const char* HttpClient::kUserAgent = "Arduino/2.2.0"; +const char* HttpClient::kContentLengthPrefix = HTTP_HEADER_CONTENT_LENGTH ": "; +const char* HttpClient::kTransferEncodingChunked = HTTP_HEADER_TRANSFER_ENCODING ": " HTTP_HEADER_VALUE_CHUNKED; + +HttpClient::HttpClient(Client& aClient, const char* aServerName, uint16_t aServerPort) + : iClient(&aClient), iServerName(aServerName), iServerAddress(), iServerPort(aServerPort), + iConnectionClose(true), iSendDefaultRequestHeaders(true) +{ + resetState(); +} + +HttpClient::HttpClient(Client& aClient, const String& aServerName, uint16_t aServerPort) + : HttpClient(aClient, aServerName.c_str(), aServerPort) +{ +} + +HttpClient::HttpClient(Client& aClient, const IPAddress& aServerAddress, uint16_t aServerPort) + : iClient(&aClient), iServerName(NULL), iServerAddress(aServerAddress), iServerPort(aServerPort), + iConnectionClose(true), iSendDefaultRequestHeaders(true) +{ + resetState(); +} + +void HttpClient::resetState() +{ + iState = eIdle; + iStatusCode = 0; + iContentLength = kNoContentLengthHeader; + iBodyLengthConsumed = 0; + iContentLengthPtr = kContentLengthPrefix; + iTransferEncodingChunkedPtr = kTransferEncodingChunked; + iIsChunked = false; + iChunkLength = 0; + iHttpResponseTimeout = kHttpResponseTimeout; + iHttpWaitForDataDelay = kHttpWaitForDataDelay; +} + +void HttpClient::stop() +{ + iClient->stop(); + resetState(); +} + +void HttpClient::connectionKeepAlive() +{ + iConnectionClose = false; +} + +void HttpClient::noDefaultRequestHeaders() +{ + iSendDefaultRequestHeaders = false; +} + +void HttpClient::beginRequest() +{ + iState = eRequestStarted; +} + +int HttpClient::startRequest(const char* aURLPath, const char* aHttpMethod, + const char* aContentType, int aContentLength, const byte aBody[]) +{ + if (iState == eReadingBody || iState == eReadingChunkLength || iState == eReadingBodyChunk) + { + flushClientRx(); + + resetState(); + } + + tHttpState initialState = iState; + + if ((eIdle != iState) && (eRequestStarted != iState)) + { + return HTTP_ERROR_API; + } + + if (iConnectionClose || !iClient->connected()) + { + if (iServerName) + { + if (!(iClient->connect(iServerName, iServerPort) > 0)) + { + #ifdef LOGGING + Serial.println("Connection failed"); + #endif + return HTTP_ERROR_CONNECTION_FAILED; + } + } + else + { + if (!(iClient->connect(iServerAddress, iServerPort) > 0)) + { + #ifdef LOGGING + Serial.println("Connection failed"); + #endif + return HTTP_ERROR_CONNECTION_FAILED; + } + } + } + else + { + #ifdef LOGGING + Serial.println("Connection already open"); + #endif + } + + // Now we're connected, send the first part of the request + int ret = sendInitialHeaders(aURLPath, aHttpMethod); + + if (HTTP_SUCCESS == ret) + { + if (aContentType) + { + sendHeader(HTTP_HEADER_CONTENT_TYPE, aContentType); + } + + if (aContentLength > 0) + { + sendHeader(HTTP_HEADER_CONTENT_LENGTH, aContentLength); + } + + bool hasBody = (aBody && aContentLength > 0); + + if (initialState == eIdle || hasBody) + { + // This was a simple version of the API, so terminate the headers now + finishHeaders(); + } + // else we'll call it in endRequest or in the first call to print, etc. + + if (hasBody) + { + write(aBody, aContentLength); + } + } + + return ret; +} + +int HttpClient::sendInitialHeaders(const char* aURLPath, const char* aHttpMethod) +{ + #ifdef LOGGING + Serial.println("Connected"); + #endif + // Send the HTTP command, i.e. "GET /somepath/ HTTP/1.0" + iClient->print(aHttpMethod); + iClient->print(" "); + + iClient->print(aURLPath); + iClient->println(" HTTP/1.1"); + if (iSendDefaultRequestHeaders) + { + // The host header, if required + if (iServerName) + { + iClient->print("Host: "); + iClient->print(iServerName); + if (iServerPort != kHttpPort && iServerPort != kHttpsPort) + { + iClient->print(":"); + iClient->print(iServerPort); + } + iClient->println(); + } + // And user-agent string + sendHeader(HTTP_HEADER_USER_AGENT, kUserAgent); + } + + if (iConnectionClose) + { + // Tell the server to + // close this connection after we're done + sendHeader(HTTP_HEADER_CONNECTION, "close"); + } + + // Everything has gone well + iState = eRequestStarted; + return HTTP_SUCCESS; +} + +void HttpClient::sendHeader(const char* aHeader) +{ + iClient->println(aHeader); +} + +void HttpClient::sendHeader(const char* aHeaderName, const char* aHeaderValue) +{ + iClient->print(aHeaderName); + iClient->print(": "); + iClient->println(aHeaderValue); +} + +void HttpClient::sendHeader(const char* aHeaderName, const int aHeaderValue) +{ + iClient->print(aHeaderName); + iClient->print(": "); + iClient->println(aHeaderValue); +} + +void HttpClient::sendBasicAuth(const char* aUser, const char* aPassword) +{ + // Send the initial part of this header line + iClient->print("Authorization: Basic "); + // Now Base64 encode "aUser:aPassword" and send that + // This seems trickier than it should be but it's mostly to avoid either + // (a) some arbitrarily sized buffer which hopes to be big enough, or + // (b) allocating and freeing memory + // ...so we'll loop through 3 bytes at a time, outputting the results as we + // go. + // In Base64, each 3 bytes of unencoded data become 4 bytes of encoded data + unsigned char input[3]; + unsigned char output[5]; // Leave space for a '\0' terminator so we can easily print + int userLen = strlen(aUser); + int passwordLen = strlen(aPassword); + int inputOffset = 0; + for (int i = 0; i < (userLen + 1 + passwordLen); i++) + { + // Copy the relevant input byte into the input + if (i < userLen) + { + input[inputOffset++] = aUser[i]; + } + else if (i == userLen) + { + input[inputOffset++] = ':'; + } + else + { + input[inputOffset++] = aPassword[i - (userLen + 1)]; + } + // See if we've got a chunk to encode + if ((inputOffset == 3) || (i == userLen + passwordLen) ) + { + // We've either got to a 3-byte boundary, or we've reached then end + b64_encode(input, inputOffset, output, 4); + // NUL-terminate the output string + output[4] = '\0'; + // And write it out + iClient->print((char*)output); +// FIXME We might want to fill output with '=' characters if b64_encode doesn't +// FIXME do it for us when we're encoding the final chunk + inputOffset = 0; + } + } + // And end the header we've sent + iClient->println(); +} + +void HttpClient::finishHeaders() +{ + iClient->println(); + iState = eRequestSent; +} + +void HttpClient::flushClientRx() +{ + while (iClient->available()) + { + iClient->read(); + } +} + +void HttpClient::endRequest() +{ + beginBody(); +} + +void HttpClient::beginBody() +{ + if (iState < eRequestSent) + { + // We still need to finish off the headers + finishHeaders(); + } + // else the end of headers has already been sent, so nothing to do here +} + +int HttpClient::get(const char* aURLPath) +{ + return startRequest(aURLPath, HTTP_METHOD_GET); +} + +int HttpClient::get(const String& aURLPath) +{ + return get(aURLPath.c_str()); +} + +int HttpClient::post(const char* aURLPath) +{ + return startRequest(aURLPath, HTTP_METHOD_POST); +} + +int HttpClient::post(const String& aURLPath) +{ + return post(aURLPath.c_str()); +} + +int HttpClient::post(const char* aURLPath, const char* aContentType, const char* aBody) +{ + return post(aURLPath, aContentType, strlen(aBody), (const byte*)aBody); +} + +int HttpClient::post(const String& aURLPath, const String& aContentType, const String& aBody) +{ + return post(aURLPath.c_str(), aContentType.c_str(), aBody.length(), (const byte*)aBody.c_str()); +} + +int HttpClient::post(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]) +{ + return startRequest(aURLPath, HTTP_METHOD_POST, aContentType, aContentLength, aBody); +} + +int HttpClient::put(const char* aURLPath) +{ + return startRequest(aURLPath, HTTP_METHOD_PUT); +} + +int HttpClient::put(const String& aURLPath) +{ + return put(aURLPath.c_str()); +} + +int HttpClient::put(const char* aURLPath, const char* aContentType, const char* aBody) +{ + return put(aURLPath, aContentType, strlen(aBody), (const byte*)aBody); +} + +int HttpClient::put(const String& aURLPath, const String& aContentType, const String& aBody) +{ + return put(aURLPath.c_str(), aContentType.c_str(), aBody.length(), (const byte*)aBody.c_str()); +} + +int HttpClient::put(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]) +{ + return startRequest(aURLPath, HTTP_METHOD_PUT, aContentType, aContentLength, aBody); +} + +int HttpClient::patch(const char* aURLPath) +{ + return startRequest(aURLPath, HTTP_METHOD_PATCH); +} + +int HttpClient::patch(const String& aURLPath) +{ + return patch(aURLPath.c_str()); +} + +int HttpClient::patch(const char* aURLPath, const char* aContentType, const char* aBody) +{ + return patch(aURLPath, aContentType, strlen(aBody), (const byte*)aBody); +} + +int HttpClient::patch(const String& aURLPath, const String& aContentType, const String& aBody) +{ + return patch(aURLPath.c_str(), aContentType.c_str(), aBody.length(), (const byte*)aBody.c_str()); +} + +int HttpClient::patch(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]) +{ + return startRequest(aURLPath, HTTP_METHOD_PATCH, aContentType, aContentLength, aBody); +} + +int HttpClient::del(const char* aURLPath) +{ + return startRequest(aURLPath, HTTP_METHOD_DELETE); +} + +int HttpClient::del(const String& aURLPath) +{ + return del(aURLPath.c_str()); +} + +int HttpClient::del(const char* aURLPath, const char* aContentType, const char* aBody) +{ + return del(aURLPath, aContentType, strlen(aBody), (const byte*)aBody); +} + +int HttpClient::del(const String& aURLPath, const String& aContentType, const String& aBody) +{ + return del(aURLPath.c_str(), aContentType.c_str(), aBody.length(), (const byte*)aBody.c_str()); +} + +int HttpClient::del(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]) +{ + return startRequest(aURLPath, HTTP_METHOD_DELETE, aContentType, aContentLength, aBody); +} + +int HttpClient::responseStatusCode() +{ + if (iState < eRequestSent) + { + return HTTP_ERROR_API; + } + // The first line will be of the form Status-Line: + // HTTP-Version SP Status-Code SP Reason-Phrase CRLF + // Where HTTP-Version is of the form: + // HTTP-Version = "HTTP" "/" 1*DIGIT "." 1*DIGIT + + int c = '\0'; + do + { + // Make sure the status code is reset, and likewise the state. This + // lets us easily cope with 1xx informational responses by just + // ignoring them really, and reading the next line for a proper response + iStatusCode = 0; + iState = eRequestSent; + + unsigned long timeoutStart = millis(); + // Psuedo-regexp we're expecting before the status-code + const char* statusPrefix = "HTTP/*.* "; + const char* statusPtr = statusPrefix; + // Whilst we haven't timed out & haven't reached the end of the headers + while ((c != '\n') && + ((millis() - timeoutStart) < iHttpResponseTimeout )) + { + if (available()) + { + c = HttpClient::read(); + if (c != -1) + { + switch (iState) + { + case eRequestSent: + // We haven't reached the status code yet + if ((*statusPtr == '*') || (*statusPtr == c) ) + { + // This character matches, just move along + statusPtr++; + if (*statusPtr == '\0') + { + // We've reached the end of the prefix + iState = eReadingStatusCode; + } + } + else + { + return HTTP_ERROR_INVALID_RESPONSE; + } + break; + case eReadingStatusCode: + if (isdigit(c)) + { + // This assumes we won't get more than the 3 digits we + // want + iStatusCode = iStatusCode * 10 + (c - '0'); + } + else + { + // We've reached the end of the status code + // We could sanity check it here or double-check for ' ' + // rather than anything else, but let's be lenient + iState = eStatusCodeRead; + } + break; + case eStatusCodeRead: + // We're just waiting for the end of the line now + break; + + default: + break; + }; + // We read something, reset the timeout counter + timeoutStart = millis(); + } + } + else + { + if (!iClient->connected()) { + // Server closed connection without any response + log_e("Connection closed, empty reply"); + return HTTP_ERROR_INVALID_RESPONSE; + } + // We haven't got any data, so let's pause to allow some to + // arrive + delay(iHttpWaitForDataDelay); + } + } + if ((c == '\n') && (iStatusCode < 200 && iStatusCode != 101) ) + { + // We've reached the end of an informational status line + c = '\0'; // Clear c so we'll go back into the data reading loop + } + } + // If we've read a status code successfully but it's informational (1xx) + // loop back to the start + while ((iState == eStatusCodeRead) && (iStatusCode < 200 && iStatusCode != 101) ); + + if ((c == '\n') && (iState == eStatusCodeRead) ) + { + // We've read the status-line successfully + return iStatusCode; + } + else if (c != '\n') + { + // We must've timed out before we reached the end of the line + return HTTP_ERROR_TIMED_OUT; + } + else + { + // This wasn't a properly formed status line, or at least not one we + // could understand + return HTTP_ERROR_INVALID_RESPONSE; + } +} + +int HttpClient::skipResponseHeaders() +{ + // Just keep reading until we finish reading the headers or time out + unsigned long timeoutStart = millis(); + // Whilst we haven't timed out & haven't reached the end of the headers + while ((!endOfHeadersReached()) && + ((millis() - timeoutStart) < iHttpResponseTimeout )) + { + if (available()) + { + (void)readHeader(); + // We read something, reset the timeout counter + timeoutStart = millis(); + } + else + { + // We haven't got any data, so let's pause to allow some to + // arrive + delay(iHttpWaitForDataDelay); + } + } + if (endOfHeadersReached()) + { + // Success + return HTTP_SUCCESS; + } + else + { + // We must've timed out + return HTTP_ERROR_TIMED_OUT; + } +} + +bool HttpClient::endOfHeadersReached() +{ + return (iState == eReadingBody || iState == eReadingChunkLength || iState == eReadingBodyChunk); +}; + +long HttpClient::contentLength() +{ + // skip the response headers, if they haven't been read already + if (!endOfHeadersReached()) + { + skipResponseHeaders(); + } + + return iContentLength; +} + +String HttpClient::responseBody() +{ + int bodyLength = contentLength(); + String response; + + if (bodyLength > 0) + { + // try to reserve bodyLength bytes + if (response.reserve(bodyLength) == 0) { + // String reserve failed + return String((const char*)NULL); + } + } + + // keep on timedRead'ing, until: + // - we have a content length: body length equals consumed or no bytes + // available + // - no content length: no bytes are available + while (iBodyLengthConsumed != bodyLength) + { + int c = timedRead(); + + if (c == -1) { + // read timed out, done + break; + } + + if (!response.concat((char)c)) { + // adding char failed + return String((const char*)NULL); + } + } + + if (bodyLength > 0 && (unsigned int)bodyLength != response.length()) { + // failure, we did not read in response content length bytes + return String((const char*)NULL); + } + + return response; +} + +int HttpClient::responseBody(Stream& stream) +{ + long bodyLength = contentLength(); + int writtenLength = 0; + size_t timestamp = millis(); + +// allocate buffer on the heap, note that it will be freed automatically when the function returns + std::vector readBuf(1024); + + int noDataCount = 0; + + // keep on timedRead'ing, until: + // - we have a content length: body length equals consumed or no bytes + // available + // - no content length: no bytes are available + while ( writtenLength < bodyLength) { + + int maxReadSize = std::min(readBuf.size(), available()); + int bytesRead = read(readBuf.data(), maxReadSize); + + if (bytesRead <= 0) { + noDataCount++; + if (noDataCount > 10) { + // no data for 300ms, abort + break; + } + delay(30); + continue; + } else { + noDataCount = 0; + } + + if (stream.write(readBuf.data(), bytesRead) != bytesRead) { + return 0; + } + + writtenLength += bytesRead; + + // Avoid watchdogs triggering + if (millis() - timestamp > 3000) { + delay(1); + timestamp = millis(); + } + } + + if (bodyLength > 0 && (unsigned long)bodyLength != writtenLength) { + // failure, we did not read in response content length bytes + return 0; + } + + return writtenLength; +} + +bool HttpClient::endOfBodyReached() +{ + if (endOfHeadersReached() && (contentLength() != kNoContentLengthHeader)) + { + // We've got to the body and we know how long it will be + return (iBodyLengthConsumed >= contentLength()); + } + return false; +} + +int HttpClient::available() +{ + if (iState == eReadingChunkLength) + { + while (iClient->available()) + { + char c = iClient->read(); + + if (c == '\n') + { + iState = eReadingBodyChunk; + break; + } + else if (c == '\r') + { + // no-op + } + else if (isHexadecimalDigit(c)) + { + char digit[2] = {c, '\0'}; + + iChunkLength = (iChunkLength * 16) + strtol(digit, NULL, 16); + } + } + } + + if (iState == eReadingBodyChunk && iChunkLength == 0) + { + iState = eReadingChunkLength; + } + + if (iState == eReadingChunkLength) + { + return 0; + } + + int clientAvailable = iClient->available(); + + if (iState == eReadingBodyChunk) + { + return min(clientAvailable, iChunkLength); + } + else + { + return clientAvailable; + } +} + + +int HttpClient::read() +{ + if (iIsChunked && !available()) + { + return -1; + } + + int ret = iClient->read(); + if (ret >= 0) + { + if (endOfHeadersReached() && iContentLength > 0) + { + // We're outputting the body now and we've seen a Content-Length header + // So keep track of how many bytes are left + iBodyLengthConsumed++; + } + + if (iState == eReadingBodyChunk) + { + iChunkLength--; + + if (iChunkLength == 0) + { + iState = eReadingChunkLength; + } + } + } + return ret; +} + +bool HttpClient::headerAvailable() +{ + // clear the currently stored header line + iHeaderLine = ""; + + while (!endOfHeadersReached()) + { + // read a byte from the header + int c = readHeader(); + if (c == 255) { + continue; + } + + if (c == '\r' || c == '\n') + { + if (iHeaderLine.length()) + { + // end of the line, all done + break; + } + else + { + // ignore any CR or LF characters + continue; + } + } + + // append byte to header line + iHeaderLine += (char)c; + } + + return (iHeaderLine.length() > 0); +} + +String HttpClient::readHeaderName() +{ + int colonIndex = iHeaderLine.indexOf(':'); + + if (colonIndex == -1) + { + return ""; + } + + return iHeaderLine.substring(0, colonIndex); +} + +String HttpClient::readHeaderValue() +{ + int colonIndex = iHeaderLine.indexOf(':'); + int startIndex = colonIndex + 1; + + if (colonIndex == -1) + { + return ""; + } + + // trim any leading whitespace + while (startIndex < (int)iHeaderLine.length() && isSpace(iHeaderLine[startIndex])) + { + startIndex++; + } + + return iHeaderLine.substring(startIndex); +} + +int HttpClient::read(uint8_t* buf, size_t size) +{ + int ret = iClient->read(buf, size); + if (endOfHeadersReached() && iContentLength > 0) + { + // We're outputting the body now and we've seen a Content-Length header + // So keep track of how many bytes are left + if (ret >= 0) + { + iBodyLengthConsumed += ret; + } + } + return ret; +} + +int HttpClient::readHeader() +{ + char c = HttpClient::read(); + + if (endOfHeadersReached() || (c == 0xFF)) + { + // We've passed the headers, but rather than return an error, we'll just + // act as a slightly less efficient version of read() + return c; + } + + // Whilst reading out the headers to whoever wants them, we'll keep an + // eye out for the "Content-Length" header + switch (iState) + { + case eStatusCodeRead: + // We're at the start of a line, or somewhere in the middle of reading + // the Content-Length prefix + if (*iContentLengthPtr == c) + { + // This character matches, just move along + iContentLengthPtr++; + if (*iContentLengthPtr == '\0') + { + // We've reached the end of the prefix + iState = eReadingContentLength; + // Just in case we get multiple Content-Length headers, this + // will ensure we just get the value of the last one + iContentLength = 0; + iBodyLengthConsumed = 0; + } + } + else if (*iTransferEncodingChunkedPtr == c) + { + // This character matches, just move along + iTransferEncodingChunkedPtr++; + if (*iTransferEncodingChunkedPtr == '\0') + { + // We've reached the end of the Transfer Encoding: chunked header + iIsChunked = true; + iState = eSkipToEndOfHeader; + } + } + else if (((iContentLengthPtr == kContentLengthPrefix) && (iTransferEncodingChunkedPtr == kTransferEncodingChunked)) && (c == '\r')) + { + // We've found a '\r' at the start of a line, so this is probably + // the end of the headers + iState = eLineStartingCRFound; + } + else + { + // This isn't the Content-Length or Transfer Encoding chunked header, skip to the end of the line + iState = eSkipToEndOfHeader; + } + break; + case eReadingContentLength: + if (isdigit(c)) + { + long _iContentLength = iContentLength * 10 + (c - '0'); + // Only apply if the value didn't wrap around + if (_iContentLength > iContentLength) { + iContentLength = _iContentLength; + } + } + else + { + // We've reached the end of the content length + // We could sanity check it here or double-check for "\r\n" + // rather than anything else, but let's be lenient + iState = eSkipToEndOfHeader; + } + break; + case eLineStartingCRFound: + if (c == '\n') + { + if (iIsChunked) + { + iState = eReadingChunkLength; + iChunkLength = 0; + } + else + { + iState = eReadingBody; + } + } + break; + default: + // We're just waiting for the end of the line now + break; + }; + + if ((c == '\n') && !endOfHeadersReached() ) + { + // We've got to the end of this line, start processing again + iState = eStatusCodeRead; + iContentLengthPtr = kContentLengthPrefix; + iTransferEncodingChunkedPtr = kTransferEncodingChunked; + } + // And return the character read to whoever wants it + return c; +} + + diff --git a/src/HttpClient.h b/src/ArdHttpClient.h similarity index 82% rename from src/HttpClient.h rename to src/ArdHttpClient.h index 38fd799..13e2163 100644 --- a/src/HttpClient.h +++ b/src/ArdHttpClient.h @@ -9,19 +9,19 @@ #include #include "Client.h" -static const int HTTP_SUCCESS =0; +static const int HTTP_SUCCESS = 0; // The end of the headers has been reached. This consumes the '\n' // Could not connect to the server -static const int HTTP_ERROR_CONNECTION_FAILED =-1; +static const int HTTP_ERROR_CONNECTION_FAILED = -1; // This call was made when the HttpClient class wasn't expecting it // to be called. Usually indicates your code is using the class // incorrectly -static const int HTTP_ERROR_API =-2; +static const int HTTP_ERROR_API = -2; // Spent too long waiting for a reply -static const int HTTP_ERROR_TIMED_OUT =-3; +static const int HTTP_ERROR_TIMED_OUT = -3; // The response from the server is invalid, is it definitely an HTTP // server? -static const int HTTP_ERROR_INVALID_RESPONSE =-4; +static const int HTTP_ERROR_INVALID_RESPONSE = -4; // Define some of the common methods and headers here // That lets other code reuse them without having to declare another copy @@ -40,9 +40,10 @@ static const int HTTP_ERROR_INVALID_RESPONSE =-4; class HttpClient : public Client { -public: - static const int kNoContentLengthHeader =-1; - static const int kHttpPort =80; + public: + static const int kNoContentLengthHeader = -1; + static const int kHttpPort = 80; + static const int kHttpsPort = 443; static const char* kUserAgent; // FIXME Write longer API request, using port and user-agent, example @@ -173,7 +174,9 @@ class HttpClient : public Client void sendHeader(const char* aHeader); void sendHeader(const String& aHeader) - { sendHeader(aHeader.c_str()); } + { + sendHeader(aHeader.c_str()); + } /** Send an additional header line. This is an alternate form of sendHeader() which takes the header name and content as separate strings. @@ -185,7 +188,9 @@ class HttpClient : public Client void sendHeader(const char* aHeaderName, const char* aHeaderValue); void sendHeader(const String& aHeaderName, const String& aHeaderValue) - { sendHeader(aHeaderName.c_str(), aHeaderValue.c_str()); } + { + sendHeader(aHeaderName.c_str(), aHeaderValue.c_str()); + } /** Send an additional header line. This is an alternate form of sendHeader() which takes the header name and content separately but where @@ -198,7 +203,9 @@ class HttpClient : public Client void sendHeader(const char* aHeaderName, const int aHeaderValue); void sendHeader(const String& aHeaderName, const int aHeaderValue) - { sendHeader(aHeaderName.c_str(), aHeaderValue); } + { + sendHeader(aHeaderName.c_str(), aHeaderValue); + } /** Send a basic authentication header. This will encode the given username and password, and send them in suitable header line for doing Basic @@ -209,7 +216,9 @@ class HttpClient : public Client void sendBasicAuth(const char* aUser, const char* aPassword); void sendBasicAuth(const String& aUser, const String& aPassword) - { sendBasicAuth(aUser.c_str(), aPassword.c_str()); } + { + sendBasicAuth(aUser.c_str(), aPassword.c_str()); + } /** Get the HTTP status code contained in the response. For example, 200 for successful request, 404 for file not found, etc. @@ -228,7 +237,7 @@ class HttpClient : public Client */ String readHeaderName(); - /** Read the vallue of the current response header. + /** Read the value of the current response header. Returns empty string if a header is not available. */ String readHeaderValue(); @@ -263,8 +272,12 @@ class HttpClient : public Client @return true if we are now at the end of the body, else false */ bool endOfBodyReached(); - virtual bool endOfStream() { return endOfBodyReached(); }; - virtual bool completed() { return endOfBodyReached(); }; + virtual bool endOfStream() { + return endOfBodyReached(); + }; + virtual bool completed() { + return endOfBodyReached(); + }; /** Return the length of the body. Also skips response headers if they have not been read already @@ -272,12 +285,14 @@ class HttpClient : public Client @return Length of the body, in bytes, or kNoContentLengthHeader if no Content-Length header was returned by the server */ - int contentLength(); + long contentLength(); /** Returns if the response body is chunked @return true if response body is chunked, false otherwise */ - int isResponseChunked() { return iIsChunked; } + int isResponseChunked() { + return iIsChunked; + } /** Return the response body as a String Also skips response headers if they have not been read already @@ -285,6 +300,7 @@ class HttpClient : public Client @return response body of request as a String */ String responseBody(); + int responseBody(Stream& stream); /** Enables connection keep-alive mode */ @@ -297,27 +313,72 @@ class HttpClient : public Client // Inherited from Print // Note: 1st call to these indicates the user is sending the body, so if need // Note: be we should finish the header first - virtual size_t write(uint8_t aByte) { if (iState < eRequestSent) { finishHeaders(); }; return iClient-> write(aByte); }; - virtual size_t write(const uint8_t *aBuffer, size_t aSize) { if (iState < eRequestSent) { finishHeaders(); }; return iClient->write(aBuffer, aSize); }; + virtual size_t write(uint8_t aByte) { + if (iState < eRequestSent) { + finishHeaders(); + }; + return iClient-> write(aByte); + }; + virtual size_t write(const uint8_t* aBuffer, size_t aSize) { + if (iState < eRequestSent) { + finishHeaders(); + }; + return iClient->write(aBuffer, aSize); + }; // Inherited from Stream virtual int available(); /** Read the next byte from the server. @return Byte read or -1 if there are no bytes available. */ virtual int read(); - virtual int read(uint8_t *buf, size_t size); - virtual int peek() { return iClient->peek(); }; - virtual void flush() { iClient->flush(); }; + virtual int read(uint8_t* buf, size_t size); + + /** Read bytes from server in buffer, + * + * this method is used i.e by Updater.writeStream() + * */ + size_t readBytes(uint8_t* buf, size_t size) override { + int result = iClient->read(buf, size); + if (result < 0) { + return 0; + } + return result; + }; + + virtual int peek() { + return iClient->peek(); + }; + virtual void flush() { + iClient->flush(); + }; // Inherited from Client - virtual int connect(IPAddress ip, uint16_t port) { return iClient->connect(ip, port); }; - virtual int connect(const char *host, uint16_t port) { return iClient->connect(host, port); }; + virtual int connect(IPAddress ip, uint16_t port) { + return iClient->connect(ip, port); + }; + virtual int connect(const char* host, uint16_t port) { + return iClient->connect(host, port); + }; virtual void stop(); - virtual uint8_t connected() { return iClient->connected(); }; - virtual operator bool() { return bool(iClient); }; - virtual uint32_t httpResponseTimeout() { return iHttpResponseTimeout; }; - virtual void setHttpResponseTimeout(uint32_t timeout) { iHttpResponseTimeout = timeout; }; -protected: + virtual uint8_t connected() { + return iClient->connected(); + }; + virtual operator bool() { + return bool(iClient); + }; + virtual uint32_t httpResponseTimeout() { + return iHttpResponseTimeout; + }; + virtual void setHttpResponseTimeout(uint32_t timeout) { + iHttpResponseTimeout = timeout; + }; + virtual uint32_t httpWaitForDataDelay() { + return iHttpWaitForDataDelay; + }; + virtual void setHttpWaitForDataDelay(uint32_t delay) { + iHttpWaitForDataDelay = delay; + }; + protected: /** Reset internal state data back to the "just initialised" state */ void resetState(); @@ -328,7 +389,7 @@ class HttpClient : public Client @return 0 if successful, else error */ int sendInitialHeaders(const char* aURLPath, - const char* aHttpMethod); + const char* aHttpMethod); /* Let the server know that we've reached the end of the headers */ @@ -340,25 +401,25 @@ class HttpClient : public Client // Number of milliseconds that we wait each time there isn't any data // available to be read (during status code and header processing) - static const int kHttpWaitForDataDelay = 1000; - // Number of milliseconds that we'll wait in total without receiveing any + static const int kHttpWaitForDataDelay = 30; + // Number of milliseconds that we'll wait in total without receiving any // data before returning HTTP_ERROR_TIMED_OUT (during status code and header // processing) - static const int kHttpResponseTimeout = 30*1000; + static const int kHttpResponseTimeout = 30 * 1000; static const char* kContentLengthPrefix; static const char* kTransferEncodingChunked; typedef enum { - eIdle, - eRequestStarted, - eRequestSent, - eReadingStatusCode, - eStatusCodeRead, - eReadingContentLength, - eSkipToEndOfHeader, - eLineStartingCRFound, - eReadingBody, - eReadingChunkLength, - eReadingBodyChunk + eIdle, + eRequestStarted, + eRequestSent, + eReadingStatusCode, + eStatusCodeRead, + eReadingContentLength, + eSkipToEndOfHeader, + eLineStartingCRFound, + eReadingBody, + eReadingChunkLength, + eReadingBodyChunk } tHttpState; // Client we're using Client* iClient; @@ -372,7 +433,7 @@ class HttpClient : public Client // Stores the status code for the response, once known int iStatusCode; // Stores the value of the Content-Length header, if present - int iContentLength; + long iContentLength; // How many bytes of the response body have been read by the user int iBodyLengthConsumed; // How far through a Content-Length header prefix we are @@ -384,9 +445,10 @@ class HttpClient : public Client // Stores the value of the current chunk length, if present int iChunkLength; uint32_t iHttpResponseTimeout; + uint32_t iHttpWaitForDataDelay; bool iConnectionClose; bool iSendDefaultRequestHeaders; String iHeaderLine; }; -#endif +#endif \ No newline at end of file diff --git a/src/ArduinoHttpClient.h b/src/ArduinoHttpClient.h index abb8494..7cf2c40 100644 --- a/src/ArduinoHttpClient.h +++ b/src/ArduinoHttpClient.h @@ -5,8 +5,8 @@ #ifndef ArduinoHttpClient_h #define ArduinoHttpClient_h -#include "HttpClient.h" +#include "ArdHttpClient.h" #include "WebSocketClient.h" #include "URLEncoder.h" -#endif +#endif \ No newline at end of file diff --git a/src/HttpClient.cpp b/src/HttpClient.cpp deleted file mode 100644 index 7517eea..0000000 --- a/src/HttpClient.cpp +++ /dev/null @@ -1,862 +0,0 @@ -// Class to simplify HTTP fetching on Arduino -// (c) Copyright 2010-2011 MCQN Ltd -// Released under Apache License, version 2.0 - -#include "HttpClient.h" -#include "b64.h" - -// Initialize constants -const char* HttpClient::kUserAgent = "Arduino/2.2.0"; -const char* HttpClient::kContentLengthPrefix = HTTP_HEADER_CONTENT_LENGTH ": "; -const char* HttpClient::kTransferEncodingChunked = HTTP_HEADER_TRANSFER_ENCODING ": " HTTP_HEADER_VALUE_CHUNKED; - -HttpClient::HttpClient(Client& aClient, const char* aServerName, uint16_t aServerPort) - : iClient(&aClient), iServerName(aServerName), iServerAddress(), iServerPort(aServerPort), - iConnectionClose(true), iSendDefaultRequestHeaders(true) -{ - resetState(); -} - -HttpClient::HttpClient(Client& aClient, const String& aServerName, uint16_t aServerPort) - : HttpClient(aClient, aServerName.c_str(), aServerPort) -{ -} - -HttpClient::HttpClient(Client& aClient, const IPAddress& aServerAddress, uint16_t aServerPort) - : iClient(&aClient), iServerName(NULL), iServerAddress(aServerAddress), iServerPort(aServerPort), - iConnectionClose(true), iSendDefaultRequestHeaders(true) -{ - resetState(); -} - -void HttpClient::resetState() -{ - iState = eIdle; - iStatusCode = 0; - iContentLength = kNoContentLengthHeader; - iBodyLengthConsumed = 0; - iContentLengthPtr = kContentLengthPrefix; - iTransferEncodingChunkedPtr = kTransferEncodingChunked; - iIsChunked = false; - iChunkLength = 0; - iHttpResponseTimeout = kHttpResponseTimeout; -} - -void HttpClient::stop() -{ - iClient->stop(); - resetState(); -} - -void HttpClient::connectionKeepAlive() -{ - iConnectionClose = false; -} - -void HttpClient::noDefaultRequestHeaders() -{ - iSendDefaultRequestHeaders = false; -} - -void HttpClient::beginRequest() -{ - iState = eRequestStarted; -} - -int HttpClient::startRequest(const char* aURLPath, const char* aHttpMethod, - const char* aContentType, int aContentLength, const byte aBody[]) -{ - if (iState == eReadingBody || iState == eReadingChunkLength || iState == eReadingBodyChunk) - { - flushClientRx(); - - resetState(); - } - - tHttpState initialState = iState; - - if ((eIdle != iState) && (eRequestStarted != iState)) - { - return HTTP_ERROR_API; - } - - if (iConnectionClose || !iClient->connected()) - { - if (iServerName) - { - if (!iClient->connect(iServerName, iServerPort) > 0) - { -#ifdef LOGGING - Serial.println("Connection failed"); -#endif - return HTTP_ERROR_CONNECTION_FAILED; - } - } - else - { - if (!iClient->connect(iServerAddress, iServerPort) > 0) - { -#ifdef LOGGING - Serial.println("Connection failed"); -#endif - return HTTP_ERROR_CONNECTION_FAILED; - } - } - } - else - { -#ifdef LOGGING - Serial.println("Connection already open"); -#endif - } - - // Now we're connected, send the first part of the request - int ret = sendInitialHeaders(aURLPath, aHttpMethod); - - if (HTTP_SUCCESS == ret) - { - if (aContentType) - { - sendHeader(HTTP_HEADER_CONTENT_TYPE, aContentType); - } - - if (aContentLength > 0) - { - sendHeader(HTTP_HEADER_CONTENT_LENGTH, aContentLength); - } - - bool hasBody = (aBody && aContentLength > 0); - - if (initialState == eIdle || hasBody) - { - // This was a simple version of the API, so terminate the headers now - finishHeaders(); - } - // else we'll call it in endRequest or in the first call to print, etc. - - if (hasBody) - { - write(aBody, aContentLength); - } - } - - return ret; -} - -int HttpClient::sendInitialHeaders(const char* aURLPath, const char* aHttpMethod) -{ -#ifdef LOGGING - Serial.println("Connected"); -#endif - // Send the HTTP command, i.e. "GET /somepath/ HTTP/1.0" - iClient->print(aHttpMethod); - iClient->print(" "); - - iClient->print(aURLPath); - iClient->println(" HTTP/1.1"); - if (iSendDefaultRequestHeaders) - { - // The host header, if required - if (iServerName) - { - iClient->print("Host: "); - iClient->print(iServerName); - if (iServerPort != kHttpPort) - { - iClient->print(":"); - iClient->print(iServerPort); - } - iClient->println(); - } - // And user-agent string - sendHeader(HTTP_HEADER_USER_AGENT, kUserAgent); - } - - if (iConnectionClose) - { - // Tell the server to - // close this connection after we're done - sendHeader(HTTP_HEADER_CONNECTION, "close"); - } - - // Everything has gone well - iState = eRequestStarted; - return HTTP_SUCCESS; -} - -void HttpClient::sendHeader(const char* aHeader) -{ - iClient->println(aHeader); -} - -void HttpClient::sendHeader(const char* aHeaderName, const char* aHeaderValue) -{ - iClient->print(aHeaderName); - iClient->print(": "); - iClient->println(aHeaderValue); -} - -void HttpClient::sendHeader(const char* aHeaderName, const int aHeaderValue) -{ - iClient->print(aHeaderName); - iClient->print(": "); - iClient->println(aHeaderValue); -} - -void HttpClient::sendBasicAuth(const char* aUser, const char* aPassword) -{ - // Send the initial part of this header line - iClient->print("Authorization: Basic "); - // Now Base64 encode "aUser:aPassword" and send that - // This seems trickier than it should be but it's mostly to avoid either - // (a) some arbitrarily sized buffer which hopes to be big enough, or - // (b) allocating and freeing memory - // ...so we'll loop through 3 bytes at a time, outputting the results as we - // go. - // In Base64, each 3 bytes of unencoded data become 4 bytes of encoded data - unsigned char input[3]; - unsigned char output[5]; // Leave space for a '\0' terminator so we can easily print - int userLen = strlen(aUser); - int passwordLen = strlen(aPassword); - int inputOffset = 0; - for (int i = 0; i < (userLen+1+passwordLen); i++) - { - // Copy the relevant input byte into the input - if (i < userLen) - { - input[inputOffset++] = aUser[i]; - } - else if (i == userLen) - { - input[inputOffset++] = ':'; - } - else - { - input[inputOffset++] = aPassword[i-(userLen+1)]; - } - // See if we've got a chunk to encode - if ( (inputOffset == 3) || (i == userLen+passwordLen) ) - { - // We've either got to a 3-byte boundary, or we've reached then end - b64_encode(input, inputOffset, output, 4); - // NUL-terminate the output string - output[4] = '\0'; - // And write it out - iClient->print((char*)output); -// FIXME We might want to fill output with '=' characters if b64_encode doesn't -// FIXME do it for us when we're encoding the final chunk - inputOffset = 0; - } - } - // And end the header we've sent - iClient->println(); -} - -void HttpClient::finishHeaders() -{ - iClient->println(); - iState = eRequestSent; -} - -void HttpClient::flushClientRx() -{ - while (iClient->available()) - { - iClient->read(); - } -} - -void HttpClient::endRequest() -{ - beginBody(); -} - -void HttpClient::beginBody() -{ - if (iState < eRequestSent) - { - // We still need to finish off the headers - finishHeaders(); - } - // else the end of headers has already been sent, so nothing to do here -} - -int HttpClient::get(const char* aURLPath) -{ - return startRequest(aURLPath, HTTP_METHOD_GET); -} - -int HttpClient::get(const String& aURLPath) -{ - return get(aURLPath.c_str()); -} - -int HttpClient::post(const char* aURLPath) -{ - return startRequest(aURLPath, HTTP_METHOD_POST); -} - -int HttpClient::post(const String& aURLPath) -{ - return post(aURLPath.c_str()); -} - -int HttpClient::post(const char* aURLPath, const char* aContentType, const char* aBody) -{ - return post(aURLPath, aContentType, strlen(aBody), (const byte*)aBody); -} - -int HttpClient::post(const String& aURLPath, const String& aContentType, const String& aBody) -{ - return post(aURLPath.c_str(), aContentType.c_str(), aBody.length(), (const byte*)aBody.c_str()); -} - -int HttpClient::post(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]) -{ - return startRequest(aURLPath, HTTP_METHOD_POST, aContentType, aContentLength, aBody); -} - -int HttpClient::put(const char* aURLPath) -{ - return startRequest(aURLPath, HTTP_METHOD_PUT); -} - -int HttpClient::put(const String& aURLPath) -{ - return put(aURLPath.c_str()); -} - -int HttpClient::put(const char* aURLPath, const char* aContentType, const char* aBody) -{ - return put(aURLPath, aContentType, strlen(aBody), (const byte*)aBody); -} - -int HttpClient::put(const String& aURLPath, const String& aContentType, const String& aBody) -{ - return put(aURLPath.c_str(), aContentType.c_str(), aBody.length(), (const byte*)aBody.c_str()); -} - -int HttpClient::put(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]) -{ - return startRequest(aURLPath, HTTP_METHOD_PUT, aContentType, aContentLength, aBody); -} - -int HttpClient::patch(const char* aURLPath) -{ - return startRequest(aURLPath, HTTP_METHOD_PATCH); -} - -int HttpClient::patch(const String& aURLPath) -{ - return patch(aURLPath.c_str()); -} - -int HttpClient::patch(const char* aURLPath, const char* aContentType, const char* aBody) -{ - return patch(aURLPath, aContentType, strlen(aBody), (const byte*)aBody); -} - -int HttpClient::patch(const String& aURLPath, const String& aContentType, const String& aBody) -{ - return patch(aURLPath.c_str(), aContentType.c_str(), aBody.length(), (const byte*)aBody.c_str()); -} - -int HttpClient::patch(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]) -{ - return startRequest(aURLPath, HTTP_METHOD_PATCH, aContentType, aContentLength, aBody); -} - -int HttpClient::del(const char* aURLPath) -{ - return startRequest(aURLPath, HTTP_METHOD_DELETE); -} - -int HttpClient::del(const String& aURLPath) -{ - return del(aURLPath.c_str()); -} - -int HttpClient::del(const char* aURLPath, const char* aContentType, const char* aBody) -{ - return del(aURLPath, aContentType, strlen(aBody), (const byte*)aBody); -} - -int HttpClient::del(const String& aURLPath, const String& aContentType, const String& aBody) -{ - return del(aURLPath.c_str(), aContentType.c_str(), aBody.length(), (const byte*)aBody.c_str()); -} - -int HttpClient::del(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]) -{ - return startRequest(aURLPath, HTTP_METHOD_DELETE, aContentType, aContentLength, aBody); -} - -int HttpClient::responseStatusCode() -{ - if (iState < eRequestSent) - { - return HTTP_ERROR_API; - } - // The first line will be of the form Status-Line: - // HTTP-Version SP Status-Code SP Reason-Phrase CRLF - // Where HTTP-Version is of the form: - // HTTP-Version = "HTTP" "/" 1*DIGIT "." 1*DIGIT - - int c = '\0'; - do - { - // Make sure the status code is reset, and likewise the state. This - // lets us easily cope with 1xx informational responses by just - // ignoring them really, and reading the next line for a proper response - iStatusCode = 0; - iState = eRequestSent; - - unsigned long timeoutStart = millis(); - // Psuedo-regexp we're expecting before the status-code - const char* statusPrefix = "HTTP/*.* "; - const char* statusPtr = statusPrefix; - // Whilst we haven't timed out & haven't reached the end of the headers - while ((c != '\n') && - ( (millis() - timeoutStart) < iHttpResponseTimeout )) - { - if (available()) - { - c = read(); - if (c != -1) - { - switch(iState) - { - case eRequestSent: - // We haven't reached the status code yet - if ( (*statusPtr == '*') || (*statusPtr == c) ) - { - // This character matches, just move along - statusPtr++; - if (*statusPtr == '\0') - { - // We've reached the end of the prefix - iState = eReadingStatusCode; - } - } - else - { - return HTTP_ERROR_INVALID_RESPONSE; - } - break; - case eReadingStatusCode: - if (isdigit(c)) - { - // This assumes we won't get more than the 3 digits we - // want - iStatusCode = iStatusCode*10 + (c - '0'); - } - else - { - // We've reached the end of the status code - // We could sanity check it here or double-check for ' ' - // rather than anything else, but let's be lenient - iState = eStatusCodeRead; - } - break; - case eStatusCodeRead: - // We're just waiting for the end of the line now - break; - - default: - break; - }; - // We read something, reset the timeout counter - timeoutStart = millis(); - } - } - else - { - // We haven't got any data, so let's pause to allow some to - // arrive - delay(kHttpWaitForDataDelay); - } - } - if ( (c == '\n') && (iStatusCode < 200 && iStatusCode != 101) ) - { - // We've reached the end of an informational status line - c = '\0'; // Clear c so we'll go back into the data reading loop - } - } - // If we've read a status code successfully but it's informational (1xx) - // loop back to the start - while ( (iState == eStatusCodeRead) && (iStatusCode < 200 && iStatusCode != 101) ); - - if ( (c == '\n') && (iState == eStatusCodeRead) ) - { - // We've read the status-line successfully - return iStatusCode; - } - else if (c != '\n') - { - // We must've timed out before we reached the end of the line - return HTTP_ERROR_TIMED_OUT; - } - else - { - // This wasn't a properly formed status line, or at least not one we - // could understand - return HTTP_ERROR_INVALID_RESPONSE; - } -} - -int HttpClient::skipResponseHeaders() -{ - // Just keep reading until we finish reading the headers or time out - unsigned long timeoutStart = millis(); - // Whilst we haven't timed out & haven't reached the end of the headers - while ((!endOfHeadersReached()) && - ( (millis() - timeoutStart) < iHttpResponseTimeout )) - { - if (available()) - { - (void)readHeader(); - // We read something, reset the timeout counter - timeoutStart = millis(); - } - else - { - // We haven't got any data, so let's pause to allow some to - // arrive - delay(kHttpWaitForDataDelay); - } - } - if (endOfHeadersReached()) - { - // Success - return HTTP_SUCCESS; - } - else - { - // We must've timed out - return HTTP_ERROR_TIMED_OUT; - } -} - -bool HttpClient::endOfHeadersReached() -{ - return (iState == eReadingBody || iState == eReadingChunkLength || iState == eReadingBodyChunk); -}; - -int HttpClient::contentLength() -{ - // skip the response headers, if they haven't been read already - if (!endOfHeadersReached()) - { - skipResponseHeaders(); - } - - return iContentLength; -} - -String HttpClient::responseBody() -{ - int bodyLength = contentLength(); - String response; - - if (bodyLength > 0) - { - // try to reserve bodyLength bytes - if (response.reserve(bodyLength) == 0) { - // String reserve failed - return String((const char*)NULL); - } - } - - // keep on timedRead'ing, until: - // - we have a content length: body length equals consumed or no bytes - // available - // - no content length: no bytes are available - while (iBodyLengthConsumed != bodyLength) - { - int c = timedRead(); - - if (c == -1) { - // read timed out, done - break; - } - - if (!response.concat((char)c)) { - // adding char failed - return String((const char*)NULL); - } - } - - if (bodyLength > 0 && (unsigned int)bodyLength != response.length()) { - // failure, we did not read in reponse content length bytes - return String((const char*)NULL); - } - - return response; -} - -bool HttpClient::endOfBodyReached() -{ - if (endOfHeadersReached() && (contentLength() != kNoContentLengthHeader)) - { - // We've got to the body and we know how long it will be - return (iBodyLengthConsumed >= contentLength()); - } - return false; -} - -int HttpClient::available() -{ - if (iState == eReadingChunkLength) - { - while (iClient->available()) - { - char c = iClient->read(); - - if (c == '\n') - { - iState = eReadingBodyChunk; - break; - } - else if (c == '\r') - { - // no-op - } - else if (isHexadecimalDigit(c)) - { - char digit[2] = {c, '\0'}; - - iChunkLength = (iChunkLength * 16) + strtol(digit, NULL, 16); - } - } - } - - if (iState == eReadingBodyChunk && iChunkLength == 0) - { - iState = eReadingChunkLength; - } - - if (iState == eReadingChunkLength) - { - return 0; - } - - int clientAvailable = iClient->available(); - - if (iState == eReadingBodyChunk) - { - return min(clientAvailable, iChunkLength); - } - else - { - return clientAvailable; - } -} - - -int HttpClient::read() -{ - if (iIsChunked && !available()) - { - return -1; - } - - int ret = iClient->read(); - if (ret >= 0) - { - if (endOfHeadersReached() && iContentLength > 0) - { - // We're outputting the body now and we've seen a Content-Length header - // So keep track of how many bytes are left - iBodyLengthConsumed++; - } - - if (iState == eReadingBodyChunk) - { - iChunkLength--; - - if (iChunkLength == 0) - { - iState = eReadingChunkLength; - } - } - } - return ret; -} - -bool HttpClient::headerAvailable() -{ - // clear the currently store header line - iHeaderLine = ""; - - while (!endOfHeadersReached()) - { - // read a byte from the header - int c = readHeader(); - - if (c == '\r' || c == '\n') - { - if (iHeaderLine.length()) - { - // end of the line, all done - break; - } - else - { - // ignore any CR or LF characters - continue; - } - } - - // append byte to header line - iHeaderLine += (char)c; - } - - return (iHeaderLine.length() > 0); -} - -String HttpClient::readHeaderName() -{ - int colonIndex = iHeaderLine.indexOf(':'); - - if (colonIndex == -1) - { - return ""; - } - - return iHeaderLine.substring(0, colonIndex); -} - -String HttpClient::readHeaderValue() -{ - int colonIndex = iHeaderLine.indexOf(':'); - int startIndex = colonIndex + 1; - - if (colonIndex == -1) - { - return ""; - } - - // trim any leading whitespace - while (startIndex < (int)iHeaderLine.length() && isSpace(iHeaderLine[startIndex])) - { - startIndex++; - } - - return iHeaderLine.substring(startIndex); -} - -int HttpClient::read(uint8_t *buf, size_t size) -{ - int ret =iClient->read(buf, size); - if (endOfHeadersReached() && iContentLength > 0) - { - // We're outputting the body now and we've seen a Content-Length header - // So keep track of how many bytes are left - if (ret >= 0) - { - iBodyLengthConsumed += ret; - } - } - return ret; -} - -int HttpClient::readHeader() -{ - char c = read(); - - if (endOfHeadersReached()) - { - // We've passed the headers, but rather than return an error, we'll just - // act as a slightly less efficient version of read() - return c; - } - - // Whilst reading out the headers to whoever wants them, we'll keep an - // eye out for the "Content-Length" header - switch(iState) - { - case eStatusCodeRead: - // We're at the start of a line, or somewhere in the middle of reading - // the Content-Length prefix - if (*iContentLengthPtr == c) - { - // This character matches, just move along - iContentLengthPtr++; - if (*iContentLengthPtr == '\0') - { - // We've reached the end of the prefix - iState = eReadingContentLength; - // Just in case we get multiple Content-Length headers, this - // will ensure we just get the value of the last one - iContentLength = 0; - iBodyLengthConsumed = 0; - } - } - else if (*iTransferEncodingChunkedPtr == c) - { - // This character matches, just move along - iTransferEncodingChunkedPtr++; - if (*iTransferEncodingChunkedPtr == '\0') - { - // We've reached the end of the Transfer Encoding: chunked header - iIsChunked = true; - iState = eSkipToEndOfHeader; - } - } - else if (((iContentLengthPtr == kContentLengthPrefix) && (iTransferEncodingChunkedPtr == kTransferEncodingChunked)) && (c == '\r')) - { - // We've found a '\r' at the start of a line, so this is probably - // the end of the headers - iState = eLineStartingCRFound; - } - else - { - // This isn't the Content-Length or Transfer Encoding chunked header, skip to the end of the line - iState = eSkipToEndOfHeader; - } - break; - case eReadingContentLength: - if (isdigit(c)) - { - iContentLength = iContentLength*10 + (c - '0'); - } - else - { - // We've reached the end of the content length - // We could sanity check it here or double-check for "\r\n" - // rather than anything else, but let's be lenient - iState = eSkipToEndOfHeader; - } - break; - case eLineStartingCRFound: - if (c == '\n') - { - if (iIsChunked) - { - iState = eReadingChunkLength; - iChunkLength = 0; - } - else - { - iState = eReadingBody; - } - } - break; - default: - // We're just waiting for the end of the line now - break; - }; - - if ( (c == '\n') && !endOfHeadersReached() ) - { - // We've got to the end of this line, start processing again - iState = eStatusCodeRead; - iContentLengthPtr = kContentLengthPrefix; - iTransferEncodingChunkedPtr = kTransferEncodingChunked; - } - // And return the character read to whoever wants it - return c; -} - - - diff --git a/src/URLParser.h b/src/URLParser.h new file mode 100644 index 0000000..fd31e93 --- /dev/null +++ b/src/URLParser.h @@ -0,0 +1,108 @@ +/* + * PackageLicenseDeclared: Apache-2.0 + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * The following class is defined in mbed libraries, in case of STM32H7 include the original library + */ +#if defined __has_include +# if __has_include() +# include +# else +# define NO_HTTP_PARSED +# endif +#endif + +#ifdef NO_HTTP_PARSED +#ifndef _MBED_HTTP_PARSED_URL_H_ +#define _MBED_HTTP_PARSED_URL_H_ + +#include "utility/URLParser/http_parser.h" + +class ParsedUrl { +public: + ParsedUrl(const char* url) { + struct http_parser_url parsed_url; + http_parser_parse_url(url, strlen(url), false, &parsed_url); + + for (size_t ix = 0; ix < UF_MAX; ix++) { + char* value; + if (parsed_url.field_set & (1 << ix)) { + value = (char*)calloc(parsed_url.field_data[ix].len + 1, 1); + memcpy(value, url + parsed_url.field_data[ix].off, + parsed_url.field_data[ix].len); + } + else { + value = (char*)calloc(1, 1); + } + + switch ((http_parser_url_fields)ix) { + case UF_SCHEMA: _schema = value; break; + case UF_HOST: _host = value; break; + case UF_PATH: _path = value; break; + case UF_QUERY: _query = value; break; + case UF_USERINFO: _userinfo = value; break; + default: + // PORT is already parsed, FRAGMENT is not relevant for HTTP requests + free(value); + break; + } + } + + _port = parsed_url.port; + if (!_port) { + if (strcmp(_schema, "https") == 0 || strcmp(_schema, "wss") == 0) { + _port = 443; + } + else { + _port = 80; + } + } + + if (strcmp(_path, "") == 0) { + free(_path); + _path = (char*)calloc(2, 1); + _path[0] = '/'; + } + } + + ~ParsedUrl() { + if (_schema) free(_schema); + if (_host) free(_host); + if (_path) free(_path); + if (_query) free(_query); + if (_userinfo) free(_userinfo); + } + + uint16_t port() const { return _port; } + char* schema() const { return _schema; } + char* host() const { return _host; } + char* path() const { return _path; } + char* query() const { return _query; } + char* userinfo() const { return _userinfo; } + +private: + uint16_t _port; + char* _schema; + char* _host; + char* _path; + char* _query; + char* _userinfo; +}; + +#endif // _MBED_HTTP_PARSED_URL_H_ +#endif // NO_HTTP_PARSED +#undef NO_HTTP_PARSED \ No newline at end of file diff --git a/src/WebSocketClient.h b/src/WebSocketClient.h index 4b009e6..2e84abe 100644 --- a/src/WebSocketClient.h +++ b/src/WebSocketClient.h @@ -6,7 +6,11 @@ #include -#include "HttpClient.h" +#include "ArdHttpClient.h" + +#ifndef WS_TX_BUFFER_SIZE + #define WS_TX_BUFFER_SIZE 128 +#endif static const int TYPE_CONTINUATION = 0x0; static const int TYPE_TEXT = 0x1; @@ -86,7 +90,7 @@ class WebSocketClient : public HttpClient private: bool iTxStarted; uint8_t iTxMessageType; - uint8_t iTxBuffer[128]; + uint8_t iTxBuffer[WS_TX_BUFFER_SIZE]; uint64_t iTxSize; uint8_t iRxOpCode; diff --git a/src/utility/URLParser/LICENSE b/src/utility/URLParser/LICENSE new file mode 100644 index 0000000..5baf7c0 --- /dev/null +++ b/src/utility/URLParser/LICENSE @@ -0,0 +1,23 @@ +http_parser.c is based on src/http/ngx_http_parse.c from NGINX copyright +Igor Sysoev. + +Additional changes are licensed under the same terms as NGINX and +copyright Joyent, Inc. and other Node contributors. All rights reserved. + +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. diff --git a/src/utility/URLParser/README.md b/src/utility/URLParser/README.md new file mode 100644 index 0000000..56f8c96 --- /dev/null +++ b/src/utility/URLParser/README.md @@ -0,0 +1,5 @@ +# http_parser library + +This code is imported from: https://github.com/arduino/ArduinoCore-mbed/tree/4.1.1/libraries/SocketWrapper/src/utility/http_parser + +The code is shrinked in size by deleting all the unrelated code to url parse. diff --git a/src/utility/URLParser/http_parser.c b/src/utility/URLParser/http_parser.c new file mode 100644 index 0000000..a572a4c --- /dev/null +++ b/src/utility/URLParser/http_parser.c @@ -0,0 +1,591 @@ +#if defined __has_include +# if ! __has_include() && ! __has_include() +# define NO_HTTP_PARSER +# endif +#endif + +#ifdef NO_HTTP_PARSER +/* Based on src/http/ngx_http_parse.c from NGINX copyright Igor Sysoev + * + * Additional changes are licensed under the same terms as NGINX and + * copyright Joyent, Inc. and other Node contributors. All rights reserved. + * + * 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. + */ +#include "http_parser.h" +#include +#include +#include +#include +#include +#include + +#ifndef BIT_AT +# define BIT_AT(a, i) \ + (!!((unsigned int) (a)[(unsigned int) (i) >> 3] & \ + (1 << ((unsigned int) (i) & 7)))) +#endif + +#define SET_ERRNO(e) \ +do { \ + parser->http_errno = (e); \ +} while(0) + +#if HTTP_PARSER_STRICT +# define T(v) 0 +#else +# define T(v) v +#endif + + +static const uint8_t normal_url_char[32] = { +/* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */ + 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, +/* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */ + 0 | T(2) | 0 | 0 | T(16) | 0 | 0 | 0, +/* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */ + 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, +/* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */ + 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, +/* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */ + 0 | 2 | 4 | 0 | 16 | 32 | 64 | 128, +/* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0, +/* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0, }; + +#undef T + +enum state + { s_dead = 1 /* important that this is > 0 */ + + , s_start_req + + , s_req_spaces_before_url + , s_req_schema + , s_req_schema_slash + , s_req_schema_slash_slash + , s_req_server_start + , s_req_server + , s_req_server_with_at + , s_req_path + , s_req_query_string_start + , s_req_query_string + , s_req_fragment_start + , s_req_fragment + , s_headers_done + }; + +enum http_host_state + { + s_http_host_dead = 1 + , s_http_userinfo_start + , s_http_userinfo + , s_http_host_start + , s_http_host_v6_start + , s_http_host + , s_http_host_v6 + , s_http_host_v6_end + , s_http_host_v6_zone_start + , s_http_host_v6_zone + , s_http_host_port_start + , s_http_host_port +}; + +/* Macros for character classes; depends on strict-mode */ +#define LOWER(c) (unsigned char)(c | 0x20) +#define IS_ALPHA(c) (LOWER(c) >= 'a' && LOWER(c) <= 'z') +#define IS_NUM(c) ((c) >= '0' && (c) <= '9') +#define IS_ALPHANUM(c) (IS_ALPHA(c) || IS_NUM(c)) +#define IS_HEX(c) (IS_NUM(c) || (LOWER(c) >= 'a' && LOWER(c) <= 'f')) +#define IS_MARK(c) ((c) == '-' || (c) == '_' || (c) == '.' || \ + (c) == '!' || (c) == '~' || (c) == '*' || (c) == '\'' || (c) == '(' || \ + (c) == ')') +#define IS_USERINFO_CHAR(c) (IS_ALPHANUM(c) || IS_MARK(c) || (c) == '%' || \ + (c) == ';' || (c) == ':' || (c) == '&' || (c) == '=' || (c) == '+' || \ + (c) == '$' || (c) == ',') + +#if HTTP_PARSER_STRICT +#define IS_URL_CHAR(c) (BIT_AT(normal_url_char, (unsigned char)c)) +#define IS_HOST_CHAR(c) (IS_ALPHANUM(c) || (c) == '.' || (c) == '-') +#else +#define IS_URL_CHAR(c) \ + (BIT_AT(normal_url_char, (unsigned char)c) || ((c) & 0x80)) +#define IS_HOST_CHAR(c) \ + (IS_ALPHANUM(c) || (c) == '.' || (c) == '-' || (c) == '_') +#endif + +/* Our URL parser. + * + * This is designed to be shared by http_parser_execute() for URL validation, + * hence it has a state transition + byte-for-byte interface. In addition, it + * is meant to be embedded in http_parser_parse_url(), which does the dirty + * work of turning state transitions URL components for its API. + * + * This function should only be invoked with non-space characters. It is + * assumed that the caller cares about (and can detect) the transition between + * URL and non-URL states by looking for these. + */ +static enum state +parse_url_char(enum state s, const char ch) +{ + if (ch == ' ' || ch == '\r' || ch == '\n') { + return s_dead; + } + +#if HTTP_PARSER_STRICT + if (ch == '\t' || ch == '\f') { + return s_dead; + } +#endif + + switch (s) { + case s_req_spaces_before_url: + /* Proxied requests are followed by scheme of an absolute URI (alpha). + * All methods except CONNECT are followed by '/' or '*'. + */ + + if (ch == '/' || ch == '*') { + return s_req_path; + } + + if (IS_ALPHA(ch)) { + return s_req_schema; + } + + break; + + case s_req_schema: + if (IS_ALPHA(ch)) { + return s; + } + + if (ch == ':') { + return s_req_schema_slash; + } + + break; + + case s_req_schema_slash: + if (ch == '/') { + return s_req_schema_slash_slash; + } + + break; + + case s_req_schema_slash_slash: + if (ch == '/') { + return s_req_server_start; + } + + break; + + case s_req_server_with_at: + if (ch == '@') { + return s_dead; + } + + /* FALLTHROUGH */ + case s_req_server_start: + case s_req_server: + if (ch == '/') { + return s_req_path; + } + + if (ch == '?') { + return s_req_query_string_start; + } + + if (ch == '@') { + return s_req_server_with_at; + } + + if (IS_USERINFO_CHAR(ch) || ch == '[' || ch == ']') { + return s_req_server; + } + + break; + + case s_req_path: + if (IS_URL_CHAR(ch)) { + return s; + } + + switch (ch) { + case '?': + return s_req_query_string_start; + + case '#': + return s_req_fragment_start; + } + + break; + + case s_req_query_string_start: + case s_req_query_string: + if (IS_URL_CHAR(ch)) { + return s_req_query_string; + } + + switch (ch) { + case '?': + /* allow extra '?' in query string */ + return s_req_query_string; + + case '#': + return s_req_fragment_start; + } + + break; + + case s_req_fragment_start: + if (IS_URL_CHAR(ch)) { + return s_req_fragment; + } + + switch (ch) { + case '?': + return s_req_fragment; + + case '#': + return s; + } + + break; + + case s_req_fragment: + if (IS_URL_CHAR(ch)) { + return s; + } + + switch (ch) { + case '?': + case '#': + return s; + } + + break; + + default: + break; + } + + /* We should never fall out of the switch above unless there's an error */ + return s_dead; +} + +static enum http_host_state +http_parse_host_char(enum http_host_state s, const char ch) { + switch(s) { + case s_http_userinfo: + case s_http_userinfo_start: + if (ch == '@') { + return s_http_host_start; + } + + if (IS_USERINFO_CHAR(ch)) { + return s_http_userinfo; + } + break; + + case s_http_host_start: + if (ch == '[') { + return s_http_host_v6_start; + } + + if (IS_HOST_CHAR(ch)) { + return s_http_host; + } + + break; + + case s_http_host: + if (IS_HOST_CHAR(ch)) { + return s_http_host; + } + + /* FALLTHROUGH */ + case s_http_host_v6_end: + if (ch == ':') { + return s_http_host_port_start; + } + + break; + + case s_http_host_v6: + if (ch == ']') { + return s_http_host_v6_end; + } + + /* FALLTHROUGH */ + case s_http_host_v6_start: + if (IS_HEX(ch) || ch == ':' || ch == '.') { + return s_http_host_v6; + } + + if (s == s_http_host_v6 && ch == '%') { + return s_http_host_v6_zone_start; + } + break; + + case s_http_host_v6_zone: + if (ch == ']') { + return s_http_host_v6_end; + } + + /* FALLTHROUGH */ + case s_http_host_v6_zone_start: + /* RFC 6874 Zone ID consists of 1*( unreserved / pct-encoded) */ + if (IS_ALPHANUM(ch) || ch == '%' || ch == '.' || ch == '-' || ch == '_' || + ch == '~') { + return s_http_host_v6_zone; + } + break; + + case s_http_host_port: + case s_http_host_port_start: + if (IS_NUM(ch)) { + return s_http_host_port; + } + + break; + + default: + break; + } + return s_http_host_dead; +} + +static int +http_parse_host(const char * buf, struct http_parser_url *u, int found_at) { + enum http_host_state s; + + const char *p; + uint32_t buflen = u->field_data[UF_HOST].off + u->field_data[UF_HOST].len; + + assert(u->field_set & (1 << UF_HOST)); + + u->field_data[UF_HOST].len = 0; + + s = found_at ? s_http_userinfo_start : s_http_host_start; + + for (p = buf + u->field_data[UF_HOST].off; p < buf + buflen; p++) { + enum http_host_state new_s = http_parse_host_char(s, *p); + + if (new_s == s_http_host_dead) { + return 1; + } + + switch(new_s) { + case s_http_host: + if (s != s_http_host) { + u->field_data[UF_HOST].off = p - buf; + } + u->field_data[UF_HOST].len++; + break; + + case s_http_host_v6: + if (s != s_http_host_v6) { + u->field_data[UF_HOST].off = p - buf; + } + u->field_data[UF_HOST].len++; + break; + + case s_http_host_v6_zone_start: + case s_http_host_v6_zone: + u->field_data[UF_HOST].len++; + break; + + case s_http_host_port: + if (s != s_http_host_port) { + u->field_data[UF_PORT].off = p - buf; + u->field_data[UF_PORT].len = 0; + u->field_set |= (1 << UF_PORT); + } + u->field_data[UF_PORT].len++; + break; + + case s_http_userinfo: + if (s != s_http_userinfo) { + u->field_data[UF_USERINFO].off = p - buf ; + u->field_data[UF_USERINFO].len = 0; + u->field_set |= (1 << UF_USERINFO); + } + u->field_data[UF_USERINFO].len++; + break; + + default: + break; + } + s = new_s; + } + + /* Make sure we don't end somewhere unexpected */ + switch (s) { + case s_http_host_start: + case s_http_host_v6_start: + case s_http_host_v6: + case s_http_host_v6_zone_start: + case s_http_host_v6_zone: + case s_http_host_port_start: + case s_http_userinfo: + case s_http_userinfo_start: + return 1; + default: + break; + } + + return 0; +} + +void +http_parser_url_init(struct http_parser_url *u) { + memset(u, 0, sizeof(*u)); +} + +int +http_parser_parse_url(const char *buf, uint32_t buflen, int is_connect, + struct http_parser_url *u) +{ + enum state s; + const char *p; + enum http_parser_url_fields uf, old_uf; + int found_at = 0; + + u->port = u->field_set = 0; + s = is_connect ? s_req_server_start : s_req_spaces_before_url; + old_uf = UF_MAX; + + for (p = buf; p < buf + buflen; p++) { + s = parse_url_char(s, *p); + + /* Figure out the next field that we're operating on */ + switch (s) { + case s_dead: + return 1; + + /* Skip delimeters */ + case s_req_schema_slash: + case s_req_schema_slash_slash: + case s_req_server_start: + case s_req_query_string_start: + case s_req_fragment_start: + continue; + + case s_req_schema: + uf = UF_SCHEMA; + break; + + case s_req_server_with_at: + found_at = 1; + + /* FALLTROUGH */ + case s_req_server: + uf = UF_HOST; + break; + + case s_req_path: + uf = UF_PATH; + break; + + case s_req_query_string: + uf = UF_QUERY; + break; + + case s_req_fragment: + uf = UF_FRAGMENT; + break; + + default: + assert(!"Unexpected state"); + return 1; + } + + /* Nothing's changed; soldier on */ + if (uf == old_uf) { + u->field_data[uf].len++; + continue; + } + + u->field_data[uf].off = p - buf; + u->field_data[uf].len = 1; + + u->field_set |= (1 << uf); + old_uf = uf; + } + + /* host must be present if there is a schema */ + /* parsing http:///toto will fail */ + if ((u->field_set & (1 << UF_SCHEMA)) && + (u->field_set & (1 << UF_HOST)) == 0) { + return 1; + } + + if (u->field_set & (1 << UF_HOST)) { + if (http_parse_host(buf, u, found_at) != 0) { + return 1; + } + } + + /* CONNECT requests can only contain "hostname:port" */ + if (is_connect && u->field_set != ((1 << UF_HOST)|(1 << UF_PORT))) { + return 1; + } + + if (u->field_set & (1 << UF_PORT)) { + /* Don't bother with endp; we've already validated the string */ + unsigned long v = strtoul(buf + u->field_data[UF_PORT].off, NULL, 10); + + /* Ports have a max value of 2^16 */ + if (v > 0xffff) { + return 1; + } + + u->port = (uint16_t) v; + } + + return 0; +} + +unsigned long +http_parser_version(void) { + return HTTP_PARSER_VERSION_MAJOR * 0x10000 | + HTTP_PARSER_VERSION_MINOR * 0x00100 | + HTTP_PARSER_VERSION_PATCH * 0x00001; +} + +#endif // NO_HTTP_PARSER \ No newline at end of file diff --git a/src/utility/URLParser/http_parser.h b/src/utility/URLParser/http_parser.h new file mode 100644 index 0000000..85a5238 --- /dev/null +++ b/src/utility/URLParser/http_parser.h @@ -0,0 +1,96 @@ +/* Copyright Joyent, Inc. and other Node contributors. All rights reserved. + * + * 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. + */ +#ifndef http_parser_h +#define http_parser_h + +#ifdef __cplusplus +extern "C" { +#endif + +/* Also update SONAME in the Makefile whenever you change these. */ +#define HTTP_PARSER_VERSION_MAJOR 2 +#define HTTP_PARSER_VERSION_MINOR 7 +#define HTTP_PARSER_VERSION_PATCH 1 + +#include + +/* Compile with -DHTTP_PARSER_STRICT=0 to make less checks, but run + * faster + */ +#ifndef HTTP_PARSER_STRICT +# define HTTP_PARSER_STRICT 1 +#endif + + +enum http_parser_url_fields + { UF_SCHEMA = 0 + , UF_HOST = 1 + , UF_PORT = 2 + , UF_PATH = 3 + , UF_QUERY = 4 + , UF_FRAGMENT = 5 + , UF_USERINFO = 6 + , UF_MAX = 7 + }; + + +/* Result structure for http_parser_parse_url(). + * + * Callers should index into field_data[] with UF_* values iff field_set + * has the relevant (1 << UF_*) bit set. As a courtesy to clients (and + * because we probably have padding left over), we convert any port to + * a uint16_t. + */ +struct http_parser_url { + uint16_t field_set; /* Bitmask of (1 << UF_*) values */ + uint16_t port; /* Converted UF_PORT string */ + + struct { + uint16_t off; /* Offset into buffer in which field starts */ + uint16_t len; /* Length of run in buffer */ + } field_data[UF_MAX]; +}; + + +/* Returns the library version. Bits 16-23 contain the major version number, + * bits 8-15 the minor version number and bits 0-7 the patch level. + * Usage example: + * + * unsigned long version = http_parser_version(); + * unsigned major = (version >> 16) & 255; + * unsigned minor = (version >> 8) & 255; + * unsigned patch = version & 255; + * printf("http_parser v%u.%u.%u\n", major, minor, patch); + */ +unsigned long http_parser_version(void); + +/* Initialize all http_parser_url members to 0 */ +void http_parser_url_init(struct http_parser_url *u); + +/* Parse a URL; return nonzero on failure */ +int http_parser_parse_url(const char *buf, uint32_t buflen, + int is_connect, + struct http_parser_url *u); + +#ifdef __cplusplus +} +#endif +#endif \ No newline at end of file